@yumiai/chat-widget 0.1.2 → 0.2.0

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 +92 -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 +96 -17
@@ -0,0 +1,3950 @@
1
+ import {
2
+ extractRar,
3
+ initUrlIdHandler,
4
+ parseUrlFileUrl,
5
+ parseUrlId,
6
+ require_lib
7
+ } from "./chunk-7A4FY6FK.js";
8
+ import {
9
+ __toESM
10
+ } from "./chunk-7D4SUZUM.js";
11
+
12
+ // src/components/jetPaveGerberViewer/src/viewer-src/2dPage/js/gerber-parser-direct.js
13
+ import earcut from "earcut";
14
+ var GerberDirectParser = class {
15
+ constructor(options = {}) {
16
+ this.vertices = [];
17
+ this.colors = [];
18
+ this.indices = [];
19
+ this.triangleIndices = [];
20
+ this.features = [];
21
+ this.minLineWidth = options.minLineWidth || 0;
22
+ this.state = {
23
+ x: 0,
24
+ y: 0,
25
+ i: 0,
26
+ j: 0,
27
+ d: 0,
28
+ // D-code (1: draw, 2: move, 3: flash)
29
+ g: 1,
30
+ // G-code (1: linear, 2: cw arc, 3: ccw arc, 36/37: region)
31
+ aperture: null,
32
+ // 当前光圈 D-code (例如 10, 11...)
33
+ interpolation: "linear",
34
+ // linear, cw, ccw
35
+ regionMode: false,
36
+ // G36/G37
37
+ quadrant: "multi",
38
+ // 'single' or 'multi' (G74/G75)
39
+ unit: "mm",
40
+ // 'mm' or 'inch'
41
+ polarity: "dark",
42
+ // 'dark' (正极性,填充) 或 'clear' (负极性,挖空)
43
+ format: {
44
+ integer: 2,
45
+ decimal: 4,
46
+ zero: "L"
47
+ // 'L'eading or 'T'railing
48
+ }
49
+ };
50
+ this.polarityRuns = [{ polarity: this.state.polarity, start: 0, end: 0 }];
51
+ this.apertures = {};
52
+ this.macros = {};
53
+ this.currentRegionContours = [];
54
+ this._currentRegionContour = null;
55
+ }
56
+ parse(content) {
57
+ const lines = content.split(/[\n\r]+/);
58
+ for (let line of lines) {
59
+ line = line.trim();
60
+ if (!line) continue;
61
+ if (line.startsWith("%") && line.endsWith("%")) {
62
+ this.parseExtendedCommand(line.substring(1, line.length - 1));
63
+ continue;
64
+ }
65
+ const commands = line.split("*");
66
+ for (let cmd of commands) {
67
+ cmd = cmd.trim();
68
+ if (!cmd) continue;
69
+ this.parseCommand(cmd);
70
+ }
71
+ }
72
+ if (this.polarityRuns.length > 0) {
73
+ this.polarityRuns[this.polarityRuns.length - 1].end = this.triangleIndices.length;
74
+ }
75
+ return {
76
+ vertices: new Float32Array(this.vertices),
77
+ colors: new Float32Array(this.colors),
78
+ triangleIndices: new Uint32Array(this.triangleIndices),
79
+ // 改用 Uint32Array 支持大索引
80
+ features: this.features,
81
+ // 返回特征点
82
+ // 仅返回非空 run,避免产生大量空段
83
+ polarityRuns: this.polarityRuns.filter((r) => r.end - r.start > 0)
84
+ };
85
+ }
86
+ parseExtendedCommand(cmd) {
87
+ if (cmd.startsWith("FS")) {
88
+ const match = cmd.match(/FS([LT])?[AI]X(\d)(\d)Y(\d)(\d)/);
89
+ if (match) {
90
+ this.state.format.zero = match[1] || "L";
91
+ this.state.format.integer = parseInt(match[2]);
92
+ this.state.format.decimal = parseInt(match[3]);
93
+ }
94
+ } else if (cmd.startsWith("MO")) {
95
+ this.state.unit = cmd.includes("IN") ? "inch" : "mm";
96
+ } else if (cmd.startsWith("AD")) {
97
+ const match = cmd.match(/ADD(\d+)([a-zA-Z]+),?(.*)/);
98
+ if (match) {
99
+ const dCode = parseInt(match[1]);
100
+ const type = match[2];
101
+ const params = match[3].split("X").map((p) => parseFloat(p));
102
+ this.apertures[dCode] = { type, params };
103
+ }
104
+ } else if (cmd.startsWith("LP")) {
105
+ if (cmd.startsWith("LPD")) this._switchPolarity("dark");
106
+ else if (cmd.startsWith("LPC")) this._switchPolarity("clear");
107
+ }
108
+ }
109
+ _switchPolarity(nextPolarity) {
110
+ if (!nextPolarity || this.state.polarity === nextPolarity) return;
111
+ if (this.polarityRuns.length > 0) {
112
+ this.polarityRuns[this.polarityRuns.length - 1].end = this.triangleIndices.length;
113
+ }
114
+ this.state.polarity = nextPolarity;
115
+ const start = this.triangleIndices.length;
116
+ this.polarityRuns.push({ polarity: nextPolarity, start, end: start });
117
+ }
118
+ parseCommand(cmd) {
119
+ if (cmd.startsWith("G")) {
120
+ const gCode = parseInt(cmd.substring(1, 3));
121
+ if (gCode === 54) {
122
+ const dSel = cmd.match(/D(\d+)/);
123
+ if (dSel) {
124
+ const dCode = parseInt(dSel[1], 10);
125
+ if (dCode >= 10) {
126
+ this.state.aperture = dCode;
127
+ return;
128
+ }
129
+ }
130
+ return;
131
+ }
132
+ if (gCode === 1) this.state.interpolation = "linear";
133
+ if (gCode === 2) this.state.interpolation = "cw";
134
+ if (gCode === 3) this.state.interpolation = "ccw";
135
+ if (gCode === 36) {
136
+ this.state.regionMode = true;
137
+ this.currentRegionContours = [];
138
+ this._currentRegionContour = null;
139
+ }
140
+ if (gCode === 37) {
141
+ this.state.regionMode = false;
142
+ this.finishRegion();
143
+ }
144
+ if (cmd.length > 3) {
145
+ this.parseCoordinate(cmd.substring(3));
146
+ }
147
+ return;
148
+ }
149
+ if (cmd.startsWith("D")) {
150
+ const dCode = parseInt(cmd.substring(1));
151
+ if (dCode >= 10) {
152
+ this.state.aperture = dCode;
153
+ return;
154
+ }
155
+ if (dCode === 1 || dCode === 2 || dCode === 3) {
156
+ const x = this.state.x;
157
+ const y = this.state.y;
158
+ if (dCode === 1) {
159
+ if (this.state.aperture) {
160
+ }
161
+ } else if (dCode === 2) {
162
+ } else if (dCode === 3) {
163
+ this.addFlash(x, y);
164
+ }
165
+ return;
166
+ }
167
+ }
168
+ this.parseCoordinate(cmd);
169
+ }
170
+ parseCoordinate(cmd) {
171
+ let x = this.state.x;
172
+ let y = this.state.y;
173
+ let iOffset = 0;
174
+ let jOffset = 0;
175
+ let d = null;
176
+ const xMatch = cmd.match(/X([-+]?\d+)/);
177
+ if (xMatch) {
178
+ x = this.parseValue(xMatch[1]);
179
+ }
180
+ const yMatch = cmd.match(/Y([-+]?\d+)/);
181
+ if (yMatch) {
182
+ y = this.parseValue(yMatch[1]);
183
+ }
184
+ const iMatch = cmd.match(/I([-+]?\d+)/);
185
+ if (iMatch) {
186
+ iOffset = this.parseValue(iMatch[1]);
187
+ }
188
+ const jMatch = cmd.match(/J([-+]?\d+)/);
189
+ if (jMatch) {
190
+ jOffset = this.parseValue(jMatch[1]);
191
+ }
192
+ const dMatch = cmd.match(/D(\d+)/);
193
+ if (dMatch) {
194
+ d = parseInt(dMatch[1]);
195
+ }
196
+ const prevX = this.state.x;
197
+ const prevY = this.state.y;
198
+ this.state.x = x;
199
+ this.state.y = y;
200
+ if (d !== null) {
201
+ if (d === 1) {
202
+ if (this.state.regionMode) {
203
+ if (this.state.interpolation === "linear") {
204
+ if (!this._currentRegionContour) {
205
+ this._startRegionContour(prevX, prevY);
206
+ }
207
+ this._currentRegionContour.push({ x, y });
208
+ } else {
209
+ if (!this._currentRegionContour) {
210
+ this._startRegionContour(prevX, prevY);
211
+ }
212
+ this.addArcToRegion(prevX, prevY, x, y, iOffset, jOffset, this.state.interpolation === "cw");
213
+ }
214
+ } else {
215
+ if (this.state.interpolation === "linear") {
216
+ this.addStroke(prevX, prevY, x, y);
217
+ if (this.state.aperture && this.apertures[this.state.aperture]?.type === "C") {
218
+ let width = this.apertures[this.state.aperture].params[0];
219
+ if (this.state.unit === "inch") width *= 25.4;
220
+ if (width < this.minLineWidth) width = this.minLineWidth;
221
+ this.addCircle(prevX, prevY, width / 2);
222
+ this.addCircle(x, y, width / 2);
223
+ }
224
+ } else {
225
+ this.addArcStroke(prevX, prevY, x, y, iOffset, jOffset, this.state.interpolation === "cw");
226
+ }
227
+ }
228
+ } else if (d === 2) {
229
+ if (this.state.regionMode) {
230
+ this._startRegionContour(x, y);
231
+ }
232
+ } else if (d === 3) {
233
+ this.addFlash(x, y);
234
+ }
235
+ }
236
+ }
237
+ _startRegionContour(x, y) {
238
+ const contour = [{ x, y }];
239
+ this.currentRegionContours.push(contour);
240
+ this._currentRegionContour = contour;
241
+ }
242
+ // 添加圆弧绘制逻辑
243
+ addArcStroke(x1, y1, x2, y2, iOffset, jOffset, isCw) {
244
+ const cx = x1 + iOffset;
245
+ const cy = y1 + jOffset;
246
+ const radius = Math.sqrt(iOffset * iOffset + jOffset * jOffset);
247
+ if (radius < 1e-4) {
248
+ console.warn("Radius too small, skipping arc");
249
+ return;
250
+ }
251
+ let startAngle = Math.atan2(y1 - cy, x1 - cx);
252
+ let endAngle = Math.atan2(y2 - cy, x2 - cx);
253
+ const dist = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
254
+ const isFullCircle = dist < 1e-4;
255
+ if (isCw) {
256
+ if (isFullCircle) {
257
+ endAngle = startAngle - 2 * Math.PI;
258
+ } else if (endAngle >= startAngle) {
259
+ endAngle -= 2 * Math.PI;
260
+ }
261
+ } else {
262
+ if (isFullCircle) {
263
+ endAngle = startAngle + 2 * Math.PI;
264
+ } else if (endAngle <= startAngle) {
265
+ endAngle += 2 * Math.PI;
266
+ }
267
+ }
268
+ const arcLength = Math.abs(endAngle - startAngle) * radius;
269
+ const segments = Math.ceil(arcLength / 0.1);
270
+ const limitedSegments = Math.min(Math.max(segments, 8), 64);
271
+ let prevPx = x1;
272
+ let prevPy = y1;
273
+ if (this.state.aperture && this.apertures[this.state.aperture]?.type === "C") {
274
+ let width = this.apertures[this.state.aperture].params[0];
275
+ if (this.state.unit === "inch") width *= 25.4;
276
+ if (width < this.minLineWidth) width = this.minLineWidth;
277
+ this.addCircle(x1, y1, width / 2);
278
+ }
279
+ for (let k = 1; k <= limitedSegments; k++) {
280
+ const t = k / limitedSegments;
281
+ const angle = startAngle + (endAngle - startAngle) * t;
282
+ const px = cx + radius * Math.cos(angle);
283
+ const py = cy + radius * Math.sin(angle);
284
+ this.addStroke(prevPx, prevPy, px, py);
285
+ prevPx = px;
286
+ prevPy = py;
287
+ }
288
+ if (this.state.aperture && this.apertures[this.state.aperture]?.type === "C") {
289
+ let width = this.apertures[this.state.aperture].params[0];
290
+ if (this.state.unit === "inch") width *= 25.4;
291
+ if (width < this.minLineWidth) width = this.minLineWidth;
292
+ this.addCircle(x2, y2, width / 2);
293
+ }
294
+ }
295
+ addArcToRegion(x1, y1, x2, y2, iOffset, jOffset, isCw) {
296
+ const cx = x1 + iOffset;
297
+ const cy = y1 + jOffset;
298
+ const radius = Math.sqrt(iOffset * iOffset + jOffset * jOffset);
299
+ if (radius < 1e-4) return;
300
+ let startAngle = Math.atan2(y1 - cy, x1 - cx);
301
+ let endAngle = Math.atan2(y2 - cy, x2 - cx);
302
+ const dist = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
303
+ const isFullCircle = dist < 1e-4;
304
+ if (isCw) {
305
+ if (isFullCircle) {
306
+ endAngle = startAngle - 2 * Math.PI;
307
+ } else if (endAngle >= startAngle) {
308
+ endAngle -= 2 * Math.PI;
309
+ }
310
+ } else {
311
+ if (isFullCircle) {
312
+ endAngle = startAngle + 2 * Math.PI;
313
+ } else if (endAngle <= startAngle) {
314
+ endAngle += 2 * Math.PI;
315
+ }
316
+ }
317
+ const arcLength = Math.abs(endAngle - startAngle) * radius;
318
+ const segments = Math.max(8, Math.ceil(arcLength / 0.1));
319
+ for (let k = 1; k <= segments; k++) {
320
+ const t = k / segments;
321
+ const angle = startAngle + (endAngle - startAngle) * t;
322
+ if (this._currentRegionContour) {
323
+ this._currentRegionContour.push({
324
+ x: cx + radius * Math.cos(angle),
325
+ y: cy + radius * Math.sin(angle)
326
+ });
327
+ }
328
+ }
329
+ }
330
+ parseValue(str) {
331
+ if (str.includes(".")) return parseFloat(str);
332
+ const totalLen = this.state.format.integer + this.state.format.decimal;
333
+ const sign = str.startsWith("-") || str.startsWith("+") ? str[0] : "";
334
+ const numStr = sign ? str.substring(1) : str;
335
+ let val = 0;
336
+ const scale2 = Math.pow(10, this.state.format.decimal);
337
+ val = parseInt(str) / scale2;
338
+ if (this.state.unit === "inch") val *= 25.4;
339
+ return val;
340
+ }
341
+ addStroke(x1, y1, x2, y2) {
342
+ if (!this.state.aperture) {
343
+ console.warn("[addStroke] No aperture selected");
344
+ return;
345
+ }
346
+ const ap = this.apertures[this.state.aperture];
347
+ if (!ap) {
348
+ console.warn(`[addStroke] Aperture D${this.state.aperture} not defined`);
349
+ return;
350
+ }
351
+ let width = 0;
352
+ if (ap.type === "C") width = ap.params[0];
353
+ if (ap.type === "R") width = ap.params[0];
354
+ if (this.state.unit === "inch") width *= 25.4;
355
+ if (width === 0) {
356
+ console.warn(`[GerberDirect] Aperture D${this.state.aperture} \u5BBD\u5EA6\u4E3A0\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u5BBD\u5EA6 0.1mm`);
357
+ width = 0.1;
358
+ }
359
+ if (width < this.minLineWidth) {
360
+ width = this.minLineWidth;
361
+ }
362
+ const dx = x2 - x1;
363
+ const dy = y2 - y1;
364
+ const len = Math.sqrt(dx * dx + dy * dy);
365
+ if (len === 0) return;
366
+ const ux = -dy / len * (width / 2);
367
+ const uy = dx / len * (width / 2);
368
+ const p1 = { x: x1 + ux, y: y1 + uy };
369
+ const p2 = { x: x1 - ux, y: y1 - uy };
370
+ const p3 = { x: x2 - ux, y: y2 - uy };
371
+ const p4 = { x: x2 + ux, y: y2 + uy };
372
+ this.addQuad(p1, p2, p3, p4);
373
+ if (ap.type === "C") {
374
+ this.addCircle(x1, y1, width / 2);
375
+ this.addCircle(x2, y2, width / 2);
376
+ }
377
+ }
378
+ addFlash(x, y) {
379
+ if (!this.state.aperture) return;
380
+ const ap = this.apertures[this.state.aperture];
381
+ if (!ap) return;
382
+ if (ap.type === "C") {
383
+ let r = ap.params[0] / 2;
384
+ if (this.state.unit === "inch") r *= 25.4;
385
+ if (r === 0) {
386
+ console.warn(`[GerberDirect] Aperture D${this.state.aperture} \u534A\u5F84\u4E3A0\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u534A\u5F84 0.05mm (\u76F4\u5F840.1mm)`);
387
+ r = 0.05;
388
+ }
389
+ this.addCircle(x, y, r);
390
+ this.features.push({
391
+ type: "circle",
392
+ x,
393
+ y,
394
+ r
395
+ });
396
+ } else if (ap.type === "R") {
397
+ let w = ap.params[0];
398
+ let h = ap.params[1];
399
+ if (this.state.unit === "inch") {
400
+ w *= 25.4;
401
+ h *= 25.4;
402
+ }
403
+ const p1 = { x: x - w / 2, y: y - h / 2 };
404
+ const p2 = { x: x + w / 2, y: y - h / 2 };
405
+ const p3 = { x: x + w / 2, y: y + h / 2 };
406
+ const p4 = { x: x - w / 2, y: y + h / 2 };
407
+ this.addQuad(p1, p2, p3, p4);
408
+ this.features.push({
409
+ type: "rect",
410
+ x,
411
+ y,
412
+ w,
413
+ h
414
+ });
415
+ } else if (ap.type === "O") {
416
+ let w = ap.params[0];
417
+ let h = ap.params[1];
418
+ if (this.state.unit === "inch") {
419
+ w *= 25.4;
420
+ h *= 25.4;
421
+ }
422
+ this.addObround(x, y, w, h);
423
+ this.features.push({
424
+ type: "obround",
425
+ x,
426
+ y,
427
+ w,
428
+ h
429
+ });
430
+ }
431
+ }
432
+ finishRegion() {
433
+ const rawContours = this.currentRegionContours || [];
434
+ if (rawContours.length === 0) return;
435
+ const cleanContour = (pts) => {
436
+ if (!pts || pts.length === 0) return [];
437
+ const out = [];
438
+ let last = null;
439
+ for (const p of pts) {
440
+ if (!last || Math.abs(p.x - last.x) > 1e-9 || Math.abs(p.y - last.y) > 1e-9) {
441
+ out.push({ x: p.x, y: p.y });
442
+ last = p;
443
+ }
444
+ }
445
+ if (out.length >= 2) {
446
+ const first = out[0];
447
+ const end = out[out.length - 1];
448
+ if (Math.abs(first.x - end.x) < 1e-9 && Math.abs(first.y - end.y) < 1e-9) {
449
+ out.pop();
450
+ }
451
+ }
452
+ if (out.length < 3) return [];
453
+ return out;
454
+ };
455
+ const contours = rawContours.map(cleanContour).filter((c) => c.length >= 3);
456
+ if (contours.length === 0) return;
457
+ const signedArea = (pts) => {
458
+ let a = 0;
459
+ for (let i = 0; i < pts.length; i++) {
460
+ const p = pts[i];
461
+ const q = pts[(i + 1) % pts.length];
462
+ a += p.x * q.y - q.x * p.y;
463
+ }
464
+ return a / 2;
465
+ };
466
+ const bboxOf = (pts) => {
467
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
468
+ for (const p of pts) {
469
+ minX = Math.min(minX, p.x);
470
+ minY = Math.min(minY, p.y);
471
+ maxX = Math.max(maxX, p.x);
472
+ maxY = Math.max(maxY, p.y);
473
+ }
474
+ return { minX, minY, maxX, maxY };
475
+ };
476
+ const pointInPoly = (pt, poly) => {
477
+ let inside = false;
478
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
479
+ const xi = poly[i].x, yi = poly[i].y;
480
+ const xj = poly[j].x, yj = poly[j].y;
481
+ const intersect = yi > pt.y !== yj > pt.y && pt.x < (xj - xi) * (pt.y - yi) / (yj - yi || 1e-30) + xi;
482
+ if (intersect) inside = !inside;
483
+ }
484
+ return inside;
485
+ };
486
+ const metas = contours.map((pts, idx) => ({
487
+ id: idx,
488
+ pts,
489
+ areaAbs: Math.abs(signedArea(pts)),
490
+ bbox: bboxOf(pts),
491
+ parent: -1,
492
+ depth: 0
493
+ }));
494
+ for (const a of metas) {
495
+ const p = a.pts[0];
496
+ let bestParent = null;
497
+ for (const b of metas) {
498
+ if (a.id === b.id) continue;
499
+ if (p.x < b.bbox.minX || p.x > b.bbox.maxX || p.y < b.bbox.minY || p.y > b.bbox.maxY) continue;
500
+ if (!pointInPoly(p, b.pts)) continue;
501
+ if (!bestParent || b.areaAbs < bestParent.areaAbs) {
502
+ bestParent = b;
503
+ }
504
+ }
505
+ if (bestParent) a.parent = bestParent.id;
506
+ }
507
+ const depthOf = (id) => {
508
+ let d = 0;
509
+ let cur = metas[id];
510
+ while (cur.parent !== -1) {
511
+ d++;
512
+ cur = metas[cur.parent];
513
+ }
514
+ return d;
515
+ };
516
+ for (const m of metas) m.depth = depthOf(m.id);
517
+ const childrenMap = /* @__PURE__ */ new Map();
518
+ for (const m of metas) {
519
+ if (!childrenMap.has(m.parent)) childrenMap.set(m.parent, []);
520
+ childrenMap.get(m.parent).push(m.id);
521
+ }
522
+ const triangulateOne = (outerPts, holePtsList) => {
523
+ const flat = [];
524
+ const holeIndices = [];
525
+ for (const p of outerPts) flat.push(p.x, p.y);
526
+ let cursor = outerPts.length;
527
+ for (const holePts of holePtsList) {
528
+ holeIndices.push(cursor);
529
+ for (const p of holePts) flat.push(p.x, p.y);
530
+ cursor += holePts.length;
531
+ }
532
+ const triangles = earcut(flat, holeIndices.length ? holeIndices : null);
533
+ if (!triangles || triangles.length === 0) return;
534
+ const baseIndex = this.vertices.length / 3;
535
+ for (let i = 0; i < flat.length; i += 2) {
536
+ this.vertices.push(flat[i], flat[i + 1], 0);
537
+ this.colors.push(1, 0, 0);
538
+ }
539
+ for (let i = 0; i < triangles.length; i++) {
540
+ this.triangleIndices.push(baseIndex + triangles[i]);
541
+ }
542
+ };
543
+ for (const m of metas) {
544
+ if (m.depth % 2 !== 0) continue;
545
+ const childIds = childrenMap.get(m.id) || [];
546
+ const directHoles = childIds.map((id) => metas[id]).filter((c) => c.depth === m.depth + 1).map((c) => c.pts);
547
+ triangulateOne(m.pts, directHoles);
548
+ }
549
+ }
550
+ addQuad(p1, p2, p3, p4) {
551
+ const baseIndex = this.vertices.length / 3;
552
+ this.vertices.push(p1.x, p1.y, 0);
553
+ this.colors.push(1, 0, 0);
554
+ this.vertices.push(p2.x, p2.y, 0);
555
+ this.colors.push(1, 0, 0);
556
+ this.vertices.push(p3.x, p3.y, 0);
557
+ this.colors.push(1, 0, 0);
558
+ this.vertices.push(p4.x, p4.y, 0);
559
+ this.colors.push(1, 0, 0);
560
+ this.triangleIndices.push(baseIndex, baseIndex + 1, baseIndex + 2);
561
+ this.triangleIndices.push(baseIndex, baseIndex + 2, baseIndex + 3);
562
+ }
563
+ addCircle(cx, cy, r) {
564
+ if (r === 0) {
565
+ r = 0.05;
566
+ }
567
+ const circumference = 2 * Math.PI * r;
568
+ const segments = Math.min(Math.max(Math.ceil(circumference / 0.2), 12), 64);
569
+ const baseIndex = this.vertices.length / 3;
570
+ this.vertices.push(cx, cy, 0);
571
+ this.colors.push(1, 0, 0);
572
+ for (let i = 0; i <= segments; i++) {
573
+ const theta = i / segments * Math.PI * 2;
574
+ this.vertices.push(cx + Math.cos(theta) * r, cy + Math.sin(theta) * r, 0);
575
+ this.colors.push(1, 0, 0);
576
+ }
577
+ for (let i = 0; i < segments; i++) {
578
+ this.triangleIndices.push(baseIndex, baseIndex + 1 + i, baseIndex + 1 + i + 1);
579
+ }
580
+ }
581
+ addObround(cx, cy, w, h) {
582
+ if (w === 0) w = 0.1;
583
+ if (h === 0) h = 0.1;
584
+ if (w > h) {
585
+ const radius = h / 2;
586
+ const rectWidth = w - h;
587
+ const segments = Math.max(8, Math.ceil(radius * 4));
588
+ const leftCx = cx - rectWidth / 2;
589
+ let baseIndex = this.vertices.length / 3;
590
+ this.vertices.push(leftCx, cy, 0);
591
+ this.colors.push(1, 0, 0);
592
+ for (let i = 0; i <= segments; i++) {
593
+ const angle = Math.PI / 2 + i / segments * Math.PI;
594
+ this.vertices.push(leftCx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius, 0);
595
+ this.colors.push(1, 0, 0);
596
+ }
597
+ for (let i = 0; i < segments; i++) {
598
+ this.triangleIndices.push(baseIndex, baseIndex + 1 + i, baseIndex + 1 + i + 1);
599
+ }
600
+ if (rectWidth > 0) {
601
+ const p1 = { x: cx - rectWidth / 2, y: cy - radius };
602
+ const p2 = { x: cx - rectWidth / 2, y: cy + radius };
603
+ const p3 = { x: cx + rectWidth / 2, y: cy + radius };
604
+ const p4 = { x: cx + rectWidth / 2, y: cy - radius };
605
+ this.addQuad(p1, p2, p3, p4);
606
+ }
607
+ const rightCx = cx + rectWidth / 2;
608
+ baseIndex = this.vertices.length / 3;
609
+ this.vertices.push(rightCx, cy, 0);
610
+ this.colors.push(1, 0, 0);
611
+ for (let i = 0; i <= segments; i++) {
612
+ const angle = -Math.PI / 2 + i / segments * Math.PI;
613
+ this.vertices.push(rightCx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius, 0);
614
+ this.colors.push(1, 0, 0);
615
+ }
616
+ for (let i = 0; i < segments; i++) {
617
+ this.triangleIndices.push(baseIndex, baseIndex + 1 + i, baseIndex + 1 + i + 1);
618
+ }
619
+ } else {
620
+ const radius = w / 2;
621
+ const rectHeight = h - w;
622
+ const segments = Math.max(8, Math.ceil(radius * 4));
623
+ const topCy = cy + rectHeight / 2;
624
+ let baseIndex = this.vertices.length / 3;
625
+ this.vertices.push(cx, topCy, 0);
626
+ this.colors.push(1, 0, 0);
627
+ for (let i = 0; i <= segments; i++) {
628
+ const angle = i / segments * Math.PI;
629
+ this.vertices.push(cx + Math.cos(angle) * radius, topCy + Math.sin(angle) * radius, 0);
630
+ this.colors.push(1, 0, 0);
631
+ }
632
+ for (let i = 0; i < segments; i++) {
633
+ this.triangleIndices.push(baseIndex, baseIndex + 1 + i, baseIndex + 1 + i + 1);
634
+ }
635
+ if (rectHeight > 0) {
636
+ const p1 = { x: cx - radius, y: cy + rectHeight / 2 };
637
+ const p2 = { x: cx - radius, y: cy - rectHeight / 2 };
638
+ const p3 = { x: cx + radius, y: cy - rectHeight / 2 };
639
+ const p4 = { x: cx + radius, y: cy + rectHeight / 2 };
640
+ this.addQuad(p1, p2, p3, p4);
641
+ }
642
+ const bottomCy = cy - rectHeight / 2;
643
+ baseIndex = this.vertices.length / 3;
644
+ this.vertices.push(cx, bottomCy, 0);
645
+ this.colors.push(1, 0, 0);
646
+ for (let i = 0; i <= segments; i++) {
647
+ const angle = Math.PI + i / segments * Math.PI;
648
+ this.vertices.push(cx + Math.cos(angle) * radius, bottomCy + Math.sin(angle) * radius, 0);
649
+ this.colors.push(1, 0, 0);
650
+ }
651
+ for (let i = 0; i < segments; i++) {
652
+ this.triangleIndices.push(baseIndex, baseIndex + 1 + i, baseIndex + 1 + i + 1);
653
+ }
654
+ }
655
+ }
656
+ };
657
+
658
+ // src/components/jetPaveGerberViewer/src/viewer-src/2dPage/js/main.js
659
+ var import_jszip = __toESM(require_lib(), 1);
660
+ import { parse, plot, renderSVG } from "web-gerber";
661
+ import earcut2 from "earcut";
662
+ var canvas = document.getElementById("canvas");
663
+ var fileInput = document.getElementById("fileInput");
664
+ var status = document.getElementById("status");
665
+ var fileInfo = document.getElementById("fileInfo");
666
+ var fileName = document.getElementById("fileName");
667
+ var fileSize = document.getElementById("fileSize");
668
+ var layerList = document.getElementById("layerList");
669
+ var layerToggleAll = document.getElementById("layerToggleAll");
670
+ var layerToggleAllCheckbox = document.getElementById("layerToggleAllCheckbox");
671
+ var loadingOverlay = document.getElementById("loadingOverlay");
672
+ function showLoading() {
673
+ if (loadingOverlay) loadingOverlay.classList.add("active");
674
+ }
675
+ function hideLoading() {
676
+ if (loadingOverlay) loadingOverlay.classList.remove("active");
677
+ }
678
+ var gl = canvas.getContext("webgl", {
679
+ stencil: true,
680
+ // 启用模版缓冲,用于处理层内重叠
681
+ alpha: false,
682
+ preserveDrawingBuffer: true
683
+ });
684
+ var program = null;
685
+ var positionLocation;
686
+ var uColorLocation;
687
+ var uAlphaLocation;
688
+ var uMatrixLocation;
689
+ var quadVertexBuffer = null;
690
+ var quadIndexBuffer = null;
691
+ var IDENTITY_MATRIX = [
692
+ 1,
693
+ 0,
694
+ 0,
695
+ 0,
696
+ 0,
697
+ 1,
698
+ 0,
699
+ 0,
700
+ 0,
701
+ 0,
702
+ 1,
703
+ 0,
704
+ 0,
705
+ 0,
706
+ 0,
707
+ 1
708
+ ];
709
+ var layers = [];
710
+ var scale = 1;
711
+ var transX = 0;
712
+ var transY = 0;
713
+ var isDragging = false;
714
+ var dragStartX = 0;
715
+ var dragStartY = 0;
716
+ var dragStartTransX = 0;
717
+ var dragStartTransY = 0;
718
+ var cacheNeedsUpdate = true;
719
+ var baseColors = [
720
+ "#FF0000",
721
+ "#14FF00",
722
+ "#0014FF",
723
+ "#FFFF14",
724
+ "#FF930C",
725
+ "#B2FF0C",
726
+ "#3CB2FF",
727
+ "#C9B20A",
728
+ "#B2C932",
729
+ "#6432C9",
730
+ "#93B20A",
731
+ "#B27C52",
732
+ "#7EC755",
733
+ "#C73C7E",
734
+ "#3778C7",
735
+ "#9B9B40",
736
+ "#7A409B",
737
+ "#407A9B",
738
+ "#7A9B40",
739
+ "#583778"
740
+ ];
741
+ function pickColor(idx) {
742
+ return idx < baseColors.length ? baseColors[idx] : "#FFFFFF";
743
+ }
744
+ var layerColorMap = /* @__PURE__ */ new Map();
745
+ var isSoloMode = false;
746
+ var soloLayerOrder = [];
747
+ var soloTargetLayerIndex = null;
748
+ if (!gl) {
749
+ alert("WebGL not supported");
750
+ } else {
751
+ initWebGL();
752
+ animate();
753
+ }
754
+ function initWebGL() {
755
+ const vsSource = `
756
+ attribute vec3 a_position;
757
+ uniform mat4 u_matrix;
758
+ void main() {
759
+ gl_Position = u_matrix * vec4(a_position, 1.0);
760
+ }
761
+ `;
762
+ const fsSource = `
763
+ precision mediump float;
764
+ uniform vec3 u_color;
765
+ uniform float u_alpha;
766
+ void main() {
767
+ gl_FragColor = vec4(u_color, u_alpha);
768
+ }
769
+ `;
770
+ const ext = gl.getExtension("OES_element_index_uint");
771
+ if (!ext) {
772
+ console.warn("OES_element_index_uint not supported");
773
+ }
774
+ const vs = createShader(gl, gl.VERTEX_SHADER, vsSource);
775
+ const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
776
+ program = createProgram(gl, vs, fs);
777
+ positionLocation = gl.getAttribLocation(program, "a_position");
778
+ uColorLocation = gl.getUniformLocation(program, "u_color");
779
+ uAlphaLocation = gl.getUniformLocation(program, "u_alpha");
780
+ uMatrixLocation = gl.getUniformLocation(program, "u_matrix");
781
+ quadVertexBuffer = gl.createBuffer();
782
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadVertexBuffer);
783
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
784
+ -1,
785
+ -1,
786
+ 0,
787
+ 1,
788
+ -1,
789
+ 0,
790
+ 1,
791
+ 1,
792
+ 0,
793
+ -1,
794
+ 1,
795
+ 0
796
+ ]), gl.STATIC_DRAW);
797
+ quadIndexBuffer = gl.createBuffer();
798
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, quadIndexBuffer);
799
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW);
800
+ gl.enable(gl.BLEND);
801
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
802
+ resize();
803
+ window.addEventListener("resize", resize);
804
+ canvas.style.cursor = "grab";
805
+ canvas.addEventListener("mousedown", (e) => {
806
+ if (e.button !== 0) return;
807
+ isDragging = true;
808
+ dragStartX = e.clientX;
809
+ dragStartY = e.clientY;
810
+ dragStartTransX = transX;
811
+ dragStartTransY = transY;
812
+ canvas.style.cursor = "grabbing";
813
+ });
814
+ window.addEventListener("mousemove", (e) => {
815
+ if (!isDragging) return;
816
+ const deltaX = e.clientX - dragStartX;
817
+ const deltaY = e.clientY - dragStartY;
818
+ const rect = canvas.getBoundingClientRect();
819
+ const normalizedDeltaX = deltaX / rect.width * 2;
820
+ const normalizedDeltaY = -(deltaY / rect.height) * 2;
821
+ transX = dragStartTransX + normalizedDeltaX;
822
+ transY = dragStartTransY + normalizedDeltaY;
823
+ });
824
+ window.addEventListener("mouseup", () => {
825
+ if (isDragging) {
826
+ isDragging = false;
827
+ canvas.style.cursor = "grab";
828
+ }
829
+ });
830
+ canvas.addEventListener("mouseleave", () => {
831
+ if (isDragging) {
832
+ isDragging = false;
833
+ canvas.style.cursor = "grab";
834
+ }
835
+ });
836
+ canvas.addEventListener("wheel", (e) => {
837
+ e.preventDefault();
838
+ const rect = canvas.getBoundingClientRect();
839
+ const mouseX = e.clientX - rect.left;
840
+ const mouseY = e.clientY - rect.top;
841
+ const normalizedX = mouseX / rect.width * 2 - 1;
842
+ const normalizedY = -(mouseY / rect.height * 2 - 1);
843
+ const canvasAspect = rect.width / rect.height;
844
+ const worldXBefore = (normalizedX - transX) / (scale / canvasAspect);
845
+ const worldYBefore = (normalizedY - transY) / scale;
846
+ const scaleFactor = e.deltaY > 0 ? 0.9 : 1.1;
847
+ scale *= scaleFactor;
848
+ transX = normalizedX - worldXBefore * (scale / canvasAspect);
849
+ transY = normalizedY - worldYBefore * scale;
850
+ });
851
+ }
852
+ function createShader(gl2, type, source) {
853
+ const shader = gl2.createShader(type);
854
+ gl2.shaderSource(shader, source);
855
+ gl2.compileShader(shader);
856
+ if (!gl2.getShaderParameter(shader, gl2.COMPILE_STATUS)) {
857
+ console.error(gl2.getShaderInfoLog(shader));
858
+ gl2.deleteShader(shader);
859
+ return null;
860
+ }
861
+ return shader;
862
+ }
863
+ function createProgram(gl2, vs, fs) {
864
+ const p = gl2.createProgram();
865
+ gl2.attachShader(p, vs);
866
+ gl2.attachShader(p, fs);
867
+ gl2.linkProgram(p);
868
+ return p;
869
+ }
870
+ function resize() {
871
+ const oldWidth = canvas.width;
872
+ const oldHeight = canvas.height;
873
+ canvas.width = canvas.clientWidth;
874
+ canvas.height = canvas.clientHeight;
875
+ gl.viewport(0, 0, canvas.width, canvas.height);
876
+ if (oldWidth > 0 && oldHeight > 0 && layers.length > 0) {
877
+ const oldAspect = oldWidth / oldHeight;
878
+ const newAspect = canvas.width / canvas.height;
879
+ transX = transX * (oldAspect / newAspect);
880
+ }
881
+ if (layers.length > 0) {
882
+ cacheNeedsUpdate = true;
883
+ requestAnimationFrame(() => {
884
+ if (typeof render === "function") {
885
+ render();
886
+ }
887
+ });
888
+ }
889
+ }
890
+ function hexToRgb(hex) {
891
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
892
+ return result ? {
893
+ r: parseInt(result[1], 16) / 255,
894
+ g: parseInt(result[2], 16) / 255,
895
+ b: parseInt(result[3], 16) / 255
896
+ } : { r: 1, g: 0, b: 0 };
897
+ }
898
+ var inheritedGerberFormatHint = null;
899
+ function updateInheritedGerberFormatHintFromText(text, fileName2) {
900
+ if (!text || typeof text !== "string") return;
901
+ if (/\bM48\b/i.test(text)) return;
902
+ const fsMatch = text.match(/%FS([LT])?[AI]X(\d)(\d)Y(\d)(\d)\*%/i);
903
+ if (!fsMatch) return;
904
+ const zero = ((fsMatch[1] || "L") + "").toUpperCase() === "T" ? "T" : "L";
905
+ const integerPlaces = parseInt(fsMatch[2], 10);
906
+ const decimalPlaces = parseInt(fsMatch[3], 10);
907
+ if (!Number.isFinite(integerPlaces) || !Number.isFinite(decimalPlaces)) return;
908
+ let unit = null;
909
+ const moMatch = text.match(/%MO(IN|MM)\*%/i);
910
+ if (moMatch) {
911
+ unit = moMatch[1].toUpperCase() === "IN" ? "in" : "mm";
912
+ } else {
913
+ if (/%MOIN\*%/i.test(text) || /\bG70\b/.test(text)) unit = "in";
914
+ if (/%MOMM\*%/i.test(text) || /\bG71\b/.test(text)) unit = "mm";
915
+ }
916
+ inheritedGerberFormatHint = {
917
+ integerPlaces,
918
+ decimalPlaces,
919
+ totalPlaces: integerPlaces + decimalPlaces,
920
+ places: [integerPlaces, decimalPlaces],
921
+ zero,
922
+ unit,
923
+ from: fileName2
924
+ };
925
+ console.log(
926
+ `[\u683C\u5F0F\u515C\u5E95] \u4ECE ${fileName2} \u63D0\u53D6 Gerber \u683C\u5F0F: ${zero === "L" ? "LZ" : "TZ"} ${integerPlaces}:${decimalPlaces} (unit=${unit || "unknown"})`
927
+ );
928
+ }
929
+ async function parseWithWebGerber(text, fileName2) {
930
+ try {
931
+ let parseSvgPath2 = function(d, scale3) {
932
+ const coords = [];
933
+ const commands = d.match(/[MLHVCSQTAZ][^MLHVCSQTAZ]*/gi) || [];
934
+ let currentX = 0, currentY = 0;
935
+ let startX = 0, startY = 0;
936
+ commands.forEach((cmd) => {
937
+ const type = cmd[0].toUpperCase();
938
+ const isRelative = cmd[0] === cmd[0].toLowerCase();
939
+ const args = cmd.slice(1).trim().split(/[\s,]+/).filter((s) => s).map(parseFloat);
940
+ switch (type) {
941
+ case "M":
942
+ if (args.length >= 2) {
943
+ currentX = isRelative ? currentX + args[0] : args[0];
944
+ currentY = isRelative ? currentY + args[1] : args[1];
945
+ startX = currentX;
946
+ startY = currentY;
947
+ coords.push(currentX * scale3, currentY * scale3 * -1);
948
+ }
949
+ break;
950
+ case "L":
951
+ if (args.length >= 2) {
952
+ currentX = isRelative ? currentX + args[0] : args[0];
953
+ currentY = isRelative ? currentY + args[1] : args[1];
954
+ coords.push(currentX * scale3, currentY * scale3 * -1);
955
+ }
956
+ break;
957
+ case "H":
958
+ if (args.length >= 1) {
959
+ currentX = isRelative ? currentX + args[0] : args[0];
960
+ coords.push(currentX * scale3, currentY * scale3 * -1);
961
+ }
962
+ break;
963
+ case "V":
964
+ if (args.length >= 1) {
965
+ currentY = isRelative ? currentY + args[0] : args[0];
966
+ coords.push(currentX * scale3, currentY * scale3 * -1);
967
+ }
968
+ break;
969
+ case "Z":
970
+ if (currentX !== startX || currentY !== startY) {
971
+ coords.push(startX * scale3, startY * scale3 * -1);
972
+ }
973
+ break;
974
+ }
975
+ });
976
+ return coords;
977
+ };
978
+ var parseSvgPath = parseSvgPath2;
979
+ console.log(`[web-gerber] \u5F00\u59CB\u89E3\u6790 ${fileName2}`);
980
+ const isDrillFile = /\bM48\b/i.test(text);
981
+ const fileType = isDrillFile ? "drill" : "gerber";
982
+ console.log(`[web-gerber] \u6587\u4EF6\u7C7B\u578B: ${fileType}`);
983
+ let detectedFormat = null;
984
+ let drillUnits = null;
985
+ let drillZero = null;
986
+ if (isDrillFile) {
987
+ const lines = text.split("\n").map((line) => line.trim());
988
+ for (const line of lines) {
989
+ if (!drillUnits) {
990
+ if (/^METRIC\b/i.test(line) || /^M71\b/i.test(line)) drillUnits = "mm";
991
+ if (/^INCH\b/i.test(line) || /^M72\b/i.test(line)) drillUnits = "in";
992
+ }
993
+ if (!drillZero) {
994
+ if (/\bLZ\b/i.test(line)) drillZero = "LZ";
995
+ if (/\bTZ\b/i.test(line)) drillZero = "TZ";
996
+ }
997
+ if (drillUnits && drillZero) break;
998
+ }
999
+ for (const line of lines) {
1000
+ const formatMatch = line.match(/FILE_FORMAT[=:](\d+)[:.](\d+)/i);
1001
+ if (formatMatch) {
1002
+ const integerPlaces = parseInt(formatMatch[1]);
1003
+ const decimalPlaces = parseInt(formatMatch[2]);
1004
+ detectedFormat = {
1005
+ integerPlaces,
1006
+ decimalPlaces,
1007
+ totalPlaces: integerPlaces + decimalPlaces,
1008
+ places: [integerPlaces, decimalPlaces]
1009
+ };
1010
+ console.log(`[web-gerber] \u68C0\u6D4B\u5230\u683C\u5F0F: ${integerPlaces}:${decimalPlaces} (${integerPlaces}\u4F4D\u6574\u6570, ${decimalPlaces}\u4F4D\u5C0F\u6570)`);
1011
+ break;
1012
+ }
1013
+ }
1014
+ let maxDigits = 0;
1015
+ const coordMatchesAll = text.match(/[XY](-?\d+)/g);
1016
+ if (coordMatchesAll) {
1017
+ for (const coord of coordMatchesAll) {
1018
+ const digits = coord.substring(1).replace(/-/g, "").length;
1019
+ maxDigits = Math.max(maxDigits, digits);
1020
+ }
1021
+ }
1022
+ if (!detectedFormat) {
1023
+ if (inheritedGerberFormatHint && inheritedGerberFormatHint.places) {
1024
+ detectedFormat = {
1025
+ integerPlaces: inheritedGerberFormatHint.integerPlaces,
1026
+ decimalPlaces: inheritedGerberFormatHint.decimalPlaces,
1027
+ totalPlaces: inheritedGerberFormatHint.integerPlaces + inheritedGerberFormatHint.decimalPlaces,
1028
+ places: [inheritedGerberFormatHint.integerPlaces, inheritedGerberFormatHint.decimalPlaces]
1029
+ };
1030
+ console.warn(
1031
+ `[web-gerber] Drill\u672A\u58F0\u660EFILE_FORMAT\uFF0C\u7EE7\u627F Gerber(${inheritedGerberFormatHint.from}) \u683C\u5F0F: ${detectedFormat.integerPlaces}:${detectedFormat.decimalPlaces}`
1032
+ );
1033
+ } else if (maxDigits > 0) {
1034
+ const integerPlaces = Math.min(2, maxDigits);
1035
+ const decimalPlaces = Math.max(0, maxDigits - integerPlaces);
1036
+ detectedFormat = {
1037
+ integerPlaces,
1038
+ decimalPlaces,
1039
+ totalPlaces: integerPlaces + decimalPlaces,
1040
+ places: [integerPlaces, decimalPlaces]
1041
+ };
1042
+ console.warn(
1043
+ `[web-gerber] Drill\u672A\u58F0\u660EFILE_FORMAT\uFF0C\u6309\u5750\u6807\u4F4D\u6570\u63A8\u65AD\u683C\u5F0F: ${integerPlaces}:${decimalPlaces} (maxDigits=${maxDigits})`
1044
+ );
1045
+ }
1046
+ }
1047
+ if (!drillUnits && inheritedGerberFormatHint && inheritedGerberFormatHint.unit) {
1048
+ drillUnits = inheritedGerberFormatHint.unit === "in" ? "in" : "mm";
1049
+ }
1050
+ if (!drillZero && inheritedGerberFormatHint && inheritedGerberFormatHint.zero) {
1051
+ drillZero = inheritedGerberFormatHint.zero === "T" ? "TZ" : "LZ";
1052
+ }
1053
+ if (detectedFormat) {
1054
+ const coordMatches = coordMatchesAll;
1055
+ if (coordMatches) {
1056
+ const definedDigits = detectedFormat.totalPlaces;
1057
+ console.log(`[web-gerber] \u5B9A\u4E49\u683C\u5F0F: [${detectedFormat.integerPlaces}, ${detectedFormat.decimalPlaces}] (${definedDigits}\u4F4D)`);
1058
+ console.log(`[web-gerber] \u5B9E\u9645\u6700\u957F\u5750\u6807: ${maxDigits}\u4F4D`);
1059
+ if (maxDigits > definedDigits) {
1060
+ const extraDigits = maxDigits - definedDigits;
1061
+ detectedFormat.integerPlaces += extraDigits;
1062
+ detectedFormat.totalPlaces = detectedFormat.integerPlaces + detectedFormat.decimalPlaces;
1063
+ detectedFormat.places = [detectedFormat.integerPlaces, detectedFormat.decimalPlaces];
1064
+ console.log(`[web-gerber] \u2713 \u52A8\u6001\u8C03\u6574\u683C\u5F0F\u4E3A: [${detectedFormat.integerPlaces}, ${detectedFormat.decimalPlaces}] (${maxDigits}\u4F4D)`);
1065
+ const oldFormatPattern = /FILE_FORMAT[=:]\d+[:.]\d+/i;
1066
+ const newFormatStr = `FILE_FORMAT=${detectedFormat.integerPlaces}:${detectedFormat.decimalPlaces}`;
1067
+ if (oldFormatPattern.test(text)) {
1068
+ text = text.replace(oldFormatPattern, newFormatStr);
1069
+ console.log(`[web-gerber] \u2713 \u5DF2\u4FEE\u6539\u6587\u4EF6\u683C\u5F0F\u5B9A\u4E49 \u2192 ${newFormatStr}`);
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+ }
1075
+ let parseResult = null;
1076
+ if (isDrillFile && detectedFormat && detectedFormat.places) {
1077
+ const zeroChar = (drillZero || "LZ").toUpperCase() === "TZ" ? "T" : "L";
1078
+ const parseOpts = {
1079
+ // 兼容不同版本的 web-gerber:同时提供 type/filetype
1080
+ type: "drill",
1081
+ filetype: "drill",
1082
+ format: { places: detectedFormat.places, zero: zeroChar }
1083
+ };
1084
+ try {
1085
+ console.log(
1086
+ `[web-gerber] Drill\u4F7F\u7528\u663E\u5F0Fformat\u89E3\u6790: places=[${detectedFormat.places[0]},${detectedFormat.places[1]}], zero=${zeroChar}, units=${drillUnits || "auto"}`
1087
+ );
1088
+ parseResult = parse(text, parseOpts);
1089
+ } catch (e) {
1090
+ console.warn(`[web-gerber] Drill\u663E\u5F0Fformat\u89E3\u6790\u5931\u8D25\uFF0C\u56DE\u9000\u9ED8\u8BA4\u89E3\u6790: ${e?.message || e}`);
1091
+ parseResult = null;
1092
+ }
1093
+ }
1094
+ if (!parseResult) {
1095
+ parseResult = parse(text, { filetype: fileType });
1096
+ }
1097
+ const plotResult = plot(parseResult);
1098
+ console.log(`[web-gerber] plotResult.children \u6570\u91CF:`, plotResult.children?.length);
1099
+ const svgTree = renderSVG(plotResult);
1100
+ console.log(`[web-gerber] SVG\u6811\u751F\u6210\u5B8C\u6210, children \u6570\u91CF:`, svgTree?.children?.length);
1101
+ const fileNameOnly = fileName2.split(/[/\\]/).pop();
1102
+ const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
1103
+ const fileNameUpper = fileNameOnly.toUpperCase();
1104
+ const isOutlineLayer = fileExtension === ".GKO" || fileExtension === ".GM" || /^\.GM\d+$/i.test(fileExtension) || fileNameUpper.includes("OUTLINE") || /EDGE[._-]CUTS/i.test(fileNameUpper) || fileNameUpper.includes("WX") || fileNameUpper.includes("BOARD") || fileNameUpper.startsWith("GKO") || fileNameUpper.startsWith("OUT");
1105
+ const minWidth = isOutlineLayer ? 0.3 : 0;
1106
+ if (isOutlineLayer) {
1107
+ console.log(`[\u8F6E\u5ED3\u5C42] ${fileNameOnly}: minWidth=${minWidth}mm`);
1108
+ }
1109
+ const vertices = [];
1110
+ const indices = [];
1111
+ const drillPosScaleRuns = isDrillFile ? [] : null;
1112
+ const polarityRuns = [];
1113
+ let currentPolarity = "dark";
1114
+ let currentRunStart = 0;
1115
+ const ensurePolarity = (nextPolarity) => {
1116
+ const p = nextPolarity || "dark";
1117
+ if (p === currentPolarity) return;
1118
+ if (indices.length > currentRunStart) {
1119
+ polarityRuns.push({ polarity: currentPolarity, start: currentRunStart, end: indices.length });
1120
+ }
1121
+ currentPolarity = p;
1122
+ currentRunStart = indices.length;
1123
+ };
1124
+ const scale2 = plotResult.units === "in" ? 25.4 : 1;
1125
+ let coordPosScaleFix = 1;
1126
+ const parseFixedCoord = (rawDigits, fmt, zeroMode) => {
1127
+ if (!fmt || rawDigits == null) return null;
1128
+ const s = String(rawDigits).trim();
1129
+ const sign = s.startsWith("-") ? -1 : 1;
1130
+ let digits = s.replace(/[^0-9]/g, "");
1131
+ if (!digits) return null;
1132
+ const need = fmt.totalPlaces;
1133
+ if (need && digits.length < need) {
1134
+ if ((zeroMode || "").toUpperCase() === "LZ") {
1135
+ digits = digits.padStart(need, "0");
1136
+ } else {
1137
+ digits = digits.padEnd(need, "0");
1138
+ }
1139
+ }
1140
+ const denom = Math.pow(10, fmt.decimalPlaces || 0);
1141
+ const n = parseInt(digits, 10);
1142
+ if (!Number.isFinite(n) || denom === 0) return null;
1143
+ return sign * (n / denom);
1144
+ };
1145
+ if (isDrillFile && detectedFormat && Array.isArray(plotResult.children) && plotResult.children.length > 0) {
1146
+ try {
1147
+ const xyMatch = text.match(/X(-?\d+)\s*Y(-?\d+)/i);
1148
+ const firstCircle = plotResult.children.find(
1149
+ (c) => c?.type === "imageShape" && c.shape?.type === "circle" && Number.isFinite(c.shape.cx) && Number.isFinite(c.shape.cy)
1150
+ );
1151
+ if (xyMatch && firstCircle) {
1152
+ const actX = firstCircle.shape.cx;
1153
+ const actY = firstCircle.shape.cy;
1154
+ const plotUnits = plotResult.units === "in" ? "in" : "mm";
1155
+ const srcUnits = drillUnits || plotUnits;
1156
+ const evalZeroMode = (zeroMode) => {
1157
+ let expX = parseFixedCoord(xyMatch[1], detectedFormat, zeroMode);
1158
+ let expY = parseFixedCoord(xyMatch[2], detectedFormat, zeroMode);
1159
+ if (expX != null && expY != null) {
1160
+ if (srcUnits === "in" && plotUnits === "mm") {
1161
+ expX *= 25.4;
1162
+ expY *= 25.4;
1163
+ }
1164
+ if (srcUnits === "mm" && plotUnits === "in") {
1165
+ expX /= 25.4;
1166
+ expY /= 25.4;
1167
+ }
1168
+ }
1169
+ const ratios = [];
1170
+ if (Number.isFinite(expX) && actX !== 0) ratios.push(expX / actX);
1171
+ if (Number.isFinite(expY) && actY !== 0) ratios.push(expY / actY);
1172
+ if (ratios.length === 0) return null;
1173
+ const avg = ratios.reduce((s, v) => s + v, 0) / ratios.length;
1174
+ const max = Math.max(...ratios);
1175
+ const min = Math.min(...ratios);
1176
+ const spread = avg !== 0 ? Math.abs(max - min) / Math.abs(avg) : 999;
1177
+ return { zeroMode, expX, expY, avg, spread };
1178
+ };
1179
+ const primaryZero = (drillZero || "").toUpperCase() === "TZ" ? "TZ" : "LZ";
1180
+ const altZero = primaryZero === "TZ" ? "LZ" : "TZ";
1181
+ const primary = evalZeroMode(primaryZero);
1182
+ const alt = evalZeroMode(altZero);
1183
+ const near1 = (r) => r && Number.isFinite(r.avg) && r.spread < 0.05 && Math.abs(r.avg - 1) <= 0.05;
1184
+ if (near1(primary)) {
1185
+ } else if (near1(alt)) {
1186
+ console.warn(`[web-gerber] \u26A0\uFE0F Drill\u96F6\u6291\u5236\u58F0\u660E\u4E3A ${primaryZero}\uFF0C\u4F46\u5750\u6807\u66F4\u7B26\u5408 ${altZero}\uFF08ratio\u22481\uFF09\uFF0C\u8DF3\u8FC7 posScaleFix \u4EE5\u907F\u514D\u8BEF\u7F29\u653E`);
1187
+ } else {
1188
+ const chosen = primary || alt;
1189
+ if (chosen && Number.isFinite(chosen.avg) && Math.abs(chosen.avg - 1) > 0.05 && chosen.spread < 0.05) {
1190
+ const absAvg = Math.abs(chosen.avg);
1191
+ const pow10 = Math.pow(10, Math.round(Math.log10(absAvg)));
1192
+ const snapped = Math.abs(absAvg - pow10) / pow10 < 0.15 ? pow10 : absAvg;
1193
+ if (snapped >= 0.01 && snapped <= 1e4) {
1194
+ coordPosScaleFix = snapped;
1195
+ console.warn(`[web-gerber] \u26A0\uFE0F Drill\u5750\u6807\u683C\u5F0F\u7591\u4F3C\u88AB\u9519\u8BEF\u89E3\u91CA\uFF0C\u542F\u7528 FILE_FORMAT \u5750\u6807\u7F29\u653E\u4FEE\u6B63\uFF1AposScaleFix=${coordPosScaleFix} (units=${plotUnits}, zero=${chosen.zeroMode})`);
1196
+ }
1197
+ }
1198
+ }
1199
+ }
1200
+ } catch (e) {
1201
+ console.warn("[web-gerber] Drill FILE_FORMAT \u5750\u6807\u7F29\u653E\u4FEE\u6B63\u5931\u8D25\uFF0C\u5FFD\u7565:", e?.message || e);
1202
+ }
1203
+ }
1204
+ const posScale = scale2 * coordPosScaleFix;
1205
+ const sizeScale = scale2;
1206
+ const processSvgElement = (element) => {
1207
+ if (!element || !element.tagName) return;
1208
+ const tagName = element.tagName.toLowerCase();
1209
+ if (tagName === "circle") {
1210
+ const cx = parseFloat(element.properties?.cx || element.attributes?.cx || 0) * scale2;
1211
+ const cy = parseFloat(element.properties?.cy || element.attributes?.cy || 0) * scale2 * -1;
1212
+ const r = parseFloat(element.properties?.r || element.attributes?.r || 0) * scale2;
1213
+ if (r > 0) {
1214
+ const segments = Math.min(Math.max(Math.ceil(2 * Math.PI * r / 0.2), 12), 64);
1215
+ const baseIndex = vertices.length / 3;
1216
+ vertices.push(cx, cy, 0);
1217
+ for (let i = 0; i <= segments; i++) {
1218
+ const angle = i / segments * Math.PI * 2;
1219
+ vertices.push(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r, 0);
1220
+ }
1221
+ for (let i = 0; i < segments; i++) {
1222
+ indices.push(baseIndex, baseIndex + 1 + i, baseIndex + 1 + i + 1);
1223
+ }
1224
+ for (let i = 0; i < 10; i++) {
1225
+ vertices.push(cx, cy, 0);
1226
+ }
1227
+ }
1228
+ } else if (tagName === "ellipse") {
1229
+ const cx = parseFloat(element.properties?.cx || element.attributes?.cx || 0) * scale2;
1230
+ const cy = parseFloat(element.properties?.cy || element.attributes?.cy || 0) * scale2 * -1;
1231
+ const rx = parseFloat(element.properties?.rx || element.attributes?.rx || 0) * scale2;
1232
+ const ry = parseFloat(element.properties?.ry || element.attributes?.ry || 0) * scale2;
1233
+ if (rx > 0 && ry > 0) {
1234
+ const segments = Math.min(Math.max(Math.ceil(2 * Math.PI * Math.max(rx, ry) / 0.2), 12), 64);
1235
+ const baseIndex = vertices.length / 3;
1236
+ vertices.push(cx, cy, 0);
1237
+ for (let i = 0; i <= segments; i++) {
1238
+ const angle = i / segments * Math.PI * 2;
1239
+ vertices.push(cx + Math.cos(angle) * rx, cy + Math.sin(angle) * ry, 0);
1240
+ }
1241
+ for (let i = 0; i < segments; i++) {
1242
+ indices.push(baseIndex, baseIndex + 1 + i, baseIndex + 1 + i + 1);
1243
+ }
1244
+ for (let i = 0; i < 10; i++) {
1245
+ vertices.push(cx, cy, 0);
1246
+ }
1247
+ }
1248
+ } else if (tagName === "rect") {
1249
+ const x = parseFloat(element.properties?.x || element.attributes?.x || 0) * scale2;
1250
+ const y = parseFloat(element.properties?.y || element.attributes?.y || 0) * scale2 * -1;
1251
+ const width = parseFloat(element.properties?.width || element.attributes?.width || 0) * scale2;
1252
+ const height = parseFloat(element.properties?.height || element.attributes?.height || 0) * scale2;
1253
+ const rx = parseFloat(element.properties?.rx || element.attributes?.rx || 0) * scale2;
1254
+ const ry = parseFloat(element.properties?.ry || element.attributes?.ry || 0) * scale2;
1255
+ if (width > 0 && height > 0) {
1256
+ if (rx > 0 || ry > 0) {
1257
+ const cornerRadius = Math.max(rx, ry);
1258
+ if (width > height) {
1259
+ const radius = height / 2;
1260
+ const rectWidth = width - height;
1261
+ const leftCx = x + radius;
1262
+ const leftCy = y - radius;
1263
+ const segments = Math.max(12, Math.ceil(radius * 2));
1264
+ let centerIdx = vertices.length / 3;
1265
+ vertices.push(leftCx, leftCy, 0);
1266
+ for (let j = 0; j <= segments; j++) {
1267
+ const angle = j / segments * Math.PI * 2;
1268
+ vertices.push(leftCx + Math.cos(angle) * radius, leftCy + Math.sin(angle) * radius, 0);
1269
+ }
1270
+ for (let j = 0; j < segments; j++) {
1271
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1272
+ }
1273
+ if (rectWidth > 0) {
1274
+ const baseIndex = vertices.length / 3;
1275
+ vertices.push(x + radius, y, 0);
1276
+ vertices.push(x + radius + rectWidth, y, 0);
1277
+ vertices.push(x + radius + rectWidth, y - height, 0);
1278
+ vertices.push(x + radius, y - height, 0);
1279
+ indices.push(baseIndex, baseIndex + 1, baseIndex + 2);
1280
+ indices.push(baseIndex, baseIndex + 2, baseIndex + 3);
1281
+ }
1282
+ const rightCx = x + width - radius;
1283
+ const rightCy = y - radius;
1284
+ centerIdx = vertices.length / 3;
1285
+ vertices.push(rightCx, rightCy, 0);
1286
+ for (let j = 0; j <= segments; j++) {
1287
+ const angle = j / segments * Math.PI * 2;
1288
+ vertices.push(rightCx + Math.cos(angle) * radius, rightCy + Math.sin(angle) * radius, 0);
1289
+ }
1290
+ for (let j = 0; j < segments; j++) {
1291
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1292
+ }
1293
+ } else {
1294
+ const radius = width / 2;
1295
+ const rectHeight = height - width;
1296
+ const topCx = x + radius;
1297
+ const topCy = y - radius;
1298
+ const segments = Math.max(12, Math.ceil(radius * 2));
1299
+ let centerIdx = vertices.length / 3;
1300
+ vertices.push(topCx, topCy, 0);
1301
+ for (let j = 0; j <= segments; j++) {
1302
+ const angle = j / segments * Math.PI * 2;
1303
+ vertices.push(topCx + Math.cos(angle) * radius, topCy + Math.sin(angle) * radius, 0);
1304
+ }
1305
+ for (let j = 0; j < segments; j++) {
1306
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1307
+ }
1308
+ if (rectHeight > 0) {
1309
+ const baseIndex = vertices.length / 3;
1310
+ vertices.push(x, y - radius, 0);
1311
+ vertices.push(x + width, y - radius, 0);
1312
+ vertices.push(x + width, y - radius - rectHeight, 0);
1313
+ vertices.push(x, y - radius - rectHeight, 0);
1314
+ indices.push(baseIndex, baseIndex + 1, baseIndex + 2);
1315
+ indices.push(baseIndex, baseIndex + 2, baseIndex + 3);
1316
+ }
1317
+ const bottomCx = x + radius;
1318
+ const bottomCy = y - height + radius;
1319
+ centerIdx = vertices.length / 3;
1320
+ vertices.push(bottomCx, bottomCy, 0);
1321
+ for (let j = 0; j <= segments; j++) {
1322
+ const angle = j / segments * Math.PI * 2;
1323
+ vertices.push(bottomCx + Math.cos(angle) * radius, bottomCy + Math.sin(angle) * radius, 0);
1324
+ }
1325
+ for (let j = 0; j < segments; j++) {
1326
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1327
+ }
1328
+ }
1329
+ } else {
1330
+ const baseIndex = vertices.length / 3;
1331
+ vertices.push(x, y, 0);
1332
+ vertices.push(x + width, y, 0);
1333
+ vertices.push(x + width, y - height, 0);
1334
+ vertices.push(x, y - height, 0);
1335
+ indices.push(baseIndex, baseIndex + 1, baseIndex + 2);
1336
+ indices.push(baseIndex, baseIndex + 2, baseIndex + 3);
1337
+ }
1338
+ }
1339
+ } else if (tagName === "path") {
1340
+ const d = element.properties?.d || element.attributes?.d || "";
1341
+ const fill = element.properties?.fill || element.attributes?.fill || "";
1342
+ const stroke = element.properties?.stroke || element.attributes?.stroke || "";
1343
+ const isFillPath = fill && fill !== "none";
1344
+ const isStrokePath = stroke && stroke !== "none";
1345
+ if (d) {
1346
+ const pathPoints = parseSvgPath2(d, scale2);
1347
+ if (isFillPath && pathPoints && pathPoints.length >= 6) {
1348
+ try {
1349
+ const triangles = earcut2(pathPoints);
1350
+ if (triangles && triangles.length > 0) {
1351
+ const baseIndex = vertices.length / 3;
1352
+ for (let i = 0; i < pathPoints.length; i += 2) {
1353
+ vertices.push(pathPoints[i], pathPoints[i + 1], 0);
1354
+ }
1355
+ for (let i = 0; i < triangles.length; i++) {
1356
+ indices.push(baseIndex + triangles[i]);
1357
+ }
1358
+ }
1359
+ } catch (e) {
1360
+ console.warn("[SVG Path Fill] \u4E09\u89D2\u5316\u5931\u8D25:", e);
1361
+ }
1362
+ } else if (isStrokePath && pathPoints && pathPoints.length >= 4) {
1363
+ let strokeWidth = parseFloat(element.properties?.["stroke-width"] || element.attributes?.["stroke-width"] || 0.15) * scale2;
1364
+ if (strokeWidth < minWidth) strokeWidth = minWidth;
1365
+ const radius = strokeWidth / 2;
1366
+ for (let i = 0; i < pathPoints.length - 2; i += 2) {
1367
+ const x1 = pathPoints[i];
1368
+ const y1 = pathPoints[i + 1];
1369
+ const x2 = pathPoints[i + 2];
1370
+ const y2 = pathPoints[i + 3];
1371
+ const dx = x2 - x1;
1372
+ const dy = y2 - y1;
1373
+ const len = Math.sqrt(dx * dx + dy * dy);
1374
+ if (len > 1e-3) {
1375
+ const nx = -dy / len * radius;
1376
+ const ny = dx / len * radius;
1377
+ const baseIndex = vertices.length / 3;
1378
+ vertices.push(x1 + nx, y1 + ny, 0);
1379
+ vertices.push(x1 - nx, y1 - ny, 0);
1380
+ vertices.push(x2 + nx, y2 + ny, 0);
1381
+ vertices.push(x2 - nx, y2 - ny, 0);
1382
+ indices.push(baseIndex, baseIndex + 1, baseIndex + 2);
1383
+ indices.push(baseIndex + 1, baseIndex + 3, baseIndex + 2);
1384
+ if (i === 0) {
1385
+ const segments = Math.max(8, Math.ceil(radius * 2));
1386
+ const centerIdx = vertices.length / 3;
1387
+ vertices.push(x1, y1, 0);
1388
+ for (let j = 0; j <= segments; j++) {
1389
+ const angle = j / segments * Math.PI * 2;
1390
+ vertices.push(x1 + Math.cos(angle) * radius, y1 + Math.sin(angle) * radius, 0);
1391
+ }
1392
+ for (let j = 0; j < segments; j++) {
1393
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1394
+ }
1395
+ }
1396
+ {
1397
+ const segments = Math.max(8, Math.ceil(radius * 2));
1398
+ const centerIdx = vertices.length / 3;
1399
+ vertices.push(x2, y2, 0);
1400
+ for (let j = 0; j <= segments; j++) {
1401
+ const angle = j / segments * Math.PI * 2;
1402
+ vertices.push(x2 + Math.cos(angle) * radius, y2 + Math.sin(angle) * radius, 0);
1403
+ }
1404
+ for (let j = 0; j < segments; j++) {
1405
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1406
+ }
1407
+ }
1408
+ }
1409
+ }
1410
+ }
1411
+ }
1412
+ } else if (tagName === "line") {
1413
+ const x1 = parseFloat(element.properties?.x1 || element.attributes?.x1 || 0) * scale2;
1414
+ const y1 = parseFloat(element.properties?.y1 || element.attributes?.y1 || 0) * scale2 * -1;
1415
+ const x2 = parseFloat(element.properties?.x2 || element.attributes?.x2 || 0) * scale2;
1416
+ const y2 = parseFloat(element.properties?.y2 || element.attributes?.y2 || 0) * scale2 * -1;
1417
+ let strokeWidth = parseFloat(element.properties?.["stroke-width"] || element.attributes?.["stroke-width"] || 0.15) * scale2;
1418
+ if (strokeWidth < minWidth) strokeWidth = minWidth;
1419
+ const radius = strokeWidth / 2;
1420
+ const dx = x2 - x1;
1421
+ const dy = y2 - y1;
1422
+ const len = Math.sqrt(dx * dx + dy * dy);
1423
+ if (len > 0) {
1424
+ const nx = -dy / len * radius;
1425
+ const ny = dx / len * radius;
1426
+ const baseIndex = vertices.length / 3;
1427
+ vertices.push(x1 + nx, y1 + ny, 0);
1428
+ vertices.push(x1 - nx, y1 - ny, 0);
1429
+ vertices.push(x2 + nx, y2 + ny, 0);
1430
+ vertices.push(x2 - nx, y2 - ny, 0);
1431
+ indices.push(baseIndex, baseIndex + 1, baseIndex + 2);
1432
+ indices.push(baseIndex + 1, baseIndex + 3, baseIndex + 2);
1433
+ const segments = Math.max(8, Math.ceil(radius * 2));
1434
+ let centerIdx = vertices.length / 3;
1435
+ vertices.push(x1, y1, 0);
1436
+ for (let j = 0; j <= segments; j++) {
1437
+ const angle = j / segments * Math.PI * 2;
1438
+ vertices.push(x1 + Math.cos(angle) * radius, y1 + Math.sin(angle) * radius, 0);
1439
+ }
1440
+ for (let j = 0; j < segments; j++) {
1441
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1442
+ }
1443
+ centerIdx = vertices.length / 3;
1444
+ vertices.push(x2, y2, 0);
1445
+ for (let j = 0; j <= segments; j++) {
1446
+ const angle = j / segments * Math.PI * 2;
1447
+ vertices.push(x2 + Math.cos(angle) * radius, y2 + Math.sin(angle) * radius, 0);
1448
+ }
1449
+ for (let j = 0; j < segments; j++) {
1450
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1451
+ }
1452
+ }
1453
+ } else if (tagName === "polyline" || tagName === "polygon") {
1454
+ const pointsStr = element.properties?.points || element.attributes?.points || "";
1455
+ if (pointsStr) {
1456
+ const coords = pointsStr.trim().split(/[\s,]+/).map(parseFloat);
1457
+ if (coords.length >= 6) {
1458
+ const pathPoints = [];
1459
+ for (let i = 0; i < coords.length; i += 2) {
1460
+ if (!isNaN(coords[i]) && !isNaN(coords[i + 1])) {
1461
+ pathPoints.push(coords[i] * scale2, coords[i + 1] * scale2 * -1);
1462
+ }
1463
+ }
1464
+ if (pathPoints.length >= 6) {
1465
+ try {
1466
+ const triangles = earcut2(pathPoints);
1467
+ if (triangles && triangles.length > 0) {
1468
+ const baseIndex = vertices.length / 3;
1469
+ for (let i = 0; i < pathPoints.length; i += 2) {
1470
+ vertices.push(pathPoints[i], pathPoints[i + 1], 0);
1471
+ }
1472
+ for (let i = 0; i < triangles.length; i++) {
1473
+ indices.push(baseIndex + triangles[i]);
1474
+ }
1475
+ }
1476
+ } catch (e) {
1477
+ console.warn("[SVG Polyline] \u4E09\u89D2\u5316\u5931\u8D25:", e);
1478
+ }
1479
+ }
1480
+ }
1481
+ }
1482
+ }
1483
+ if (element.children && element.children.length > 0) {
1484
+ element.children.forEach((child) => processSvgElement(child));
1485
+ }
1486
+ };
1487
+ if (!isDrillFile && svgTree && svgTree.children) {
1488
+ svgTree.children.forEach((child) => processSvgElement(child));
1489
+ console.log(`[web-gerber] \u4ECESVG\u6811\u63D0\u53D6: ${vertices.length / 3} \u9876\u70B9, ${indices.length / 3} \u4E09\u89D2\u5F62`);
1490
+ }
1491
+ if (plotResult.children && plotResult.children.length > 0) {
1492
+ const verticesBefore = vertices.length / 3;
1493
+ console.log(`[web-gerber] \u8865\u5145\u5904\u7406 plotResult.children (${plotResult.children.length} \u4E2A)`);
1494
+ const typeCount = {};
1495
+ plotResult.children.forEach((child) => {
1496
+ typeCount[child.type] = (typeCount[child.type] || 0) + 1;
1497
+ });
1498
+ plotResult.children.forEach((child, idx) => {
1499
+ ensurePolarity(child.polarity || "dark");
1500
+ if (child.type === "imagePath" && child.segments && child.segments.length > 0) {
1501
+ child.segments.forEach((segment, segIdx) => {
1502
+ if (segment.type === "line" && segment.start && segment.end) {
1503
+ const startX = segment.start[0] * scale2;
1504
+ const startY = segment.start[1] * scale2;
1505
+ const endX = segment.end[0] * scale2;
1506
+ const endY = segment.end[1] * scale2;
1507
+ const baseIndex = vertices.length / 3;
1508
+ vertices.push(startX, startY, 0);
1509
+ vertices.push(endX, endY, 0);
1510
+ let width = (segment.width ?? child.width ?? 0.15) * scale2;
1511
+ if (width < minWidth) width = minWidth;
1512
+ const radius = width / 2;
1513
+ const dx = endX - startX;
1514
+ const dy = endY - startY;
1515
+ const len = Math.sqrt(dx * dx + dy * dy);
1516
+ if (len > 0) {
1517
+ const nx = -dy / len * radius;
1518
+ const ny = dx / len * radius;
1519
+ const v1 = vertices.length / 3;
1520
+ vertices.push(startX + nx, startY + ny, 0);
1521
+ vertices.push(startX - nx, startY - ny, 0);
1522
+ vertices.push(endX + nx, endY + ny, 0);
1523
+ vertices.push(endX - nx, endY - ny, 0);
1524
+ indices.push(v1, v1 + 1, v1 + 2);
1525
+ indices.push(v1 + 1, v1 + 3, v1 + 2);
1526
+ if (segIdx === 0) {
1527
+ const segments = Math.max(8, Math.ceil(radius * 2));
1528
+ const centerIdx = vertices.length / 3;
1529
+ vertices.push(startX, startY, 0);
1530
+ for (let j = 0; j <= segments; j++) {
1531
+ const angle = j / segments * Math.PI * 2;
1532
+ vertices.push(startX + Math.cos(angle) * radius, startY + Math.sin(angle) * radius, 0);
1533
+ }
1534
+ for (let j = 0; j < segments; j++) {
1535
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1536
+ }
1537
+ }
1538
+ {
1539
+ const segments = Math.max(8, Math.ceil(radius * 2));
1540
+ const centerIdx = vertices.length / 3;
1541
+ vertices.push(endX, endY, 0);
1542
+ for (let j = 0; j <= segments; j++) {
1543
+ const angle = j / segments * Math.PI * 2;
1544
+ vertices.push(endX + Math.cos(angle) * radius, endY + Math.sin(angle) * radius, 0);
1545
+ }
1546
+ for (let j = 0; j < segments; j++) {
1547
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1548
+ }
1549
+ }
1550
+ }
1551
+ } else if (segment.type === "arc") {
1552
+ const sx = segment.start[0];
1553
+ const sy = segment.start[1];
1554
+ const ex = segment.end[0];
1555
+ const ey = segment.end[1];
1556
+ const cx0 = segment.center[0];
1557
+ const cy0 = segment.center[1];
1558
+ const startX = sx * scale2;
1559
+ const startY = sy * scale2;
1560
+ const endX = ex * scale2;
1561
+ const endY = ey * scale2;
1562
+ const cx = cx0 * scale2;
1563
+ const cy = cy0 * scale2;
1564
+ const startAngle = segment.start.length > 2 && typeof segment.start[2] === "number" ? segment.start[2] : Math.atan2(sy - cy0, sx - cx0);
1565
+ const endAngle = segment.end.length > 2 && typeof segment.end[2] === "number" ? segment.end[2] : Math.atan2(ey - cy0, ex - cx0);
1566
+ const arcRadius = (typeof segment.radius === "number" && segment.radius > 0 ? segment.radius : Math.sqrt(Math.pow(sx - cx0, 2) + Math.pow(sy - cy0, 2))) * scale2;
1567
+ const isFullCircle = Math.abs(startX - endX) < 1e-3 && Math.abs(startY - endY) < 1e-3;
1568
+ let angleDiff = endAngle - startAngle;
1569
+ if (isFullCircle) {
1570
+ angleDiff = Math.PI * 2;
1571
+ if (idx < 5) {
1572
+ }
1573
+ } else if (Math.abs(angleDiff) < 1e-3) {
1574
+ return;
1575
+ }
1576
+ const arcSegments = Math.max(8, Math.ceil(Math.abs(angleDiff) * arcRadius / 0.5));
1577
+ let arcWidth = (segment.width ?? child.width ?? 0.15) * scale2;
1578
+ if (arcWidth < minWidth) arcWidth = minWidth;
1579
+ const lineRadius = arcWidth / 2;
1580
+ for (let i = 0; i < arcSegments; i++) {
1581
+ const t1 = i / arcSegments;
1582
+ const t2 = (i + 1) / arcSegments;
1583
+ const angle1 = startAngle + angleDiff * t1;
1584
+ const angle2 = startAngle + angleDiff * t2;
1585
+ const x1 = cx + Math.cos(angle1) * arcRadius;
1586
+ const y1 = cy + Math.sin(angle1) * arcRadius;
1587
+ const x2 = cx + Math.cos(angle2) * arcRadius;
1588
+ const y2 = cy + Math.sin(angle2) * arcRadius;
1589
+ const dx = x2 - x1;
1590
+ const dy = y2 - y1;
1591
+ const len = Math.sqrt(dx * dx + dy * dy);
1592
+ if (len > 0) {
1593
+ const nx = -dy / len * lineRadius;
1594
+ const ny = dx / len * lineRadius;
1595
+ const v1 = vertices.length / 3;
1596
+ vertices.push(x1 + nx, y1 + ny, 0);
1597
+ vertices.push(x1 - nx, y1 - ny, 0);
1598
+ vertices.push(x2 + nx, y2 + ny, 0);
1599
+ vertices.push(x2 - nx, y2 - ny, 0);
1600
+ indices.push(v1, v1 + 1, v1 + 2);
1601
+ indices.push(v1 + 1, v1 + 3, v1 + 2);
1602
+ }
1603
+ }
1604
+ if (!isFullCircle && segIdx === 0) {
1605
+ const capSegments = Math.max(8, Math.ceil(lineRadius * 2));
1606
+ const centerIdx = vertices.length / 3;
1607
+ vertices.push(startX, startY, 0);
1608
+ for (let j = 0; j <= capSegments; j++) {
1609
+ const angle = j / capSegments * Math.PI * 2;
1610
+ vertices.push(startX + Math.cos(angle) * lineRadius, startY + Math.sin(angle) * lineRadius, 0);
1611
+ }
1612
+ for (let j = 0; j < capSegments; j++) {
1613
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1614
+ }
1615
+ }
1616
+ if (!isFullCircle) {
1617
+ const capSegments = Math.max(8, Math.ceil(lineRadius * 2));
1618
+ const centerIdx = vertices.length / 3;
1619
+ vertices.push(endX, endY, 0);
1620
+ for (let j = 0; j <= capSegments; j++) {
1621
+ const angle = j / capSegments * Math.PI * 2;
1622
+ vertices.push(endX + Math.cos(angle) * lineRadius, endY + Math.sin(angle) * lineRadius, 0);
1623
+ }
1624
+ for (let j = 0; j < capSegments; j++) {
1625
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1626
+ }
1627
+ }
1628
+ }
1629
+ });
1630
+ } else if (child.type === "imageShape" && child.shape) {
1631
+ const shape = child.shape;
1632
+ const polarity = child.polarity || "dark";
1633
+ const emitCircle = (cx, cy, r) => {
1634
+ if (!(r > 0)) return;
1635
+ const x = cx * posScale;
1636
+ const y = cy * posScale;
1637
+ const radius = r * sizeScale;
1638
+ const segments = Math.min(Math.max(Math.ceil(2 * Math.PI * radius / 0.2), 12), 64);
1639
+ const baseIndex = vertices.length / 3;
1640
+ vertices.push(x, y, 0);
1641
+ for (let i = 0; i <= segments; i++) {
1642
+ const angle = i / segments * Math.PI * 2;
1643
+ vertices.push(x + Math.cos(angle) * radius, y + Math.sin(angle) * radius, 0);
1644
+ }
1645
+ for (let i = 0; i < segments; i++) {
1646
+ indices.push(baseIndex, baseIndex + 1 + i, baseIndex + 1 + i + 1);
1647
+ }
1648
+ for (let i = 0; i < 10; i++) {
1649
+ vertices.push(x, y, 0);
1650
+ }
1651
+ if (drillPosScaleRuns) {
1652
+ const endIndex = vertices.length / 3;
1653
+ drillPosScaleRuns.push({ start: baseIndex, count: endIndex - baseIndex, cx: x, cy: y, kind: "circle" });
1654
+ }
1655
+ };
1656
+ const emitRectangle = (x, y, xSize, ySize, r) => {
1657
+ const x0 = (x || 0) * posScale;
1658
+ const y0 = (y || 0) * posScale;
1659
+ const w = (xSize || 0) * sizeScale;
1660
+ const h = (ySize || 0) * sizeScale;
1661
+ if (!(w > 0 && h > 0)) return;
1662
+ const baseIndex = vertices.length / 3;
1663
+ vertices.push(x0, y0, 0);
1664
+ vertices.push(x0 + w, y0, 0);
1665
+ vertices.push(x0 + w, y0 + h, 0);
1666
+ vertices.push(x0, y0 + h, 0);
1667
+ indices.push(baseIndex, baseIndex + 1, baseIndex + 2);
1668
+ indices.push(baseIndex, baseIndex + 2, baseIndex + 3);
1669
+ const cx = x0 + w / 2;
1670
+ const cy = y0 + h / 2;
1671
+ for (let i = 0; i < 10; i++) {
1672
+ vertices.push(cx, cy, 0);
1673
+ }
1674
+ if (drillPosScaleRuns) {
1675
+ const endIndex = vertices.length / 3;
1676
+ drillPosScaleRuns.push({ start: baseIndex, count: endIndex - baseIndex, cx, cy, kind: "rectangle" });
1677
+ }
1678
+ };
1679
+ const emitPolygon = (pts) => {
1680
+ if (!Array.isArray(pts) || pts.length < 3) return;
1681
+ const flat = [];
1682
+ for (const p of pts) {
1683
+ if (!p || p.length < 2) continue;
1684
+ flat.push(p[0] * posScale, p[1] * posScale);
1685
+ }
1686
+ if (flat.length < 6) return;
1687
+ const triangles = earcut2(flat);
1688
+ if (!triangles || triangles.length === 0) return;
1689
+ const baseIndex = vertices.length / 3;
1690
+ for (let i = 0; i < flat.length; i += 2) {
1691
+ vertices.push(flat[i], flat[i + 1], 0);
1692
+ }
1693
+ for (let i = 0; i < triangles.length; i++) {
1694
+ indices.push(baseIndex + triangles[i]);
1695
+ }
1696
+ };
1697
+ const emitOutline = (segments) => {
1698
+ if (!Array.isArray(segments) || segments.length === 0) return;
1699
+ const pts = [];
1700
+ let started = false;
1701
+ const pushPoint = (x, y) => {
1702
+ pts.push(x * posScale, y * posScale);
1703
+ };
1704
+ for (const seg of segments) {
1705
+ if (!seg || !seg.type) continue;
1706
+ if (seg.type === "line" && seg.start && seg.end) {
1707
+ if (!started) {
1708
+ pushPoint(seg.start[0], seg.start[1]);
1709
+ started = true;
1710
+ }
1711
+ pushPoint(seg.end[0], seg.end[1]);
1712
+ } else if (seg.type === "arc" && seg.start && seg.end && seg.center) {
1713
+ const cx = seg.center[0] * posScale;
1714
+ const cy = seg.center[1] * posScale;
1715
+ const radius = (seg.radius ?? Math.sqrt(Math.pow(seg.start[0] - seg.center[0], 2) + Math.pow(seg.start[1] - seg.center[1], 2))) * posScale;
1716
+ const startTheta = seg.start.length > 2 ? seg.start[2] : Math.atan2(seg.start[1] - seg.center[1], seg.start[0] - seg.center[0]);
1717
+ let endTheta = seg.end.length > 2 ? seg.end[2] : Math.atan2(seg.end[1] - seg.center[1], seg.end[0] - seg.center[0]);
1718
+ if (endTheta < startTheta) endTheta += Math.PI * 2;
1719
+ if (!started) {
1720
+ pushPoint(seg.start[0], seg.start[1]);
1721
+ started = true;
1722
+ }
1723
+ const arcLen = Math.abs(endTheta - startTheta) * radius;
1724
+ const arcSegments = Math.max(8, Math.ceil(arcLen / 0.5));
1725
+ for (let i = 1; i <= arcSegments; i++) {
1726
+ const t = i / arcSegments;
1727
+ const theta = startTheta + (endTheta - startTheta) * t;
1728
+ const x = cx + Math.cos(theta) * radius;
1729
+ const y = cy + Math.sin(theta) * radius;
1730
+ pts.push(x, y);
1731
+ }
1732
+ }
1733
+ }
1734
+ if (pts.length < 6) return;
1735
+ const triangles = earcut2(pts);
1736
+ if (!triangles || triangles.length === 0) return;
1737
+ const baseIndex = vertices.length / 3;
1738
+ for (let i = 0; i < pts.length; i += 2) {
1739
+ vertices.push(pts[i], pts[i + 1], 0);
1740
+ }
1741
+ for (let i = 0; i < triangles.length; i++) {
1742
+ indices.push(baseIndex + triangles[i]);
1743
+ }
1744
+ };
1745
+ const emitSimpleShape = (s) => {
1746
+ if (!s || !s.type) return;
1747
+ if (s.type === "circle") {
1748
+ emitCircle(s.cx || 0, s.cy || 0, s.r || 0);
1749
+ } else if (s.type === "rectangle") {
1750
+ emitRectangle(s.x || 0, s.y || 0, s.xSize || 0, s.ySize || 0, s.r || 0);
1751
+ } else if (s.type === "polygon") {
1752
+ emitPolygon(s.points || []);
1753
+ } else if (s.type === "outline") {
1754
+ emitOutline(s.segments || []);
1755
+ }
1756
+ };
1757
+ if (shape.type === "layeredShape" && Array.isArray(shape.shapes)) {
1758
+ for (const sub of shape.shapes) {
1759
+ const localPolarity = sub && sub.erase === true ? "clear" : polarity;
1760
+ ensurePolarity(localPolarity);
1761
+ emitSimpleShape(sub);
1762
+ }
1763
+ ensurePolarity(polarity);
1764
+ } else {
1765
+ emitSimpleShape(shape);
1766
+ }
1767
+ } else if (child.type === "imageRegion" && child.segments && child.segments.length > 0) {
1768
+ const regionPoints = [];
1769
+ const holeIndices = [];
1770
+ let lastEnd = null;
1771
+ child.segments.forEach((segment, segIdx) => {
1772
+ let startX, startY, endX, endY;
1773
+ let isArc = false;
1774
+ let arcParams = null;
1775
+ if (segment.type === "line") {
1776
+ startX = segment.start[0] * scale2;
1777
+ startY = segment.start[1] * scale2;
1778
+ endX = segment.end[0] * scale2;
1779
+ endY = segment.end[1] * scale2;
1780
+ } else if (segment.type === "arc") {
1781
+ isArc = true;
1782
+ const sx = segment.start[0];
1783
+ const sy = segment.start[1];
1784
+ const ex = segment.end[0];
1785
+ const ey = segment.end[1];
1786
+ startX = sx * scale2;
1787
+ startY = sy * scale2;
1788
+ endX = ex * scale2;
1789
+ endY = ey * scale2;
1790
+ const cx0 = segment.center[0];
1791
+ const cy0 = segment.center[1];
1792
+ const cx = cx0 * scale2;
1793
+ const cy = cy0 * scale2;
1794
+ const startAngle = segment.start.length > 2 && typeof segment.start[2] === "number" ? segment.start[2] : Math.atan2(sy - cy0, sx - cx0);
1795
+ const endAngle = segment.end.length > 2 && typeof segment.end[2] === "number" ? segment.end[2] : Math.atan2(ey - cy0, ex - cx0);
1796
+ const radius0 = typeof segment.radius === "number" && segment.radius > 0 ? segment.radius : Math.sqrt(Math.pow(sx - cx0, 2) + Math.pow(sy - cy0, 2));
1797
+ const radius = radius0 * scale2;
1798
+ let angleDiff = endAngle - startAngle;
1799
+ const isSamePoint = Math.abs(sx - ex) < 1e-9 && Math.abs(sy - ey) < 1e-9;
1800
+ if (isSamePoint && Math.abs(angleDiff) < 1e-6) {
1801
+ angleDiff = Math.PI * 2;
1802
+ }
1803
+ arcParams = { cx, cy, radius, startAngle, angleDiff };
1804
+ }
1805
+ let isNewContour = false;
1806
+ if (lastEnd) {
1807
+ const dist = Math.sqrt(Math.pow(startX - lastEnd.x, 2) + Math.pow(startY - lastEnd.y, 2));
1808
+ if (dist > 0.01) {
1809
+ isNewContour = true;
1810
+ holeIndices.push(regionPoints.length / 2);
1811
+ }
1812
+ } else {
1813
+ isNewContour = true;
1814
+ }
1815
+ if (isNewContour) {
1816
+ regionPoints.push(startX, startY);
1817
+ }
1818
+ if (isArc && arcParams) {
1819
+ const { cx, cy, radius, startAngle, angleDiff } = arcParams;
1820
+ const arcLength = Math.abs(angleDiff) * radius;
1821
+ const arcSegments = Math.max(8, Math.ceil(arcLength / 0.5));
1822
+ for (let i = 1; i <= arcSegments; i++) {
1823
+ const t = i / arcSegments;
1824
+ const angle = startAngle + angleDiff * t;
1825
+ regionPoints.push(
1826
+ cx + Math.cos(angle) * radius,
1827
+ cy + Math.sin(angle) * radius
1828
+ );
1829
+ }
1830
+ } else {
1831
+ regionPoints.push(endX, endY);
1832
+ }
1833
+ lastEnd = { x: endX, y: endY };
1834
+ });
1835
+ if (regionPoints.length >= 6) {
1836
+ try {
1837
+ const triangles = earcut2(regionPoints, holeIndices);
1838
+ if (triangles.length > 0) {
1839
+ const baseIndex = vertices.length / 3;
1840
+ for (let i = 0; i < regionPoints.length; i += 2) {
1841
+ vertices.push(regionPoints[i], regionPoints[i + 1], 0);
1842
+ }
1843
+ for (let i = 0; i < triangles.length; i++) {
1844
+ indices.push(baseIndex + triangles[i]);
1845
+ }
1846
+ if (isOutlineLayer && minWidth > 0) {
1847
+ const strokeRadius = minWidth / 2;
1848
+ for (let i = 0; i < regionPoints.length - 2; i += 2) {
1849
+ const x1 = regionPoints[i];
1850
+ const y1 = regionPoints[i + 1];
1851
+ const x2 = regionPoints[i + 2];
1852
+ const y2 = regionPoints[i + 3];
1853
+ const dx = x2 - x1;
1854
+ const dy = y2 - y1;
1855
+ const len = Math.sqrt(dx * dx + dy * dy);
1856
+ if (len > 1e-3) {
1857
+ const nx = -dy / len * strokeRadius;
1858
+ const ny = dx / len * strokeRadius;
1859
+ const v1 = vertices.length / 3;
1860
+ vertices.push(x1 + nx, y1 + ny, 0);
1861
+ vertices.push(x1 - nx, y1 - ny, 0);
1862
+ vertices.push(x2 + nx, y2 + ny, 0);
1863
+ vertices.push(x2 - nx, y2 - ny, 0);
1864
+ indices.push(v1, v1 + 1, v1 + 2);
1865
+ indices.push(v1 + 1, v1 + 3, v1 + 2);
1866
+ const segments = Math.max(8, Math.ceil(strokeRadius * 4));
1867
+ if (i === 0) {
1868
+ const centerIdx2 = vertices.length / 3;
1869
+ vertices.push(x1, y1, 0);
1870
+ for (let j = 0; j <= segments; j++) {
1871
+ const angle = j / segments * Math.PI * 2;
1872
+ vertices.push(x1 + Math.cos(angle) * strokeRadius, y1 + Math.sin(angle) * strokeRadius, 0);
1873
+ }
1874
+ for (let j = 0; j < segments; j++) {
1875
+ indices.push(centerIdx2, centerIdx2 + 1 + j, centerIdx2 + 1 + j + 1);
1876
+ }
1877
+ }
1878
+ const centerIdx = vertices.length / 3;
1879
+ vertices.push(x2, y2, 0);
1880
+ for (let j = 0; j <= segments; j++) {
1881
+ const angle = j / segments * Math.PI * 2;
1882
+ vertices.push(x2 + Math.cos(angle) * strokeRadius, y2 + Math.sin(angle) * strokeRadius, 0);
1883
+ }
1884
+ for (let j = 0; j < segments; j++) {
1885
+ indices.push(centerIdx, centerIdx + 1 + j, centerIdx + 1 + j + 1);
1886
+ }
1887
+ }
1888
+ }
1889
+ }
1890
+ }
1891
+ } catch (error) {
1892
+ console.error(`[web-gerber] Region ${idx} \u4E09\u89D2\u5316\u5931\u8D25:`, error);
1893
+ }
1894
+ }
1895
+ }
1896
+ });
1897
+ if (indices.length > currentRunStart) {
1898
+ polarityRuns.push({ polarity: currentPolarity, start: currentRunStart, end: indices.length });
1899
+ }
1900
+ const verticesAdded = vertices.length / 3 - verticesBefore;
1901
+ const trianglesAdded = indices.length / 3 - (verticesBefore > 0 ? indices.length / 3 - verticesBefore : 0);
1902
+ console.log(`[web-gerber] \u8865\u5145\u63D0\u53D6: +${verticesAdded} \u9876\u70B9, \u603B\u8BA1 ${vertices.length / 3} \u9876\u70B9, ${indices.length / 3} \u4E09\u89D2\u5F62`);
1903
+ }
1904
+ console.log(`[web-gerber] ${fileName2}: ${vertices.length / 3} \u9876\u70B9, ${indices.length / 3} \u4E09\u89D2\u5F62`);
1905
+ if (vertices.length > 0) {
1906
+ let minX = Infinity, maxX = -Infinity;
1907
+ let minY = Infinity, maxY = -Infinity;
1908
+ for (let i = 0; i < vertices.length; i += 3) {
1909
+ minX = Math.min(minX, vertices[i]);
1910
+ maxX = Math.max(maxX, vertices[i]);
1911
+ minY = Math.min(minY, vertices[i + 1]);
1912
+ maxY = Math.max(maxY, vertices[i + 1]);
1913
+ }
1914
+ console.log(`[web-gerber] ${fileName2} \u5750\u6807\u8303\u56F4: X=[${minX.toFixed(2)}, ${maxX.toFixed(2)}] mm, Y=[${minY.toFixed(2)}, ${maxY.toFixed(2)}] mm`);
1915
+ }
1916
+ return {
1917
+ vertices: new Float32Array(vertices),
1918
+ triangleIndices: new Uint32Array(indices),
1919
+ polarityRuns: polarityRuns.filter((r) => r.end - r.start > 0),
1920
+ // 仅 drill 文件会有内容
1921
+ drillPosScaleRuns: drillPosScaleRuns && drillPosScaleRuns.length > 0 ? drillPosScaleRuns : null
1922
+ };
1923
+ } catch (error) {
1924
+ console.error(`[web-gerber] \u89E3\u6790\u5931\u8D25:`, error);
1925
+ console.error(`[web-gerber] \u9519\u8BEF\u5806\u6808:`, error.stack);
1926
+ throw error;
1927
+ }
1928
+ }
1929
+ function getLayerDisplayName(fileNameOrLayer) {
1930
+ if (typeof fileNameOrLayer === "object" && fileNameOrLayer !== null) {
1931
+ if (fileNameOrLayer.phoLayerType) {
1932
+ return fileNameOrLayer.phoLayerType;
1933
+ }
1934
+ fileNameOrLayer = fileNameOrLayer.name;
1935
+ }
1936
+ const fileName2 = fileNameOrLayer;
1937
+ const fileNameOnly = fileName2.split(/[/\\]/).pop();
1938
+ const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
1939
+ const fileNameUpper = fileNameOnly.toUpperCase();
1940
+ if (fileExtension === ".GBR") {
1941
+ const nameUpper = fileNameOnly.toUpperCase();
1942
+ const nameLower = fileNameOnly.toLowerCase();
1943
+ const baseNameUpper = fileNameOnly.replace(/\.[^.]+$/, "").toUpperCase();
1944
+ const directLayerNames = /* @__PURE__ */ new Set([
1945
+ "GTO",
1946
+ "GTP",
1947
+ "GTS",
1948
+ "GTL",
1949
+ "GBL",
1950
+ "GBS",
1951
+ "GBP",
1952
+ "GBO",
1953
+ "GKO"
1954
+ ]);
1955
+ if (directLayerNames.has(baseNameUpper) || /^G\d+$/i.test(baseNameUpper) || /^SIG\d+$/i.test(baseNameUpper)) {
1956
+ return baseNameUpper;
1957
+ }
1958
+ if (nameLower.includes("legend_top") || nameLower.includes("legend") && nameLower.includes("top") && !nameLower.includes("signal")) {
1959
+ return "GTO";
1960
+ }
1961
+ if (nameLower.includes("paste_top")) {
1962
+ return "GTP";
1963
+ }
1964
+ if (nameLower.includes("paste_bot")) {
1965
+ return "GBP";
1966
+ }
1967
+ if (nameLower.includes("soldermask_top")) {
1968
+ return "GTS";
1969
+ }
1970
+ if (nameLower.includes("soldermask_bot")) {
1971
+ return "GBS";
1972
+ }
1973
+ const signalMatch = nameLower.match(/(?:copper_)?signal[_\s](\d+)/i);
1974
+ if (signalMatch) {
1975
+ const number = parseInt(signalMatch[1], 10);
1976
+ const displayNumber = number + 1;
1977
+ return "SIG" + displayNumber;
1978
+ }
1979
+ if (nameLower.includes("signal_top") || nameLower.includes("copper_signal_top")) {
1980
+ return "GTL";
1981
+ }
1982
+ if (nameLower.includes("signal_bot") || nameLower.includes("copper_signal_bot")) {
1983
+ return "GBL";
1984
+ }
1985
+ if (nameUpper.includes("PROFILE")) {
1986
+ return "GKO";
1987
+ }
1988
+ if (/EDGE[._-]CUTS/i.test(nameUpper)) return "GKO";
1989
+ if (/F[._-]CU\b/i.test(nameUpper)) return "GTL";
1990
+ if (/B[._-]CU\b/i.test(nameUpper)) return "GBL";
1991
+ if (/F[._-]MASK/i.test(nameUpper)) return "GTS";
1992
+ if (/B[._-]MASK/i.test(nameUpper)) return "GBS";
1993
+ if (/F[._-]SILKS?/i.test(nameUpper)) return "GTO";
1994
+ if (/B[._-]SILKS?/i.test(nameUpper)) return "GBO";
1995
+ if (/F[._-]PASTE/i.test(nameUpper)) return "GTP";
1996
+ if (/B[._-]PASTE/i.test(nameUpper)) return "GBP";
1997
+ if (/F[._-]FAB/i.test(nameUpper)) return "GTO";
1998
+ if (/B[._-]FAB/i.test(nameUpper)) return "GBO";
1999
+ if (/IN(\d+)[._-]CU/i.test(nameUpper)) {
2000
+ const m = nameUpper.match(/IN(\d+)[._-]CU/);
2001
+ return "SIG" + (parseInt(m[1], 10) + 1);
2002
+ }
2003
+ if (nameUpper.startsWith("ZH-TOP") || nameUpper.includes("ZH-TOP")) return "GTS";
2004
+ if (nameUpper.startsWith("ZH-BOT") || nameUpper.includes("ZH-BOT")) return "GBS";
2005
+ if (nameUpper.startsWith("XL-TOP") || nameUpper.includes("XL-TOP")) return "GTL";
2006
+ if (nameUpper.startsWith("XL-BOT") || nameUpper.includes("XL-BOT")) return "GBL";
2007
+ const keywords = ["_Drawing", "_Drillmap", "_NPTH", "_Pads", "_PTH"];
2008
+ for (const keyword of keywords) {
2009
+ if (fileNameOnly.includes(keyword)) {
2010
+ const keywordIndex = fileNameOnly.indexOf(keyword);
2011
+ return fileNameOnly.substring(keywordIndex);
2012
+ }
2013
+ }
2014
+ return fileNameOnly;
2015
+ }
2016
+ if (fileNameUpper.endsWith("_SST") || fileNameUpper.includes("_SST.")) return "GTO";
2017
+ if (fileNameUpper.endsWith("_PMT") || fileNameUpper.includes("_PMT.")) return "GTP";
2018
+ if (fileNameUpper.endsWith("_SMT") || fileNameUpper.includes("_SMT.")) return "GTS";
2019
+ if (fileNameUpper.endsWith("_TOP") || fileNameUpper.includes("_TOP.")) return "GTL";
2020
+ if (fileNameUpper.endsWith("_SMB") || fileNameUpper.includes("_SMB.")) return "GBS";
2021
+ if (fileNameUpper.endsWith("_PMB") || fileNameUpper.includes("_PMB.")) return "GBP";
2022
+ if (fileNameUpper.endsWith("_SSB") || fileNameUpper.includes("_SSB.")) return "GBO";
2023
+ if (fileNameUpper.includes("OUTLINE")) return "GKO";
2024
+ if (/.*_INT\d+$/i.test(fileNameUpper) || /.*_INT\d+\./i.test(fileNameUpper)) return fileNameOnly;
2025
+ if (fileExtension === ".D" || fileExtension === ".DRL" || fileExtension === ".DRI") {
2026
+ if (fileNameUpper === "NP.DRL" || fileNameUpper === "NP.D") return "DRL2";
2027
+ if (fileNameUpper === "P.DRL" || fileNameUpper === "P.D") return "DRL";
2028
+ return "DRL";
2029
+ }
2030
+ if (fileExtension === ".ROU") return "rout";
2031
+ if (fileExtension === ".BOT") return "GBL";
2032
+ if (fileExtension === ".TOP") return "GTL";
2033
+ if (fileExtension === ".SOB") return "GBS";
2034
+ if (fileExtension === ".SOT") return "GTS";
2035
+ if (fileExtension === ".SST") return "GTO";
2036
+ if (fileExtension === ".SSB") return "GBO";
2037
+ if (fileExtension === ".SMT") return "GTS";
2038
+ if (fileExtension === ".SMB") return "GBS";
2039
+ if (fileExtension === ".PMT") return "GTP";
2040
+ if (fileExtension === ".PMB") return "GBP";
2041
+ if (fileExtension === ".SER") return fileNameOnly;
2042
+ if (fileExtension === ".ART") {
2043
+ const dot = fileNameOnly.lastIndexOf(".");
2044
+ const stemUpper = (dot > 0 ? fileNameOnly.substring(0, dot) : fileNameOnly).toUpperCase();
2045
+ if (stemUpper === "SST") return "GTO";
2046
+ if (stemUpper === "SMT") return "GTS";
2047
+ if (stemUpper === "TOP") return "GTL";
2048
+ if (stemUpper === "BOT") return "GBL";
2049
+ if (stemUpper === "SMB") return "GBS";
2050
+ if (stemUpper === "SSB") return "GBO";
2051
+ if (stemUpper === "OUT") return "GKO";
2052
+ return fileNameOnly;
2053
+ }
2054
+ if (fileExtension === ".PHO") {
2055
+ const nameUpper = fileNameOnly.toUpperCase();
2056
+ if (nameUpper.startsWith("SST")) {
2057
+ return "GTO";
2058
+ }
2059
+ if (nameUpper.startsWith("SMD")) {
2060
+ const numberMatch = nameUpper.match(/^SMD(\d+)/);
2061
+ if (numberMatch) {
2062
+ const number = parseInt(numberMatch[1], 10);
2063
+ return number < 20 ? "GTP" : "GBP";
2064
+ } else {
2065
+ return "GTP";
2066
+ }
2067
+ }
2068
+ if (nameUpper.startsWith("SM")) {
2069
+ const numberMatch = nameUpper.match(/^SM(\d+)/);
2070
+ if (numberMatch) {
2071
+ const number = parseInt(numberMatch[1], 10);
2072
+ return number < 20 ? "GTS" : "GBS";
2073
+ } else {
2074
+ return "GTS";
2075
+ }
2076
+ }
2077
+ if (nameUpper.startsWith("ART")) {
2078
+ const numberMatch = nameUpper.match(/^ART(\d+)/);
2079
+ if (numberMatch) {
2080
+ const number = parseInt(numberMatch[1], 10);
2081
+ return number < 20 ? "GTL" : "GBL";
2082
+ } else {
2083
+ return "GTL";
2084
+ }
2085
+ }
2086
+ if (nameUpper.startsWith("SSB")) {
2087
+ return "GBO";
2088
+ }
2089
+ if (nameUpper.startsWith("DRL")) {
2090
+ return "DRL";
2091
+ }
2092
+ if (nameUpper.startsWith("DD")) {
2093
+ return fileNameOnly;
2094
+ }
2095
+ return fileNameOnly;
2096
+ }
2097
+ if (/^\.GD\d+$/i.test(fileExtension) || /^\.GG\d+$/i.test(fileExtension)) {
2098
+ return fileNameOnly;
2099
+ }
2100
+ if (fileExtension === ".GM" || /^\.GM\d+$/i.test(fileExtension)) {
2101
+ return fileNameOnly;
2102
+ }
2103
+ if (/^\.INT\d+$/i.test(fileExtension)) {
2104
+ return fileNameOnly;
2105
+ }
2106
+ const gNumberMatch = fileExtension.match(/^\.G(\d+)$/i);
2107
+ if (gNumberMatch) {
2108
+ const inCuMatch = fileNameUpper.match(/IN(\d+)[._-]CU/i);
2109
+ if (inCuMatch) return "SIG" + (parseInt(inCuMatch[1], 10) + 1);
2110
+ const number = parseInt(gNumberMatch[1], 10);
2111
+ return "SIG" + (number + 1);
2112
+ }
2113
+ const gpNumberMatch = fileExtension.match(/^\.GP(\d+)$/i);
2114
+ if (gpNumberMatch) {
2115
+ const number = parseInt(gpNumberMatch[1], 10);
2116
+ return "GP" + number;
2117
+ }
2118
+ const isTxFile = /^\.TX\d*$/i.test(fileExtension) || fileExtension === ".TXT";
2119
+ if (isTxFile) {
2120
+ if (/slotholes/i.test(fileName2)) {
2121
+ return "SLOT";
2122
+ }
2123
+ if (/[_\s]via/i.test(fileName2)) {
2124
+ return "VIA";
2125
+ }
2126
+ return "DRL";
2127
+ }
2128
+ return fileExtension.substring(1);
2129
+ }
2130
+ function getLayerOrder(fileName2) {
2131
+ const fileNameOnly = fileName2.split(/[/\\]/).pop();
2132
+ const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
2133
+ const fileNameUpper = fileNameOnly.toUpperCase();
2134
+ const ext = fileExtension.substring(1);
2135
+ const layerTypeOrder = {
2136
+ "GTO": 0,
2137
+ "GTP": 1,
2138
+ "GTS": 2,
2139
+ "GTL": 3,
2140
+ "G1": 4,
2141
+ "G2": 5,
2142
+ "GBL": 6,
2143
+ "GBS": 7,
2144
+ "GBP": 7.5,
2145
+ "GBO": 8,
2146
+ "GKO": 9,
2147
+ "SLOT": 9.5,
2148
+ "DRL_TXT": 9.6,
2149
+ "VIA": 11,
2150
+ "rout": 11.5,
2151
+ "GDD": 12,
2152
+ "GDL": 13,
2153
+ "GM": 14,
2154
+ "GPB": 14.5,
2155
+ "GPT": 15,
2156
+ "DRL": 200
2157
+ };
2158
+ if (ext === "GBR") {
2159
+ const nameUpper = fileNameOnly.toUpperCase();
2160
+ const nameLower = fileNameOnly.toLowerCase();
2161
+ const baseNameUpper = fileNameOnly.replace(/\.[^.]+$/, "").toUpperCase();
2162
+ if (layerTypeOrder[baseNameUpper] !== void 0 || /^G\d+$/i.test(baseNameUpper) || /^SIG\d+$/i.test(baseNameUpper)) {
2163
+ const order = layerTypeOrder[baseNameUpper] !== void 0 ? layerTypeOrder[baseNameUpper] : 10;
2164
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A GBR->${baseNameUpper} (basename), order=${order}`);
2165
+ return order;
2166
+ }
2167
+ if (nameLower.includes("legend_top") || nameLower.includes("legend") && nameLower.includes("top") && !nameLower.includes("signal")) {
2168
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A GBR->GTO (legend_Top), order=0`);
2169
+ return 0;
2170
+ }
2171
+ if (nameLower.includes("paste_top")) {
2172
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A GBR->GTP (Paste_Top), order=1`);
2173
+ return 1;
2174
+ }
2175
+ if (nameLower.includes("paste_bot")) {
2176
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A GBR->GBP (Paste_Bot), order=7.5`);
2177
+ return 7.5;
2178
+ }
2179
+ if (nameLower.includes("soldermask_top")) {
2180
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A GBR->GTS (soldermask_Top), order=2`);
2181
+ return 2;
2182
+ }
2183
+ if (nameLower.includes("soldermask_bot")) {
2184
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A GBR->GBS (soldermask_Bot), order=7`);
2185
+ return 7;
2186
+ }
2187
+ const signalMatch = nameLower.match(/(?:copper_)?signal[_\s](\d+)/i);
2188
+ if (signalMatch) {
2189
+ const number = parseInt(signalMatch[1], 10);
2190
+ const order = Math.min(3 + number * 0.1, 5.9);
2191
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A GBR->SIG${number + 1} (signal_${number}), order=${order}`);
2192
+ return order;
2193
+ }
2194
+ if (nameLower.includes("signal_top") || nameLower.includes("copper_signal_top")) {
2195
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A GBR->GTL (signal_Top), order=3`);
2196
+ return 3;
2197
+ }
2198
+ if (nameLower.includes("signal_bot") || nameLower.includes("copper_signal_bot")) {
2199
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A GBR->GBL (signal_Bot), order=6`);
2200
+ return 6;
2201
+ }
2202
+ if (nameUpper.includes("PROFILE")) {
2203
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A GBR->GKO (Profile), order=9`);
2204
+ return 9;
2205
+ }
2206
+ if (/EDGE[._-]CUTS/i.test(nameUpper)) return 9;
2207
+ if (/F[._-]CU\b/i.test(nameUpper)) return 3;
2208
+ if (/B[._-]CU\b/i.test(nameUpper)) return 6;
2209
+ if (/F[._-]MASK/i.test(nameUpper)) return 2;
2210
+ if (/B[._-]MASK/i.test(nameUpper)) return 7;
2211
+ if (/F[._-]SILKS?/i.test(nameUpper)) return 0;
2212
+ if (/B[._-]SILKS?/i.test(nameUpper)) return 8;
2213
+ if (/F[._-]PASTE/i.test(nameUpper)) return 1;
2214
+ if (/B[._-]PASTE/i.test(nameUpper)) return 7.5;
2215
+ if (/F[._-]FAB/i.test(nameUpper)) return 0;
2216
+ if (/B[._-]FAB/i.test(nameUpper)) return 8;
2217
+ if (/IN(\d+)[._-]CU/i.test(nameUpper)) {
2218
+ const m = nameUpper.match(/IN(\d+)[._-]CU/);
2219
+ return Math.min(3 + parseInt(m[1], 10) * 0.1, 5.9);
2220
+ }
2221
+ if (nameUpper.startsWith("ZH-TOP") || nameUpper.includes("ZH-TOP")) return 2;
2222
+ if (nameUpper.startsWith("ZH-BOT") || nameUpper.includes("ZH-BOT")) return 7;
2223
+ if (nameUpper.startsWith("XL-TOP") || nameUpper.includes("XL-TOP")) return 3;
2224
+ if (nameUpper.startsWith("XL-BOT") || nameUpper.includes("XL-BOT")) return 6;
2225
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A GBR (\u672A\u8BC6\u522B), order=10`);
2226
+ return 10;
2227
+ }
2228
+ if (fileNameUpper.endsWith("_SST") || fileNameUpper.includes("_SST.")) return 0;
2229
+ if (fileNameUpper.endsWith("_PMT") || fileNameUpper.includes("_PMT.")) return 1;
2230
+ if (fileNameUpper.endsWith("_SMT") || fileNameUpper.includes("_SMT.")) return 2;
2231
+ if (fileNameUpper.endsWith("_TOP") || fileNameUpper.includes("_TOP.")) return 3;
2232
+ if (fileNameUpper.endsWith("_SMB") || fileNameUpper.includes("_SMB.")) return 7;
2233
+ if (fileNameUpper.endsWith("_PMB") || fileNameUpper.includes("_PMB.")) return 7.5;
2234
+ if (fileNameUpper.endsWith("_SSB") || fileNameUpper.includes("_SSB.")) return 8;
2235
+ if (fileNameUpper.includes("OUTLINE")) return 9;
2236
+ const intMatch = fileNameUpper.match(/_INT(\d+)$/i) || fileNameUpper.match(/_INT(\d+)\./i);
2237
+ if (intMatch) {
2238
+ const number = parseInt(intMatch[1], 10);
2239
+ return 100 + number;
2240
+ }
2241
+ if ((ext === "DRL" || ext === "D") && fileNameOnly.toUpperCase() === "NP.DRL") {
2242
+ return 9.65;
2243
+ }
2244
+ if ((ext === "DRL" || ext === "D") && fileNameOnly.toUpperCase() === "P.DRL") {
2245
+ return 9.6;
2246
+ }
2247
+ if (layerTypeOrder[ext] !== void 0) {
2248
+ return layerTypeOrder[ext];
2249
+ }
2250
+ const inCuForOrder = fileNameUpper.match(/IN(\d+)[._-]CU/i);
2251
+ if (inCuForOrder && /^G\d+$/i.test(ext)) {
2252
+ return Math.min(3 + parseInt(inCuForOrder[1], 10) * 0.1, 5.9);
2253
+ }
2254
+ const gNumberMatch = ext.match(/^G(\d+)$/i);
2255
+ if (gNumberMatch) {
2256
+ const number = parseInt(gNumberMatch[1], 10);
2257
+ return Math.min(3 + number * 0.1, 5.9);
2258
+ }
2259
+ const gpNumberMatch = ext.match(/^GP(\d+)$/i);
2260
+ if (gpNumberMatch) {
2261
+ const number = parseInt(gpNumberMatch[1], 10);
2262
+ return Math.min(5.9 + number * 0.01, 5.99);
2263
+ }
2264
+ const intNumberMatch = ext.match(/^INT(\d+)$/i);
2265
+ if (intNumberMatch) {
2266
+ const number = parseInt(intNumberMatch[1], 10);
2267
+ return 100 + number;
2268
+ }
2269
+ const gdNumberMatch = ext.match(/^GD(\d+)$/i);
2270
+ if (gdNumberMatch) {
2271
+ const number = parseInt(gdNumberMatch[1], 10);
2272
+ return Math.min(14 + number * 0.1, 14.4);
2273
+ }
2274
+ const ggNumberMatch = ext.match(/^GG(\d+)$/i);
2275
+ if (ggNumberMatch) {
2276
+ const number = parseInt(ggNumberMatch[1], 10);
2277
+ return Math.min(14 + number * 0.1, 14.4);
2278
+ }
2279
+ const gmNumberMatch = ext.match(/^GM(\d+)$/i);
2280
+ if (gmNumberMatch) {
2281
+ const number = parseInt(gmNumberMatch[1], 10);
2282
+ if (number === 1) {
2283
+ return 14.2;
2284
+ } else {
2285
+ return Math.min(14 + number * 0.1, 14.4);
2286
+ }
2287
+ }
2288
+ const isTxFile = /^TX\d*$/i.test(ext) || ext === "TXT";
2289
+ if (isTxFile && /slotholes/i.test(fileName2)) {
2290
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A SLOT, order=9.5`);
2291
+ return 9.5;
2292
+ }
2293
+ if (/[_\s]via/i.test(fileName2) && isTxFile) {
2294
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A VIA, order=11`);
2295
+ return 11;
2296
+ }
2297
+ if (isTxFile) {
2298
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A DRL (TXT), order=9.6`);
2299
+ return 9.6;
2300
+ }
2301
+ if (ext === "ART") {
2302
+ const dot = fileNameOnly.lastIndexOf(".");
2303
+ const stemUpper = (dot > 0 ? fileNameOnly.substring(0, dot) : fileNameOnly).toUpperCase();
2304
+ if (stemUpper === "SST") return 0;
2305
+ if (stemUpper === "SMT") return 2;
2306
+ if (stemUpper === "TOP") return 3;
2307
+ if (stemUpper === "BOT") return 6;
2308
+ if (stemUpper === "SMB") return 7;
2309
+ if (stemUpper === "SSB") return 8;
2310
+ if (stemUpper === "OUT") return 9;
2311
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A ART(unknown), order=100`);
2312
+ return 100;
2313
+ }
2314
+ if (ext === "PHO") {
2315
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A PHO (\u4E34\u65F6order=100\uFF0C\u540E\u5904\u7406\u4F1A\u66F4\u65B0)`);
2316
+ return 100;
2317
+ }
2318
+ if (ext === "DRL" || ext === "D" || ext === "DRI") {
2319
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A DRL, order=200`);
2320
+ return 200;
2321
+ }
2322
+ if (ext === "ROU") {
2323
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A ROU, order=11.5`);
2324
+ return 11.5;
2325
+ }
2326
+ if (ext === "BOT") return 6;
2327
+ if (ext === "TOP") return 3;
2328
+ if (ext === "SOB") return 7;
2329
+ if (ext === "SOT") return 2;
2330
+ if (ext === "SST") return 0;
2331
+ if (ext === "SSB") return 8;
2332
+ if (ext === "SMT") return 2;
2333
+ if (ext === "SMB") return 7;
2334
+ if (ext === "PMT") return 1;
2335
+ if (ext === "PMB") return 7.5;
2336
+ if (ext === "SER") {
2337
+ console.log(`\u6587\u4EF6 ${fileName2} \u8BC6\u522B\u4E3A SER (\u663E\u793A\u6587\u4EF6\u540D), order=1000`);
2338
+ return 1e3;
2339
+ }
2340
+ return 100;
2341
+ }
2342
+ function animate() {
2343
+ render();
2344
+ requestAnimationFrame(animate);
2345
+ }
2346
+ function render() {
2347
+ gl.clearColor(0, 0, 0, 1);
2348
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
2349
+ if (layers.length === 0) return;
2350
+ gl.useProgram(program);
2351
+ gl.enableVertexAttribArray(positionLocation);
2352
+ gl.enable(gl.STENCIL_TEST);
2353
+ const aspect = canvas.width / canvas.height;
2354
+ const matrix = [
2355
+ scale / aspect,
2356
+ 0,
2357
+ 0,
2358
+ 0,
2359
+ 0,
2360
+ scale,
2361
+ 0,
2362
+ 0,
2363
+ 0,
2364
+ 0,
2365
+ 1,
2366
+ 0,
2367
+ transX,
2368
+ transY,
2369
+ 0,
2370
+ 1
2371
+ ];
2372
+ gl.uniformMatrix4fv(uMatrixLocation, false, matrix);
2373
+ const layerHasClearPolarity = (layer) => {
2374
+ return Array.isArray(layer.polarityRuns) && layer.polarityRuns.some((r) => r && r.polarity === "clear" && r.end - r.start > 0);
2375
+ };
2376
+ const drawLayer = (layer, blendFunc, alpha) => {
2377
+ if (layerHasClearPolarity(layer) && quadVertexBuffer && quadIndexBuffer) {
2378
+ gl.clear(gl.STENCIL_BUFFER_BIT);
2379
+ gl.colorMask(false, false, false, false);
2380
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
2381
+ gl.uniformMatrix4fv(uMatrixLocation, false, matrix);
2382
+ gl.bindBuffer(gl.ARRAY_BUFFER, layer.vertexBuffer);
2383
+ gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
2384
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, layer.indexBuffer);
2385
+ for (const run of layer.polarityRuns) {
2386
+ if (!run) continue;
2387
+ const count = run.end - run.start;
2388
+ if (count <= 0) continue;
2389
+ const ref = run.polarity === "clear" ? 0 : 1;
2390
+ gl.stencilFunc(gl.ALWAYS, ref, 255);
2391
+ gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_INT, run.start * 4);
2392
+ }
2393
+ gl.colorMask(true, true, true, true);
2394
+ gl.stencilFunc(gl.EQUAL, 1, 255);
2395
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
2396
+ gl.blendEquation(gl.FUNC_ADD);
2397
+ gl.blendFunc(...blendFunc);
2398
+ gl.uniform3fv(uColorLocation, layer.color);
2399
+ gl.uniform1f(uAlphaLocation, alpha);
2400
+ gl.uniformMatrix4fv(uMatrixLocation, false, IDENTITY_MATRIX);
2401
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadVertexBuffer);
2402
+ gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
2403
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, quadIndexBuffer);
2404
+ gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
2405
+ gl.uniformMatrix4fv(uMatrixLocation, false, matrix);
2406
+ return;
2407
+ }
2408
+ gl.clear(gl.STENCIL_BUFFER_BIT);
2409
+ gl.stencilFunc(gl.NOTEQUAL, 1, 255);
2410
+ gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
2411
+ gl.blendEquation(gl.FUNC_ADD);
2412
+ gl.blendFunc(...blendFunc);
2413
+ gl.uniform3fv(uColorLocation, layer.color);
2414
+ gl.uniform1f(uAlphaLocation, alpha);
2415
+ gl.bindBuffer(gl.ARRAY_BUFFER, layer.vertexBuffer);
2416
+ gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
2417
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, layer.indexBuffer);
2418
+ gl.drawElements(gl.TRIANGLES, layer.vertexCount, gl.UNSIGNED_INT, 0);
2419
+ };
2420
+ const diffBlendNames = ["GTS", "GBS", "GTP", "GBP", "GPT", "GPB"];
2421
+ const diffIndices = [];
2422
+ const drillIndices = [];
2423
+ const gdgggmIndices = [];
2424
+ for (let i = layers.length - 1; i >= 0; i--) {
2425
+ const layer = layers[i];
2426
+ if (!layer.visible || !layer.vertexBuffer || !layer.indexBuffer) continue;
2427
+ const displayName = getLayerDisplayName(layer);
2428
+ const order = layer.order !== void 0 ? layer.order : 100;
2429
+ const nameUpper = layer.name.toUpperCase();
2430
+ const isGdNum = /\.GD\d+$/.test(nameUpper);
2431
+ const isGgNum = /\.GG\d+$/.test(nameUpper);
2432
+ const gmNumMatch = nameUpper.match(/\.GM(\d+)$/);
2433
+ const isGm1 = gmNumMatch && gmNumMatch[1] === "1";
2434
+ const isGdGgGmNum = isGdNum || isGgNum || isGm1;
2435
+ if (isGdGgGmNum) {
2436
+ gdgggmIndices.push(i);
2437
+ continue;
2438
+ }
2439
+ if (layer.isDrillFile || layer.isSlotFile) {
2440
+ drillIndices.push(i);
2441
+ continue;
2442
+ }
2443
+ const isSigInner = /^SIG\d+$/i.test(displayName);
2444
+ const isDiffBlend = diffBlendNames.includes(displayName) || isSigInner;
2445
+ if (isDiffBlend) {
2446
+ diffIndices.push(i);
2447
+ continue;
2448
+ }
2449
+ const additiveNames = ["GTS", "GBS", "GTL", "GBL", "GPT", "GBP", "GTP", "GPB"];
2450
+ const isAdditiveLayer = additiveNames.includes(displayName) || order >= 2 && order <= 15;
2451
+ const isSpecialLayer = Math.abs(order - 0) < 0.1 || Math.abs(order - 1) < 0.1;
2452
+ const alpha = 1;
2453
+ let blendFunc = null;
2454
+ if (isSpecialLayer) {
2455
+ blendFunc = [gl.ONE_MINUS_DST_COLOR, gl.ONE_MINUS_SRC_COLOR];
2456
+ } else if (isAdditiveLayer) {
2457
+ blendFunc = [gl.ONE, gl.ONE];
2458
+ } else {
2459
+ blendFunc = [gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA];
2460
+ }
2461
+ drawLayer(layer, blendFunc, alpha);
2462
+ }
2463
+ for (const i of diffIndices.reverse()) {
2464
+ const layer = layers[i];
2465
+ if (!layer.visible || !layer.vertexBuffer || !layer.indexBuffer) continue;
2466
+ const alpha = 1;
2467
+ drawLayer(layer, [gl.ONE_MINUS_DST_COLOR, gl.ONE_MINUS_SRC_COLOR], alpha);
2468
+ }
2469
+ for (const i of drillIndices.reverse()) {
2470
+ const layer = layers[i];
2471
+ if (!layer.visible || !layer.vertexBuffer || !layer.indexBuffer) continue;
2472
+ const alpha = 1;
2473
+ drawLayer(layer, [gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA], alpha);
2474
+ }
2475
+ for (const i of gdgggmIndices.reverse()) {
2476
+ const layer = layers[i];
2477
+ if (!layer.visible || !layer.vertexBuffer || !layer.indexBuffer) continue;
2478
+ const alpha = 1;
2479
+ drawLayer(layer, [gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA], alpha);
2480
+ }
2481
+ gl.disable(gl.STENCIL_TEST);
2482
+ }
2483
+ function sortLayersByOrder(layers2) {
2484
+ return layers2.map((layer, originalIndex) => ({
2485
+ layer,
2486
+ originalIndex,
2487
+ order: layer.order !== void 0 ? layer.order : 100,
2488
+ displayName: getLayerDisplayName(layer)
2489
+ })).sort((a, b) => {
2490
+ const sigMatchA = a.displayName.match(/^SIG(\d+)$/i);
2491
+ const sigMatchB = b.displayName.match(/^SIG(\d+)$/i);
2492
+ if (sigMatchA && sigMatchB) {
2493
+ const numA = parseInt(sigMatchA[1], 10);
2494
+ const numB = parseInt(sigMatchB[1], 10);
2495
+ return numA - numB;
2496
+ }
2497
+ if (sigMatchA || sigMatchB) {
2498
+ return a.order - b.order;
2499
+ }
2500
+ const orderDiff = a.order - b.order;
2501
+ if (orderDiff !== 0) return orderDiff;
2502
+ return a.originalIndex - b.originalIndex;
2503
+ });
2504
+ }
2505
+ function reassignColorsByOrder(layers2) {
2506
+ const sortedLayers = sortLayersByOrder(layers2);
2507
+ sortedLayers.forEach(({ layer, originalIndex }, colorIndex) => {
2508
+ const hexColor = pickColor(colorIndex);
2509
+ const rgb = hexToRgb(hexColor);
2510
+ layer.hexColor = hexColor;
2511
+ layer.color = [rgb.r, rgb.g, rgb.b];
2512
+ layerColorMap.set(originalIndex, hexColor);
2513
+ console.log(`[\u989C\u8272\u91CD\u65B0\u5206\u914D] ${layer.name}: order=${layer.order}, originalIndex=${originalIndex}, colorIndex=${colorIndex}, color=${hexColor}`);
2514
+ });
2515
+ }
2516
+ function updatePhoLayerTypes(layers2) {
2517
+ const phoFiles = layers2.filter((layer) => {
2518
+ const fileNameOnly = (layer.name || "").split(/[/\\]/).pop();
2519
+ const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
2520
+ return fileExtension === ".PHO";
2521
+ });
2522
+ if (phoFiles.length === 0) return;
2523
+ const prefixGroups = {};
2524
+ for (const layer of phoFiles) {
2525
+ const fileNameOnly = (layer.name || "").split(/[/\\]/).pop();
2526
+ const nameUpper = fileNameOnly.toUpperCase();
2527
+ let prefix = null;
2528
+ let number = null;
2529
+ if (nameUpper.startsWith("SST")) {
2530
+ prefix = "sst";
2531
+ const match = nameUpper.match(/^SST(\d+)/);
2532
+ number = match ? parseInt(match[1], 10) : 0;
2533
+ } else if (nameUpper.startsWith("SMD")) {
2534
+ prefix = "smd";
2535
+ const match = nameUpper.match(/^SMD(\d+)/);
2536
+ number = match ? parseInt(match[1], 10) : 0;
2537
+ } else if (nameUpper.startsWith("SM")) {
2538
+ prefix = "sm";
2539
+ const match = nameUpper.match(/^SM(\d+)/);
2540
+ number = match ? parseInt(match[1], 10) : 0;
2541
+ } else if (nameUpper.startsWith("ART")) {
2542
+ prefix = "art";
2543
+ const match = nameUpper.match(/^ART(\d+)/);
2544
+ number = match ? parseInt(match[1], 10) : 0;
2545
+ } else if (nameUpper.startsWith("SSB")) {
2546
+ prefix = "ssb";
2547
+ number = 0;
2548
+ } else if (nameUpper.startsWith("DRL")) {
2549
+ prefix = "drl";
2550
+ number = 0;
2551
+ } else if (nameUpper.startsWith("DD")) {
2552
+ prefix = "dd";
2553
+ number = 0;
2554
+ }
2555
+ if (prefix) {
2556
+ if (!prefixGroups[prefix]) {
2557
+ prefixGroups[prefix] = [];
2558
+ }
2559
+ prefixGroups[prefix].push({ layer, number, fileNameOnly });
2560
+ }
2561
+ }
2562
+ for (const [prefix, files] of Object.entries(prefixGroups)) {
2563
+ files.sort((a, b) => a.number - b.number);
2564
+ for (let i = 0; i < files.length; i++) {
2565
+ const { layer, number } = files[i];
2566
+ const isSmallest = i === 0;
2567
+ let layerType = null;
2568
+ let order = null;
2569
+ switch (prefix) {
2570
+ case "sst":
2571
+ layerType = "GTO";
2572
+ order = 0;
2573
+ break;
2574
+ case "smd":
2575
+ if (isSmallest) {
2576
+ layerType = "GTP";
2577
+ order = 1;
2578
+ } else {
2579
+ layerType = "GBP";
2580
+ order = 7.5;
2581
+ }
2582
+ break;
2583
+ case "sm":
2584
+ if (isSmallest) {
2585
+ layerType = "GTS";
2586
+ order = 2;
2587
+ } else {
2588
+ layerType = "GBS";
2589
+ order = 7;
2590
+ }
2591
+ break;
2592
+ case "art":
2593
+ if (isSmallest) {
2594
+ layerType = "GTL";
2595
+ order = 3;
2596
+ } else {
2597
+ layerType = "GBL";
2598
+ order = 6;
2599
+ }
2600
+ break;
2601
+ case "ssb":
2602
+ layerType = "GBO";
2603
+ order = 8;
2604
+ break;
2605
+ case "drl":
2606
+ layerType = "DRL";
2607
+ order = 200;
2608
+ break;
2609
+ case "dd":
2610
+ layerType = null;
2611
+ order = 201;
2612
+ break;
2613
+ }
2614
+ if (order !== null) {
2615
+ layer.order = order;
2616
+ if (layerType) {
2617
+ layer.phoLayerType = layerType;
2618
+ console.log(`[PHO\u56FE\u5C42\u8BC6\u522B] ${layer.name}: \u524D\u7F00=${prefix}, \u6570\u5B57=${number}, \u7C7B\u578B=${layerType}, order=${order}`);
2619
+ } else {
2620
+ console.log(`[PHO\u56FE\u5C42\u8BC6\u522B] ${layer.name}: \u524D\u7F00=${prefix}, \u6570\u5B57=${number}, \u663E\u793A\u5B8C\u6574\u6587\u4EF6\u540D, order=${order}`);
2621
+ }
2622
+ }
2623
+ }
2624
+ }
2625
+ }
2626
+ fileInput.addEventListener("change", async (e) => {
2627
+ const file = e.target.files[0];
2628
+ if (!file) return;
2629
+ showStatus(`Processing ${file.name}...`, "loading");
2630
+ showFileInfo(file.name, file.size);
2631
+ showLoading();
2632
+ let filesToProcess = [];
2633
+ const ext = "." + file.name.split(".").pop().toLowerCase();
2634
+ try {
2635
+ if (ext === ".zip") {
2636
+ filesToProcess = await extractZip(file);
2637
+ } else if (ext === ".rar") {
2638
+ filesToProcess = await extractRar(file);
2639
+ } else {
2640
+ filesToProcess = [file];
2641
+ }
2642
+ if (filesToProcess.length === 0) {
2643
+ showStatus("No Gerber files found.", "error");
2644
+ hideLoading();
2645
+ return;
2646
+ }
2647
+ filesToProcess.sort((a, b) => getLayerOrder(a.name) - getLayerOrder(b.name));
2648
+ layers = [];
2649
+ layerList.innerHTML = "";
2650
+ layerColorMap.clear();
2651
+ isSoloMode = false;
2652
+ soloLayerOrder = [];
2653
+ soloTargetLayerIndex = null;
2654
+ cacheNeedsUpdate = true;
2655
+ render();
2656
+ let colorIndex = 0;
2657
+ const pendingLayers = [];
2658
+ for (let i = 0; i < filesToProcess.length; i++) {
2659
+ const f = filesToProcess[i];
2660
+ showStatus(`Parsing ${i + 1}/${filesToProcess.length}: ${f.name}...`, "loading");
2661
+ const fileExt = "." + f.name.split(".").pop().toLowerCase();
2662
+ const text = await f.text();
2663
+ updateInheritedGerberFormatHintFromText(text, f.name);
2664
+ const isTxFile = /^\.tx\d*$/i.test(fileExt) || fileExt === ".txt";
2665
+ const isGbrFile = fileExt === ".gbr";
2666
+ const isPhoFile = fileExt === ".pho";
2667
+ const isArtFile = fileExt === ".art";
2668
+ const isDrlFile = fileExt === ".drl" || fileExt === ".d" || fileExt === ".dri";
2669
+ const isRouFile = fileExt === ".rou";
2670
+ let isM48DrillFile = false;
2671
+ let isSlotFile = false;
2672
+ let useWebGerber = false;
2673
+ let useDirectParser = false;
2674
+ if (isTxFile) {
2675
+ if (/slotholes/i.test(f.name)) {
2676
+ isSlotFile = true;
2677
+ console.log(`[SLOT] ${f.name} \u662F SlotHoles \u6587\u4EF6`);
2678
+ }
2679
+ const lines = text.trim().split("\n").slice(0, 10);
2680
+ const hasM48 = lines.some((line) => /^M48$/i.test(line.trim()));
2681
+ if (hasM48) {
2682
+ isM48DrillFile = true;
2683
+ useWebGerber = true;
2684
+ if (isSlotFile) {
2685
+ console.log(`[SLOT] ${f.name} \u662F Excellon SlotHoles \u6587\u4EF6\uFF08\u542BM48\uFF09\uFF0C\u4F7F\u7528 web-gerber \u89E3\u6790`);
2686
+ } else {
2687
+ console.log(`[\u94BB\u5B54] ${f.name} \u662F Excellon \u94BB\u5B54\u6587\u4EF6\uFF08\u542BM48\uFF09\uFF0C\u4F7F\u7528 web-gerber \u89E3\u6790`);
2688
+ }
2689
+ } else {
2690
+ console.log(`[Gerber] ${f.name} \u662F TXT \u6587\u4EF6\u4F46\u4E0D\u542B M48\uFF0C\u4F7F\u7528 web-gerber \u89E3\u6790`);
2691
+ useWebGerber = true;
2692
+ }
2693
+ } else if (isGbrFile) {
2694
+ console.log(`[Gerber] ${f.name} \u662F GBR \u6587\u4EF6\uFF0C\u4F7F\u7528 web-gerber \u89E3\u6790`);
2695
+ useWebGerber = true;
2696
+ } else if (isPhoFile) {
2697
+ console.log(`[Gerber] ${f.name} \u662F PHO \u6587\u4EF6\uFF0C\u4F7F\u7528 web-gerber \u89E3\u6790`);
2698
+ useWebGerber = true;
2699
+ } else if (isArtFile) {
2700
+ console.log(`[Gerber] ${f.name} \u662F ART \u6587\u4EF6\uFF0C\u4F7F\u7528 web-gerber \u89E3\u6790`);
2701
+ useWebGerber = true;
2702
+ } else if (isDrlFile) {
2703
+ console.log(`[\u94BB\u5B54] ${f.name} \u662F DRL \u94BB\u5B54\u6587\u4EF6\uFF0C\u4F7F\u7528 web-gerber \u89E3\u6790`);
2704
+ isM48DrillFile = true;
2705
+ useWebGerber = true;
2706
+ } else if (isRouFile) {
2707
+ console.log(`[\u94E3\u8FB9] ${f.name} \u662F ROU \u94E3\u8FB9\u6587\u4EF6\uFF0C\u4F7F\u7528 web-gerber \u89E3\u6790`);
2708
+ useWebGerber = true;
2709
+ } else {
2710
+ const isCam350File = [".bot", ".top", ".sob", ".sot", ".ser", ".sst", ".ssb", ".smt", ".smb", ".pmt", ".pmb"].includes(fileExt);
2711
+ if (isCam350File) {
2712
+ console.log(`[Gerber] ${f.name} \u662F CAM350 \u683C\u5F0F\u6587\u4EF6\uFF0C\u4F7F\u7528 web-gerber \u89E3\u6790`);
2713
+ useWebGerber = true;
2714
+ } else {
2715
+ const hasMacroDefinition = /%AM[A-Z0-9_]+\*/i.test(text);
2716
+ const hasRegionFill = /G36\*/i.test(text);
2717
+ if (hasMacroDefinition) {
2718
+ console.log(`[\u5B8F\u5B9A\u4E49] ${f.name} \u5305\u542B Aperture Macro \u5B9A\u4E49\uFF0C\u4F7F\u7528 web-gerber \u89E3\u6790`);
2719
+ useWebGerber = true;
2720
+ } else if (hasRegionFill) {
2721
+ console.log(`[\u533A\u57DF\u586B\u5145] ${f.name} \u5305\u542B G36 \u533A\u57DF\u586B\u5145\u547D\u4EE4\uFF0C\u4F7F\u7528 web-gerber \u89E3\u6790`);
2722
+ useWebGerber = true;
2723
+ }
2724
+ }
2725
+ }
2726
+ try {
2727
+ let data;
2728
+ const fileNameOnly = f.name.split(/[/\\]/).pop();
2729
+ const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
2730
+ const fileNameUpper = fileNameOnly.toUpperCase();
2731
+ const isOutlineLayer = getLayerDisplayName(f.name) === "GKO" || fileExtension === ".GKO" || fileExtension === ".GM" || /^\.GM\d+$/i.test(fileExtension) || fileNameUpper.includes("OUTLINE") || /EDGE[._-]CUTS/i.test(fileNameUpper) || fileNameUpper.includes("WX") || fileNameUpper.includes("BOARD") || fileNameUpper.startsWith("GKO") || fileNameUpper.startsWith("OUT");
2732
+ const parserOpts = {
2733
+ minLineWidth: isOutlineLayer ? 0.3 : 0
2734
+ // 轮廓层(GKO、GM1等)最小线宽 0.3mm
2735
+ };
2736
+ if (isOutlineLayer) {
2737
+ console.log(`[\u8F6E\u5ED3\u5C42] ${fileNameOnly}, minLineWidth=${parserOpts.minLineWidth}mm`);
2738
+ }
2739
+ if (useWebGerber) {
2740
+ data = await parseWithWebGerber(text, f.name);
2741
+ if ((!data.vertices || data.vertices.length === 0) && !isM48DrillFile) {
2742
+ console.log(`[\u56DE\u9000] ${f.name} web-gerber \u65E0\u51E0\u4F55\u6570\u636E\uFF0C\u5C1D\u8BD5 GerberDirectParser`);
2743
+ const parser = new GerberDirectParser(parserOpts);
2744
+ data = parser.parse(text);
2745
+ console.log(`[\u56DE\u9000\u7ED3\u679C] ${f.name}: ${data.vertices.length / 3} \u9876\u70B9, ${data.triangleIndices.length / 3} \u4E09\u89D2\u5F62`);
2746
+ }
2747
+ } else if (useDirectParser) {
2748
+ console.log(`[\u76F4\u63A5\u89E3\u6790] \u5F00\u59CB\u89E3\u6790 ${f.name}`);
2749
+ const parser = new GerberDirectParser(parserOpts);
2750
+ data = parser.parse(text);
2751
+ console.log(`[\u76F4\u63A5\u89E3\u6790] ${f.name}: ${data.vertices.length / 3} \u9876\u70B9, ${data.triangleIndices.length / 3} \u4E09\u89D2\u5F62`);
2752
+ } else {
2753
+ const parser = new GerberDirectParser(parserOpts);
2754
+ data = parser.parse(text);
2755
+ }
2756
+ if (!data.vertices || data.vertices.length === 0) {
2757
+ console.log(`[\u8DF3\u8FC7] ${f.name} \u6CA1\u6709\u51E0\u4F55\u6570\u636E`);
2758
+ continue;
2759
+ }
2760
+ let featureExtractionVertices = null;
2761
+ const refLayerFileName = f.name.split(/[/\\]/).pop();
2762
+ const refLayerExt = "." + refLayerFileName.split(".").pop().toUpperCase();
2763
+ const refLayerExtName = refLayerExt.substring(1);
2764
+ const logicalLayerType = getLayerDisplayName(f.name);
2765
+ const isRefLayer = ["GTS", "GBS", "GTL", "GBL"].includes(refLayerExtName) || ["GTS", "GBS", "GTL", "GBL"].includes(logicalLayerType);
2766
+ if (isRefLayer && !useWebGerber) {
2767
+ console.log(`[\u7279\u5F81\u63D0\u53D6] ${f.name} (${refLayerExtName}) \u989D\u5916\u7528web-gerber\u89E3\u6790\u4EE5\u63D0\u53D6\u5706\u5FC3`);
2768
+ try {
2769
+ const originalLog = console.log;
2770
+ const silencedPatterns = ["[web-gerber]", "plotResult", "SVG", "Child"];
2771
+ console.log = (...args) => {
2772
+ const message = args.join(" ");
2773
+ if (!silencedPatterns.some((pattern) => message.includes(pattern))) {
2774
+ originalLog.apply(console, args);
2775
+ }
2776
+ };
2777
+ const featureData = await parseWithWebGerber(text, f.name);
2778
+ console.log = originalLog;
2779
+ if (featureData && featureData.vertices && featureData.vertices.length > 0) {
2780
+ featureExtractionVertices = featureData.vertices;
2781
+ console.log(`[\u7279\u5F81\u63D0\u53D6] ${f.name} \u2713 \u63D0\u53D6\u5230 ${featureExtractionVertices.length / 3} \u4E2A\u9876\u70B9\u7528\u4E8E\u5706\u5FC3\u8BC6\u522B`);
2782
+ }
2783
+ } catch (err) {
2784
+ console.log = console.log.bind(console);
2785
+ console.warn(`[\u7279\u5F81\u63D0\u53D6] ${f.name} \u2717 web-gerber\u89E3\u6790\u5931\u8D25:`, err.message);
2786
+ }
2787
+ }
2788
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
2789
+ for (let k = 0; k < data.vertices.length; k += 3) {
2790
+ const x = data.vertices[k];
2791
+ const y = data.vertices[k + 1];
2792
+ minX = Math.min(minX, x);
2793
+ maxX = Math.max(maxX, x);
2794
+ minY = Math.min(minY, y);
2795
+ maxY = Math.max(maxY, y);
2796
+ }
2797
+ const bounds = { minX, minY, maxX, maxY };
2798
+ const hexColor = pickColor(colorIndex);
2799
+ const rgb = hexToRgb(hexColor);
2800
+ colorIndex++;
2801
+ const vertexBuffer = gl.createBuffer();
2802
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
2803
+ gl.bufferData(gl.ARRAY_BUFFER, data.vertices, gl.STATIC_DRAW);
2804
+ const indexBuffer = gl.createBuffer();
2805
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
2806
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data.triangleIndices, gl.STATIC_DRAW);
2807
+ let rawVerticesForFeatureExtraction = null;
2808
+ if (featureExtractionVertices) {
2809
+ rawVerticesForFeatureExtraction = featureExtractionVertices;
2810
+ } else if (isRefLayer && useWebGerber) {
2811
+ rawVerticesForFeatureExtraction = data.vertices;
2812
+ } else if (isM48DrillFile || isDrlFile) {
2813
+ rawVerticesForFeatureExtraction = data.vertices;
2814
+ }
2815
+ pendingLayers.push({
2816
+ name: f.name,
2817
+ visible: true,
2818
+ color: [rgb.r, rgb.g, rgb.b],
2819
+ hexColor,
2820
+ // vertices: Array.from(data.vertices), // 不再保留 CPU 端数据以节省内存
2821
+ // indices: Array.from(data.triangleIndices),
2822
+ vertexBuffer,
2823
+ indexBuffer,
2824
+ vertexCount: data.triangleIndices.length,
2825
+ // Gerber LP(LPC/LPD)极性段落:用于 stencil 挖空(主要影响铜层大面积铺铜的“避空/开窗”)
2826
+ polarityRuns: data.polarityRuns || null,
2827
+ // Drill:记录每个孔(或中心+尺寸形状)的顶点区间,用于后续“只缩放孔位、不缩放孔径”的修正
2828
+ drillPosScaleRuns: data.drillPosScaleRuns || null,
2829
+ order: getLayerOrder(f.name),
2830
+ isDrillFile: isM48DrillFile,
2831
+ isSlotFile,
2832
+ bounds,
2833
+ // rawVertices用于特征提取(钻孔层用渲染顶点,参考层用web-gerber生成的专用顶点)
2834
+ rawVertices: rawVerticesForFeatureExtraction
2835
+ });
2836
+ console.log(`[\u6210\u529F] ${f.name}: ${data.vertices.length / 3} \u9876\u70B9, ${data.triangleIndices.length / 3} \u4E09\u89D2\u5F62`);
2837
+ } catch (error) {
2838
+ console.error(`[\u9519\u8BEF] \u89E3\u6790\u6587\u4EF6 ${f.name} \u5931\u8D25:`, error.message);
2839
+ }
2840
+ if (i < filesToProcess.length - 1) {
2841
+ await new Promise((resolve) => setTimeout(resolve, 0));
2842
+ }
2843
+ }
2844
+ if (pendingLayers.length === 0) {
2845
+ showStatus("No geometry generated.", "error");
2846
+ hideLoading();
2847
+ return;
2848
+ }
2849
+ layers = pendingLayers;
2850
+ updatePhoLayerTypes(layers);
2851
+ reassignColorsByOrder(layers);
2852
+ checkDrillLayerAlignment();
2853
+ const totalTris = layers.reduce((sum, l) => sum + l.vertexCount / 3, 0);
2854
+ showStatus(`\u2713 Rendered ${layers.length} layers (${Math.floor(totalTris)} tris)`, "success");
2855
+ hideLoading();
2856
+ updateLayerList();
2857
+ cacheNeedsUpdate = true;
2858
+ centerView();
2859
+ } catch (err) {
2860
+ console.error(err);
2861
+ showStatus("\u2717 Error: " + err.message, "error");
2862
+ hideLoading();
2863
+ }
2864
+ });
2865
+ function showStatus(msg, type = "") {
2866
+ status.textContent = msg;
2867
+ status.className = `status ${type}`;
2868
+ }
2869
+ function showFileInfo(name, size) {
2870
+ fileName.textContent = name;
2871
+ fileSize.textContent = (size / 1024).toFixed(2) + " KB";
2872
+ fileInfo.classList.add("active");
2873
+ }
2874
+ function updateLayerList() {
2875
+ layerList.innerHTML = "";
2876
+ const sortedLayers = sortLayersByOrder(layers);
2877
+ console.log("[\u56FE\u5C42\u5217\u8868\u6392\u5E8F] \u6392\u5E8F\u540E\u7684\u56FE\u5C42\u987A\u5E8F:");
2878
+ sortedLayers.forEach(({ layer, originalIndex }, index) => {
2879
+ const displayName = getLayerDisplayName(layer);
2880
+ console.log(` ${index + 1}. ${displayName} (order=${layer.order || 100}, originalIndex=${originalIndex})`);
2881
+ });
2882
+ sortedLayers.forEach(({ layer, originalIndex }, displayIndex) => {
2883
+ const item = document.createElement("div");
2884
+ item.className = layer.visible ? "layer-item" : "layer-item disabled";
2885
+ const indexSpan = document.createElement("span");
2886
+ indexSpan.className = "layer-index";
2887
+ indexSpan.textContent = displayIndex + 1;
2888
+ const toggle = document.createElement("div");
2889
+ toggle.className = `layer-toggle ${layer.visible ? "checked" : ""}`;
2890
+ toggle.onclick = (e) => {
2891
+ e.stopPropagation();
2892
+ toggleLayer(originalIndex);
2893
+ };
2894
+ const colorBox = document.createElement("div");
2895
+ colorBox.className = "layer-color";
2896
+ if (!layer.visible) {
2897
+ colorBox.style.backgroundColor = "#cccccc";
2898
+ } else {
2899
+ let displayColor = layer.hexColor;
2900
+ if (layerColorMap.has(originalIndex)) {
2901
+ displayColor = layerColorMap.get(originalIndex);
2902
+ }
2903
+ colorBox.style.backgroundColor = displayColor;
2904
+ }
2905
+ const nameSpan = document.createElement("div");
2906
+ nameSpan.className = "layer-name";
2907
+ let displayName = getLayerDisplayName(layer);
2908
+ if (layer.isSlotFile) {
2909
+ displayName = "SLOT";
2910
+ } else if (layer.isDrillFile) {
2911
+ const fileName2 = layer.name;
2912
+ if (/[_\s]via/i.test(fileName2)) {
2913
+ displayName = "VIA";
2914
+ } else {
2915
+ displayName = "DRL";
2916
+ }
2917
+ }
2918
+ nameSpan.textContent = displayName;
2919
+ nameSpan.setAttribute("title", layer.name);
2920
+ nameSpan.addEventListener("dblclick", (e) => {
2921
+ e.stopPropagation();
2922
+ soloLayer(originalIndex);
2923
+ });
2924
+ item.appendChild(indexSpan);
2925
+ item.appendChild(toggle);
2926
+ item.appendChild(colorBox);
2927
+ item.appendChild(nameSpan);
2928
+ layerList.appendChild(item);
2929
+ });
2930
+ updateToggleAllState();
2931
+ }
2932
+ function toggleLayer(index) {
2933
+ if (index < 0 || index >= layers.length) return;
2934
+ layers[index].visible = !layers[index].visible;
2935
+ if (isSoloMode) {
2936
+ if (layers[index].visible) {
2937
+ if (soloLayerOrder.indexOf(index) === -1) {
2938
+ soloLayerOrder.push(index);
2939
+ if (layerColorMap.has(index)) {
2940
+ const existingColor = layerColorMap.get(index);
2941
+ updateLayerColor(index, existingColor);
2942
+ } else {
2943
+ const colorIndex = layerColorMap.size;
2944
+ const newColor = pickColor(colorIndex);
2945
+ layerColorMap.set(index, newColor);
2946
+ updateLayerColor(index, newColor);
2947
+ }
2948
+ } else {
2949
+ if (layerColorMap.has(index)) {
2950
+ const existingColor = layerColorMap.get(index);
2951
+ updateLayerColor(index, existingColor);
2952
+ }
2953
+ }
2954
+ }
2955
+ }
2956
+ updateLayerList();
2957
+ cacheNeedsUpdate = true;
2958
+ }
2959
+ function soloLayer(targetLayerIndex) {
2960
+ console.log(`\u5355\u72EC\u663E\u793A\u56FE\u5C42 ${targetLayerIndex}`);
2961
+ isSoloMode = true;
2962
+ layerColorMap.clear();
2963
+ soloLayerOrder = [];
2964
+ layers.forEach((layer, index) => {
2965
+ layer.visible = false;
2966
+ });
2967
+ layers[targetLayerIndex].visible = true;
2968
+ soloLayerOrder.push(targetLayerIndex);
2969
+ soloTargetLayerIndex = targetLayerIndex;
2970
+ const newColor = baseColors[0];
2971
+ layerColorMap.set(targetLayerIndex, newColor);
2972
+ updateLayerColor(targetLayerIndex, newColor);
2973
+ updateLayerList();
2974
+ cacheNeedsUpdate = true;
2975
+ console.log(`\u56FE\u5C42 ${targetLayerIndex} \u989C\u8272\u91CD\u7F6E\u4E3A: ${newColor}`);
2976
+ }
2977
+ function updateLayerColor(layerIndex, hexColor) {
2978
+ if (layerIndex < 0 || layerIndex >= layers.length) return;
2979
+ const layer = layers[layerIndex];
2980
+ const rgb = hexToRgb(hexColor);
2981
+ layer.color = [rgb.r, rgb.g, rgb.b];
2982
+ layer.hexColor = hexColor;
2983
+ }
2984
+ layerToggleAll.onclick = () => {
2985
+ if (layers.length === 0) return;
2986
+ const allVisible = layers.every((l) => l.visible);
2987
+ const newVisible = !allVisible;
2988
+ if (isSoloMode && !allVisible) {
2989
+ const sortedLayers = layers.map((layer, idx) => ({ layer, idx })).sort((a, b) => (a.layer.order || 100) - (b.layer.order || 100));
2990
+ soloLayerOrder = [];
2991
+ let colorIndex = 1;
2992
+ sortedLayers.forEach(({ layer, idx }) => {
2993
+ layer.visible = true;
2994
+ if (idx === soloTargetLayerIndex) {
2995
+ const newColor = baseColors[0];
2996
+ layerColorMap.set(idx, newColor);
2997
+ updateLayerColor(idx, newColor);
2998
+ soloLayerOrder.push(idx);
2999
+ } else {
3000
+ const newColor = pickColor(colorIndex);
3001
+ layerColorMap.set(idx, newColor);
3002
+ updateLayerColor(idx, newColor);
3003
+ soloLayerOrder.push(idx);
3004
+ colorIndex++;
3005
+ }
3006
+ });
3007
+ console.log("solo\u6A21\u5F0F\u4E0B\u5168\u90E8\u663E\u793A\uFF0C\u53CC\u51FB\u7684\u56FE\u5C42\u4FDD\u6301\u7EA2\u8272\uFF0C\u5176\u4F59\u56FE\u5C42\u6309\u6392\u5E8F\u987A\u5E8F\u5206\u914D\u989C\u8272");
3008
+ } else {
3009
+ layers.forEach((layer) => {
3010
+ layer.visible = newVisible;
3011
+ });
3012
+ }
3013
+ updateLayerList();
3014
+ cacheNeedsUpdate = true;
3015
+ };
3016
+ function updateToggleAllState() {
3017
+ const allVisible = layers.every((l) => l.visible);
3018
+ if (allVisible) {
3019
+ layerToggleAllCheckbox.classList.add("checked");
3020
+ } else {
3021
+ layerToggleAllCheckbox.classList.remove("checked");
3022
+ }
3023
+ }
3024
+ async function extractZip(file) {
3025
+ const files = [];
3026
+ try {
3027
+ const zip = await import_jszip.default.loadAsync(file);
3028
+ for (const [filename, zipEntry] of Object.entries(zip.files)) {
3029
+ if (!zipEntry.dir && isGerberFile(filename)) {
3030
+ const blob = await zipEntry.async("blob");
3031
+ files.push(new File([blob], filename));
3032
+ }
3033
+ }
3034
+ } catch (e) {
3035
+ console.error(e);
3036
+ throw new Error("Failed to unzip");
3037
+ }
3038
+ return files;
3039
+ }
3040
+ function isGerberFile(fileName2) {
3041
+ const fileNameOnly = fileName2.split(/[/\\]/).pop();
3042
+ const fileNameUpper = fileNameOnly.toUpperCase();
3043
+ const specialSuffixes = ["_SST", "_PMT", "_SMT", "_TOP", "_SMB", "_PMB", "_SSB", "OUTLINE"];
3044
+ for (const suffix of specialSuffixes) {
3045
+ if (fileNameUpper.endsWith(suffix) || fileNameUpper.includes(suffix + ".")) {
3046
+ return true;
3047
+ }
3048
+ }
3049
+ if (/.*_INT\d+$/i.test(fileNameUpper) || /.*_INT\d+\./i.test(fileNameUpper)) {
3050
+ return true;
3051
+ }
3052
+ const validExtensions = [
3053
+ ".gtl",
3054
+ ".gbl",
3055
+ ".gts",
3056
+ ".gbs",
3057
+ ".gto",
3058
+ ".gbo",
3059
+ ".gtp",
3060
+ ".gbp",
3061
+ ".gm1",
3062
+ ".drl",
3063
+ ".gbr",
3064
+ ".gko",
3065
+ ".gdl",
3066
+ ".gdd",
3067
+ ".gm",
3068
+ ".gd1",
3069
+ ".gg1",
3070
+ ".gpb",
3071
+ ".gpt",
3072
+ ".txt",
3073
+ ".art",
3074
+ ".d",
3075
+ ".rou",
3076
+ ".pho",
3077
+ // 新增:CAM350 等软件导出的扩展名
3078
+ ".bot",
3079
+ ".top",
3080
+ ".sob",
3081
+ ".sot",
3082
+ ".dri",
3083
+ ".ser",
3084
+ ".sst",
3085
+ ".ssb",
3086
+ ".smt",
3087
+ ".smb",
3088
+ ".pmt",
3089
+ ".pmb"
3090
+ ];
3091
+ const fileExtension = "." + fileNameOnly.split(".").pop().toLowerCase();
3092
+ return validExtensions.includes(fileExtension) || /^\.g\d+$/i.test(fileExtension) || /^\.gm\d+$/i.test(fileExtension) || /^\.gp\d+$/i.test(fileExtension) || /^\.tx\d+$/i.test(fileExtension) || /^\.int\d+$/i.test(fileExtension);
3093
+ }
3094
+ function checkDrillLayerAlignment() {
3095
+ const drillLayers = layers.filter((layer) => layer.isDrillFile || layer.isSlotFile);
3096
+ if (drillLayers.length === 0) {
3097
+ console.log("[\u94BB\u5B54\u5BF9\u9F50] \u672A\u627E\u5230\u94BB\u5B54\u5C42\uFF0C\u8DF3\u8FC7");
3098
+ return;
3099
+ }
3100
+ const combineBounds = (layersToCombine) => {
3101
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3102
+ for (const l of layersToCombine) {
3103
+ if (!l || !l.bounds) continue;
3104
+ minX = Math.min(minX, l.bounds.minX);
3105
+ minY = Math.min(minY, l.bounds.minY);
3106
+ maxX = Math.max(maxX, l.bounds.maxX);
3107
+ maxY = Math.max(maxY, l.bounds.maxY);
3108
+ }
3109
+ if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
3110
+ return null;
3111
+ }
3112
+ return { minX, minY, maxX, maxY };
3113
+ };
3114
+ const translateBounds = (b, dx, dy) => ({
3115
+ minX: b.minX + dx,
3116
+ maxX: b.maxX + dx,
3117
+ minY: b.minY + dy,
3118
+ maxY: b.maxY + dy
3119
+ });
3120
+ const computeOverflow = (b, ref) => {
3121
+ const left = Math.max(0, ref.minX - b.minX);
3122
+ const right = Math.max(0, b.maxX - ref.maxX);
3123
+ const bottom = Math.max(0, ref.minY - b.minY);
3124
+ const top = Math.max(0, b.maxY - ref.maxY);
3125
+ const total = left + right + bottom + top;
3126
+ return { left, right, bottom, top, total };
3127
+ };
3128
+ const outlineLayer = layers.find((layer) => {
3129
+ const fileNameOnly = (layer.name || "").split(/[/\\]/).pop();
3130
+ const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
3131
+ const ext = fileExtension.substring(1);
3132
+ const upper = fileNameOnly.toUpperCase();
3133
+ return ext === "GKO" || ext === "GM" || /^GM\d+$/i.test(ext) || upper.includes("_PROFILE") || upper.includes("PROFILE") || upper.includes("OUTLINE") || upper.includes("BOARD") || upper.startsWith("GKO");
3134
+ });
3135
+ let outlineBounds = outlineLayer && outlineLayer.bounds ? outlineLayer.bounds : null;
3136
+ let combinedDrillBounds = combineBounds(drillLayers);
3137
+ if (!combinedDrillBounds) {
3138
+ console.log("[\u94BB\u5B54\u5BF9\u9F50] \u65E0\u6CD5\u8BA1\u7B97\u94BB\u5B54\u5C42 bounds\uFF0C\u8DF3\u8FC7");
3139
+ return;
3140
+ }
3141
+ const getLayerType = (name) => getLayerDisplayName(name);
3142
+ const sizeOfBounds = (b) => Math.max(b.maxX - b.minX, b.maxY - b.minY);
3143
+ if (outlineBounds) {
3144
+ const priority = ["GTL", "GBL", "GTS", "GBS"];
3145
+ let ref = null;
3146
+ for (const t of priority) {
3147
+ const l = layers.find((x) => x && x.bounds && !x.isDrillFile && !x.isSlotFile && getLayerType(x.name) === t);
3148
+ if (l) {
3149
+ ref = { name: l.name, bounds: l.bounds, reason: t };
3150
+ break;
3151
+ }
3152
+ }
3153
+ if (!ref) {
3154
+ const any = layers.find((x) => x && x.bounds && !x.isDrillFile && !x.isSlotFile);
3155
+ if (any) ref = { name: any.name, bounds: any.bounds, reason: "any" };
3156
+ }
3157
+ if (ref && ref.bounds) {
3158
+ const outlineSize = sizeOfBounds(outlineBounds);
3159
+ const refSize = sizeOfBounds(ref.bounds);
3160
+ const ratio = refSize > 0 ? outlineSize / refSize : Infinity;
3161
+ if (Number.isFinite(ratio) && ratio > 5) {
3162
+ console.warn(`[\u94BB\u5B54\u5BF9\u9F50] \u26A0\uFE0F Outline(${outlineLayer?.name || "unknown"}) \u5C3A\u5BF8=${outlineSize.toFixed(3)}mm \u8FDC\u5927\u4E8E\u53C2\u8003\u5C42(${ref.reason}): ${ref.name} \u5C3A\u5BF8=${refSize.toFixed(3)}mm (ratio=${ratio.toFixed(2)})\uFF0C\u5224\u5B9A\u4E3A\u201C\u4E0D\u53EF\u4FE1Outline\u201D\uFF0C\u5C06\u8DF3\u8FC7GKO\u515C\u5E95\u5E76\u7528\u94DC\u5C42\u505A\u5BF9\u9F50\u53C2\u8003\u3002`);
3163
+ outlineBounds = null;
3164
+ }
3165
+ }
3166
+ }
3167
+ const pickReferenceForScale = () => {
3168
+ if (outlineBounds) return { name: outlineLayer.name, bounds: outlineBounds, reason: "outline" };
3169
+ const priority = ["GTL", "GBL", "GTS", "GBS"];
3170
+ for (const t of priority) {
3171
+ const l = layers.find((x) => x && x.bounds && !x.isDrillFile && !x.isSlotFile && getLayerType(x.name) === t);
3172
+ if (l) return { name: l.name, bounds: l.bounds, reason: t };
3173
+ }
3174
+ const any = layers.find((x) => x && x.bounds && !x.isDrillFile && !x.isSlotFile);
3175
+ if (any) return { name: any.name, bounds: any.bounds, reason: "any" };
3176
+ return null;
3177
+ };
3178
+ const decidePow10ScaleFix = (ratio) => {
3179
+ if (!Number.isFinite(ratio) || ratio <= 0) return 1;
3180
+ const acceptLow = 0.7;
3181
+ const acceptHigh = 1.3;
3182
+ if (ratio > 2) {
3183
+ const pow10 = Math.pow(10, Math.round(Math.log10(ratio)));
3184
+ const cand = 1 / pow10;
3185
+ const newRatio = ratio * cand;
3186
+ if (cand !== 1 && newRatio >= acceptLow && newRatio <= acceptHigh) return cand;
3187
+ } else if (ratio < 0.5) {
3188
+ const inv = 1 / ratio;
3189
+ const pow10 = Math.pow(10, Math.round(Math.log10(inv)));
3190
+ const cand = pow10;
3191
+ const newRatio = ratio * cand;
3192
+ if (cand !== 1 && newRatio >= acceptLow && newRatio <= acceptHigh) return cand;
3193
+ }
3194
+ return 1;
3195
+ };
3196
+ const applyPosScaleFixToDrills = (scaleX, scaleY, tag) => {
3197
+ const isUniform = scaleX === scaleY;
3198
+ if (isUniform) {
3199
+ console.warn(`[\u94BB\u5B54\u5BF9\u9F50] \u26A0\uFE0F \u5E94\u7528\u94BB\u5B54\u5750\u6807\u7F29\u653E(${tag}): scale=${scaleX}\uFF08\u4EC5\u7F29\u653E\u5B54\u4F4D\uFF0C\u4E0D\u7F29\u653E\u5B54\u5F84\uFF09`);
3200
+ } else {
3201
+ console.warn(`[\u94BB\u5B54\u5BF9\u9F50] \u26A0\uFE0F \u5E94\u7528\u94BB\u5B54\u5750\u6807\u7F29\u653E(${tag}): scaleX=${scaleX}, scaleY=${scaleY}\uFF08\u4EC5\u7F29\u653E\u5B54\u4F4D\uFF0C\u4E0D\u7F29\u653E\u5B54\u5F84\uFF09`);
3202
+ }
3203
+ for (const dl of drillLayers) {
3204
+ if (!dl.rawVertices || !dl.drillPosScaleRuns || dl.drillPosScaleRuns.length === 0) continue;
3205
+ const vertices = dl.rawVertices;
3206
+ const runs = dl.drillPosScaleRuns;
3207
+ for (const run of runs) {
3208
+ if (!run || !(run.count > 0) || run.start == null) continue;
3209
+ const dx = (run.cx || 0) * (scaleX - 1);
3210
+ const dy = (run.cy || 0) * (scaleY - 1);
3211
+ const start = run.start * 3;
3212
+ const end = (run.start + run.count) * 3;
3213
+ for (let i = start; i < end; i += 3) {
3214
+ vertices[i] += dx;
3215
+ vertices[i + 1] += dy;
3216
+ }
3217
+ run.cx = (run.cx || 0) * scaleX;
3218
+ run.cy = (run.cy || 0) * scaleY;
3219
+ }
3220
+ gl.bindBuffer(gl.ARRAY_BUFFER, dl.vertexBuffer);
3221
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
3222
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
3223
+ for (let k = 0; k < vertices.length; k += 3) {
3224
+ const x = vertices[k];
3225
+ const y = vertices[k + 1];
3226
+ minX = Math.min(minX, x);
3227
+ maxX = Math.max(maxX, x);
3228
+ minY = Math.min(minY, y);
3229
+ maxY = Math.max(maxY, y);
3230
+ }
3231
+ dl.bounds = { minX, minY, maxX, maxY };
3232
+ if (isUniform) {
3233
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u2713 ${dl.name} \u5DF2\u5E94\u7528\u5750\u6807\u7F29\u653E scale=${scaleX}`);
3234
+ } else {
3235
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u2713 ${dl.name} \u5DF2\u5E94\u7528\u5750\u6807\u7F29\u653E scaleX=${scaleX}, scaleY=${scaleY}`);
3236
+ }
3237
+ }
3238
+ };
3239
+ const refForScale = pickReferenceForScale();
3240
+ if (refForScale && refForScale.bounds) {
3241
+ const refBounds = refForScale.bounds;
3242
+ const refWidth = refBounds.maxX - refBounds.minX;
3243
+ const refHeight = refBounds.maxY - refBounds.minY;
3244
+ const drillWidth = combinedDrillBounds.maxX - combinedDrillBounds.minX;
3245
+ const drillHeight = combinedDrillBounds.maxY - combinedDrillBounds.minY;
3246
+ const holeCount = drillLayers.reduce((n, dl) => n + (dl && dl.drillPosScaleRuns ? dl.drillPosScaleRuns.length : 0), 0);
3247
+ const overflowToRef = computeOverflow(combinedDrillBounds, refBounds);
3248
+ const refSize = Math.max(refWidth, refHeight);
3249
+ const inRef = overflowToRef.total <= 1e-3;
3250
+ if (inRef) {
3251
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u2139\uFE0F Drill bounds \u5DF2\u5728\u53C2\u8003\u5C42(${refForScale.reason})\u8303\u56F4\u5185(holes=${holeCount}, drillW=${drillWidth.toFixed(3)}, drillH=${drillHeight.toFixed(3)}), \u8DF3\u8FC7 bounds-fit \u5750\u6807\u7F29\u653E\u515C\u5E95`);
3252
+ } else if (holeCount > 0 && holeCount < 50 && overflowToRef.total < refSize * 0.2) {
3253
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u2139\uFE0F Drill \u5B54\u6570\u8F83\u5C11(${holeCount})\u4E14\u8D8A\u754C\u4E0D\u660E\u663E(total=${overflowToRef.total.toFixed(3)}mm)\uFF0C\u8DF3\u8FC7 bounds-fit \u5750\u6807\u7F29\u653E\u515C\u5E95`);
3254
+ } else {
3255
+ const ratioX = drillWidth > 0 && refWidth > 0 ? drillWidth / refWidth : 1;
3256
+ const ratioY = drillHeight > 0 && refHeight > 0 ? drillHeight / refHeight : 1;
3257
+ const scaleX = decidePow10ScaleFix(ratioX);
3258
+ const scaleY = decidePow10ScaleFix(ratioY);
3259
+ if ((scaleX !== 1 || scaleY !== 1) && scaleX !== scaleY) {
3260
+ console.warn(`[\u94BB\u5B54\u5BF9\u9F50] \u26A0\uFE0F bounds-fit \u63A8\u65AD\u4E3A\u975E\u7B49\u6BD4(scaleX=${scaleX}, scaleY=${scaleY})\uFF0C\u4E3A\u907F\u514D\u8BEF\u7F29\u653E\u5DF2\u5FFD\u7565\uFF1B\u5C06\u8F6C\u800C\u5C1D\u8BD5\u5E73\u79FB/\u7279\u5F81\u70B9\u5BF9\u9F50`);
3261
+ } else if (scaleX !== 1 || scaleY !== 1) {
3262
+ console.warn(`[\u94BB\u5B54\u5BF9\u9F50] \u26A0\uFE0F Drill bounds \u500D\u6570\u5F02\u5E38\uFF1AdrillW=${drillWidth.toFixed(3)} drillH=${drillHeight.toFixed(3)} refW=${refWidth.toFixed(3)} refH=${refHeight.toFixed(3)} ratioX=${ratioX.toFixed(2)} ratioY=${ratioY.toFixed(2)} ref=${refForScale.name}(${refForScale.reason}) \u2192 \u5C1D\u8BD5 scaleX=${scaleX} scaleY=${scaleY}`);
3263
+ applyPosScaleFixToDrills(scaleX, scaleY, `bounds-fit (ref=${refForScale.reason})`);
3264
+ combinedDrillBounds = combineBounds(drillLayers) || combinedDrillBounds;
3265
+ }
3266
+ }
3267
+ } else {
3268
+ console.log("[\u94BB\u5B54\u5BF9\u9F50] \u672A\u627E\u5230\u53C2\u8003\u5C42 bounds\uFF0C\u8DF3\u8FC7\u94BB\u5B54\u5750\u6807\u7F29\u653E\u515C\u5E95");
3269
+ }
3270
+ let overflowBefore = null;
3271
+ if (outlineBounds) {
3272
+ overflowBefore = computeOverflow(combinedDrillBounds, outlineBounds);
3273
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] GKO/Outline\u5C42: ${outlineLayer.name}`);
3274
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] Drill bounds(before): minX=${combinedDrillBounds.minX.toFixed(3)}, minY=${combinedDrillBounds.minY.toFixed(3)}, maxX=${combinedDrillBounds.maxX.toFixed(3)}, maxY=${combinedDrillBounds.maxY.toFixed(3)}`);
3275
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] Outline bounds: minX=${outlineBounds.minX.toFixed(3)}, minY=${outlineBounds.minY.toFixed(3)}, maxX=${outlineBounds.maxX.toFixed(3)}, maxY=${outlineBounds.maxY.toFixed(3)}`);
3276
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] Overflow(before): left=${overflowBefore.left.toFixed(3)} right=${overflowBefore.right.toFixed(3)} bottom=${overflowBefore.bottom.toFixed(3)} top=${overflowBefore.top.toFixed(3)} total=${overflowBefore.total.toFixed(3)} (mm)`);
3277
+ } else {
3278
+ console.log("[\u94BB\u5B54\u5BF9\u9F50] \u672A\u627E\u5230GKO/Outline\u5C42\uFF0C\u5C06\u4EC5\u4F7F\u7528\u7279\u5F81\u70B9\u5339\u914D\u5BF9\u9F50\uFF08\u82E5\u53EF\u7528\uFF09");
3279
+ }
3280
+ const applyOffsetToAllDrills = (dx, dy, tag) => {
3281
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u26A0\uFE0F \u5E94\u7528\u5168\u5C40\u504F\u79FB(${tag}): X=${dx.toFixed(3)}mm, Y=${dy.toFixed(3)}mm`);
3282
+ for (const dl of drillLayers) {
3283
+ if (!dl.rawVertices) continue;
3284
+ const vertices = dl.rawVertices;
3285
+ for (let k = 0; k < vertices.length; k += 3) {
3286
+ vertices[k] += dx;
3287
+ vertices[k + 1] += dy;
3288
+ }
3289
+ gl.bindBuffer(gl.ARRAY_BUFFER, dl.vertexBuffer);
3290
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
3291
+ if (dl.bounds) {
3292
+ dl.bounds.minX += dx;
3293
+ dl.bounds.maxX += dx;
3294
+ dl.bounds.minY += dy;
3295
+ dl.bounds.maxY += dy;
3296
+ }
3297
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u2713 ${dl.name} \u5DF2\u5E94\u7528\u5168\u5C40\u504F\u79FB`);
3298
+ }
3299
+ if (outlineBounds) {
3300
+ const afterBounds = translateBounds(combinedDrillBounds, dx, dy);
3301
+ const overflowAfter = computeOverflow(afterBounds, outlineBounds);
3302
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] Overflow(after): left=${overflowAfter.left.toFixed(3)} right=${overflowAfter.right.toFixed(3)} bottom=${overflowAfter.bottom.toFixed(3)} top=${overflowAfter.top.toFixed(3)} total=${overflowAfter.total.toFixed(3)} (mm)`);
3303
+ }
3304
+ console.log("[\u94BB\u5B54\u5BF9\u9F50] \u5BF9\u9F50\u68C0\u67E5\u5B8C\u6210");
3305
+ };
3306
+ let featureOffset = null;
3307
+ let anchorLayer = null;
3308
+ let anchorFeatures = null;
3309
+ let anchorCircleCount = 0;
3310
+ let anchorScore = -Infinity;
3311
+ let fallbackLayer = null;
3312
+ let fallbackFeatures = null;
3313
+ let fallbackScore = -Infinity;
3314
+ for (const dl of drillLayers) {
3315
+ if (!dl.rawVertices) continue;
3316
+ const featsAll = extractFeaturesFromLayer(dl);
3317
+ if (featsAll.length < 3) continue;
3318
+ const nameUpper = (dl.name || "").toUpperCase();
3319
+ const viaPenalty = /VIA/.test(nameUpper) ? 1e5 : 0;
3320
+ const slotPenalty = dl.isSlotFile ? 5e4 : 0;
3321
+ const circleFeats = featsAll.filter((f) => f && f.type === "circle");
3322
+ const circleCount = circleFeats.length;
3323
+ if (circleCount >= 3) {
3324
+ const score = circleCount * 1e3 - viaPenalty - slotPenalty;
3325
+ if (score > anchorScore) {
3326
+ anchorScore = score;
3327
+ anchorLayer = dl;
3328
+ anchorFeatures = circleFeats;
3329
+ anchorCircleCount = circleCount;
3330
+ }
3331
+ } else {
3332
+ const score = featsAll.length - viaPenalty - slotPenalty;
3333
+ if (score > fallbackScore) {
3334
+ fallbackScore = score;
3335
+ fallbackLayer = dl;
3336
+ fallbackFeatures = featsAll;
3337
+ }
3338
+ }
3339
+ }
3340
+ if (!anchorLayer && fallbackLayer) {
3341
+ anchorLayer = fallbackLayer;
3342
+ anchorFeatures = fallbackFeatures;
3343
+ anchorCircleCount = (anchorFeatures || []).filter((f) => f && f.type === "circle").length;
3344
+ }
3345
+ if (!anchorLayer || !anchorFeatures) {
3346
+ console.log("[\u94BB\u5B54\u5BF9\u9F50] \u26A0\uFE0F \u672A\u627E\u5230\u53EF\u7528\u7684\u951A\u70B9\u94BB\u5B54\u5C42\uFF08\u7279\u5F81\u70B9\u4E0D\u8DB3\uFF09\uFF0C\u8DF3\u8FC7\u7279\u5F81\u70B9\u5BF9\u9F50");
3347
+ } else {
3348
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u9009\u5B9A\u951A\u70B9\u94BB\u5B54\u5C42: ${anchorLayer.name} (features=${anchorFeatures.length}, circle=${anchorCircleCount}, slot=${!!anchorLayer.isSlotFile})`);
3349
+ const nonDrillLayers = layers.filter((layer) => !layer.isDrillFile && !layer.isSlotFile);
3350
+ const getLayerType2 = (name) => getLayerDisplayName(name);
3351
+ const refTypePriority = ["GTL", "GBL", "GTS", "GBS"];
3352
+ const refCandidates = [];
3353
+ for (const t of refTypePriority) {
3354
+ for (const l of nonDrillLayers) {
3355
+ if (!l || !l.rawVertices) continue;
3356
+ if (getLayerType2(l.name) === t) refCandidates.push(l);
3357
+ }
3358
+ }
3359
+ if (refCandidates.length === 0) {
3360
+ console.log("[\u94BB\u5B54\u5BF9\u9F50] \u672A\u627E\u5230\u53EF\u7528\u53C2\u8003\u5C42\uFF08GTL/GBL/GTS/GBS \u6216\u7F3A\u5C11 rawVertices\uFF09\uFF0C\u8DF3\u8FC7\u7279\u5F81\u70B9\u5BF9\u9F50");
3361
+ } else {
3362
+ const selectedAnchorFeatures = selectFeaturesWithXYDistribution(anchorFeatures, Math.min(80, anchorFeatures.length));
3363
+ let best = null;
3364
+ let secondBest = null;
3365
+ for (const refLayer of refCandidates) {
3366
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u5C1D\u8BD5\u53C2\u8003\u5C42: ${refLayer.name}`);
3367
+ const refAll = extractFeaturesFromLayer(refLayer);
3368
+ const refCircle = refAll.filter((f) => f && f.type === "circle");
3369
+ const refFeatures = refCircle.length >= 3 ? refCircle : refAll;
3370
+ if (refFeatures.length < 3) {
3371
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u53C2\u8003\u5C42\u7279\u5F81\u70B9\u4E0D\u8DB3\uFF08${refFeatures.length}\u4E2A\uFF09\uFF0C\u8DF3\u8FC7`);
3372
+ continue;
3373
+ }
3374
+ const selectedRefFeatures = selectFeaturesWithXYDistribution(refFeatures, Math.min(80, refFeatures.length));
3375
+ const vote = estimateOffsetByVoting(selectedAnchorFeatures, selectedRefFeatures, { binSize: 0.2, maxCount: 80, minVotes: 3 });
3376
+ if (!vote) {
3377
+ console.log("[\u94BB\u5B54\u5BF9\u9F50] \u5E73\u79FB\u6295\u7968\u672A\u5F97\u5230\u7A33\u5B9A\u5019\u9009");
3378
+ } else {
3379
+ const tolLoose = 0.6;
3380
+ const tolRefine = 0.3;
3381
+ const tolTight = 0.2;
3382
+ const vRef = verifyOffset(anchorFeatures, refFeatures, vote.dx, vote.dy, tolRefine);
3383
+ if (vRef.matchCount >= 2) {
3384
+ let sumDx = 0, sumDy = 0;
3385
+ for (const p of vRef.matchedPairs) {
3386
+ sumDx += p.refPoint.x - p.drillPoint.x;
3387
+ sumDy += p.refPoint.y - p.drillPoint.y;
3388
+ }
3389
+ const refinedDx = sumDx / vRef.matchedPairs.length;
3390
+ const refinedDy = sumDy / vRef.matchedPairs.length;
3391
+ const vTight = verifyOffset(anchorFeatures, refFeatures, refinedDx, refinedDy, tolTight);
3392
+ const vLoose = verifyOffset(anchorFeatures, refFeatures, refinedDx, refinedDy, tolLoose);
3393
+ const cand = {
3394
+ dx: refinedDx,
3395
+ dy: refinedDy,
3396
+ matchTight: vTight.matchCount,
3397
+ maxErrorTight: vTight.maxError,
3398
+ avgErrorTight: vTight.avgError,
3399
+ matchLoose: vLoose.matchCount,
3400
+ maxErrorLoose: vLoose.maxError,
3401
+ avgErrorLoose: vLoose.avgError,
3402
+ refType: getLayerType2(refLayer.name),
3403
+ refTypeRank: { GTL: 0, GBL: 1, GTS: 2, GBS: 3 }[getLayerType2(refLayer.name)] ?? 9,
3404
+ refLayerName: refLayer.name,
3405
+ method: `vote(bin=${vote.binSize}, votes=${vote.votes})`
3406
+ };
3407
+ const isBetter = (a, b) => {
3408
+ if (!b) return true;
3409
+ if (a.matchTight !== b.matchTight) return a.matchTight > b.matchTight;
3410
+ if (a.avgErrorTight !== b.avgErrorTight) return a.avgErrorTight < b.avgErrorTight;
3411
+ if (a.matchLoose !== b.matchLoose) return a.matchLoose > b.matchLoose;
3412
+ if (a.avgErrorLoose !== b.avgErrorLoose) return a.avgErrorLoose < b.avgErrorLoose;
3413
+ if ((a.refTypeRank ?? 9) !== (b.refTypeRank ?? 9)) return (a.refTypeRank ?? 9) < (b.refTypeRank ?? 9);
3414
+ const sa = Math.abs(a.dx) + Math.abs(a.dy);
3415
+ const sb = Math.abs(b.dx) + Math.abs(b.dy);
3416
+ if (sa !== sb) return sa < sb;
3417
+ return false;
3418
+ };
3419
+ if (isBetter(cand, best)) {
3420
+ secondBest = best;
3421
+ best = cand;
3422
+ } else if (isBetter(cand, secondBest)) {
3423
+ secondBest = cand;
3424
+ }
3425
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u6295\u7968\u5019\u9009: dx=${cand.dx.toFixed(3)} dy=${cand.dy.toFixed(3)} tight=${cand.matchTight} loose=${cand.matchLoose} errT(avg/max)=${(cand.avgErrorTight ?? 0).toFixed(3)}/${(cand.maxErrorTight ?? 0).toFixed(3)} (${cand.method})`);
3426
+ } else {
3427
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u6295\u7968\u5019\u9009\u5339\u914D\u4E0D\u8DB3\uFF08matchRef=${vRef.matchCount}\uFF09\uFF0C\u5FFD\u7565`);
3428
+ }
3429
+ }
3430
+ }
3431
+ const totalAnchor = anchorFeatures?.length || 0;
3432
+ const minMatches = totalAnchor <= 10 ? 3 : totalAnchor <= 50 ? 5 : Math.max(8, Math.ceil(totalAnchor * 0.02));
3433
+ const bestTight = best?.matchTight ?? 0;
3434
+ const secondTight = secondBest?.matchTight ?? 0;
3435
+ const separationByCount = secondTight === 0 ? true : bestTight >= secondTight + 3 || bestTight >= Math.ceil(secondTight * 1.5);
3436
+ let separationByError = true;
3437
+ if (!separationByCount && secondBest && bestTight === secondTight && secondTight > 0) {
3438
+ const be = best?.avgErrorTight ?? Infinity;
3439
+ const se = secondBest?.avgErrorTight ?? Infinity;
3440
+ separationByError = be + 1e-6 < se * 0.8 || se - be > 0.05;
3441
+ }
3442
+ let separationOk = separationByCount || separationByError;
3443
+ if (!separationOk && best && secondBest && bestTight === secondTight && secondTight > 0) {
3444
+ const delta = Math.hypot((best.dx ?? 0) - (secondBest.dx ?? 0), (best.dy ?? 0) - (secondBest.dy ?? 0));
3445
+ if (delta < 0.05) {
3446
+ separationOk = true;
3447
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u2139\uFE0F tight \u6253\u5E73\u4F46\u504F\u79FB\u51E0\u4E4E\u4E00\u81F4(\u0394=${delta.toFixed(3)}mm)\uFF0C\u89C6\u4E3A\u53EF\u9760`);
3448
+ }
3449
+ }
3450
+ if (best && bestTight >= minMatches && separationOk) {
3451
+ featureOffset = { dx: best.dx, dy: best.dy, matchCount: bestTight, maxError: best.maxErrorTight, avgError: best.avgErrorTight, refLayerName: best.refLayerName };
3452
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u2713 \u9009\u62E9\u6700\u4F73\u53C2\u8003\u5C42: ${best.refLayerName}`);
3453
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u2713 \u7279\u5F81\u70B9\u5019\u9009\u504F\u79FB: X=${featureOffset.dx.toFixed(3)}mm, Y=${featureOffset.dy.toFixed(3)}mm (tight=${bestTight}/${totalAnchor}, secondTight=${secondTight}, errT(avg/max)=${(best.avgErrorTight ?? 0).toFixed(3)}/${(best.maxErrorTight ?? 0).toFixed(3)}mm, method=${best.method})`);
3454
+ } else {
3455
+ const bestLoose = best?.matchLoose ?? 0;
3456
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u26A0\uFE0F \u591A\u53C2\u8003\u5C42\u5C1D\u8BD5\u540E\u672A\u8FBE\u5230\u53EF\u9760\u5339\u914D\u9608\u503C\uFF1AbestTight=${bestTight}/${totalAnchor} (min=${minMatches}), secondTight=${secondTight}, separationOk=${separationOk}, bestLoose=${bestLoose}\uFF0C\u8DF3\u8FC7\u7279\u5F81\u70B9\u5BF9\u9F50`);
3457
+ }
3458
+ }
3459
+ }
3460
+ const applyThresholdMm = 0.2;
3461
+ let chosen = null;
3462
+ if (featureOffset) {
3463
+ const dx = featureOffset.dx;
3464
+ const dy = featureOffset.dy;
3465
+ if (Math.abs(dx) < applyThresholdMm && Math.abs(dy) < applyThresholdMm) {
3466
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] \u2713 \u7279\u5F81\u70B9\u5339\u914D\u663E\u793A\u94BB\u5B54\u5DF2\u5BF9\u9F50(\u0394<${applyThresholdMm}mm, match=${featureOffset.matchCount}, avgErr=${(featureOffset.avgError ?? 0).toFixed(3)}). \u8DF3\u8FC7\u5BF9\u9F50\u4E0EGKO\u515C\u5E95`);
3467
+ return;
3468
+ } else if (outlineBounds && overflowBefore) {
3469
+ const afterBounds = translateBounds(combinedDrillBounds, dx, dy);
3470
+ const overflowAfter = computeOverflow(afterBounds, outlineBounds);
3471
+ const improvement = overflowBefore.total - overflowAfter.total;
3472
+ if (improvement > 0.2 || overflowAfter.total <= overflowBefore.total + 0.1) {
3473
+ chosen = { dx, dy, tag: `feature-match (improve=${improvement.toFixed(3)}mm)` };
3474
+ } else {
3475
+ console.warn(`[\u94BB\u5B54\u5BF9\u9F50] \u26A0\uFE0F \u7279\u5F81\u70B9\u504F\u79FB\u4F1A\u8BA9\u94BB\u5B54\u66F4\u8D8A\u754C\uFF08before=${overflowBefore.total.toFixed(3)} after=${overflowAfter.total.toFixed(3)}\uFF09\uFF0C\u62D2\u7EDD\u5E76\u542F\u7528GKO\u515C\u5E95`);
3476
+ }
3477
+ } else {
3478
+ chosen = { dx, dy, tag: "feature-match (no-outline-check)" };
3479
+ }
3480
+ }
3481
+ if (!chosen && outlineBounds && overflowBefore) {
3482
+ if (overflowBefore.total <= 0.2) {
3483
+ console.log("[\u94BB\u5B54\u5BF9\u9F50] \u2713 \u94BB\u5B54\u5C42\u672A\u660E\u663E\u8D8A\u754C\uFF0C\u65E0\u9700GKO\u515C\u5E95\u5BF9\u9F50");
3484
+ } else {
3485
+ const drillCenterX = (combinedDrillBounds.minX + combinedDrillBounds.maxX) / 2;
3486
+ const drillCenterY = (combinedDrillBounds.minY + combinedDrillBounds.maxY) / 2;
3487
+ const outlineCenterX = (outlineBounds.minX + outlineBounds.maxX) / 2;
3488
+ const outlineCenterY = (outlineBounds.minY + outlineBounds.maxY) / 2;
3489
+ const candidatesX = [
3490
+ 0,
3491
+ outlineBounds.minX - combinedDrillBounds.minX,
3492
+ outlineBounds.maxX - combinedDrillBounds.maxX,
3493
+ outlineCenterX - drillCenterX
3494
+ ];
3495
+ const candidatesY = [
3496
+ 0,
3497
+ outlineBounds.minY - combinedDrillBounds.minY,
3498
+ outlineBounds.maxY - combinedDrillBounds.maxY,
3499
+ outlineCenterY - drillCenterY
3500
+ ];
3501
+ let best = { dx: 0, dy: 0, error: Infinity, shift: Infinity };
3502
+ for (const dx of candidatesX) {
3503
+ for (const dy of candidatesY) {
3504
+ const afterBounds = translateBounds(combinedDrillBounds, dx, dy);
3505
+ const ov = computeOverflow(afterBounds, outlineBounds);
3506
+ const shift = Math.abs(dx) + Math.abs(dy);
3507
+ if (ov.total < best.error - 1e-6 || Math.abs(ov.total - best.error) < 1e-6 && shift < best.shift) {
3508
+ best = { dx, dy, error: ov.total, shift };
3509
+ }
3510
+ }
3511
+ }
3512
+ const improvement = overflowBefore.total - best.error;
3513
+ console.log(`[\u94BB\u5B54\u5BF9\u9F50] GKO\u515C\u5E95\u5019\u9009\u504F\u79FB: X=${best.dx.toFixed(3)}mm, Y=${best.dy.toFixed(3)}mm (overflowAfter=${best.error.toFixed(3)}mm, improve=${improvement.toFixed(3)}mm)`);
3514
+ if ((Math.abs(best.dx) >= applyThresholdMm || Math.abs(best.dy) >= applyThresholdMm) && (improvement > 0.2 || best.error <= overflowBefore.total + 0.1)) {
3515
+ chosen = { dx: best.dx, dy: best.dy, tag: `outline-fit (improve=${improvement.toFixed(3)}mm)` };
3516
+ } else {
3517
+ console.log("[\u94BB\u5B54\u5BF9\u9F50] \u26A0\uFE0F GKO\u515C\u5E95\u504F\u79FB\u4E0D\u663E\u8457/\u8FC7\u5C0F\uFF0C\u8DF3\u8FC7");
3518
+ }
3519
+ }
3520
+ }
3521
+ if (!chosen && !outlineBounds) {
3522
+ const ref = pickReferenceForScale();
3523
+ if (ref && ref.bounds) {
3524
+ const overflowRefBefore = computeOverflow(combinedDrillBounds, ref.bounds);
3525
+ if (overflowRefBefore.total > 0.2) {
3526
+ const drillCenterX = (combinedDrillBounds.minX + combinedDrillBounds.maxX) / 2;
3527
+ const drillCenterY = (combinedDrillBounds.minY + combinedDrillBounds.maxY) / 2;
3528
+ const refCenterX = (ref.bounds.minX + ref.bounds.maxX) / 2;
3529
+ const refCenterY = (ref.bounds.minY + ref.bounds.maxY) / 2;
3530
+ const candidatesX = [
3531
+ 0,
3532
+ ref.bounds.minX - combinedDrillBounds.minX,
3533
+ ref.bounds.maxX - combinedDrillBounds.maxX,
3534
+ refCenterX - drillCenterX
3535
+ ];
3536
+ const candidatesY = [
3537
+ 0,
3538
+ ref.bounds.minY - combinedDrillBounds.minY,
3539
+ ref.bounds.maxY - combinedDrillBounds.maxY,
3540
+ refCenterY - drillCenterY
3541
+ ];
3542
+ let best = { dx: 0, dy: 0, error: Infinity, shift: Infinity };
3543
+ for (const dx of candidatesX) {
3544
+ for (const dy of candidatesY) {
3545
+ const afterBounds = translateBounds(combinedDrillBounds, dx, dy);
3546
+ const ov = computeOverflow(afterBounds, ref.bounds);
3547
+ const shift = Math.abs(dx) + Math.abs(dy);
3548
+ if (ov.total < best.error - 1e-6 || Math.abs(ov.total - best.error) < 1e-6 && shift < best.shift) {
3549
+ best = { dx, dy, error: ov.total, shift };
3550
+ }
3551
+ }
3552
+ }
3553
+ const improvement = overflowRefBefore.total - best.error;
3554
+ if ((Math.abs(best.dx) >= applyThresholdMm || Math.abs(best.dy) >= applyThresholdMm) && (improvement > 0.2 || best.error <= overflowRefBefore.total + 0.1)) {
3555
+ console.warn(`[\u94BB\u5B54\u5BF9\u9F50] \u26A0\uFE0F \u65E0Outline\u65F6\u53C2\u8003\u5C42\u56DE\u62C9: ref=${ref.name}(${ref.reason}), dx=${best.dx.toFixed(3)} dy=${best.dy.toFixed(3)} improve=${improvement.toFixed(3)}mm`);
3556
+ chosen = { dx: best.dx, dy: best.dy, tag: `ref-fit (ref=${ref.reason}, improve=${improvement.toFixed(3)}mm)` };
3557
+ }
3558
+ }
3559
+ }
3560
+ }
3561
+ if (!chosen) {
3562
+ console.log("[\u94BB\u5B54\u5BF9\u9F50] \u672A\u9009\u62E9\u4EFB\u4F55\u6709\u6548\u504F\u79FB\uFF0C\u8DF3\u8FC7");
3563
+ return;
3564
+ }
3565
+ applyOffsetToAllDrills(chosen.dx, chosen.dy, chosen.tag);
3566
+ }
3567
+ function extractFeaturesFromLayer(layer) {
3568
+ if (!layer.rawVertices) return [];
3569
+ const features = [];
3570
+ const vertices = layer.rawVertices;
3571
+ const centerCandidates = /* @__PURE__ */ new Map();
3572
+ for (let i = 0; i < vertices.length; i += 3) {
3573
+ const x = vertices[i];
3574
+ const y = vertices[i + 1];
3575
+ const key = `${Math.round(x * 100) / 100},${Math.round(y * 100) / 100}`;
3576
+ centerCandidates.set(key, (centerCandidates.get(key) || 0) + 1);
3577
+ }
3578
+ const circleThreshold = 8;
3579
+ centerCandidates.forEach((count, key) => {
3580
+ if (count >= circleThreshold) {
3581
+ const [x, y] = key.split(",").map(Number);
3582
+ features.push({ x, y, type: "circle", count });
3583
+ }
3584
+ });
3585
+ console.log(`[\u63D0\u53D6\u7279\u5F81\u70B9] ${layer.name} - \u627E\u5230 ${features.length} \u4E2A\u5706\u5FC3\u5019\u9009\u70B9 (\u9608\u503C=${circleThreshold})`);
3586
+ if (features.length > 0) {
3587
+ features.slice(0, 5).forEach((f, idx) => {
3588
+ console.log(` \u5706\u5FC3${idx + 1}: (${f.x.toFixed(3)}, ${f.y.toFixed(3)}), \u91CD\u590D\u6B21\u6570=${f.count}`);
3589
+ });
3590
+ }
3591
+ if (features.length === 0) {
3592
+ console.log(`[\u63D0\u53D6\u7279\u5F81\u70B9] ${layer.name} - \u672A\u627E\u5230\u5706\u5FC3\uFF0C\u5C1D\u8BD5\u63D0\u53D6\u552F\u4E00\u70B9`);
3593
+ const uniquePoints = /* @__PURE__ */ new Set();
3594
+ for (let i = 0; i < vertices.length; i += 3) {
3595
+ const x = Math.round(vertices[i] * 100) / 100;
3596
+ const y = Math.round(vertices[i + 1] * 100) / 100;
3597
+ uniquePoints.add(`${x},${y}`);
3598
+ }
3599
+ uniquePoints.forEach((key) => {
3600
+ const [x, y] = key.split(",").map(Number);
3601
+ features.push({ x, y, type: "point" });
3602
+ });
3603
+ console.log(`[\u63D0\u53D6\u7279\u5F81\u70B9] ${layer.name} - \u63D0\u53D6\u5230 ${features.length} \u4E2A\u552F\u4E00\u70B9`);
3604
+ }
3605
+ return features;
3606
+ }
3607
+ function selectFeaturesWithXYDistribution(features, maxCount) {
3608
+ if (features.length <= maxCount) {
3609
+ return features;
3610
+ }
3611
+ let minX = Infinity;
3612
+ let maxX = -Infinity;
3613
+ let minY = Infinity;
3614
+ let maxY = -Infinity;
3615
+ for (const feature of features) {
3616
+ minX = Math.min(minX, feature.x);
3617
+ maxX = Math.max(maxX, feature.x);
3618
+ minY = Math.min(minY, feature.y);
3619
+ maxY = Math.max(maxY, feature.y);
3620
+ }
3621
+ const rangeX = maxX - minX;
3622
+ const rangeY = maxY - minY;
3623
+ if (rangeX < 1e-3 || rangeY < 1e-3) {
3624
+ features.sort((a, b) => {
3625
+ if (Math.abs(a.y - b.y) > 1e-4) {
3626
+ return a.y - b.y;
3627
+ }
3628
+ return a.x - b.x;
3629
+ });
3630
+ return features.slice(0, maxCount);
3631
+ }
3632
+ const selectedFeatures = [];
3633
+ const usedFeatures = /* @__PURE__ */ new Set();
3634
+ const cornerRegionSize = 0.33;
3635
+ const corners = [
3636
+ { name: "\u5DE6\u4E0B\u89D2", xMin: minX, xMax: minX + rangeX * cornerRegionSize, yMin: minY, yMax: minY + rangeY * cornerRegionSize },
3637
+ { name: "\u53F3\u4E0B\u89D2", xMin: maxX - rangeX * cornerRegionSize, xMax: maxX, yMin: minY, yMax: minY + rangeY * cornerRegionSize },
3638
+ { name: "\u5DE6\u4E0A\u89D2", xMin: minX, xMax: minX + rangeX * cornerRegionSize, yMin: maxY - rangeY * cornerRegionSize, yMax: maxY },
3639
+ { name: "\u53F3\u4E0A\u89D2", xMin: maxX - rangeX * cornerRegionSize, xMax: maxX, yMin: maxY - rangeY * cornerRegionSize, yMax: maxY }
3640
+ ];
3641
+ for (const corner of corners) {
3642
+ const cornerFeatures = features.filter(
3643
+ (f) => f.x >= corner.xMin && f.x <= corner.xMax && f.y >= corner.yMin && f.y <= corner.yMax && !usedFeatures.has(f)
3644
+ );
3645
+ if (cornerFeatures.length > 0) {
3646
+ let cornerCenterX, cornerCenterY;
3647
+ if (corner.name === "\u5DE6\u4E0B\u89D2") {
3648
+ cornerCenterX = minX;
3649
+ cornerCenterY = minY;
3650
+ } else if (corner.name === "\u53F3\u4E0B\u89D2") {
3651
+ cornerCenterX = maxX;
3652
+ cornerCenterY = minY;
3653
+ } else if (corner.name === "\u5DE6\u4E0A\u89D2") {
3654
+ cornerCenterX = minX;
3655
+ cornerCenterY = maxY;
3656
+ } else {
3657
+ cornerCenterX = maxX;
3658
+ cornerCenterY = maxY;
3659
+ }
3660
+ const circleFeatures = cornerFeatures.filter((f) => f.type === "circle");
3661
+ let closestFeature = null;
3662
+ let minDistance = Infinity;
3663
+ const featuresToCheck = circleFeatures.length > 0 ? circleFeatures : cornerFeatures;
3664
+ for (const feature of featuresToCheck) {
3665
+ const distance = Math.sqrt(
3666
+ Math.pow(feature.x - cornerCenterX, 2) + Math.pow(feature.y - cornerCenterY, 2)
3667
+ );
3668
+ if (distance < minDistance) {
3669
+ minDistance = distance;
3670
+ closestFeature = feature;
3671
+ }
3672
+ }
3673
+ if (closestFeature) {
3674
+ selectedFeatures.push(closestFeature);
3675
+ usedFeatures.add(closestFeature);
3676
+ }
3677
+ }
3678
+ }
3679
+ console.log(`[\u7279\u5F81\u70B9\u9009\u62E9] \u56DB\u4E2A\u89D2\u5171\u9009\u62E9 ${selectedFeatures.length} \u4E2A\u7279\u5F81\u70B9`);
3680
+ if (selectedFeatures.length < maxCount) {
3681
+ const remainingFeatures = features.filter((f) => !usedFeatures.has(f));
3682
+ if (remainingFeatures.length > 0) {
3683
+ const gridSize = Math.ceil(Math.sqrt(maxCount - selectedFeatures.length));
3684
+ const cellWidth = rangeX / gridSize;
3685
+ const cellHeight = rangeY / gridSize;
3686
+ const grid = Array(gridSize).fill(null).map(() => Array(gridSize).fill(null).map(() => []));
3687
+ for (const feature of remainingFeatures) {
3688
+ const gridX = Math.min(Math.floor((feature.x - minX) / cellWidth), gridSize - 1);
3689
+ const gridY = Math.min(Math.floor((feature.y - minY) / cellHeight), gridSize - 1);
3690
+ grid[gridY][gridX].push(feature);
3691
+ }
3692
+ for (let y = 0; y < gridSize; y++) {
3693
+ for (let x = 0; x < gridSize; x++) {
3694
+ const cellFeatures = grid[y][x];
3695
+ if (cellFeatures.length > 0) {
3696
+ const cellCenterX = minX + (x + 0.5) * cellWidth;
3697
+ const cellCenterY = minY + (y + 0.5) * cellHeight;
3698
+ let closestFeature = cellFeatures[0];
3699
+ let minDistance = Infinity;
3700
+ for (const feature of cellFeatures) {
3701
+ const distance = Math.sqrt(
3702
+ Math.pow(feature.x - cellCenterX, 2) + Math.pow(feature.y - cellCenterY, 2)
3703
+ );
3704
+ if (distance < minDistance) {
3705
+ minDistance = distance;
3706
+ closestFeature = feature;
3707
+ }
3708
+ }
3709
+ selectedFeatures.push(closestFeature);
3710
+ usedFeatures.add(closestFeature);
3711
+ if (selectedFeatures.length >= maxCount) {
3712
+ break;
3713
+ }
3714
+ }
3715
+ }
3716
+ if (selectedFeatures.length >= maxCount) {
3717
+ break;
3718
+ }
3719
+ }
3720
+ console.log(`[\u7279\u5F81\u70B9\u9009\u62E9] \u4ECE\u5269\u4F59\u533A\u57DF\u8865\u5145 ${selectedFeatures.length - (selectedFeatures.length > 4 ? 4 : selectedFeatures.length)} \u4E2A\u7279\u5F81\u70B9`);
3721
+ }
3722
+ }
3723
+ console.log(`[\u7279\u5F81\u70B9\u9009\u62E9] \u6700\u7EC8\u4ECE ${features.length} \u4E2A\u7279\u5F81\u70B9\u4E2D\u9009\u62E9 ${selectedFeatures.length} \u4E2A`);
3724
+ return selectedFeatures;
3725
+ }
3726
+ function verifyOffset(drillFeatures, refFeatures, offsetX, offsetY, tolerance) {
3727
+ let matchCount = 0;
3728
+ let maxError = 0;
3729
+ let sumError = 0;
3730
+ const matchedPairs = [];
3731
+ const usedRefIndices = /* @__PURE__ */ new Set();
3732
+ for (const drillPoint of drillFeatures) {
3733
+ const transformedX = drillPoint.x + offsetX;
3734
+ const transformedY = drillPoint.y + offsetY;
3735
+ let minDistance = Infinity;
3736
+ let nearestRefPoint = null;
3737
+ let nearestRefIndex = -1;
3738
+ for (let i = 0; i < refFeatures.length; i++) {
3739
+ if (usedRefIndices.has(i)) continue;
3740
+ const refPoint = refFeatures[i];
3741
+ const distance = Math.sqrt(
3742
+ Math.pow(transformedX - refPoint.x, 2) + Math.pow(transformedY - refPoint.y, 2)
3743
+ );
3744
+ if (distance < minDistance) {
3745
+ minDistance = distance;
3746
+ nearestRefPoint = refPoint;
3747
+ nearestRefIndex = i;
3748
+ }
3749
+ }
3750
+ if (minDistance <= tolerance && nearestRefIndex !== -1) {
3751
+ matchCount++;
3752
+ maxError = Math.max(maxError, minDistance);
3753
+ sumError += minDistance;
3754
+ usedRefIndices.add(nearestRefIndex);
3755
+ matchedPairs.push({
3756
+ drillPoint,
3757
+ refPoint: nearestRefPoint,
3758
+ error: minDistance
3759
+ });
3760
+ }
3761
+ }
3762
+ const avgError = matchCount > 0 ? sumError / matchCount : Infinity;
3763
+ return { matchCount, maxError, avgError, matchedPairs };
3764
+ }
3765
+ function estimateOffsetByVoting(drillFeatures, refFeatures, options = {}) {
3766
+ const binSize = options.binSize ?? 0.2;
3767
+ const maxCount = options.maxCount ?? 80;
3768
+ const minVotes = options.minVotes ?? 3;
3769
+ if (!Array.isArray(drillFeatures) || !Array.isArray(refFeatures)) return null;
3770
+ if (drillFeatures.length === 0 || refFeatures.length === 0) return null;
3771
+ const drillSample = selectFeaturesWithXYDistribution(drillFeatures, Math.min(maxCount, drillFeatures.length));
3772
+ const refSample = selectFeaturesWithXYDistribution(refFeatures, Math.min(maxCount, refFeatures.length));
3773
+ const bins = /* @__PURE__ */ new Map();
3774
+ for (const d of drillSample) {
3775
+ for (const r of refSample) {
3776
+ const dx2 = r.x - d.x;
3777
+ const dy2 = r.y - d.y;
3778
+ const qx = Math.round(dx2 / binSize);
3779
+ const qy = Math.round(dy2 / binSize);
3780
+ const key = `${qx},${qy}`;
3781
+ const cur = bins.get(key) || { count: 0, sumDx: 0, sumDy: 0 };
3782
+ cur.count += 1;
3783
+ cur.sumDx += dx2;
3784
+ cur.sumDy += dy2;
3785
+ bins.set(key, cur);
3786
+ }
3787
+ }
3788
+ let bestKey = null;
3789
+ let best = null;
3790
+ for (const [k, v] of bins.entries()) {
3791
+ if (!best || v.count > best.count) {
3792
+ best = v;
3793
+ bestKey = k;
3794
+ }
3795
+ }
3796
+ if (!best || best.count < minVotes) return null;
3797
+ const dx = best.sumDx / best.count;
3798
+ const dy = best.sumDy / best.count;
3799
+ return { dx, dy, votes: best.count, binSize, key: bestKey };
3800
+ }
3801
+ function centerView() {
3802
+ let viewBounds = null;
3803
+ let useLargePadding = false;
3804
+ const gkoLayer = layers.find((layer) => {
3805
+ const fileName2 = layer.name;
3806
+ const fileNameOnly = fileName2.split(/[/\\]/).pop();
3807
+ const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
3808
+ const ext = fileExtension.substring(1);
3809
+ const display = getLayerDisplayName(fileName2);
3810
+ return display === "GKO" || ext === "GKO" || fileNameOnly.includes("_Profile") || fileNameOnly.toLowerCase().includes("outline");
3811
+ });
3812
+ const gtlLayer = layers.find((layer) => {
3813
+ const fileName2 = layer.name;
3814
+ const fileNameOnly = fileName2.split(/[/\\]/).pop();
3815
+ const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
3816
+ const ext = fileExtension.substring(1);
3817
+ const display = getLayerDisplayName(fileName2);
3818
+ return display === "GTL" || ext === "GTL" || fileNameOnly.includes("_TOP") || fileNameOnly.includes("_Copper_Signal_Top");
3819
+ });
3820
+ if (gkoLayer && gkoLayer.bounds && gtlLayer && gtlLayer.bounds) {
3821
+ const gkoBounds = gkoLayer.bounds;
3822
+ const gtlBounds = gtlLayer.bounds;
3823
+ const gkoWidth = gkoBounds.maxX - gkoBounds.minX;
3824
+ const gkoHeight = gkoBounds.maxY - gkoBounds.minY;
3825
+ const gkoSize = Math.max(gkoWidth, gkoHeight);
3826
+ const gtlWidth = gtlBounds.maxX - gtlBounds.minX;
3827
+ const gtlHeight = gtlBounds.maxY - gtlBounds.minY;
3828
+ const gtlSize = Math.max(gtlWidth, gtlHeight);
3829
+ const ratio = gtlSize > 0 ? gkoSize / gtlSize : Infinity;
3830
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] GKO\u5C42\u5927\u5C0F: ${gkoSize.toFixed(6)}, GTL\u5C42\u5927\u5C0F: ${gtlSize.toFixed(6)}`);
3831
+ if (Number.isFinite(ratio) && ratio > 5) {
3832
+ viewBounds = { ...gtlBounds };
3833
+ console.warn(`[\u89C6\u56FE\u5C45\u4E2D] \u26A0\uFE0F GKO\u5C3A\u5BF8\u8FDC\u5927\u4E8EGTL (ratio=${ratio.toFixed(2)} > 5)\uFF0C\u4E3A\u907F\u514D\u89C6\u56FE\u88AB\u5F02\u5E38\u6491\u5927\uFF0C\u6539\u4E3A\u4F7F\u7528GTL\u5C42bounds\u8BBE\u7F6E\u89C6\u56FE`);
3834
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] GTL: minX=${gtlBounds.minX.toFixed(6)}, minY=${gtlBounds.minY.toFixed(6)}, maxX=${gtlBounds.maxX.toFixed(6)}, maxY=${gtlBounds.maxY.toFixed(6)}`);
3835
+ } else {
3836
+ viewBounds = {
3837
+ minX: Math.min(gkoBounds.minX, gtlBounds.minX),
3838
+ minY: Math.min(gkoBounds.minY, gtlBounds.minY),
3839
+ maxX: Math.max(gkoBounds.maxX, gtlBounds.maxX),
3840
+ maxY: Math.max(gkoBounds.maxY, gtlBounds.maxY)
3841
+ };
3842
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] \u4F7F\u7528GKO\u548CGTL\u5C42\u7684\u5408\u5E76\u8FB9\u754C\u8BBE\u7F6E\u89C6\u56FE`);
3843
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] GKO: minX=${gkoBounds.minX.toFixed(6)}, minY=${gkoBounds.minY.toFixed(6)}, maxX=${gkoBounds.maxX.toFixed(6)}, maxY=${gkoBounds.maxY.toFixed(6)}`);
3844
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] GTL: minX=${gtlBounds.minX.toFixed(6)}, minY=${gtlBounds.minY.toFixed(6)}, maxX=${gtlBounds.maxX.toFixed(6)}, maxY=${gtlBounds.maxY.toFixed(6)}`);
3845
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] \u5408\u5E76: minX=${viewBounds.minX.toFixed(6)}, minY=${viewBounds.minY.toFixed(6)}, maxX=${viewBounds.maxX.toFixed(6)}, maxY=${viewBounds.maxY.toFixed(6)}`);
3846
+ }
3847
+ } else if (gkoLayer && gkoLayer.bounds) {
3848
+ viewBounds = { ...gkoLayer.bounds };
3849
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] \u4F7F\u7528GKO\u5C42 (${gkoLayer.name}) \u7684bounds\u8BBE\u7F6E\u89C6\u56FE`);
3850
+ } else if (gtlLayer && gtlLayer.bounds) {
3851
+ viewBounds = { ...gtlLayer.bounds };
3852
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] \u4F7F\u7528GTL\u5C42 (${gtlLayer.name}) \u7684bounds\u8BBE\u7F6E\u89C6\u56FE`);
3853
+ } else {
3854
+ const gtoLayer = layers.find((layer) => {
3855
+ const fileName2 = layer.name;
3856
+ const fileNameOnly = fileName2.split(/[/\\]/).pop();
3857
+ const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
3858
+ const ext = fileExtension.substring(1);
3859
+ const display = getLayerDisplayName(fileName2);
3860
+ return display === "GTO" || ext === "GTO" || fileNameOnly.includes("_Legend_Top") || fileNameOnly.toLowerCase().includes("legend") && fileNameOnly.toLowerCase().includes("top") || fileNameOnly.toLowerCase().includes("silkscreen") && fileNameOnly.toLowerCase().includes("top");
3861
+ });
3862
+ if (gtoLayer && gtoLayer.bounds) {
3863
+ viewBounds = { ...gtoLayer.bounds };
3864
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] \u4F7F\u7528GTO\u5C42 (${gtoLayer.name}) \u7684bounds\u8BBE\u7F6E\u89C6\u56FE`);
3865
+ } else {
3866
+ const artLayer = layers.find((layer) => {
3867
+ const fileName2 = layer.name;
3868
+ const fileNameOnly = fileName2.split(/[/\\]/).pop();
3869
+ return fileNameOnly.toLowerCase().startsWith("art");
3870
+ });
3871
+ if (artLayer && artLayer.bounds) {
3872
+ viewBounds = { ...artLayer.bounds };
3873
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] \u4F7F\u7528art\u5F00\u5934\u6587\u4EF6 (${artLayer.name}) \u7684bounds\u8BBE\u7F6E\u89C6\u56FE`);
3874
+ } else {
3875
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
3876
+ layers.forEach((layer) => {
3877
+ if (layer.bounds) {
3878
+ minX = Math.min(minX, layer.bounds.minX);
3879
+ maxX = Math.max(maxX, layer.bounds.maxX);
3880
+ minY = Math.min(minY, layer.bounds.minY);
3881
+ maxY = Math.max(maxY, layer.bounds.maxY);
3882
+ }
3883
+ });
3884
+ viewBounds = { minX, maxX, minY, maxY };
3885
+ useLargePadding = true;
3886
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] \u672A\u627E\u5230GKO/GTL/GTO/art\u5F00\u5934\u6587\u4EF6\uFF0C\u4F7F\u7528\u6240\u6709\u56FE\u5C42\u8FB9\u754C\uFF08\u5927\u8FB9\u8DDD\uFF09`);
3887
+ }
3888
+ }
3889
+ }
3890
+ if (!viewBounds) return;
3891
+ const width = viewBounds.maxX - viewBounds.minX;
3892
+ const height = viewBounds.maxY - viewBounds.minY;
3893
+ if (width <= 0 || height <= 0) {
3894
+ console.warn(`[\u89C6\u56FE\u5C45\u4E2D] \u65E0\u6548\u8FB9\u754C: width=${width}, height=${height}`);
3895
+ return;
3896
+ }
3897
+ const paddingRatio = useLargePadding ? 0.4 : 0.1;
3898
+ const padding = Math.max(width, height) * paddingRatio;
3899
+ const paddedWidth = width + padding * 2;
3900
+ const paddedHeight = height + padding * 2;
3901
+ const cx = (viewBounds.minX + viewBounds.maxX) / 2;
3902
+ const cy = (viewBounds.minY + viewBounds.maxY) / 2;
3903
+ const aspect = canvas.width / canvas.height;
3904
+ const contentAspect = paddedWidth / paddedHeight;
3905
+ if (contentAspect > aspect) {
3906
+ scale = 2 / paddedWidth * 1.4;
3907
+ } else {
3908
+ scale = 2 / paddedHeight / aspect * 1.4;
3909
+ }
3910
+ transX = -cx * (scale / aspect);
3911
+ transY = -cy * scale;
3912
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] \u8FB9\u754C: [${viewBounds.minX.toFixed(2)}, ${viewBounds.maxX.toFixed(2)}] x [${viewBounds.minY.toFixed(2)}, ${viewBounds.maxY.toFixed(2)}]`);
3913
+ console.log(`[\u89C6\u56FE\u5C45\u4E2D] \u7F29\u653E: ${scale.toFixed(6)}, \u5E73\u79FB: (${transX.toFixed(6)}, ${transY.toFixed(6)})`);
3914
+ }
3915
+ async function handleFileSelectionForDirect(files) {
3916
+ if (files.length === 0) return;
3917
+ const file = files[0];
3918
+ if (!file) return;
3919
+ const dataTransfer = new DataTransfer();
3920
+ dataTransfer.items.add(file);
3921
+ fileInput.files = dataTransfer.files;
3922
+ const event = new Event("change", { bubbles: true });
3923
+ fileInput.dispatchEvent(event);
3924
+ }
3925
+ window.addEventListener("load", () => {
3926
+ initUrlIdHandler(handleFileSelectionForDirect, showStatus);
3927
+ const baseUrl = "/";
3928
+ const buildPageUrl = (path) => `${baseUrl}${path.replace(/^\/+/, "")}`;
3929
+ const getTargetUrl = (basePath) => {
3930
+ const targetBase = buildPageUrl(basePath);
3931
+ const id = parseUrlId();
3932
+ if (id) return `${targetBase}?id=${encodeURIComponent(id)}`;
3933
+ const url = parseUrlFileUrl();
3934
+ if (url) return `${targetBase}?url=${encodeURIComponent(url)}`;
3935
+ return targetBase;
3936
+ };
3937
+ const mode3DBtn = document.querySelector("#mode3DBtnId");
3938
+ if (mode3DBtn) {
3939
+ mode3DBtn.addEventListener("click", () => {
3940
+ window.location.href = getTargetUrl("/3dPage");
3941
+ });
3942
+ }
3943
+ const modeSimulationBtn = document.querySelector("#modeSimulationBtnId");
3944
+ if (modeSimulationBtn) {
3945
+ modeSimulationBtn.addEventListener("click", () => {
3946
+ window.location.href = getTargetUrl("/simulation");
3947
+ });
3948
+ }
3949
+ });
3950
+ //# sourceMappingURL=gerber-2d-entry-OQ4SQRBY.js.map