@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.
- package/CHANGELOG.md +92 -0
- package/README.md +119 -22
- package/dist/ExcelCore-DJOIVQMI.js +11 -0
- package/dist/ExcelCore-DJOIVQMI.js.map +1 -0
- package/dist/ExcelViewer-3YLLYYIQ.js +65 -0
- package/dist/ExcelViewer-3YLLYYIQ.js.map +1 -0
- package/dist/GerberViewerA2UI-7CNT7HX4.css +693 -0
- package/dist/GerberViewerA2UI-7CNT7HX4.css.map +1 -0
- package/dist/GerberViewerA2UI-X5FWAD5M.js +57 -0
- package/dist/GerberViewerA2UI-X5FWAD5M.js.map +1 -0
- package/dist/GraphStatsLegend-D5bPeXB_.d.cts +607 -0
- package/dist/GraphStatsLegend-D5bPeXB_.d.ts +607 -0
- package/dist/JsonRenderStandalone-EIZM62JU.js +18 -0
- package/dist/JsonRenderStandalone-EIZM62JU.js.map +1 -0
- package/dist/JsonRenderStandalone-POB4Q3N3.css +2384 -0
- package/dist/JsonRenderStandalone-POB4Q3N3.css.map +1 -0
- package/dist/JsonRenderStandalone-UsTcST4G.d.cts +23 -0
- package/dist/JsonRenderStandalone-UsTcST4G.d.ts +23 -0
- package/dist/KicadViewer-GV6ZC4AQ.js +124 -0
- package/dist/KicadViewer-GV6ZC4AQ.js.map +1 -0
- package/dist/KicadViewerCore-U7BWZHKJ.js +11 -0
- package/dist/KicadViewerCore-U7BWZHKJ.js.map +1 -0
- package/dist/PdfViewer-CHPDRK46.js +51 -0
- package/dist/PdfViewer-CHPDRK46.js.map +1 -0
- package/dist/PdfViewer-LPYGQETK.css +1899 -0
- package/dist/PdfViewer-LPYGQETK.css.map +1 -0
- package/dist/PdfViewerCore-HJPEHSRA.js +364 -0
- package/dist/PdfViewerCore-HJPEHSRA.js.map +1 -0
- package/dist/PowerPointCore-FPDR2BL4.js +11 -0
- package/dist/PowerPointCore-FPDR2BL4.js.map +1 -0
- package/dist/PowerPointViewer-LQTO6UCU.js +61 -0
- package/dist/PowerPointViewer-LQTO6UCU.js.map +1 -0
- package/dist/StepViewerCore-7W3L3R4E.js +285 -0
- package/dist/StepViewerCore-7W3L3R4E.js.map +1 -0
- package/dist/ThreeViewerCore-N3QJD5QI.js +161 -0
- package/dist/ThreeViewerCore-N3QJD5QI.js.map +1 -0
- package/dist/WordCore-JKSXK2XD.js +11 -0
- package/dist/WordCore-JKSXK2XD.js.map +1 -0
- package/dist/WordViewer-ZHCQMHOH.js +61 -0
- package/dist/WordViewer-ZHCQMHOH.js.map +1 -0
- package/dist/chunk-2SKA3F5U.js +88 -0
- package/dist/chunk-2SKA3F5U.js.map +1 -0
- package/dist/chunk-2UC7YLVX.js +318 -0
- package/dist/chunk-2UC7YLVX.js.map +1 -0
- package/dist/chunk-3R6T3LBR.js +24 -0
- package/dist/chunk-3R6T3LBR.js.map +1 -0
- package/dist/chunk-56WRZM3R.js +398 -0
- package/dist/chunk-56WRZM3R.js.map +1 -0
- package/dist/chunk-7A4FY6FK.js +10226 -0
- package/dist/chunk-7A4FY6FK.js.map +1 -0
- package/dist/chunk-7D4SUZUM.js +38 -0
- package/dist/chunk-7D4SUZUM.js.map +1 -0
- package/dist/chunk-7S67DOHQ.js +436 -0
- package/dist/chunk-7S67DOHQ.js.map +1 -0
- package/dist/chunk-CFKGNAJM.js +14013 -0
- package/dist/chunk-CFKGNAJM.js.map +1 -0
- package/dist/chunk-GAMA3VA7.js +99 -0
- package/dist/chunk-GAMA3VA7.js.map +1 -0
- package/dist/chunk-GYXTSY22.js +639 -0
- package/dist/chunk-GYXTSY22.js.map +1 -0
- package/dist/chunk-K4KGNVL5.js +77 -0
- package/dist/chunk-K4KGNVL5.js.map +1 -0
- package/dist/chunk-KQV7IKET.js +1621 -0
- package/dist/chunk-KQV7IKET.js.map +1 -0
- package/dist/chunk-O3NXUM6C.js +1871 -0
- package/dist/chunk-O3NXUM6C.js.map +1 -0
- package/dist/chunk-PZXSASDY.js +83 -0
- package/dist/chunk-PZXSASDY.js.map +1 -0
- package/dist/chunk-QLVPIM6R.js +595 -0
- package/dist/chunk-QLVPIM6R.js.map +1 -0
- package/dist/chunk-VXJWGLZ7.js +21 -0
- package/dist/chunk-VXJWGLZ7.js.map +1 -0
- package/dist/chunk-XQ562W7I.js +116 -0
- package/dist/chunk-XQ562W7I.js.map +1 -0
- package/dist/components/JsonRender/standalone.cjs +39368 -0
- package/dist/components/JsonRender/standalone.cjs.map +1 -0
- package/dist/components/JsonRender/standalone.css +2384 -0
- package/dist/components/JsonRender/standalone.css.map +1 -0
- package/dist/components/JsonRender/standalone.d.cts +16 -0
- package/dist/components/JsonRender/standalone.d.ts +16 -0
- package/dist/components/JsonRender/standalone.js +38 -0
- package/dist/components/JsonRender/standalone.js.map +1 -0
- package/dist/gerber-2d-entry-OQ4SQRBY.js +3950 -0
- package/dist/gerber-2d-entry-OQ4SQRBY.js.map +1 -0
- package/dist/gerber-3d-entry-DEHDBOO2.js +3679 -0
- package/dist/gerber-3d-entry-DEHDBOO2.js.map +1 -0
- package/dist/gerber-simulation-entry-EBDX72XE.js +1801 -0
- package/dist/gerber-simulation-entry-EBDX72XE.js.map +1 -0
- package/dist/index.cjs +60113 -2970
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +11342 -1708
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +3275 -77
- package/dist/index.d.ts +3275 -77
- package/dist/index.js +18078 -2540
- package/dist/index.js.map +1 -1
- package/dist/provenance/index.cjs +2248 -0
- package/dist/provenance/index.cjs.map +1 -0
- package/dist/provenance/index.css +52 -0
- package/dist/provenance/index.css.map +1 -0
- package/dist/provenance/index.d.cts +12 -0
- package/dist/provenance/index.d.ts +12 -0
- package/dist/provenance/index.js +27 -0
- package/dist/provenance/index.js.map +1 -0
- package/dist/resolveToArrayBuffer-AQIDZHSQ.js +23 -0
- package/dist/resolveToArrayBuffer-AQIDZHSQ.js.map +1 -0
- package/package.json +96 -17
|
@@ -0,0 +1,3679 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GerberParser
|
|
3
|
+
} from "./chunk-O3NXUM6C.js";
|
|
4
|
+
import {
|
|
5
|
+
extractRar,
|
|
6
|
+
fetchFileById,
|
|
7
|
+
fetchFileByUrl,
|
|
8
|
+
parseUrlFileUrl,
|
|
9
|
+
parseUrlId,
|
|
10
|
+
require_lib
|
|
11
|
+
} from "./chunk-7A4FY6FK.js";
|
|
12
|
+
import {
|
|
13
|
+
__toESM
|
|
14
|
+
} from "./chunk-7D4SUZUM.js";
|
|
15
|
+
|
|
16
|
+
// src/components/jetPaveGerberViewer/src/viewer-src/3dPage/js/main.js
|
|
17
|
+
var import_jszip = __toESM(require_lib(), 1);
|
|
18
|
+
import * as THREE2 from "three";
|
|
19
|
+
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
20
|
+
import earcut from "earcut";
|
|
21
|
+
|
|
22
|
+
// src/components/jetPaveGerberViewer/src/viewer-src/3dPage/js/gerber-parser-direct.js
|
|
23
|
+
import * as THREE from "three";
|
|
24
|
+
var GerberOutlineParser = class {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.shapes = [];
|
|
27
|
+
this.strokeSegments = [];
|
|
28
|
+
this.state = {
|
|
29
|
+
x: 0,
|
|
30
|
+
y: 0,
|
|
31
|
+
i: 0,
|
|
32
|
+
j: 0,
|
|
33
|
+
d: 0,
|
|
34
|
+
lastD: null,
|
|
35
|
+
// 🚀 Modal D-code
|
|
36
|
+
g: 1,
|
|
37
|
+
// 1: linear, 2: cw, 3: ccw, 36: region start, 37: region end
|
|
38
|
+
interpolation: "linear",
|
|
39
|
+
regionMode: false,
|
|
40
|
+
quadrant: "multi",
|
|
41
|
+
unit: "mm",
|
|
42
|
+
format: {
|
|
43
|
+
integer: 2,
|
|
44
|
+
decimal: 4,
|
|
45
|
+
zero: "L"
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
this.currentRegionContours = [];
|
|
49
|
+
this._currentRegionContour = null;
|
|
50
|
+
this.segments = [];
|
|
51
|
+
}
|
|
52
|
+
parse(content) {
|
|
53
|
+
this.shapes = [];
|
|
54
|
+
this.segments = [];
|
|
55
|
+
this.currentRegionContours = [];
|
|
56
|
+
this.strokeSegments = [];
|
|
57
|
+
const lines = content.split(/[\n\r]+/);
|
|
58
|
+
const MAX_SEGMENTS_DURING_PARSE = 3e5;
|
|
59
|
+
let abortedDueToSize = false;
|
|
60
|
+
for (let line of lines) {
|
|
61
|
+
if (this.segments.length > MAX_SEGMENTS_DURING_PARSE || this.strokeSegments.length > MAX_SEGMENTS_DURING_PARSE) {
|
|
62
|
+
abortedDueToSize = true;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
line = line.trim();
|
|
66
|
+
if (!line) continue;
|
|
67
|
+
if (line.startsWith("%") && line.endsWith("%")) {
|
|
68
|
+
this.parseExtendedCommand(line.substring(1, line.length - 1));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const commands = line.split("*");
|
|
72
|
+
for (let cmd of commands) {
|
|
73
|
+
cmd = cmd.trim();
|
|
74
|
+
if (!cmd) continue;
|
|
75
|
+
this.parseCommand(cmd);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (abortedDueToSize) {
|
|
79
|
+
console.warn(`[GerberOutline] \u89E3\u6790\u4E2D\u6B62\uFF1A\u7EBF\u6BB5\u6570\u91CF\u8FC7\u591A (segments=${this.segments.length}, strokes=${this.strokeSegments.length})\uFF0C\u8DF3\u8FC7\u590D\u6742\u89E3\u6790`);
|
|
80
|
+
return { shapes: [], strokes: [] };
|
|
81
|
+
}
|
|
82
|
+
if (this.currentRegionContours.length > 0) {
|
|
83
|
+
this.processRegions();
|
|
84
|
+
}
|
|
85
|
+
if (this.segments.length > 0) {
|
|
86
|
+
this.stitchSegmentsToShapes();
|
|
87
|
+
}
|
|
88
|
+
if (this.strokeSegments.length >= 1e5) {
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
shapes: this.shapes,
|
|
92
|
+
strokes: this.strokeSegments
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
parseExtendedCommand(cmd) {
|
|
96
|
+
if (cmd.startsWith("FS")) {
|
|
97
|
+
const match = cmd.match(/FS([LT])?[AI]X(\d)(\d)Y(\d)(\d)/);
|
|
98
|
+
if (match) {
|
|
99
|
+
this.state.format.zero = match[1] || "L";
|
|
100
|
+
this.state.format.integer = parseInt(match[2]);
|
|
101
|
+
this.state.format.decimal = parseInt(match[3]);
|
|
102
|
+
}
|
|
103
|
+
} else if (cmd.startsWith("MO")) {
|
|
104
|
+
this.state.unit = cmd.includes("IN") ? "inch" : "mm";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
parseCommand(cmd) {
|
|
108
|
+
if (cmd.startsWith("G")) {
|
|
109
|
+
const gCode = parseInt(cmd.substring(1, 3));
|
|
110
|
+
if (gCode === 1) this.state.interpolation = "linear";
|
|
111
|
+
if (gCode === 2) this.state.interpolation = "cw";
|
|
112
|
+
if (gCode === 3) this.state.interpolation = "ccw";
|
|
113
|
+
if (gCode === 36) {
|
|
114
|
+
this.state.regionMode = true;
|
|
115
|
+
this.currentRegionContours = [];
|
|
116
|
+
this._currentRegionContour = null;
|
|
117
|
+
}
|
|
118
|
+
if (gCode === 37) {
|
|
119
|
+
this.state.regionMode = false;
|
|
120
|
+
this.finishRegion();
|
|
121
|
+
}
|
|
122
|
+
if (cmd.length > 3) this.parseCoordinate(cmd.substring(3));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (cmd.startsWith("D") && parseInt(cmd.substring(1)) >= 10) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.parseCoordinate(cmd);
|
|
129
|
+
}
|
|
130
|
+
parseCoordinate(cmd) {
|
|
131
|
+
let x = this.state.x;
|
|
132
|
+
let y = this.state.y;
|
|
133
|
+
let iOffset = 0;
|
|
134
|
+
let jOffset = 0;
|
|
135
|
+
let d = null;
|
|
136
|
+
const xMatch = cmd.match(/X([-+]?\d+)/);
|
|
137
|
+
if (xMatch) x = this.parseValue(xMatch[1]);
|
|
138
|
+
const yMatch = cmd.match(/Y([-+]?\d+)/);
|
|
139
|
+
if (yMatch) y = this.parseValue(yMatch[1]);
|
|
140
|
+
const iMatch = cmd.match(/I([-+]?\d+)/);
|
|
141
|
+
if (iMatch) iOffset = this.parseValue(iMatch[1]);
|
|
142
|
+
const jMatch = cmd.match(/J([-+]?\d+)/);
|
|
143
|
+
if (jMatch) jOffset = this.parseValue(jMatch[1]);
|
|
144
|
+
const dMatch = cmd.match(/D(\d+)/);
|
|
145
|
+
if (dMatch) {
|
|
146
|
+
d = parseInt(dMatch[1]);
|
|
147
|
+
if (d === 1 || d === 2) {
|
|
148
|
+
this.state.lastD = d;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
if ((xMatch || yMatch || iMatch || jMatch) && this.state.lastD) {
|
|
152
|
+
d = this.state.lastD;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const prevX = this.state.x;
|
|
156
|
+
const prevY = this.state.y;
|
|
157
|
+
this.state.x = x;
|
|
158
|
+
this.state.y = y;
|
|
159
|
+
if (d !== null) {
|
|
160
|
+
if (d === 1) {
|
|
161
|
+
if (this.state.regionMode) {
|
|
162
|
+
if (this.state.interpolation === "linear") {
|
|
163
|
+
if (!this._currentRegionContour) this._startRegionContour(prevX, prevY);
|
|
164
|
+
this._currentRegionContour.push({ x, y });
|
|
165
|
+
} else {
|
|
166
|
+
if (!this._currentRegionContour) this._startRegionContour(prevX, prevY);
|
|
167
|
+
this.addArcToRegion(prevX, prevY, x, y, iOffset, jOffset, this.state.interpolation === "cw");
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
this.addSegment(prevX, prevY, x, y, iOffset, jOffset, this.state.interpolation);
|
|
171
|
+
}
|
|
172
|
+
} else if (d === 2) {
|
|
173
|
+
if (this.state.regionMode) {
|
|
174
|
+
this._startRegionContour(x, y);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
parseValue(str) {
|
|
180
|
+
let scale = Math.pow(10, this.state.format.decimal);
|
|
181
|
+
let val = parseFloat(str) / scale;
|
|
182
|
+
if (this.state.unit === "inch") val *= 25.4;
|
|
183
|
+
return val;
|
|
184
|
+
}
|
|
185
|
+
// --- Region Logic ---
|
|
186
|
+
_startRegionContour(x, y) {
|
|
187
|
+
const contour = [{ x, y }];
|
|
188
|
+
this.currentRegionContours.push(contour);
|
|
189
|
+
this._currentRegionContour = contour;
|
|
190
|
+
}
|
|
191
|
+
addArcToRegion(x1, y1, x2, y2, iOffset, jOffset, isCw) {
|
|
192
|
+
const pts = this.discretizeArc(x1, y1, x2, y2, iOffset, jOffset, isCw);
|
|
193
|
+
for (let i = 1; i < pts.length; i++) {
|
|
194
|
+
if (this._currentRegionContour) {
|
|
195
|
+
this._currentRegionContour.push(pts[i]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
finishRegion() {
|
|
200
|
+
if (this.currentRegionContours.length > 0) {
|
|
201
|
+
const newShapes = this.contoursToShapes(this.currentRegionContours);
|
|
202
|
+
this.shapes.push(...newShapes);
|
|
203
|
+
this.currentRegionContours = [];
|
|
204
|
+
}
|
|
205
|
+
this._currentRegionContour = null;
|
|
206
|
+
}
|
|
207
|
+
processRegions() {
|
|
208
|
+
if (this.currentRegionContours.length > 0) {
|
|
209
|
+
const newShapes = this.contoursToShapes(this.currentRegionContours);
|
|
210
|
+
this.shapes.push(...newShapes);
|
|
211
|
+
this.currentRegionContours = [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
contoursToShapes(rawContours) {
|
|
215
|
+
const shapes = [];
|
|
216
|
+
for (const rawPts of rawContours) {
|
|
217
|
+
const pts = [];
|
|
218
|
+
let last = null;
|
|
219
|
+
for (const p of rawPts) {
|
|
220
|
+
if (!last || Math.hypot(p.x - last.x, p.y - last.y) > 1e-4) {
|
|
221
|
+
pts.push(p);
|
|
222
|
+
last = p;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
let forcedClose = false;
|
|
226
|
+
if (pts.length > 2) {
|
|
227
|
+
const first = pts[0];
|
|
228
|
+
const end = pts[pts.length - 1];
|
|
229
|
+
const gap = Math.hypot(first.x - end.x, first.y - end.y);
|
|
230
|
+
const WARN_GAP = 10;
|
|
231
|
+
const MAX_GAP = 1e3;
|
|
232
|
+
if (gap > WARN_GAP) {
|
|
233
|
+
forcedClose = true;
|
|
234
|
+
}
|
|
235
|
+
if (gap > MAX_GAP) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (gap > 1e-3) {
|
|
239
|
+
pts.push({ x: first.x, y: first.y });
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (pts.length >= 3) {
|
|
245
|
+
const shape = new THREE.Shape();
|
|
246
|
+
shape.moveTo(pts[0].x, pts[0].y);
|
|
247
|
+
for (let i = 1; i < pts.length; i++) {
|
|
248
|
+
shape.lineTo(pts[i].x, pts[i].y);
|
|
249
|
+
}
|
|
250
|
+
shape.closePath();
|
|
251
|
+
shape.userData = { forcedClose };
|
|
252
|
+
shapes.push(shape);
|
|
253
|
+
} else {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return shapes;
|
|
257
|
+
}
|
|
258
|
+
// --- Non-Region Segment Logic ---
|
|
259
|
+
addSegment(x1, y1, x2, y2, iOffset, jOffset, mode) {
|
|
260
|
+
if (mode === "linear") {
|
|
261
|
+
this.segments.push({
|
|
262
|
+
start: new THREE.Vector2(x1, y1),
|
|
263
|
+
end: new THREE.Vector2(x2, y2)
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
const pts = this.discretizeArc(x1, y1, x2, y2, iOffset, jOffset, mode === "cw");
|
|
267
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
268
|
+
this.segments.push({
|
|
269
|
+
start: new THREE.Vector2(pts[i].x, pts[i].y),
|
|
270
|
+
end: new THREE.Vector2(pts[i + 1].x, pts[i + 1].y)
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
discretizeArc(x1, y1, x2, y2, iOffset, jOffset, isCw) {
|
|
276
|
+
const cx = x1 + iOffset;
|
|
277
|
+
const cy = y1 + jOffset;
|
|
278
|
+
const radius = Math.sqrt(iOffset * iOffset + jOffset * jOffset);
|
|
279
|
+
if (radius < 1e-4) return [{ x: x1, y: y1 }, { x: x2, y: y2 }];
|
|
280
|
+
let startAngle = Math.atan2(y1 - cy, x1 - cx);
|
|
281
|
+
let endAngle = Math.atan2(y2 - cy, x2 - cx);
|
|
282
|
+
if (isCw) {
|
|
283
|
+
if (endAngle >= startAngle) endAngle -= 2 * Math.PI;
|
|
284
|
+
} else {
|
|
285
|
+
if (endAngle <= startAngle) endAngle += 2 * Math.PI;
|
|
286
|
+
}
|
|
287
|
+
const arcLength = Math.abs(endAngle - startAngle) * radius;
|
|
288
|
+
const angleSpan = Math.abs(endAngle - startAngle);
|
|
289
|
+
let segmentSize;
|
|
290
|
+
let minSegments;
|
|
291
|
+
const isSmallCorner = radius < 10;
|
|
292
|
+
if (isSmallCorner) {
|
|
293
|
+
segmentSize = 3e-3;
|
|
294
|
+
minSegments = Math.max(32, Math.ceil(angleSpan / (Math.PI / 2) * 32));
|
|
295
|
+
} else if (arcLength > 100) {
|
|
296
|
+
segmentSize = 0.02;
|
|
297
|
+
minSegments = 12;
|
|
298
|
+
} else if (arcLength > 10) {
|
|
299
|
+
segmentSize = 0.01;
|
|
300
|
+
minSegments = 16;
|
|
301
|
+
} else {
|
|
302
|
+
segmentSize = 5e-3;
|
|
303
|
+
minSegments = 20;
|
|
304
|
+
}
|
|
305
|
+
const segments = Math.max(minSegments, Math.ceil(arcLength / segmentSize));
|
|
306
|
+
const pts = [];
|
|
307
|
+
pts.push({ x: x1, y: y1 });
|
|
308
|
+
for (let k = 1; k <= segments; k++) {
|
|
309
|
+
const t = k / segments;
|
|
310
|
+
const angle = startAngle + (endAngle - startAngle) * t;
|
|
311
|
+
pts.push({
|
|
312
|
+
x: cx + radius * Math.cos(angle),
|
|
313
|
+
y: cy + radius * Math.sin(angle)
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
pts[pts.length - 1] = { x: x2, y: y2 };
|
|
317
|
+
return pts;
|
|
318
|
+
}
|
|
319
|
+
stitchSegmentsToShapes() {
|
|
320
|
+
const uniqueSegments = [];
|
|
321
|
+
const seen = /* @__PURE__ */ new Set();
|
|
322
|
+
const keyFn = (p) => `${Math.round(p.x * 1e5)},${Math.round(p.y * 1e5)}`;
|
|
323
|
+
for (const seg of this.segments) {
|
|
324
|
+
const k1 = keyFn(seg.start) + "|" + keyFn(seg.end);
|
|
325
|
+
const k2 = keyFn(seg.end) + "|" + keyFn(seg.start);
|
|
326
|
+
if (!seen.has(k1) && !seen.has(k2)) {
|
|
327
|
+
seen.add(k1);
|
|
328
|
+
uniqueSegments.push(seg);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (uniqueSegments.length === 0) return;
|
|
332
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
333
|
+
for (const seg of uniqueSegments) {
|
|
334
|
+
minX = Math.min(minX, seg.start.x, seg.end.x);
|
|
335
|
+
maxX = Math.max(maxX, seg.start.x, seg.end.x);
|
|
336
|
+
minY = Math.min(minY, seg.start.y, seg.end.y);
|
|
337
|
+
maxY = Math.max(maxY, seg.start.y, seg.end.y);
|
|
338
|
+
}
|
|
339
|
+
const boardSize = Math.max(maxX - minX, maxY - minY);
|
|
340
|
+
let TOLERANCE;
|
|
341
|
+
if (boardSize > 100) {
|
|
342
|
+
TOLERANCE = 0.5;
|
|
343
|
+
} else if (boardSize > 50) {
|
|
344
|
+
TOLERANCE = 0.25;
|
|
345
|
+
} else {
|
|
346
|
+
TOLERANCE = 0.1;
|
|
347
|
+
}
|
|
348
|
+
const cellSize = TOLERANCE;
|
|
349
|
+
const grid = /* @__PURE__ */ new Map();
|
|
350
|
+
const getCell = (pt) => `${Math.floor(pt.x / cellSize)},${Math.floor(pt.y / cellSize)}`;
|
|
351
|
+
this.strokeSegments = uniqueSegments.map((s) => ({
|
|
352
|
+
start: s.start.clone(),
|
|
353
|
+
end: s.end.clone()
|
|
354
|
+
}));
|
|
355
|
+
const poolItems = uniqueSegments.map((s) => ({ seg: s, used: false }));
|
|
356
|
+
const addToGrid = (item) => {
|
|
357
|
+
const k1 = getCell(item.seg.start);
|
|
358
|
+
const k2 = getCell(item.seg.end);
|
|
359
|
+
if (!grid.has(k1)) grid.set(k1, []);
|
|
360
|
+
grid.get(k1).push(item);
|
|
361
|
+
if (k1 !== k2) {
|
|
362
|
+
if (!grid.has(k2)) grid.set(k2, []);
|
|
363
|
+
grid.get(k2).push(item);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
poolItems.forEach(addToGrid);
|
|
367
|
+
const getCandidates = (pt) => {
|
|
368
|
+
const cx = Math.floor(pt.x / cellSize);
|
|
369
|
+
const cy = Math.floor(pt.y / cellSize);
|
|
370
|
+
const results = /* @__PURE__ */ new Set();
|
|
371
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
372
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
373
|
+
const k = `${cx + dx},${cy + dy}`;
|
|
374
|
+
const list = grid.get(k);
|
|
375
|
+
if (list) {
|
|
376
|
+
for (const item of list) results.add(item);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return results;
|
|
381
|
+
};
|
|
382
|
+
const chains = [];
|
|
383
|
+
for (const startItem of poolItems) {
|
|
384
|
+
if (startItem.used) continue;
|
|
385
|
+
startItem.used = true;
|
|
386
|
+
const chain = [startItem.seg.start, startItem.seg.end];
|
|
387
|
+
let tail = startItem.seg.end;
|
|
388
|
+
let head = startItem.seg.start;
|
|
389
|
+
let finding = true;
|
|
390
|
+
while (finding) {
|
|
391
|
+
finding = false;
|
|
392
|
+
const candidates = getCandidates(tail);
|
|
393
|
+
let bestNext = null;
|
|
394
|
+
let minDst = Infinity;
|
|
395
|
+
let isReverse = false;
|
|
396
|
+
for (const item of candidates) {
|
|
397
|
+
if (item.used) continue;
|
|
398
|
+
const d1 = item.seg.start.distanceTo(tail);
|
|
399
|
+
if (d1 < TOLERANCE && d1 < minDst) {
|
|
400
|
+
minDst = d1;
|
|
401
|
+
bestNext = item;
|
|
402
|
+
isReverse = false;
|
|
403
|
+
}
|
|
404
|
+
const d2 = item.seg.end.distanceTo(tail);
|
|
405
|
+
if (d2 < TOLERANCE && d2 < minDst) {
|
|
406
|
+
minDst = d2;
|
|
407
|
+
bestNext = item;
|
|
408
|
+
isReverse = true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (bestNext) {
|
|
412
|
+
bestNext.used = true;
|
|
413
|
+
if (isReverse) {
|
|
414
|
+
chain.push(bestNext.seg.start);
|
|
415
|
+
tail = bestNext.seg.start;
|
|
416
|
+
} else {
|
|
417
|
+
chain.push(bestNext.seg.end);
|
|
418
|
+
tail = bestNext.seg.end;
|
|
419
|
+
}
|
|
420
|
+
finding = true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
finding = true;
|
|
424
|
+
while (finding) {
|
|
425
|
+
finding = false;
|
|
426
|
+
const candidates = getCandidates(head);
|
|
427
|
+
let bestPrev = null;
|
|
428
|
+
let minDst = Infinity;
|
|
429
|
+
let isReverse = false;
|
|
430
|
+
for (const item of candidates) {
|
|
431
|
+
if (item.used) continue;
|
|
432
|
+
const d1 = item.seg.end.distanceTo(head);
|
|
433
|
+
if (d1 < TOLERANCE && d1 < minDst) {
|
|
434
|
+
minDst = d1;
|
|
435
|
+
bestPrev = item;
|
|
436
|
+
isReverse = false;
|
|
437
|
+
}
|
|
438
|
+
const d2 = item.seg.start.distanceTo(head);
|
|
439
|
+
if (d2 < TOLERANCE && d2 < minDst) {
|
|
440
|
+
minDst = d2;
|
|
441
|
+
bestPrev = item;
|
|
442
|
+
isReverse = true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (bestPrev) {
|
|
446
|
+
bestPrev.used = true;
|
|
447
|
+
if (isReverse) {
|
|
448
|
+
chain.unshift(bestPrev.seg.end);
|
|
449
|
+
head = bestPrev.seg.end;
|
|
450
|
+
} else {
|
|
451
|
+
chain.unshift(bestPrev.seg.start);
|
|
452
|
+
head = bestPrev.seg.start;
|
|
453
|
+
}
|
|
454
|
+
finding = true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
chains.push(chain);
|
|
458
|
+
}
|
|
459
|
+
const contours = [];
|
|
460
|
+
for (const chain of chains) {
|
|
461
|
+
if (chain.length < 3) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const pts = chain.map((p) => ({ x: p.x, y: p.y }));
|
|
465
|
+
contours.push(pts);
|
|
466
|
+
}
|
|
467
|
+
const newShapes = this.contoursToShapes(contours);
|
|
468
|
+
this.shapes.push(...newShapes);
|
|
469
|
+
if (uniqueSegments.length > 0) {
|
|
470
|
+
const fallbackShapes = this.buildLoopsFromSegments(uniqueSegments, TOLERANCE);
|
|
471
|
+
if (fallbackShapes.length > 0) {
|
|
472
|
+
this.shapes.push(...fallbackShapes);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* 从无向线段集合中提取简单闭合环,适用于拼板网格等分叉场景。
|
|
478
|
+
* 算法:构建邻接表,遍历未使用的边,沿邻接边走到回到起点即认为闭合。
|
|
479
|
+
*/
|
|
480
|
+
buildLoopsFromSegments(segments, tol = 0.2) {
|
|
481
|
+
const keyFn = (p) => `${Math.round(p.x / tol)},${Math.round(p.y / tol)}`;
|
|
482
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
483
|
+
const addNode = (p) => {
|
|
484
|
+
const k = keyFn(p);
|
|
485
|
+
if (!nodes.has(k)) {
|
|
486
|
+
nodes.set(k, { pt: p.clone(), neighbors: /* @__PURE__ */ new Set() });
|
|
487
|
+
}
|
|
488
|
+
return k;
|
|
489
|
+
};
|
|
490
|
+
const edges = /* @__PURE__ */ new Map();
|
|
491
|
+
const edgeKey = (aKey, bKey) => aKey < bKey ? `${aKey}|${bKey}` : `${bKey}|${aKey}`;
|
|
492
|
+
for (const seg of segments) {
|
|
493
|
+
const aKey = addNode(seg.start);
|
|
494
|
+
const bKey = addNode(seg.end);
|
|
495
|
+
const ek = edgeKey(aKey, bKey);
|
|
496
|
+
if (edges.has(ek)) continue;
|
|
497
|
+
edges.set(ek, { aKey, bKey, used: false });
|
|
498
|
+
nodes.get(aKey).neighbors.add(bKey);
|
|
499
|
+
nodes.get(bKey).neighbors.add(aKey);
|
|
500
|
+
}
|
|
501
|
+
const loops = [];
|
|
502
|
+
for (const [ek, edge] of edges) {
|
|
503
|
+
if (edge.used) continue;
|
|
504
|
+
const pathKeys = [];
|
|
505
|
+
let prevKey = edge.aKey;
|
|
506
|
+
let currKey = edge.bKey;
|
|
507
|
+
edge.used = true;
|
|
508
|
+
pathKeys.push(prevKey, currKey);
|
|
509
|
+
let closed = false;
|
|
510
|
+
const MAX_STEPS = segments.length * 4;
|
|
511
|
+
let steps = 0;
|
|
512
|
+
while (steps < MAX_STEPS) {
|
|
513
|
+
steps++;
|
|
514
|
+
const currNode = nodes.get(currKey);
|
|
515
|
+
if (!currNode) break;
|
|
516
|
+
let nextKey = null;
|
|
517
|
+
for (const nbKey of currNode.neighbors) {
|
|
518
|
+
if (nbKey === prevKey) continue;
|
|
519
|
+
const nk = edgeKey(currKey, nbKey);
|
|
520
|
+
const e = edges.get(nk);
|
|
521
|
+
if (e && !e.used) {
|
|
522
|
+
nextKey = nbKey;
|
|
523
|
+
e.used = true;
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (nextKey === null) {
|
|
528
|
+
const startNode = nodes.get(pathKeys[0]);
|
|
529
|
+
const currNodePt = nodes.get(currKey)?.pt;
|
|
530
|
+
if (startNode && currNodePt) {
|
|
531
|
+
const gap = startNode.pt.distanceTo(currNodePt);
|
|
532
|
+
if (gap <= tol && pathKeys.length > 2) {
|
|
533
|
+
closed = true;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
pathKeys.push(nextKey);
|
|
539
|
+
prevKey = currKey;
|
|
540
|
+
currKey = nextKey;
|
|
541
|
+
if (currKey === pathKeys[0] && pathKeys.length > 3) {
|
|
542
|
+
closed = true;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (closed) {
|
|
547
|
+
const pts = [];
|
|
548
|
+
pathKeys.forEach((k) => {
|
|
549
|
+
const n = nodes.get(k);
|
|
550
|
+
if (n) pts.push({ x: n.pt.x, y: n.pt.y });
|
|
551
|
+
});
|
|
552
|
+
if (pts.length > 1 && Math.hypot(pts[0].x - pts[pts.length - 1].x, pts[0].y - pts[pts.length - 1].y) < 1e-6) {
|
|
553
|
+
pts.pop();
|
|
554
|
+
}
|
|
555
|
+
if (pts.length >= 3) {
|
|
556
|
+
const shape = new THREE.Shape();
|
|
557
|
+
shape.moveTo(pts[0].x, pts[0].y);
|
|
558
|
+
for (let i = 1; i < pts.length; i++) {
|
|
559
|
+
shape.lineTo(pts[i].x, pts[i].y);
|
|
560
|
+
}
|
|
561
|
+
shape.closePath();
|
|
562
|
+
loops.push(shape);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return loops;
|
|
567
|
+
}
|
|
568
|
+
// 根据线框生成外接矩形兜底面(修复堆栈溢出:使用循环而不是展开运算符)
|
|
569
|
+
buildBBoxShapeFromStrokes(segments) {
|
|
570
|
+
if (!segments || segments.length === 0) return null;
|
|
571
|
+
let minX = Infinity, maxX = -Infinity;
|
|
572
|
+
let minY = Infinity, maxY = -Infinity;
|
|
573
|
+
for (let i = 0; i < segments.length; i++) {
|
|
574
|
+
const seg = segments[i];
|
|
575
|
+
if (seg.start.x < minX) minX = seg.start.x;
|
|
576
|
+
if (seg.start.x > maxX) maxX = seg.start.x;
|
|
577
|
+
if (seg.start.y < minY) minY = seg.start.y;
|
|
578
|
+
if (seg.start.y > maxY) maxY = seg.start.y;
|
|
579
|
+
if (seg.end.x < minX) minX = seg.end.x;
|
|
580
|
+
if (seg.end.x > maxX) maxX = seg.end.x;
|
|
581
|
+
if (seg.end.y < minY) minY = seg.end.y;
|
|
582
|
+
if (seg.end.y > maxY) maxY = seg.end.y;
|
|
583
|
+
}
|
|
584
|
+
if (!isFinite(minX) || !isFinite(maxX) || !isFinite(minY) || !isFinite(maxY)) return null;
|
|
585
|
+
const shape = new THREE.Shape();
|
|
586
|
+
shape.moveTo(minX, minY);
|
|
587
|
+
shape.lineTo(maxX, minY);
|
|
588
|
+
shape.lineTo(maxX, maxY);
|
|
589
|
+
shape.lineTo(minX, maxY);
|
|
590
|
+
shape.closePath();
|
|
591
|
+
return shape;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// src/components/jetPaveGerberViewer/src/viewer-src/3dPage/js/main.js
|
|
596
|
+
import { parse, plot, renderSVG } from "web-gerber";
|
|
597
|
+
var DEBUG_MODE = true;
|
|
598
|
+
var debugLog = DEBUG_MODE ? console.log.bind(console) : () => {
|
|
599
|
+
};
|
|
600
|
+
var debugWarn = DEBUG_MODE ? console.warn.bind(console) : () => {
|
|
601
|
+
};
|
|
602
|
+
function stitchSegmentsToShapes(segments) {
|
|
603
|
+
if (!segments || segments.length === 0) return [];
|
|
604
|
+
const startTime = performance.now();
|
|
605
|
+
const PRECISION = 1e4;
|
|
606
|
+
const seen = /* @__PURE__ */ new Set();
|
|
607
|
+
const uniqueSegments = [];
|
|
608
|
+
for (let i = 0; i < segments.length; i++) {
|
|
609
|
+
const seg = segments[i];
|
|
610
|
+
const sx = Math.round(seg.start.x * PRECISION);
|
|
611
|
+
const sy = Math.round(seg.start.y * PRECISION);
|
|
612
|
+
const ex = Math.round(seg.end.x * PRECISION);
|
|
613
|
+
const ey = Math.round(seg.end.y * PRECISION);
|
|
614
|
+
let key;
|
|
615
|
+
if (sx < ex || sx === ex && sy < ey) {
|
|
616
|
+
key = `${sx},${sy}|${ex},${ey}`;
|
|
617
|
+
} else {
|
|
618
|
+
key = `${ex},${ey}|${sx},${sy}`;
|
|
619
|
+
}
|
|
620
|
+
if (!seen.has(key)) {
|
|
621
|
+
seen.add(key);
|
|
622
|
+
uniqueSegments.push(seg);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
console.log(`[stitchSegmentsToShapes] \u7EBF\u6BB5\u53BB\u91CD: ${segments.length} -> ${uniqueSegments.length}`);
|
|
626
|
+
if (uniqueSegments.length === 0) return [];
|
|
627
|
+
const MAX_SEGMENTS = 15e4;
|
|
628
|
+
if (uniqueSegments.length > MAX_SEGMENTS) {
|
|
629
|
+
console.warn(`[stitchSegmentsToShapes] \u26A0\uFE0F \u7EBF\u6BB5\u8FC7\u591A (${uniqueSegments.length} > ${MAX_SEGMENTS})\uFF0C\u8DF3\u8FC7\u62FC\u63A5`);
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
633
|
+
const n = uniqueSegments.length;
|
|
634
|
+
for (let i = 0; i < n; i++) {
|
|
635
|
+
const seg = uniqueSegments[i];
|
|
636
|
+
const s = seg.start, e = seg.end;
|
|
637
|
+
if (s.x < minX) minX = s.x;
|
|
638
|
+
if (s.x > maxX) maxX = s.x;
|
|
639
|
+
if (e.x < minX) minX = e.x;
|
|
640
|
+
if (e.x > maxX) maxX = e.x;
|
|
641
|
+
if (s.y < minY) minY = s.y;
|
|
642
|
+
if (s.y > maxY) maxY = s.y;
|
|
643
|
+
if (e.y < minY) minY = e.y;
|
|
644
|
+
if (e.y > maxY) maxY = e.y;
|
|
645
|
+
}
|
|
646
|
+
const boardSize = Math.max(maxX - minX, maxY - minY);
|
|
647
|
+
const TOLERANCE = boardSize > 100 ? 0.5 : boardSize > 50 ? 0.25 : 0.1;
|
|
648
|
+
const TOLERANCE_SQ = TOLERANCE * TOLERANCE;
|
|
649
|
+
const cellSize = TOLERANCE;
|
|
650
|
+
const invCellSize = 1 / cellSize;
|
|
651
|
+
const grid = /* @__PURE__ */ new Map();
|
|
652
|
+
const poolItems = new Array(n);
|
|
653
|
+
for (let i = 0; i < n; i++) {
|
|
654
|
+
const seg = uniqueSegments[i];
|
|
655
|
+
const item = { seg, used: false, idx: i };
|
|
656
|
+
poolItems[i] = item;
|
|
657
|
+
const cx1 = Math.floor(seg.start.x * invCellSize);
|
|
658
|
+
const cy1 = Math.floor(seg.start.y * invCellSize);
|
|
659
|
+
const k1 = cx1 << 16 ^ cy1;
|
|
660
|
+
if (!grid.has(k1)) grid.set(k1, []);
|
|
661
|
+
grid.get(k1).push(item);
|
|
662
|
+
const cx2 = Math.floor(seg.end.x * invCellSize);
|
|
663
|
+
const cy2 = Math.floor(seg.end.y * invCellSize);
|
|
664
|
+
const k2 = cx2 << 16 ^ cy2;
|
|
665
|
+
if (k1 !== k2) {
|
|
666
|
+
if (!grid.has(k2)) grid.set(k2, []);
|
|
667
|
+
grid.get(k2).push(item);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const distSq = (p1, p2) => {
|
|
671
|
+
const dx = p1.x - p2.x;
|
|
672
|
+
const dy = p1.y - p2.y;
|
|
673
|
+
return dx * dx + dy * dy;
|
|
674
|
+
};
|
|
675
|
+
const getCandidates = (x, y) => {
|
|
676
|
+
const cx = Math.floor(x * invCellSize);
|
|
677
|
+
const cy = Math.floor(y * invCellSize);
|
|
678
|
+
const results = [];
|
|
679
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
680
|
+
const ccy = cy + dy;
|
|
681
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
682
|
+
const k = cx + dx << 16 ^ ccy;
|
|
683
|
+
const list = grid.get(k);
|
|
684
|
+
if (list) {
|
|
685
|
+
for (let i = 0, len = list.length; i < len; i++) {
|
|
686
|
+
const item = list[i];
|
|
687
|
+
if (!item.used) results.push(item);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return results;
|
|
693
|
+
};
|
|
694
|
+
const chains = [];
|
|
695
|
+
for (let startIdx = 0; startIdx < n; startIdx++) {
|
|
696
|
+
const startItem = poolItems[startIdx];
|
|
697
|
+
if (startItem.used) continue;
|
|
698
|
+
startItem.used = true;
|
|
699
|
+
const front = [];
|
|
700
|
+
const back = [startItem.seg.start, startItem.seg.end];
|
|
701
|
+
let tail = startItem.seg.end;
|
|
702
|
+
let head = startItem.seg.start;
|
|
703
|
+
let finding = true;
|
|
704
|
+
let tx = tail.x, ty = tail.y;
|
|
705
|
+
while (finding) {
|
|
706
|
+
finding = false;
|
|
707
|
+
const candidates = getCandidates(tx, ty);
|
|
708
|
+
let bestNext = null;
|
|
709
|
+
let minDstSq = TOLERANCE_SQ;
|
|
710
|
+
let isReverse = false;
|
|
711
|
+
for (let i = 0, len = candidates.length; i < len; i++) {
|
|
712
|
+
const item = candidates[i];
|
|
713
|
+
const seg = item.seg;
|
|
714
|
+
let dx = seg.start.x - tx;
|
|
715
|
+
let dy = seg.start.y - ty;
|
|
716
|
+
let d1 = dx * dx + dy * dy;
|
|
717
|
+
if (d1 < minDstSq) {
|
|
718
|
+
minDstSq = d1;
|
|
719
|
+
bestNext = item;
|
|
720
|
+
isReverse = false;
|
|
721
|
+
}
|
|
722
|
+
dx = seg.end.x - tx;
|
|
723
|
+
dy = seg.end.y - ty;
|
|
724
|
+
let d2 = dx * dx + dy * dy;
|
|
725
|
+
if (d2 < minDstSq) {
|
|
726
|
+
minDstSq = d2;
|
|
727
|
+
bestNext = item;
|
|
728
|
+
isReverse = true;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (bestNext) {
|
|
732
|
+
bestNext.used = true;
|
|
733
|
+
if (isReverse) {
|
|
734
|
+
tail = bestNext.seg.start;
|
|
735
|
+
tx = tail.x;
|
|
736
|
+
ty = tail.y;
|
|
737
|
+
back.push(tail);
|
|
738
|
+
} else {
|
|
739
|
+
tail = bestNext.seg.end;
|
|
740
|
+
tx = tail.x;
|
|
741
|
+
ty = tail.y;
|
|
742
|
+
back.push(tail);
|
|
743
|
+
}
|
|
744
|
+
finding = true;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
finding = true;
|
|
748
|
+
let hx = head.x, hy = head.y;
|
|
749
|
+
while (finding) {
|
|
750
|
+
finding = false;
|
|
751
|
+
const candidates = getCandidates(hx, hy);
|
|
752
|
+
let bestPrev = null;
|
|
753
|
+
let minDstSq = TOLERANCE_SQ;
|
|
754
|
+
let isReverse = false;
|
|
755
|
+
for (let i = 0, len = candidates.length; i < len; i++) {
|
|
756
|
+
const item = candidates[i];
|
|
757
|
+
const seg = item.seg;
|
|
758
|
+
let dx = seg.end.x - hx;
|
|
759
|
+
let dy = seg.end.y - hy;
|
|
760
|
+
let d1 = dx * dx + dy * dy;
|
|
761
|
+
if (d1 < minDstSq) {
|
|
762
|
+
minDstSq = d1;
|
|
763
|
+
bestPrev = item;
|
|
764
|
+
isReverse = false;
|
|
765
|
+
}
|
|
766
|
+
dx = seg.start.x - hx;
|
|
767
|
+
dy = seg.start.y - hy;
|
|
768
|
+
let d2 = dx * dx + dy * dy;
|
|
769
|
+
if (d2 < minDstSq) {
|
|
770
|
+
minDstSq = d2;
|
|
771
|
+
bestPrev = item;
|
|
772
|
+
isReverse = true;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (bestPrev) {
|
|
776
|
+
bestPrev.used = true;
|
|
777
|
+
if (isReverse) {
|
|
778
|
+
head = bestPrev.seg.end;
|
|
779
|
+
hx = head.x;
|
|
780
|
+
hy = head.y;
|
|
781
|
+
front.push(head);
|
|
782
|
+
} else {
|
|
783
|
+
head = bestPrev.seg.start;
|
|
784
|
+
hx = head.x;
|
|
785
|
+
hy = head.y;
|
|
786
|
+
front.push(head);
|
|
787
|
+
}
|
|
788
|
+
finding = true;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const chain = front.length > 0 ? front.reverse().concat(back) : back;
|
|
792
|
+
chains.push(chain);
|
|
793
|
+
}
|
|
794
|
+
const shapes = [];
|
|
795
|
+
for (let c = 0; c < chains.length; c++) {
|
|
796
|
+
const chain = chains[c];
|
|
797
|
+
if (chain.length < 3) continue;
|
|
798
|
+
const first = chain[0];
|
|
799
|
+
const last = chain[chain.length - 1];
|
|
800
|
+
const gapSq = distSq(first, last);
|
|
801
|
+
const gap = Math.sqrt(gapSq);
|
|
802
|
+
let forcedClose = false;
|
|
803
|
+
if (gap > 10) forcedClose = true;
|
|
804
|
+
if (gap > 1e3) continue;
|
|
805
|
+
const shape = new THREE2.Shape();
|
|
806
|
+
shape.moveTo(chain[0].x, chain[0].y);
|
|
807
|
+
for (let i = 1; i < chain.length; i++) {
|
|
808
|
+
shape.lineTo(chain[i].x, chain[i].y);
|
|
809
|
+
}
|
|
810
|
+
if (gap > 1e-3) {
|
|
811
|
+
shape.lineTo(first.x, first.y);
|
|
812
|
+
}
|
|
813
|
+
shape.closePath();
|
|
814
|
+
shape.userData = { forcedClose };
|
|
815
|
+
shapes.push(shape);
|
|
816
|
+
}
|
|
817
|
+
const elapsed = performance.now() - startTime;
|
|
818
|
+
console.log(`[stitchSegmentsToShapes] \u62FC\u63A5\u5B8C\u6210: ${shapes.length} \u4E2A\u5F62\u72B6\uFF0C\u8017\u65F6 ${elapsed.toFixed(1)}ms`);
|
|
819
|
+
return shapes;
|
|
820
|
+
}
|
|
821
|
+
var COLORS = {
|
|
822
|
+
OUTLINE: 1857803,
|
|
823
|
+
// 基材 (用户指定: #1C590B)
|
|
824
|
+
OUTLINE_EDGE: 0,
|
|
825
|
+
// 边框线(黑色)
|
|
826
|
+
COPPER: 4223518,
|
|
827
|
+
// 线路 (用户指定: #40721E)
|
|
828
|
+
GOLD: 13938487,
|
|
829
|
+
// 焊盘 (金色 Gold)
|
|
830
|
+
DRILL: 0,
|
|
831
|
+
// 钻孔 (黑色 Black)
|
|
832
|
+
SILKSCREEN: 15658734,
|
|
833
|
+
// 丝印 (白色 White)
|
|
834
|
+
SOLDER_MASK: 1857803,
|
|
835
|
+
// 阻焊 (同基材)
|
|
836
|
+
PASTE: 10066329
|
|
837
|
+
// 锡膏 (灰色 Gray)
|
|
838
|
+
};
|
|
839
|
+
var IGNORED_GERBER_BASENAMES_UPPER = new Set([
|
|
840
|
+
"PZT_Core-v1.0.GM15",
|
|
841
|
+
"PZT_Core-v1.0.GM2",
|
|
842
|
+
"PZT_Core-v1.0.GM3",
|
|
843
|
+
"PZT_Core-v1.0.GM4",
|
|
844
|
+
"PZT_Core-v1.0.GM5",
|
|
845
|
+
"PZT_Core-v1.0.GM6",
|
|
846
|
+
"PZT_Core-v1.0.GM8",
|
|
847
|
+
"PZT_Core-v1.0.GM13"
|
|
848
|
+
].map((s) => s.toUpperCase()));
|
|
849
|
+
var BOARD_THICKNESS = 1.6;
|
|
850
|
+
var Viewer3D = class {
|
|
851
|
+
constructor() {
|
|
852
|
+
this.scene = null;
|
|
853
|
+
this.camera = null;
|
|
854
|
+
this.renderer = null;
|
|
855
|
+
this.controls = null;
|
|
856
|
+
this.baseGroup = new THREE2.Group();
|
|
857
|
+
this.materialCache = /* @__PURE__ */ new Map();
|
|
858
|
+
this.initThree();
|
|
859
|
+
this.initEvents();
|
|
860
|
+
this.checkUrlParams();
|
|
861
|
+
}
|
|
862
|
+
initThree() {
|
|
863
|
+
this.scene = new THREE2.Scene();
|
|
864
|
+
this.scene.background = new THREE2.Color(0);
|
|
865
|
+
const getViewportSize = () => {
|
|
866
|
+
const host = document.getElementById("viewer3d-canvas-host");
|
|
867
|
+
if (host && host.clientWidth > 0 && host.clientHeight > 0) {
|
|
868
|
+
return { w: host.clientWidth, h: host.clientHeight };
|
|
869
|
+
}
|
|
870
|
+
return { w: window.innerWidth, h: window.innerHeight };
|
|
871
|
+
};
|
|
872
|
+
const { w: vw, h: vh } = getViewportSize();
|
|
873
|
+
const safeW = Math.max(vw, 1);
|
|
874
|
+
const safeH = Math.max(vh, 1);
|
|
875
|
+
this.camera = new THREE2.PerspectiveCamera(45, safeW / safeH, 0.1, 2e3);
|
|
876
|
+
this.camera.position.set(0, 0, 30);
|
|
877
|
+
this.renderer = new THREE2.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
|
|
878
|
+
const pr = Math.min(window.devicePixelRatio || 1, 2);
|
|
879
|
+
this.renderer.setPixelRatio(pr);
|
|
880
|
+
this.renderer.setSize(safeW, safeH);
|
|
881
|
+
const canvasHost = document.getElementById("viewer3d-canvas-host");
|
|
882
|
+
(canvasHost || document.body).appendChild(this.renderer.domElement);
|
|
883
|
+
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
|
884
|
+
this.controls.enableDamping = true;
|
|
885
|
+
const ambientLight = new THREE2.AmbientLight(16777215, 1.8);
|
|
886
|
+
this.scene.add(ambientLight);
|
|
887
|
+
const dirLight = new THREE2.DirectionalLight(16777215, 1.2);
|
|
888
|
+
dirLight.position.set(50, 50, 100);
|
|
889
|
+
this.scene.add(dirLight);
|
|
890
|
+
const fillLight = new THREE2.DirectionalLight(16777215, 1);
|
|
891
|
+
fillLight.position.set(-50, -50, 100);
|
|
892
|
+
this.scene.add(fillLight);
|
|
893
|
+
const backLight = new THREE2.DirectionalLight(16777215, 0.8);
|
|
894
|
+
backLight.position.set(0, 0, -100);
|
|
895
|
+
this.scene.add(backLight);
|
|
896
|
+
this.scene.add(this.baseGroup);
|
|
897
|
+
const applyResize = () => {
|
|
898
|
+
const { w, h } = getViewportSize();
|
|
899
|
+
const W = Math.max(w, 1);
|
|
900
|
+
const H = Math.max(h, 1);
|
|
901
|
+
this.camera.aspect = W / H;
|
|
902
|
+
this.camera.updateProjectionMatrix();
|
|
903
|
+
this.renderer.setSize(W, H);
|
|
904
|
+
};
|
|
905
|
+
window.addEventListener("resize", applyResize);
|
|
906
|
+
if (canvasHost && typeof ResizeObserver !== "undefined") {
|
|
907
|
+
this._canvasHostResizeObserver = new ResizeObserver(() => applyResize());
|
|
908
|
+
this._canvasHostResizeObserver.observe(canvasHost);
|
|
909
|
+
}
|
|
910
|
+
requestAnimationFrame(() => applyResize());
|
|
911
|
+
this.animate();
|
|
912
|
+
}
|
|
913
|
+
animate() {
|
|
914
|
+
requestAnimationFrame(() => this.animate());
|
|
915
|
+
this.controls.update();
|
|
916
|
+
this.renderer.render(this.scene, this.camera);
|
|
917
|
+
}
|
|
918
|
+
initEvents() {
|
|
919
|
+
const fileInput = document.getElementById("file-input");
|
|
920
|
+
fileInput.addEventListener("change", (e) => this.handleFileSelect(e));
|
|
921
|
+
const baseUrl = import.meta.env.BASE_URL || "/";
|
|
922
|
+
const buildPageUrl = (path) => `${baseUrl}${path.replace(/^\/+/, "")}`;
|
|
923
|
+
const closeBtn = document.getElementById("close-btn");
|
|
924
|
+
if (closeBtn) {
|
|
925
|
+
closeBtn.addEventListener("click", () => {
|
|
926
|
+
const id = parseUrlId();
|
|
927
|
+
if (id) {
|
|
928
|
+
window.location.href = `${buildPageUrl("/2dPage")}?id=${encodeURIComponent(id)}`;
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const url = parseUrlFileUrl();
|
|
932
|
+
if (url) {
|
|
933
|
+
window.location.href = `${buildPageUrl("/2dPage")}?url=${encodeURIComponent(url)}`;
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
window.location.href = buildPageUrl("/2dPage");
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async checkUrlParams() {
|
|
941
|
+
console.log("[\u6027\u80FD] checkUrlParams \u5F00\u59CB");
|
|
942
|
+
const startTime = performance.now();
|
|
943
|
+
const id = parseUrlId();
|
|
944
|
+
const url = parseUrlFileUrl();
|
|
945
|
+
if (id) {
|
|
946
|
+
const loadingEl = document.getElementById("loading");
|
|
947
|
+
if (loadingEl) {
|
|
948
|
+
loadingEl.textContent = "\u6B63\u5728\u5904\u7406\u4E2D...";
|
|
949
|
+
loadingEl.style.display = "flex";
|
|
950
|
+
}
|
|
951
|
+
try {
|
|
952
|
+
console.log("[3D-new] \u4F7F\u7528 fetchFileById \u83B7\u53D6\u6587\u4EF6...");
|
|
953
|
+
await fetchFileById(
|
|
954
|
+
id,
|
|
955
|
+
async (files) => {
|
|
956
|
+
console.log("[3D-new] \u6536\u5230\u6587\u4EF6\uFF0C\u5F00\u59CB\u5904\u7406...");
|
|
957
|
+
await this.processFiles(files);
|
|
958
|
+
if (loadingEl) {
|
|
959
|
+
loadingEl.style.display = "none";
|
|
960
|
+
}
|
|
961
|
+
const endTime = performance.now();
|
|
962
|
+
const totalTime = (endTime - startTime) / 1e3;
|
|
963
|
+
console.log(`[\u6027\u80FD] ========== checkUrlParams \u5B8C\u6210\uFF0C\u603B\u8017\u65F6: ${totalTime.toFixed(2)}\u79D2 ==========`);
|
|
964
|
+
},
|
|
965
|
+
(message, type) => {
|
|
966
|
+
console.log(`[3D-new] \u72B6\u6001: ${message}`);
|
|
967
|
+
}
|
|
968
|
+
);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
const endTime = performance.now();
|
|
971
|
+
const totalTime = (endTime - startTime) / 1e3;
|
|
972
|
+
console.error(`[\u9519\u8BEF] checkUrlParams \u5931\u8D25 (\u8017\u65F6: ${totalTime.toFixed(2)}\u79D2):`, error);
|
|
973
|
+
if (loadingEl) {
|
|
974
|
+
loadingEl.style.display = "none";
|
|
975
|
+
}
|
|
976
|
+
alert("\u83B7\u53D6\u6587\u4EF6\u5931\u8D25: " + error.message);
|
|
977
|
+
}
|
|
978
|
+
} else if (url) {
|
|
979
|
+
const loadingEl = document.getElementById("loading");
|
|
980
|
+
if (loadingEl) {
|
|
981
|
+
loadingEl.textContent = "\u6B63\u5728\u5904\u7406\u4E2D...";
|
|
982
|
+
loadingEl.style.display = "flex";
|
|
983
|
+
}
|
|
984
|
+
try {
|
|
985
|
+
console.log("[3D-new] \u4F7F\u7528 fetchFileByUrl \u83B7\u53D6\u6587\u4EF6...");
|
|
986
|
+
await fetchFileByUrl(
|
|
987
|
+
url,
|
|
988
|
+
async (files) => {
|
|
989
|
+
console.log("[3D-new] \u6536\u5230\u6587\u4EF6\uFF0C\u5F00\u59CB\u5904\u7406...");
|
|
990
|
+
await this.processFiles(files);
|
|
991
|
+
if (loadingEl) {
|
|
992
|
+
loadingEl.style.display = "none";
|
|
993
|
+
}
|
|
994
|
+
const endTime = performance.now();
|
|
995
|
+
const totalTime = (endTime - startTime) / 1e3;
|
|
996
|
+
console.log(`[\u6027\u80FD] ========== checkUrlParams \u5B8C\u6210\uFF0C\u603B\u8017\u65F6: ${totalTime.toFixed(2)}\u79D2 ==========`);
|
|
997
|
+
},
|
|
998
|
+
(message, type) => {
|
|
999
|
+
console.log(`[3D-new] \u72B6\u6001: ${message}`);
|
|
1000
|
+
}
|
|
1001
|
+
);
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
const endTime = performance.now();
|
|
1004
|
+
const totalTime = (endTime - startTime) / 1e3;
|
|
1005
|
+
console.error(`[\u9519\u8BEF] checkUrlParams \u5931\u8D25 (\u8017\u65F6: ${totalTime.toFixed(2)}\u79D2):`, error);
|
|
1006
|
+
if (loadingEl) {
|
|
1007
|
+
loadingEl.style.display = "none";
|
|
1008
|
+
}
|
|
1009
|
+
alert("\u83B7\u53D6\u6587\u4EF6\u5931\u8D25: " + error.message);
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
console.log("[3D-new] URL \u4E2D\u6CA1\u6709 ID \u53C2\u6570");
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
async handleFileSelect(event) {
|
|
1016
|
+
console.log("[\u6027\u80FD] handleFileSelect \u5F00\u59CB");
|
|
1017
|
+
const startTime = performance.now();
|
|
1018
|
+
const files = event.target.files;
|
|
1019
|
+
if (!files || files.length === 0) return;
|
|
1020
|
+
document.getElementById("loading").style.display = "flex";
|
|
1021
|
+
try {
|
|
1022
|
+
await this.processFiles(files);
|
|
1023
|
+
const endTime = performance.now();
|
|
1024
|
+
const totalTime = (endTime - startTime) / 1e3;
|
|
1025
|
+
console.log(`[\u6027\u80FD] ========== \u6587\u4EF6\u5904\u7406\u5B8C\u6210\uFF0C\u603B\u8017\u65F6: ${totalTime.toFixed(2)}\u79D2 ==========`);
|
|
1026
|
+
} catch (e) {
|
|
1027
|
+
const endTime = performance.now();
|
|
1028
|
+
const totalTime = (endTime - startTime) / 1e3;
|
|
1029
|
+
console.error(`[\u9519\u8BEF] \u6587\u4EF6\u5904\u7406\u5931\u8D25 (\u8017\u65F6: ${totalTime.toFixed(2)}\u79D2):`, e);
|
|
1030
|
+
alert("\u5904\u7406\u6587\u4EF6\u65F6\u51FA\u9519: " + e.message);
|
|
1031
|
+
} finally {
|
|
1032
|
+
document.getElementById("loading").style.display = "none";
|
|
1033
|
+
event.target.value = "";
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
async processFiles(fileList) {
|
|
1037
|
+
console.log(`[\u6027\u80FD] processFiles \u5F00\u59CB\uFF0C\u6587\u4EF6\u6570: ${fileList.length}`);
|
|
1038
|
+
const startTimeTotal = performance.now();
|
|
1039
|
+
this.clearScene();
|
|
1040
|
+
this.outlineBbox = null;
|
|
1041
|
+
if (this.renderer && this.renderer.domElement) {
|
|
1042
|
+
this.renderer.domElement.style.visibility = "hidden";
|
|
1043
|
+
}
|
|
1044
|
+
console.log("[\u6027\u80FD] \u5F00\u59CB\u89E3\u538B\u6587\u4EF6...");
|
|
1045
|
+
const startTimeExtract = performance.now();
|
|
1046
|
+
const gerberFiles = [];
|
|
1047
|
+
for (const file of fileList) {
|
|
1048
|
+
const ext = file.name.split(".").pop().toLowerCase();
|
|
1049
|
+
if (ext === "zip") {
|
|
1050
|
+
console.log(`[\u6027\u80FD] \u89E3\u538B ZIP: ${file.name}`);
|
|
1051
|
+
const startTimeZip = performance.now();
|
|
1052
|
+
const zip = await import_jszip.default.loadAsync(file);
|
|
1053
|
+
for (const filename in zip.files) {
|
|
1054
|
+
if (zip.files[filename].dir) continue;
|
|
1055
|
+
const content = await zip.files[filename].async("blob");
|
|
1056
|
+
gerberFiles.push(new File([content], filename));
|
|
1057
|
+
}
|
|
1058
|
+
const endTimeZip = performance.now();
|
|
1059
|
+
console.log(`[\u6027\u80FD] ZIP \u89E3\u538B\u5B8C\u6210\uFF0C\u8017\u65F6: ${(endTimeZip - startTimeZip).toFixed(2)}ms`);
|
|
1060
|
+
} else if (ext === "rar") {
|
|
1061
|
+
console.log(`[\u6027\u80FD] \u89E3\u538B RAR: ${file.name}`);
|
|
1062
|
+
const startTimeRar = performance.now();
|
|
1063
|
+
const extracted = await extractRar(file);
|
|
1064
|
+
const endTimeRar = performance.now();
|
|
1065
|
+
console.log(`[\u6027\u80FD] RAR \u89E3\u538B\u5B8C\u6210\uFF0C\u8017\u65F6: ${(endTimeRar - startTimeRar).toFixed(2)}ms`);
|
|
1066
|
+
gerberFiles.push(...extracted);
|
|
1067
|
+
} else {
|
|
1068
|
+
gerberFiles.push(file);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
const endTimeExtract = performance.now();
|
|
1072
|
+
console.log(`[\u6027\u80FD] \u6587\u4EF6\u89E3\u538B\u603B\u8017\u65F6: ${(endTimeExtract - startTimeExtract).toFixed(2)}ms, \u89E3\u538B\u540E\u6587\u4EF6\u6570: ${gerberFiles.length}`);
|
|
1073
|
+
if (IGNORED_GERBER_BASENAMES_UPPER.size > 0 && gerberFiles.length > 0) {
|
|
1074
|
+
const kept = [];
|
|
1075
|
+
const ignored = [];
|
|
1076
|
+
for (const f of gerberFiles) {
|
|
1077
|
+
const base = (f.name.split(/[/\\]/).pop() || "").toUpperCase();
|
|
1078
|
+
if (IGNORED_GERBER_BASENAMES_UPPER.has(base)) {
|
|
1079
|
+
ignored.push(f.name);
|
|
1080
|
+
} else {
|
|
1081
|
+
kept.push(f);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (ignored.length > 0) {
|
|
1085
|
+
debugWarn(`[3D-new] \u5DF2\u5FFD\u7565 ${ignored.length} \u4E2A\u6587\u4EF6\uFF08\u4E0D\u53C2\u4E0E\u6E32\u67D3/\u4E0D\u5F71\u54CD\u57FA\u6750\u5C3A\u5BF8\uFF09:`, ignored);
|
|
1086
|
+
}
|
|
1087
|
+
gerberFiles.length = 0;
|
|
1088
|
+
gerberFiles.push(...kept);
|
|
1089
|
+
}
|
|
1090
|
+
console.log(`[3D-new] \u5F00\u59CB\u89E3\u6790\u6587\u4EF6\uFF0C\u5171 ${gerberFiles.length} \u4E2A\u6587\u4EF6:`, gerberFiles.map((f) => f.name));
|
|
1091
|
+
console.log("[\u6027\u80FD] \u5F00\u59CB\u6587\u4EF6\u5206\u7C7B...");
|
|
1092
|
+
const startTimeClassify = performance.now();
|
|
1093
|
+
const buildPhoGroups = (files) => {
|
|
1094
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1095
|
+
const add = (prefix, num) => {
|
|
1096
|
+
if (!groups.has(prefix)) {
|
|
1097
|
+
groups.set(prefix, { min: num });
|
|
1098
|
+
} else {
|
|
1099
|
+
groups.get(prefix).min = Math.min(groups.get(prefix).min, num);
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
files.forEach((f) => {
|
|
1103
|
+
const nameOnly = f.name.split(/[/\\]/).pop() || "";
|
|
1104
|
+
const ext = "." + nameOnly.split(".").pop().toLowerCase();
|
|
1105
|
+
if (ext !== ".pho") return;
|
|
1106
|
+
const upper = nameOnly.toUpperCase();
|
|
1107
|
+
let prefix = null;
|
|
1108
|
+
let num = 0;
|
|
1109
|
+
if (upper.startsWith("SST")) {
|
|
1110
|
+
prefix = "sst";
|
|
1111
|
+
const m = upper.match(/^SST(\d+)/);
|
|
1112
|
+
num = m ? parseInt(m[1], 10) : 0;
|
|
1113
|
+
} else if (upper.startsWith("SMD")) {
|
|
1114
|
+
prefix = "smd";
|
|
1115
|
+
const m = upper.match(/^SMD(\d+)/);
|
|
1116
|
+
num = m ? parseInt(m[1], 10) : 0;
|
|
1117
|
+
} else if (upper.startsWith("SM")) {
|
|
1118
|
+
prefix = "sm";
|
|
1119
|
+
const m = upper.match(/^SM(\d+)/);
|
|
1120
|
+
num = m ? parseInt(m[1], 10) : 0;
|
|
1121
|
+
} else if (upper.startsWith("ART")) {
|
|
1122
|
+
prefix = "art";
|
|
1123
|
+
const m = upper.match(/^ART(\d+)/);
|
|
1124
|
+
num = m ? parseInt(m[1], 10) : 0;
|
|
1125
|
+
}
|
|
1126
|
+
if (prefix) add(prefix, num);
|
|
1127
|
+
});
|
|
1128
|
+
return groups;
|
|
1129
|
+
};
|
|
1130
|
+
const phoGroups = buildPhoGroups(gerberFiles);
|
|
1131
|
+
const getLayerType = (fileName) => {
|
|
1132
|
+
const fileNameOnly = fileName.split(/[/\\]/).pop();
|
|
1133
|
+
const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
|
|
1134
|
+
const fileNameUpper = fileNameOnly.toUpperCase();
|
|
1135
|
+
const nameLower = fileNameOnly.toLowerCase();
|
|
1136
|
+
if (fileExtension === ".GBR") {
|
|
1137
|
+
const nameWithoutExt = fileNameOnly.substring(0, fileNameOnly.lastIndexOf(".")).toUpperCase();
|
|
1138
|
+
if (nameWithoutExt === "GKO" || nameWithoutExt === "GTL" || nameWithoutExt === "GTO" || nameWithoutExt === "GTS" || nameWithoutExt === "GBL" || nameWithoutExt === "GBS" || nameWithoutExt === "GBP" || nameWithoutExt === "GBO" || nameWithoutExt === "GTP") {
|
|
1139
|
+
return nameWithoutExt;
|
|
1140
|
+
}
|
|
1141
|
+
if (nameLower.includes("outline") || nameLower.includes("boardoutline")) {
|
|
1142
|
+
return "GKO";
|
|
1143
|
+
}
|
|
1144
|
+
if (nameLower.includes("topmask") || nameLower.includes("topsolder") || nameLower.includes("soldertop") || nameLower.includes("maskstop")) {
|
|
1145
|
+
return "GTS";
|
|
1146
|
+
}
|
|
1147
|
+
if (nameLower.includes("bottommask") || nameLower.includes("bottomsolder") || nameLower.includes("solderbottom") || nameLower.includes("masksbot")) {
|
|
1148
|
+
return "GBS";
|
|
1149
|
+
}
|
|
1150
|
+
if (nameLower.includes("topsilk") || nameLower.includes("silktop") || nameLower.includes("toplegend") || nameLower.includes("legendtop")) {
|
|
1151
|
+
return "GTO";
|
|
1152
|
+
}
|
|
1153
|
+
if (nameLower.includes("bottomsilk") || nameLower.includes("silkbottom") || nameLower.includes("bottomlegend") || nameLower.includes("legendbottom")) {
|
|
1154
|
+
return "GBO";
|
|
1155
|
+
}
|
|
1156
|
+
if (nameLower.includes("toppaste") || nameLower.includes("pastetop")) {
|
|
1157
|
+
return "GTP";
|
|
1158
|
+
}
|
|
1159
|
+
if (nameLower.includes("bottompaste") || nameLower.includes("pastebottom")) {
|
|
1160
|
+
return "GBP";
|
|
1161
|
+
}
|
|
1162
|
+
if (nameLower.includes("l1") || nameLower.includes("layer1") || nameLower.includes("top") && !nameLower.includes("paste") && !nameLower.includes("mask") && !nameLower.includes("silk")) {
|
|
1163
|
+
return "GTL";
|
|
1164
|
+
}
|
|
1165
|
+
if (nameLower.includes("l4") || nameLower.includes("layer4") || nameLower.includes("bottom") && !nameLower.includes("paste") && !nameLower.includes("mask") && !nameLower.includes("silk")) {
|
|
1166
|
+
return "GBL";
|
|
1167
|
+
}
|
|
1168
|
+
if (nameLower.includes("legend_top") || nameLower.includes("legend") && nameLower.includes("top") && !nameLower.includes("signal")) {
|
|
1169
|
+
return "GTO";
|
|
1170
|
+
}
|
|
1171
|
+
if (nameLower.includes("paste_top")) return "GTP";
|
|
1172
|
+
if (nameLower.includes("paste_bot")) return "GBP";
|
|
1173
|
+
if (nameLower.includes("soldermask_top")) return "GTS";
|
|
1174
|
+
if (nameLower.includes("soldermask_bot")) return "GBS";
|
|
1175
|
+
const signalMatch = nameLower.match(/(?:copper_)?signal[_\s](\d+)/i);
|
|
1176
|
+
if (signalMatch) {
|
|
1177
|
+
const number = parseInt(signalMatch[1], 10);
|
|
1178
|
+
const displayNumber = number + 1;
|
|
1179
|
+
return "SIG" + displayNumber;
|
|
1180
|
+
}
|
|
1181
|
+
if (nameLower.includes("signal_top") || nameLower.includes("copper_signal_top")) return "GTL";
|
|
1182
|
+
if (nameLower.includes("signal_bot") || nameLower.includes("copper_signal_bot")) return "GBL";
|
|
1183
|
+
if (fileNameUpper.includes("PROFILE")) return "GKO";
|
|
1184
|
+
if (nameWithoutExt === "WX" || fileNameUpper.includes("WX")) return "GKO";
|
|
1185
|
+
if (nameLower.includes("l2") || nameLower.includes("layer2") || nameLower.includes("inner 1") || nameLower.includes("inner1")) {
|
|
1186
|
+
return "SIG2";
|
|
1187
|
+
}
|
|
1188
|
+
if (nameLower.includes("l3") || nameLower.includes("layer3") || nameLower.includes("inner 2") || nameLower.includes("inner2")) {
|
|
1189
|
+
return "SIG3";
|
|
1190
|
+
}
|
|
1191
|
+
if (fileNameUpper.startsWith("ZH-TOP") || fileNameUpper.includes("ZH-TOP")) return "GTS";
|
|
1192
|
+
if (fileNameUpper.startsWith("ZH-BOT") || fileNameUpper.includes("ZH-BOT")) return "GBS";
|
|
1193
|
+
if (fileNameUpper.startsWith("XL-TOP") || fileNameUpper.includes("XL-TOP")) return "GTL";
|
|
1194
|
+
if (fileNameUpper.startsWith("XL-BOT") || fileNameUpper.includes("XL-BOT")) return "GBL";
|
|
1195
|
+
if (/EDGE[._-]CUTS/i.test(fileNameUpper)) return "GKO";
|
|
1196
|
+
if (/F[._-]CU\b/i.test(fileNameUpper)) return "GTL";
|
|
1197
|
+
if (/B[._-]CU\b/i.test(fileNameUpper)) return "GBL";
|
|
1198
|
+
const inCuGbr = fileNameUpper.match(/IN(\d+)[._-]CU/i);
|
|
1199
|
+
if (inCuGbr) return "SIG" + (parseInt(inCuGbr[1], 10) + 1);
|
|
1200
|
+
const keywords = ["_Drawing", "_Drillmap", "_NPTH", "_Pads", "_PTH"];
|
|
1201
|
+
for (const keyword of keywords) {
|
|
1202
|
+
if (fileNameOnly.includes(keyword)) {
|
|
1203
|
+
const keywordIndex = fileNameOnly.indexOf(keyword);
|
|
1204
|
+
return fileNameOnly.substring(keywordIndex);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return fileNameOnly;
|
|
1208
|
+
}
|
|
1209
|
+
if (fileExtension === ".PHO") {
|
|
1210
|
+
if (fileNameUpper.startsWith("SST")) return "GTO";
|
|
1211
|
+
const pickByGroup = (prefix, topType, botType, defaultType = null) => {
|
|
1212
|
+
const m = fileNameUpper.match(new RegExp(`^${prefix}(\\d+)`));
|
|
1213
|
+
const num = m ? parseInt(m[1], 10) : 0;
|
|
1214
|
+
const g = phoGroups.get(prefix.toLowerCase());
|
|
1215
|
+
if (g) {
|
|
1216
|
+
return num === g.min ? topType : botType;
|
|
1217
|
+
}
|
|
1218
|
+
return defaultType || topType;
|
|
1219
|
+
};
|
|
1220
|
+
if (fileNameUpper.startsWith("SMD")) {
|
|
1221
|
+
return pickByGroup("SMD", "GTP", "GBP", "GTP");
|
|
1222
|
+
}
|
|
1223
|
+
if (fileNameUpper.startsWith("SM")) {
|
|
1224
|
+
return pickByGroup("SM", "GTS", "GBS", "GTS");
|
|
1225
|
+
}
|
|
1226
|
+
if (fileNameUpper.startsWith("ART")) {
|
|
1227
|
+
return pickByGroup("ART", "GTL", "GBL", "GTL");
|
|
1228
|
+
}
|
|
1229
|
+
if (fileNameUpper.startsWith("SSB")) return "GBO";
|
|
1230
|
+
}
|
|
1231
|
+
if (fileNameUpper.endsWith("_SST") || fileNameUpper.includes("_SST.")) return "GTO";
|
|
1232
|
+
if (fileNameUpper.endsWith("_PMT") || fileNameUpper.includes("_PMT.")) return "GTP";
|
|
1233
|
+
if (fileNameUpper.endsWith("_SMT") || fileNameUpper.includes("_SMT.")) return "GTS";
|
|
1234
|
+
if (fileNameUpper.endsWith("_TOP") || fileNameUpper.includes("_TOP.")) return "GTL";
|
|
1235
|
+
if (fileNameUpper.endsWith("_SMB") || fileNameUpper.includes("_SMB.")) return "GBS";
|
|
1236
|
+
if (fileNameUpper.endsWith("_PMB") || fileNameUpper.includes("_PMB.")) return "GBP";
|
|
1237
|
+
if (fileNameUpper.endsWith("_SSB") || fileNameUpper.includes("_SSB.")) return "GBO";
|
|
1238
|
+
if (fileNameUpper.includes("OUTLINE")) return "GKO";
|
|
1239
|
+
if (/.*_INT\d+$/i.test(fileNameUpper) || /.*_INT\d+\./i.test(fileNameUpper)) return fileNameOnly;
|
|
1240
|
+
if (fileExtension === ".GTO") return "GTO";
|
|
1241
|
+
if (fileExtension === ".GTP") return "GTP";
|
|
1242
|
+
if (fileExtension === ".GTS") return "GTS";
|
|
1243
|
+
if (fileExtension === ".GTL") return "GTL";
|
|
1244
|
+
if (fileExtension === ".GBL") return "GBL";
|
|
1245
|
+
if (fileExtension === ".GBS") return "GBS";
|
|
1246
|
+
if (fileExtension === ".GBP") return "GBP";
|
|
1247
|
+
if (fileExtension === ".GBO") return "GBO";
|
|
1248
|
+
if (fileExtension === ".GKO" || fileNameUpper.includes("PROFILE")) return "GKO";
|
|
1249
|
+
if (fileExtension === ".GM1" || fileExtension === ".GM2") return "GKO";
|
|
1250
|
+
if (fileExtension === ".GM" || fileExtension === ".GML" || fileExtension === ".OUTLINE" || fileExtension === ".OUT" || fileExtension === ".OLN") return "GKO";
|
|
1251
|
+
if (fileExtension === ".D" || fileExtension === ".DRL" || fileExtension === ".DRI") {
|
|
1252
|
+
if (fileNameUpper === "NP.DRL" || fileNameUpper === "NP.D") return "DRL2";
|
|
1253
|
+
if (fileNameUpper === "P.DRL" || fileNameUpper === "P.D") return "DRL";
|
|
1254
|
+
return "DRL";
|
|
1255
|
+
}
|
|
1256
|
+
if (fileExtension === ".ROU") return "rout";
|
|
1257
|
+
if (fileExtension === ".BOT") return "GBL";
|
|
1258
|
+
if (fileExtension === ".TOP") return "GTL";
|
|
1259
|
+
if (fileExtension === ".SOB") return "GBS";
|
|
1260
|
+
if (fileExtension === ".SOT") return "GTS";
|
|
1261
|
+
if (fileExtension === ".SST") return "GTO";
|
|
1262
|
+
if (fileExtension === ".SSB") return "GBO";
|
|
1263
|
+
if (fileExtension === ".SMT") return "GTS";
|
|
1264
|
+
if (fileExtension === ".SMB") return "GBS";
|
|
1265
|
+
if (fileExtension === ".PMT") return "GTP";
|
|
1266
|
+
if (fileExtension === ".PMB") return "GBP";
|
|
1267
|
+
if (fileExtension === ".SER") return "GTO";
|
|
1268
|
+
if (fileExtension === ".ART") {
|
|
1269
|
+
const dot = fileNameOnly.lastIndexOf(".");
|
|
1270
|
+
const stemUpper = (dot > 0 ? fileNameOnly.substring(0, dot) : fileNameOnly).toUpperCase();
|
|
1271
|
+
if (stemUpper === "SST") return "GTO";
|
|
1272
|
+
if (stemUpper === "SMT") return "GTS";
|
|
1273
|
+
if (stemUpper === "TOP") return "GTL";
|
|
1274
|
+
if (stemUpper === "BOT") return "GBL";
|
|
1275
|
+
if (stemUpper === "SMB") return "GBS";
|
|
1276
|
+
if (stemUpper === "SSB") return "GBO";
|
|
1277
|
+
if (stemUpper === "OUT") return "GKO";
|
|
1278
|
+
const layMatch = stemUpper.match(/^LAY(\d+)$/);
|
|
1279
|
+
if (layMatch) {
|
|
1280
|
+
const layerNum = parseInt(layMatch[1], 10);
|
|
1281
|
+
return "SIG" + layerNum;
|
|
1282
|
+
}
|
|
1283
|
+
return fileNameOnly;
|
|
1284
|
+
}
|
|
1285
|
+
const gExtMatch = fileExtension.match(/^\.G(\d+)$/i);
|
|
1286
|
+
if (gExtMatch) {
|
|
1287
|
+
const inCu = fileNameUpper.match(/IN(\d+)[._-]CU/i);
|
|
1288
|
+
if (inCu) return "SIG" + (parseInt(inCu[1], 10) + 1);
|
|
1289
|
+
const gn = parseInt(gExtMatch[1], 10);
|
|
1290
|
+
return "SIG" + (gn + 1);
|
|
1291
|
+
}
|
|
1292
|
+
const gpExtMatch = fileExtension.match(/^\.GP(\d+)$/i);
|
|
1293
|
+
if (gpExtMatch) {
|
|
1294
|
+
return "GP" + parseInt(gpExtMatch[1], 10);
|
|
1295
|
+
}
|
|
1296
|
+
return null;
|
|
1297
|
+
};
|
|
1298
|
+
const outlineFiles = [];
|
|
1299
|
+
const topCopperFiles = [];
|
|
1300
|
+
const bottomCopperFiles = [];
|
|
1301
|
+
const topMaskFiles = [];
|
|
1302
|
+
const bottomMaskFiles = [];
|
|
1303
|
+
const topPasteFiles = [];
|
|
1304
|
+
const bottomPasteFiles = [];
|
|
1305
|
+
const topSilkFiles = [];
|
|
1306
|
+
const bottomSilkFiles = [];
|
|
1307
|
+
const innerCopperFiles = [];
|
|
1308
|
+
for (const file of gerberFiles) {
|
|
1309
|
+
const fileNameOnly = file.name.split(/[/\\]/).pop() || "";
|
|
1310
|
+
const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
|
|
1311
|
+
if (fileExtension === ".PHO") {
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
const layerType = getLayerType(file.name);
|
|
1315
|
+
if (layerType === "GKO") {
|
|
1316
|
+
outlineFiles.push(file);
|
|
1317
|
+
} else if (layerType === "GTL") {
|
|
1318
|
+
topCopperFiles.push(file);
|
|
1319
|
+
} else if (layerType === "GBL") {
|
|
1320
|
+
bottomCopperFiles.push(file);
|
|
1321
|
+
} else if (layerType === "GTS") {
|
|
1322
|
+
topMaskFiles.push(file);
|
|
1323
|
+
} else if (layerType === "GBS") {
|
|
1324
|
+
bottomMaskFiles.push(file);
|
|
1325
|
+
} else if (layerType === "GTP") {
|
|
1326
|
+
topPasteFiles.push(file);
|
|
1327
|
+
} else if (layerType === "GBP") {
|
|
1328
|
+
bottomPasteFiles.push(file);
|
|
1329
|
+
} else if (layerType === "GTO") {
|
|
1330
|
+
topSilkFiles.push(file);
|
|
1331
|
+
} else if (layerType === "GBO") {
|
|
1332
|
+
bottomSilkFiles.push(file);
|
|
1333
|
+
} else if (layerType && /^SIG\d+$/i.test(layerType)) {
|
|
1334
|
+
innerCopperFiles.push(file);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (outlineFiles.length === 0) {
|
|
1338
|
+
const fallbackFiles = gerberFiles.filter((f) => {
|
|
1339
|
+
const name = f.name.toLowerCase();
|
|
1340
|
+
return name.endsWith(".gm2");
|
|
1341
|
+
});
|
|
1342
|
+
if (fallbackFiles.length > 0) {
|
|
1343
|
+
outlineFiles.push(...fallbackFiles);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
const pickBestOutlineFiles = async () => {
|
|
1347
|
+
if (!outlineFiles || outlineFiles.length <= 1) return { files: outlineFiles || [], refBbox: null };
|
|
1348
|
+
const baseNameUpper = (name) => (name.split(/[/\\]/).pop() || "").toUpperCase();
|
|
1349
|
+
const isExt = (nameUpper, extUpper) => nameUpper.endsWith(extUpper);
|
|
1350
|
+
const outlineCandidates = [...outlineFiles];
|
|
1351
|
+
const pickReferenceFile = () => {
|
|
1352
|
+
return topCopperFiles[0] || bottomCopperFiles[0] || topSilkFiles[0] || bottomSilkFiles[0] || topMaskFiles[0] || bottomMaskFiles[0] || topPasteFiles[0] || bottomPasteFiles[0] || null;
|
|
1353
|
+
};
|
|
1354
|
+
const getReferenceBbox = async () => {
|
|
1355
|
+
const refFile = pickReferenceFile();
|
|
1356
|
+
if (!refFile) return null;
|
|
1357
|
+
try {
|
|
1358
|
+
const res = await GerberParser.parseFile(refFile, "#ffffff");
|
|
1359
|
+
if (!res || !res.data) return null;
|
|
1360
|
+
const bbox = this.calculateLayerBbox([res.data]);
|
|
1361
|
+
if (!bbox || bbox.isEmpty()) return null;
|
|
1362
|
+
let scale = 1;
|
|
1363
|
+
if (res.units === "in" || res.units === "inch") scale = 25.4;
|
|
1364
|
+
bbox.min.multiplyScalar(scale);
|
|
1365
|
+
bbox.max.multiplyScalar(scale);
|
|
1366
|
+
return { bbox, refFileName: refFile.name, scale, units: res.units };
|
|
1367
|
+
} catch (e) {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
const computeOutlineBbox = async (file) => {
|
|
1372
|
+
const nameUpper = baseNameUpper(file.name);
|
|
1373
|
+
const parser = new GerberOutlineParser();
|
|
1374
|
+
const text = await file.text();
|
|
1375
|
+
const { shapes, strokes } = parser.parse(text);
|
|
1376
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
1377
|
+
let pointCount = 0;
|
|
1378
|
+
const pushXY = (x, y) => {
|
|
1379
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
1380
|
+
if (x < minX) minX = x;
|
|
1381
|
+
if (x > maxX) maxX = x;
|
|
1382
|
+
if (y < minY) minY = y;
|
|
1383
|
+
if (y > maxY) maxY = y;
|
|
1384
|
+
pointCount++;
|
|
1385
|
+
};
|
|
1386
|
+
const consumeShape = (s) => {
|
|
1387
|
+
const curves = s?.curves || [];
|
|
1388
|
+
if (curves.length > 0) {
|
|
1389
|
+
for (const c of curves) {
|
|
1390
|
+
if (c?.isLineCurve) {
|
|
1391
|
+
const v1 = c.v1;
|
|
1392
|
+
const v2 = c.v2;
|
|
1393
|
+
if (v1) pushXY(v1.x, v1.y);
|
|
1394
|
+
if (v2) pushXY(v2.x, v2.y);
|
|
1395
|
+
} else {
|
|
1396
|
+
const p0 = c?.getPoint?.(0);
|
|
1397
|
+
const p1 = c?.getPoint?.(1);
|
|
1398
|
+
if (p0) pushXY(p0.x, p0.y);
|
|
1399
|
+
if (p1) pushXY(p1.x, p1.y);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const pts = s?.getPoints?.() || [];
|
|
1405
|
+
for (const p of pts) pushXY(p.x, p.y);
|
|
1406
|
+
};
|
|
1407
|
+
if (shapes && shapes.length > 0) {
|
|
1408
|
+
for (const s of shapes) consumeShape(s);
|
|
1409
|
+
}
|
|
1410
|
+
if (pointCount === 0 && strokes && strokes.length > 0) {
|
|
1411
|
+
for (const seg of strokes) {
|
|
1412
|
+
pushXY(seg.start.x, seg.start.y);
|
|
1413
|
+
pushXY(seg.end.x, seg.end.y);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
const valid = Number.isFinite(minX) && Number.isFinite(maxX) && Number.isFinite(minY) && Number.isFinite(maxY) && maxX > minX && maxY > minY;
|
|
1417
|
+
if (!valid) {
|
|
1418
|
+
return {
|
|
1419
|
+
file,
|
|
1420
|
+
name: file.name,
|
|
1421
|
+
nameUpper,
|
|
1422
|
+
valid: false,
|
|
1423
|
+
shapesCount: shapes?.length || 0,
|
|
1424
|
+
strokesCount: strokes?.length || 0
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
const bbox = new THREE2.Box3(
|
|
1428
|
+
new THREE2.Vector3(minX, minY, 0),
|
|
1429
|
+
new THREE2.Vector3(maxX, maxY, 0)
|
|
1430
|
+
);
|
|
1431
|
+
const size = bbox.getSize(new THREE2.Vector3());
|
|
1432
|
+
const center = bbox.getCenter(new THREE2.Vector3());
|
|
1433
|
+
const area = size.x * size.y;
|
|
1434
|
+
return {
|
|
1435
|
+
file,
|
|
1436
|
+
name: file.name,
|
|
1437
|
+
nameUpper,
|
|
1438
|
+
valid: true,
|
|
1439
|
+
bbox,
|
|
1440
|
+
size,
|
|
1441
|
+
center,
|
|
1442
|
+
area,
|
|
1443
|
+
shapesCount: shapes?.length || 0,
|
|
1444
|
+
strokesCount: strokes?.length || 0
|
|
1445
|
+
};
|
|
1446
|
+
};
|
|
1447
|
+
const ref = await getReferenceBbox();
|
|
1448
|
+
const infos = [];
|
|
1449
|
+
for (const f of outlineCandidates) {
|
|
1450
|
+
try {
|
|
1451
|
+
infos.push(await computeOutlineBbox(f));
|
|
1452
|
+
} catch (e) {
|
|
1453
|
+
infos.push({ file: f, name: f.name, nameUpper: baseNameUpper(f.name), valid: false });
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
try {
|
|
1457
|
+
const logList = infos.map((i) => {
|
|
1458
|
+
if (!i.valid) {
|
|
1459
|
+
return { file: i.name, valid: false, shapes: i.shapesCount || 0, strokes: i.strokesCount || 0 };
|
|
1460
|
+
}
|
|
1461
|
+
return {
|
|
1462
|
+
file: i.name,
|
|
1463
|
+
valid: true,
|
|
1464
|
+
w: Number(i.size.x.toFixed(2)),
|
|
1465
|
+
h: Number(i.size.y.toFixed(2)),
|
|
1466
|
+
area: Number(i.area.toFixed(2)),
|
|
1467
|
+
cx: Number(i.center.x.toFixed(2)),
|
|
1468
|
+
cy: Number(i.center.y.toFixed(2)),
|
|
1469
|
+
shapes: i.shapesCount || 0,
|
|
1470
|
+
strokes: i.strokesCount || 0
|
|
1471
|
+
};
|
|
1472
|
+
});
|
|
1473
|
+
debugWarn("[3D-new][OutlineCandidates] \u8F6E\u5ED3\u5019\u9009\u6587\u4EF6 bbox \u5217\u8868:", logList);
|
|
1474
|
+
if (ref?.bbox) {
|
|
1475
|
+
const refSize = ref.bbox.getSize(new THREE2.Vector3());
|
|
1476
|
+
const refCenter = ref.bbox.getCenter(new THREE2.Vector3());
|
|
1477
|
+
debugWarn("[3D-new][OutlineCandidates] \u53C2\u8003\u5C42:", {
|
|
1478
|
+
file: ref.refFileName,
|
|
1479
|
+
units: ref.units,
|
|
1480
|
+
scale: ref.scale,
|
|
1481
|
+
w: Number(refSize.x.toFixed(2)),
|
|
1482
|
+
h: Number(refSize.y.toFixed(2)),
|
|
1483
|
+
cx: Number(refCenter.x.toFixed(2)),
|
|
1484
|
+
cy: Number(refCenter.y.toFixed(2))
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
} catch (e) {
|
|
1488
|
+
}
|
|
1489
|
+
const validInfos = infos.filter((i) => i.valid);
|
|
1490
|
+
if (validInfos.length === 0) return { files: outlineFiles, refBbox: ref?.bbox || null };
|
|
1491
|
+
const hasGko = validInfos.some((i) => isExt(i.nameUpper, ".GKO"));
|
|
1492
|
+
const scoreInfo = (i) => {
|
|
1493
|
+
let score = 0;
|
|
1494
|
+
if (hasGko && !isExt(i.nameUpper, ".GKO")) score += 3;
|
|
1495
|
+
if (isExt(i.nameUpper, ".GM2") || isExt(i.nameUpper, ".GM1")) score += 0.5;
|
|
1496
|
+
if ((i.shapesCount || 0) === 0 && (i.strokesCount || 0) > 0) score += 0.5;
|
|
1497
|
+
if ((i.shapesCount || 0) === 0 && (i.strokesCount || 0) === 0) score += 10;
|
|
1498
|
+
if (ref?.bbox) {
|
|
1499
|
+
const refBox = ref.bbox;
|
|
1500
|
+
const refSize = refBox.getSize(new THREE2.Vector3());
|
|
1501
|
+
const refCenter = refBox.getCenter(new THREE2.Vector3());
|
|
1502
|
+
const expandedRef = refBox.clone().expandByScalar(Math.max(10, Math.max(refSize.x, refSize.y) * 0.05));
|
|
1503
|
+
if (!expandedRef.intersectsBox(i.bbox)) score += 1e3;
|
|
1504
|
+
const rx = i.size.x / Math.max(1e-9, refSize.x);
|
|
1505
|
+
const ry = i.size.y / Math.max(1e-9, refSize.y);
|
|
1506
|
+
score += Math.abs(Math.log(rx)) + Math.abs(Math.log(ry));
|
|
1507
|
+
const dist = i.center.distanceTo(refCenter);
|
|
1508
|
+
const maxDim = Math.max(refSize.x, refSize.y);
|
|
1509
|
+
score += dist / Math.max(1e-9, maxDim);
|
|
1510
|
+
} else {
|
|
1511
|
+
}
|
|
1512
|
+
return score;
|
|
1513
|
+
};
|
|
1514
|
+
let chosen = null;
|
|
1515
|
+
let bestScore = Infinity;
|
|
1516
|
+
for (const i of validInfos) {
|
|
1517
|
+
const s = scoreInfo(i);
|
|
1518
|
+
if (s < bestScore) {
|
|
1519
|
+
bestScore = s;
|
|
1520
|
+
chosen = i;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
if (!chosen) return { files: outlineFiles, refBbox: ref?.bbox || null };
|
|
1524
|
+
const chosenFile = chosen.file;
|
|
1525
|
+
const chosenNameUpper = (chosenFile.name || "").toUpperCase();
|
|
1526
|
+
const filesToKeep = [chosenFile];
|
|
1527
|
+
if (chosenNameUpper.endsWith(".GKO")) {
|
|
1528
|
+
const gm1Files = outlineCandidates.filter((f) => {
|
|
1529
|
+
const nameUpper = (f.name || "").toUpperCase();
|
|
1530
|
+
return nameUpper.endsWith(".GM1") && f !== chosenFile;
|
|
1531
|
+
});
|
|
1532
|
+
if (gm1Files.length > 0) {
|
|
1533
|
+
filesToKeep.push(...gm1Files);
|
|
1534
|
+
debugLog("[3D-new][OutlineCandidates] \u{1F517} \u53D1\u73B0GM1\u5C42\uFF0C\u5C06\u4E0EGKO\u5408\u5E76\u751F\u6210\u5B8C\u6574\u8F6E\u5ED3");
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const ignored = outlineCandidates.filter((f) => !filesToKeep.includes(f)).map((f) => f.name);
|
|
1538
|
+
if (ignored.length > 0) {
|
|
1539
|
+
debugLog("[3D-new][OutlineCandidates] \u8F6E\u5ED3\u5C42\u5DF2\u9009\u62E9:", filesToKeep.map((f) => f.name).join(" + "), "\uFF1B\u5FFD\u7565\u5176\u4F59:", ignored);
|
|
1540
|
+
} else {
|
|
1541
|
+
debugLog("[3D-new][OutlineCandidates] \u8F6E\u5ED3\u5C42\u5DF2\u9009\u62E9:", filesToKeep.map((f) => f.name).join(" + "));
|
|
1542
|
+
}
|
|
1543
|
+
return { files: filesToKeep, refBbox: ref?.bbox || null };
|
|
1544
|
+
};
|
|
1545
|
+
const pickResult = await pickBestOutlineFiles();
|
|
1546
|
+
const referenceBbox = pickResult.refBbox;
|
|
1547
|
+
outlineFiles.splice(0, outlineFiles.length, ...pickResult.files);
|
|
1548
|
+
const drillFiles = gerberFiles.filter((f) => {
|
|
1549
|
+
const name = f.name.toLowerCase();
|
|
1550
|
+
return name.match(/(\.drl|\.dri|\.txt|\.drd)/i) && !name.includes("status report") && !name.includes(".rep") && !name.includes(".log") && !name.includes(".apr");
|
|
1551
|
+
});
|
|
1552
|
+
const endTimeClassify = performance.now();
|
|
1553
|
+
console.log(`[\u6027\u80FD] \u6587\u4EF6\u5206\u7C7B\u5B8C\u6210\uFF0C\u8017\u65F6: ${(endTimeClassify - startTimeClassify).toFixed(2)}ms`);
|
|
1554
|
+
console.log("[\u6027\u80FD] \u5F00\u59CB\u6E32\u67D3\u8F6E\u5ED3\u5C42...");
|
|
1555
|
+
const startTimeOutline = performance.now();
|
|
1556
|
+
let hasOutline = false;
|
|
1557
|
+
let fallbackLayerForCamera = null;
|
|
1558
|
+
const outlineStrokes = [];
|
|
1559
|
+
let usedBboxOutlineFallback = false;
|
|
1560
|
+
const OUTLINE_MIN_AREA_RATIO = 0.3;
|
|
1561
|
+
const OUTLINE_TOTAL_AREA_RATIO = 0.5;
|
|
1562
|
+
const OUTLINE_SHAPE_COUNT_LIMIT = 50;
|
|
1563
|
+
const OUTLINE_FORCED_RATIO_LIMIT = 0.5;
|
|
1564
|
+
const OUTLINE_MERGE_TOLERANCE = 0.2;
|
|
1565
|
+
if (outlineFiles.length > 0) {
|
|
1566
|
+
const parser = new GerberOutlineParser();
|
|
1567
|
+
let shapes = [];
|
|
1568
|
+
for (const file of outlineFiles) {
|
|
1569
|
+
const text = await file.text();
|
|
1570
|
+
const { shapes: fileShapes, strokes } = parser.parse(text);
|
|
1571
|
+
if (fileShapes?.length > 0) {
|
|
1572
|
+
for (const s of fileShapes) shapes.push(s);
|
|
1573
|
+
}
|
|
1574
|
+
if (strokes?.length > 0) {
|
|
1575
|
+
for (const seg of strokes) outlineStrokes.push(seg);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (outlineFiles.length > 1 && outlineStrokes.length > 0) {
|
|
1579
|
+
debugLog(`[\u8F6E\u5ED3\u5C42] \u{1F517} \u5408\u5E76 ${outlineFiles.length} \u4E2A\u6587\u4EF6\uFF0C\u7EBF\u6BB5\u6570: ${outlineStrokes.length}`);
|
|
1580
|
+
const mergedShapes = stitchSegmentsToShapes(outlineStrokes);
|
|
1581
|
+
if (mergedShapes?.length > 0) {
|
|
1582
|
+
debugLog(`[\u8F6E\u5ED3\u5C42] \u2705 \u5408\u5E76\u6210\u529F\uFF0C\u751F\u6210 ${mergedShapes.length} \u4E2A\u5F62\u72B6`);
|
|
1583
|
+
mergedShapes.sort((a, b) => {
|
|
1584
|
+
const areaA = Math.abs(THREE2.ShapeUtils.area(a.getPoints()));
|
|
1585
|
+
const areaB = Math.abs(THREE2.ShapeUtils.area(b.getPoints()));
|
|
1586
|
+
return areaB - areaA;
|
|
1587
|
+
});
|
|
1588
|
+
shapes = mergedShapes;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
debugLog(`[\u8F6E\u5ED3\u5C42] \u5F62\u72B6: ${shapes.length}, \u7EBF\u6BB5: ${outlineStrokes.length}`);
|
|
1592
|
+
const MAX_POINTS_PER_SHAPE = 3e3;
|
|
1593
|
+
const simplifyDouglasPeucker = (points, epsilon) => {
|
|
1594
|
+
if (!points || points.length < 3) return points || [];
|
|
1595
|
+
const sq = (v) => v * v;
|
|
1596
|
+
const distPointToSegSq = (p, a, b) => {
|
|
1597
|
+
const vx = b.x - a.x;
|
|
1598
|
+
const vy = b.y - a.y;
|
|
1599
|
+
const wx = p.x - a.x;
|
|
1600
|
+
const wy = p.y - a.y;
|
|
1601
|
+
const c1 = vx * wx + vy * wy;
|
|
1602
|
+
if (c1 <= 0) return sq(p.x - a.x) + sq(p.y - a.y);
|
|
1603
|
+
const c2 = vx * vx + vy * vy;
|
|
1604
|
+
if (c2 <= c1) return sq(p.x - b.x) + sq(p.y - b.y);
|
|
1605
|
+
const t = c1 / c2;
|
|
1606
|
+
const px = a.x + t * vx;
|
|
1607
|
+
const py = a.y + t * vy;
|
|
1608
|
+
return sq(p.x - px) + sq(p.y - py);
|
|
1609
|
+
};
|
|
1610
|
+
const epsSq = epsilon * epsilon;
|
|
1611
|
+
const keep = new Array(points.length).fill(false);
|
|
1612
|
+
keep[0] = true;
|
|
1613
|
+
keep[points.length - 1] = true;
|
|
1614
|
+
const stack = [[0, points.length - 1]];
|
|
1615
|
+
while (stack.length) {
|
|
1616
|
+
const [s, e] = stack.pop();
|
|
1617
|
+
let maxD = 0;
|
|
1618
|
+
let idx = -1;
|
|
1619
|
+
const a = points[s];
|
|
1620
|
+
const b = points[e];
|
|
1621
|
+
for (let i = s + 1; i < e; i++) {
|
|
1622
|
+
const d = distPointToSegSq(points[i], a, b);
|
|
1623
|
+
if (d > maxD) {
|
|
1624
|
+
maxD = d;
|
|
1625
|
+
idx = i;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
if (idx >= 0 && maxD > epsSq) {
|
|
1629
|
+
keep[idx] = true;
|
|
1630
|
+
stack.push([s, idx], [idx, e]);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
const out = [];
|
|
1634
|
+
for (let i = 0; i < points.length; i++) if (keep[i]) out.push(points[i]);
|
|
1635
|
+
return out;
|
|
1636
|
+
};
|
|
1637
|
+
for (let i = 0; i < shapes.length; i++) {
|
|
1638
|
+
const shape = shapes[i];
|
|
1639
|
+
let currentPoints = shape.getPoints();
|
|
1640
|
+
if (currentPoints.length > MAX_POINTS_PER_SHAPE) {
|
|
1641
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
1642
|
+
for (const p of currentPoints) {
|
|
1643
|
+
minX = Math.min(minX, p.x);
|
|
1644
|
+
maxX = Math.max(maxX, p.x);
|
|
1645
|
+
minY = Math.min(minY, p.y);
|
|
1646
|
+
maxY = Math.max(maxY, p.y);
|
|
1647
|
+
}
|
|
1648
|
+
const boardSize = Math.max(maxX - minX, maxY - minY);
|
|
1649
|
+
const targetPoints = 2e3;
|
|
1650
|
+
const epsilon = Math.max(0.05, boardSize * 3e-4);
|
|
1651
|
+
const simplified = simplifyDouglasPeucker(currentPoints, epsilon);
|
|
1652
|
+
debugLog(`[\u8F6E\u5ED3\u5C42] \u7B80\u5316\u5F62\u72B6 ${i}: ${currentPoints.length} -> ${simplified.length} \u70B9 (\u5BB9\u5DEE=${epsilon.toFixed(4)}mm)`);
|
|
1653
|
+
if (simplified.length >= 4) {
|
|
1654
|
+
const newShape = new THREE2.Shape();
|
|
1655
|
+
newShape.moveTo(simplified[0].x, simplified[0].y);
|
|
1656
|
+
for (let j = 1; j < simplified.length; j++) {
|
|
1657
|
+
newShape.lineTo(simplified[j].x, simplified[j].y);
|
|
1658
|
+
}
|
|
1659
|
+
newShape.closePath();
|
|
1660
|
+
newShape.userData = shape.userData;
|
|
1661
|
+
shapes[i] = newShape;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
const attachOutlineHoles = (inputShapes, referenceBbox2) => {
|
|
1666
|
+
if (!inputShapes || inputShapes.length <= 1) return inputShapes || [];
|
|
1667
|
+
const isPointInShape = (pt, shape) => {
|
|
1668
|
+
const points = shape.getPoints();
|
|
1669
|
+
if (!points || points.length < 3) return false;
|
|
1670
|
+
let inside = false;
|
|
1671
|
+
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
1672
|
+
const xi = points[i].x, yi = points[i].y;
|
|
1673
|
+
const xj = points[j].x, yj = points[j].y;
|
|
1674
|
+
if (yi > pt.y !== yj > pt.y && pt.x < (xj - xi) * (pt.y - yi) / (yj - yi) + xi) inside = !inside;
|
|
1675
|
+
}
|
|
1676
|
+
return inside;
|
|
1677
|
+
};
|
|
1678
|
+
const signedArea = (pts) => {
|
|
1679
|
+
return THREE2.ShapeUtils.area(pts);
|
|
1680
|
+
};
|
|
1681
|
+
const bboxOfShape = (s) => {
|
|
1682
|
+
const pts = s.getPoints();
|
|
1683
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
1684
|
+
for (const p of pts) {
|
|
1685
|
+
if (p.x < minX) minX = p.x;
|
|
1686
|
+
if (p.x > maxX) maxX = p.x;
|
|
1687
|
+
if (p.y < minY) minY = p.y;
|
|
1688
|
+
if (p.y > maxY) maxY = p.y;
|
|
1689
|
+
}
|
|
1690
|
+
const valid = isFinite(minX) && isFinite(maxX) && isFinite(minY) && isFinite(maxY) && maxX > minX && maxY > minY;
|
|
1691
|
+
if (!valid) return null;
|
|
1692
|
+
return { minX, minY, maxX, maxY, w: maxX - minX, h: maxY - minY, area: (maxX - minX) * (maxY - minY) };
|
|
1693
|
+
};
|
|
1694
|
+
const bboxContains = (outer, inner, pad = 0.2) => {
|
|
1695
|
+
return inner.minX >= outer.minX - pad && inner.maxX <= outer.maxX + pad && inner.minY >= outer.minY - pad && inner.maxY <= outer.maxY + pad;
|
|
1696
|
+
};
|
|
1697
|
+
const refArea = referenceBbox2 && !referenceBbox2.isEmpty() ? referenceBbox2.getSize(new THREE2.Vector3()).x * referenceBbox2.getSize(new THREE2.Vector3()).y : 0;
|
|
1698
|
+
const items = inputShapes.map((s) => {
|
|
1699
|
+
const pts = s.getPoints();
|
|
1700
|
+
const area = Math.abs(THREE2.ShapeUtils.area(pts));
|
|
1701
|
+
const bbox = bboxOfShape(s);
|
|
1702
|
+
let cx = 0, cy = 0;
|
|
1703
|
+
for (const p of pts) {
|
|
1704
|
+
cx += p.x;
|
|
1705
|
+
cy += p.y;
|
|
1706
|
+
}
|
|
1707
|
+
const n = Math.max(1, pts.length);
|
|
1708
|
+
const center = { x: cx / n, y: cy / n };
|
|
1709
|
+
return { shape: s, area, bbox, center };
|
|
1710
|
+
}).filter((i) => i.area > 1e-3 && i.bbox);
|
|
1711
|
+
if (items.length <= 1) return inputShapes;
|
|
1712
|
+
const largestArea = items.reduce((m, it) => Math.max(m, it.area), 0);
|
|
1713
|
+
const minHoleArea = Math.max(0.05, largestArea * 5e-5);
|
|
1714
|
+
const maxHoleAreaByRef = refArea > 0 ? refArea * 0.3 : Infinity;
|
|
1715
|
+
items.sort((a, b) => a.area - b.area);
|
|
1716
|
+
const isHole = /* @__PURE__ */ new Set();
|
|
1717
|
+
for (const child of items) {
|
|
1718
|
+
if (child.area < minHoleArea) continue;
|
|
1719
|
+
if (child.area > maxHoleAreaByRef) continue;
|
|
1720
|
+
let bestParent = null;
|
|
1721
|
+
let bestParentArea = Infinity;
|
|
1722
|
+
for (const parent of items) {
|
|
1723
|
+
if (parent === child) continue;
|
|
1724
|
+
if (parent.area <= child.area) continue;
|
|
1725
|
+
if (!bboxContains(parent.bbox, child.bbox, 0.2)) continue;
|
|
1726
|
+
if (child.area / Math.max(1e-9, parent.area) > 0.6) continue;
|
|
1727
|
+
if (!isPointInShape(child.center, parent.shape)) continue;
|
|
1728
|
+
if (parent.area < bestParentArea) {
|
|
1729
|
+
bestParent = parent;
|
|
1730
|
+
bestParentArea = parent.area;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
if (bestParent) {
|
|
1734
|
+
const parentPts = bestParent.shape.getPoints();
|
|
1735
|
+
const childPts0 = child.shape.getPoints();
|
|
1736
|
+
if (parentPts && parentPts.length >= 3 && childPts0 && childPts0.length >= 3) {
|
|
1737
|
+
let childPts = childPts0.slice();
|
|
1738
|
+
const parentA = signedArea(parentPts);
|
|
1739
|
+
const childA = signedArea(childPts);
|
|
1740
|
+
if (parentA * childA > 0) {
|
|
1741
|
+
childPts = childPts.slice().reverse();
|
|
1742
|
+
}
|
|
1743
|
+
const holePath = new THREE2.Path();
|
|
1744
|
+
holePath.moveTo(childPts[0].x, childPts[0].y);
|
|
1745
|
+
for (let k = 1; k < childPts.length; k++) {
|
|
1746
|
+
holePath.lineTo(childPts[k].x, childPts[k].y);
|
|
1747
|
+
}
|
|
1748
|
+
holePath.closePath();
|
|
1749
|
+
bestParent.shape.holes = bestParent.shape.holes || [];
|
|
1750
|
+
bestParent.shape.holes.push(holePath);
|
|
1751
|
+
}
|
|
1752
|
+
isHole.add(child.shape);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
const roots = inputShapes.filter((s) => !isHole.has(s));
|
|
1756
|
+
const holeCount = inputShapes.length - roots.length;
|
|
1757
|
+
if (holeCount > 0) {
|
|
1758
|
+
debugLog(`[Outline] \u2705 \u5DF2\u6302\u5B54\u6D1E: holes=${holeCount}, roots=${roots.length}`);
|
|
1759
|
+
}
|
|
1760
|
+
return roots;
|
|
1761
|
+
};
|
|
1762
|
+
shapes = attachOutlineHoles(shapes, referenceBbox);
|
|
1763
|
+
if (shapes.length > 0) {
|
|
1764
|
+
const isPointInShape = (pt, shape) => {
|
|
1765
|
+
const points = shape.getPoints();
|
|
1766
|
+
if (!points || points.length < 3) return false;
|
|
1767
|
+
let inside = false;
|
|
1768
|
+
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
1769
|
+
const xi = points[i].x, yi = points[i].y;
|
|
1770
|
+
const xj = points[j].x, yj = points[j].y;
|
|
1771
|
+
if (yi > pt.y !== yj > pt.y && pt.x < (xj - xi) * (pt.y - yi) / (yj - yi) + xi) {
|
|
1772
|
+
inside = !inside;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
return inside;
|
|
1776
|
+
};
|
|
1777
|
+
let selectedOuterShape = null;
|
|
1778
|
+
let selectedOuterMeta = null;
|
|
1779
|
+
if (referenceBbox && !referenceBbox.isEmpty()) {
|
|
1780
|
+
const refCenter = referenceBbox.getCenter(new THREE2.Vector3());
|
|
1781
|
+
const focusCenter = { x: refCenter.x, y: refCenter.y };
|
|
1782
|
+
const refSize = referenceBbox.getSize(new THREE2.Vector3());
|
|
1783
|
+
const refMinX = refCenter.x - refSize.x / 2;
|
|
1784
|
+
const refMaxX = refCenter.x + refSize.x / 2;
|
|
1785
|
+
const refMinY = refCenter.y - refSize.y / 2;
|
|
1786
|
+
const refMaxY = refCenter.y + refSize.y / 2;
|
|
1787
|
+
const refArea = refSize.x * refSize.y;
|
|
1788
|
+
const MIN_OUTER_AREA = 1;
|
|
1789
|
+
const candidates = [];
|
|
1790
|
+
for (const s of shapes) {
|
|
1791
|
+
const area = Math.abs(THREE2.ShapeUtils.area(s.getPoints()));
|
|
1792
|
+
if (area < MIN_OUTER_AREA) continue;
|
|
1793
|
+
try {
|
|
1794
|
+
const pts = s.getPoints();
|
|
1795
|
+
let sMinX = Infinity, sMaxX = -Infinity, sMinY = Infinity, sMaxY = -Infinity;
|
|
1796
|
+
for (const p of pts) {
|
|
1797
|
+
if (p.x < sMinX) sMinX = p.x;
|
|
1798
|
+
if (p.x > sMaxX) sMaxX = p.x;
|
|
1799
|
+
if (p.y < sMinY) sMinY = p.y;
|
|
1800
|
+
if (p.y > sMaxY) sMaxY = p.y;
|
|
1801
|
+
}
|
|
1802
|
+
const overlapMinX = Math.max(refMinX, sMinX);
|
|
1803
|
+
const overlapMaxX = Math.min(refMaxX, sMaxX);
|
|
1804
|
+
const overlapMinY = Math.max(refMinY, sMinY);
|
|
1805
|
+
const overlapMaxY = Math.min(refMaxY, sMaxY);
|
|
1806
|
+
const overlapW = Math.max(0, overlapMaxX - overlapMinX);
|
|
1807
|
+
const overlapH = Math.max(0, overlapMaxY - overlapMinY);
|
|
1808
|
+
const overlapArea = overlapW * overlapH;
|
|
1809
|
+
const overlapRatio = refArea > 0 ? overlapArea / refArea : 0;
|
|
1810
|
+
const sW = sMaxX - sMinX;
|
|
1811
|
+
const sH = sMaxY - sMinY;
|
|
1812
|
+
const sBboxArea = sW * sH;
|
|
1813
|
+
if (overlapRatio > 0.3) {
|
|
1814
|
+
candidates.push({
|
|
1815
|
+
shape: s,
|
|
1816
|
+
area,
|
|
1817
|
+
overlapRatio,
|
|
1818
|
+
shapeBbox: { minX: sMinX, maxX: sMaxX, minY: sMinY, maxY: sMaxY, w: sW, h: sH, area: sBboxArea }
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
} catch (_) {
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (candidates.length > 0) {
|
|
1825
|
+
const highOverlapCandidates = candidates.filter((c) => c.overlapRatio > 0.8);
|
|
1826
|
+
const refSize2 = referenceBbox.getSize(new THREE2.Vector3());
|
|
1827
|
+
const refArea2 = refSize2.x * refSize2.y;
|
|
1828
|
+
let selectedCandidate;
|
|
1829
|
+
if (highOverlapCandidates.length > 1) {
|
|
1830
|
+
const validBboxCandidates = highOverlapCandidates.filter((c) => (c?.shapeBbox?.area || 0) >= refArea2 * 0.8);
|
|
1831
|
+
const pool = validBboxCandidates.length > 0 ? validBboxCandidates : highOverlapCandidates;
|
|
1832
|
+
pool.sort((a, b) => {
|
|
1833
|
+
const aB = a?.shapeBbox?.area || 0;
|
|
1834
|
+
const bB = b?.shapeBbox?.area || 0;
|
|
1835
|
+
return Math.abs(aB - refArea2) - Math.abs(bB - refArea2);
|
|
1836
|
+
});
|
|
1837
|
+
selectedCandidate = pool[0];
|
|
1838
|
+
debugLog(`[Outline] \u{1F50D} \u591A\u4E2A\u9AD8overlap\u5019\u9009\uFF0C\u9009\u62E9bbox\u6700\u63A5\u8FD1\u53C2\u8003\u5C42\u7684: bbox=${(selectedCandidate.shapeBbox?.area || 0).toFixed(2)}mm\xB2 (\u53C2\u8003=${refArea2.toFixed(2)}mm\xB2), poly=${selectedCandidate.area.toFixed(2)}mm\xB2`);
|
|
1839
|
+
} else {
|
|
1840
|
+
candidates.sort((a, b) => b.overlapRatio - a.overlapRatio);
|
|
1841
|
+
selectedCandidate = candidates[0];
|
|
1842
|
+
}
|
|
1843
|
+
const selectedArea = selectedCandidate.area;
|
|
1844
|
+
selectedOuterMeta = {
|
|
1845
|
+
bbox: selectedCandidate.shapeBbox || null,
|
|
1846
|
+
polyArea: selectedArea,
|
|
1847
|
+
overlapRatio: selectedCandidate.overlapRatio || 0
|
|
1848
|
+
};
|
|
1849
|
+
if (selectedArea < refArea2 * 0.5 && candidates.length > 1) {
|
|
1850
|
+
const totalArea = candidates.reduce((sum, c) => sum + c.area, 0);
|
|
1851
|
+
if (totalArea > refArea2 * 0.6) {
|
|
1852
|
+
debugLog(`[Outline] \u{1F517} \u68C0\u6D4B\u5230\u62FC\u677F\uFF1A\u6700\u9AD8\u91CD\u53E0=${selectedArea.toFixed(2)}mm\xB2 (overlap=${(selectedCandidate.overlapRatio * 100).toFixed(1)}%)\uFF0C\u603B\u9762\u79EF=${totalArea.toFixed(2)}mm\xB2\uFF0C\u4F7F\u7528 ${candidates.length} \u4E2A\u5019\u9009`);
|
|
1853
|
+
selectedOuterShape = null;
|
|
1854
|
+
} else {
|
|
1855
|
+
selectedOuterShape = selectedCandidate.shape;
|
|
1856
|
+
debugLog(`[Outline] \u{1F3AF} \u4F7F\u7528bbox\u91CD\u53E0\u9009\u62E9\u5916\u8F6E\u5ED3: \u9762\u79EF=${selectedArea.toFixed(2)}mm\xB2, overlap=${(selectedCandidate.overlapRatio * 100).toFixed(1)}%, \u5019\u9009\u6570=${candidates.length}`);
|
|
1857
|
+
}
|
|
1858
|
+
} else {
|
|
1859
|
+
selectedOuterShape = selectedCandidate.shape;
|
|
1860
|
+
debugLog(`[Outline] \u{1F3AF} \u4F7F\u7528bbox\u91CD\u53E0\u9009\u62E9\u5916\u8F6E\u5ED3: \u9762\u79EF=${selectedArea.toFixed(2)}mm\xB2, overlap=${(selectedCandidate.overlapRatio * 100).toFixed(1)}%, \u5019\u9009\u6570=${candidates.length}`);
|
|
1861
|
+
}
|
|
1862
|
+
} else {
|
|
1863
|
+
const shapesWithArea = shapes.map((s) => ({
|
|
1864
|
+
shape: s,
|
|
1865
|
+
area: Math.abs(THREE2.ShapeUtils.area(s.getPoints()))
|
|
1866
|
+
})).filter((item) => item.area >= 1);
|
|
1867
|
+
if (shapesWithArea.length > 0) {
|
|
1868
|
+
shapesWithArea.sort((a, b) => b.area - a.area);
|
|
1869
|
+
const maxShape = shapesWithArea[0];
|
|
1870
|
+
const refSize2 = referenceBbox.getSize(new THREE2.Vector3());
|
|
1871
|
+
const refArea2 = refSize2.x * refSize2.y;
|
|
1872
|
+
if (maxShape.area > refArea2 * 0.3) {
|
|
1873
|
+
selectedOuterShape = maxShape.shape;
|
|
1874
|
+
debugLog(`[Outline] \u{1F3AF} \u4F7F\u7528\u6700\u5927\u9762\u79EF\u5F62\u72B6\u4F5C\u4E3A\u5916\u8F6E\u5ED3: \u9762\u79EF=${maxShape.area.toFixed(2)}mm\xB2 (\u53C2\u8003\u5C42\u9762\u79EF=${refArea2.toFixed(2)}mm\xB2)`);
|
|
1875
|
+
} else {
|
|
1876
|
+
debugWarn(`[Outline] \u26A0\uFE0F \u6700\u5927\u5F62\u72B6\u9762\u79EF\u8FC7\u5C0F (${maxShape.area.toFixed(2)}mm\xB2 < ${(refArea2 * 0.3).toFixed(2)}mm\xB2)\uFF0C\u56DE\u9000\u5230\u788E\u7247\u5316\u5904\u7406`);
|
|
1877
|
+
}
|
|
1878
|
+
} else {
|
|
1879
|
+
debugWarn("[Outline] \u26A0\uFE0F \u6CA1\u6709\u6709\u6548\u5F62\u72B6\uFF08\u9762\u79EF>=1mm\xB2\uFF09\uFF0C\u56DE\u9000\u5230\u788E\u7247\u5316\u5904\u7406");
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
if (selectedOuterShape) {
|
|
1884
|
+
if (selectedOuterMeta?.bbox?.area && selectedOuterMeta?.polyArea) {
|
|
1885
|
+
const ratio = selectedOuterMeta.polyArea / Math.max(1e-9, selectedOuterMeta.bbox.area);
|
|
1886
|
+
if (ratio < 0.6) {
|
|
1887
|
+
const hullShape = outlineStrokes && outlineStrokes.length > 0 ? this.buildHullShapeFromSegments(outlineStrokes) : null;
|
|
1888
|
+
if (hullShape) {
|
|
1889
|
+
selectedOuterShape = hullShape;
|
|
1890
|
+
usedBboxOutlineFallback = true;
|
|
1891
|
+
debugWarn(`[Outline] \u26A0\uFE0F \u9009\u4E2D\u5916\u5F62\u591A\u8FB9\u5F62\u9762\u79EF\u504F\u5C0F(poly/bbox=${(ratio * 100).toFixed(1)}%)\uFF0C\u6539\u7528\u51F8\u5305\u5B9E\u5FC3\u677F\u515C\u5E95`);
|
|
1892
|
+
} else {
|
|
1893
|
+
const bb = selectedOuterMeta.bbox;
|
|
1894
|
+
const rectShape = new THREE2.Shape();
|
|
1895
|
+
rectShape.moveTo(bb.minX, bb.minY);
|
|
1896
|
+
rectShape.lineTo(bb.maxX, bb.minY);
|
|
1897
|
+
rectShape.lineTo(bb.maxX, bb.maxY);
|
|
1898
|
+
rectShape.lineTo(bb.minX, bb.maxY);
|
|
1899
|
+
rectShape.closePath();
|
|
1900
|
+
selectedOuterShape = rectShape;
|
|
1901
|
+
usedBboxOutlineFallback = true;
|
|
1902
|
+
debugWarn(`[Outline] \u26A0\uFE0F \u9009\u4E2D\u5916\u5F62\u591A\u8FB9\u5F62\u9762\u79EF\u504F\u5C0F(poly/bbox=${(ratio * 100).toFixed(1)}%)\uFF0C\u6539\u7528bbox\u5B9E\u5FC3\u677F\u515C\u5E95`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
this.renderOutlineBoard([selectedOuterShape], !usedBboxOutlineFallback);
|
|
1907
|
+
hasOutline = true;
|
|
1908
|
+
debugLog("[Outline] \u2705 \u4F7F\u7528\u53C2\u8003\u5C42\u9009\u62E9\u7684\u5F02\u5F62\u5916\u8F6E\u5ED3\u6E32\u67D3\u57FA\u6750");
|
|
1909
|
+
} else {
|
|
1910
|
+
const shapeInfos = shapes.map((s) => ({
|
|
1911
|
+
shape: s,
|
|
1912
|
+
area: Math.abs(THREE2.ShapeUtils.area(s.getPoints())),
|
|
1913
|
+
forcedClose: s.userData?.forcedClose || false
|
|
1914
|
+
}));
|
|
1915
|
+
const maxInfo = shapeInfos.reduce((max, cur) => cur.area > max.area ? cur : max);
|
|
1916
|
+
const forcedCount = shapeInfos.filter((info) => info.forcedClose).length;
|
|
1917
|
+
const totalArea = shapeInfos.reduce((sum, info) => sum + info.area, 0);
|
|
1918
|
+
const shapeBboxShape = this.buildBBoxShapeFromShapes(shapes);
|
|
1919
|
+
const strokeBboxShape = outlineStrokes.length > 0 ? this.buildBBoxShapeFromSegments(outlineStrokes) : null;
|
|
1920
|
+
const strokeHullShape = outlineStrokes.length > 0 ? this.buildHullShapeFromSegments(outlineStrokes) : null;
|
|
1921
|
+
const shapeBboxArea = shapeBboxShape ? Math.abs(THREE2.ShapeUtils.area(shapeBboxShape.getPoints())) : 0;
|
|
1922
|
+
const strokeBboxArea = strokeBboxShape ? Math.abs(THREE2.ShapeUtils.area(strokeBboxShape.getPoints())) : 0;
|
|
1923
|
+
const strokeHullArea = strokeHullShape ? Math.abs(THREE2.ShapeUtils.area(strokeHullShape.getPoints())) : 0;
|
|
1924
|
+
const bboxShape = strokeBboxArea > shapeBboxArea ? strokeBboxShape : shapeBboxShape;
|
|
1925
|
+
const bboxArea = Math.max(shapeBboxArea, strokeBboxArea);
|
|
1926
|
+
const fallbackShape = strokeHullShape && strokeHullArea > 0 ? strokeHullShape : bboxShape;
|
|
1927
|
+
const fallbackType = strokeHullShape && strokeHullArea > 0 ? "\u51F8\u5305" : "\u77E9\u5F62";
|
|
1928
|
+
if (strokeHullShape && strokeHullArea > 0) {
|
|
1929
|
+
debugLog(`[Outline] \u51F8\u5305\u53EF\u7528\u4E8E\u5F02\u5F62\u515C\u5E95\uFF08\u9762\u79EF=${strokeHullArea.toFixed(2)}mm\xB2\uFF09`);
|
|
1930
|
+
}
|
|
1931
|
+
let finalShapes = shapes;
|
|
1932
|
+
if (bboxShape && bboxArea > 0) {
|
|
1933
|
+
let maxShape = null;
|
|
1934
|
+
let maxArea = 0;
|
|
1935
|
+
let forcedCount2 = 0;
|
|
1936
|
+
let bestNonForced = null;
|
|
1937
|
+
let bestNonForcedArea = 0;
|
|
1938
|
+
for (const s of shapes) {
|
|
1939
|
+
const a = Math.abs(THREE2.ShapeUtils.area(s.getPoints()));
|
|
1940
|
+
const isForced = !!s?.userData?.forcedClose;
|
|
1941
|
+
if (isForced) forcedCount2++;
|
|
1942
|
+
if (!isForced && a > bestNonForcedArea) {
|
|
1943
|
+
bestNonForcedArea = a;
|
|
1944
|
+
bestNonForced = s;
|
|
1945
|
+
}
|
|
1946
|
+
if (a > maxArea) {
|
|
1947
|
+
maxArea = a;
|
|
1948
|
+
maxShape = s;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
const refAreaForOutline = strokeHullArea > 0 ? strokeHullArea : bboxArea;
|
|
1952
|
+
const forcedAreaRatio = refAreaForOutline > 0 ? maxArea / refAreaForOutline : 0;
|
|
1953
|
+
const forcedLooksGood = forcedAreaRatio >= 0.85;
|
|
1954
|
+
if (maxShape?.userData?.forcedClose) {
|
|
1955
|
+
if (bestNonForced && bestNonForcedArea > 0 && bestNonForcedArea >= refAreaForOutline * 0.6) {
|
|
1956
|
+
finalShapes = [bestNonForced];
|
|
1957
|
+
debugWarn(`[Outline] \u6700\u5927\u8F6E\u5ED3\u4E3A\u5F3A\u5236\u95ED\u5408\uFF0C\u6539\u7528\u975E\u5F3A\u5236\u95ED\u5408\u5916\u8F6E\u5ED3 (area=${bestNonForcedArea.toFixed(2)}mm\xB2)`);
|
|
1958
|
+
} else if (!forcedLooksGood) {
|
|
1959
|
+
finalShapes = [fallbackShape];
|
|
1960
|
+
debugWarn(`[Outline] \u6700\u5927\u8F6E\u5ED3\u4E3A\u5F3A\u5236\u95ED\u5408\u4E14\u9762\u79EF\u504F\u5C0F(ratio=${(forcedAreaRatio * 100).toFixed(1)}%)\uFF0C\u4F7F\u7528${fallbackType}\u515C\u5E95`);
|
|
1961
|
+
} else if (maxArea < bboxArea * OUTLINE_MIN_AREA_RATIO) {
|
|
1962
|
+
finalShapes = [fallbackShape];
|
|
1963
|
+
debugWarn(`[Outline] \u6700\u5927\u8F6E\u5ED3\u4E3A\u5F3A\u5236\u95ED\u5408\u4E14\u9762\u79EF\u8FC7\u5C0F\uFF0C\u4F7F\u7528${fallbackType}\u515C\u5E95`);
|
|
1964
|
+
} else {
|
|
1965
|
+
debugLog(`[Outline] \u6700\u5927\u8F6E\u5ED3\u4E3A\u5F3A\u5236\u95ED\u5408\u4F46\u9762\u79EF\u63A5\u8FD1\u53C2\u8003(ratio=${(forcedAreaRatio * 100).toFixed(1)}%)\uFF0C\u4F7F\u7528\u5F02\u5F62\u8F6E\u5ED3`);
|
|
1966
|
+
}
|
|
1967
|
+
} else if (maxArea < bboxArea * OUTLINE_MIN_AREA_RATIO && (shapes.length > OUTLINE_SHAPE_COUNT_LIMIT || forcedCount2 / Math.max(1, shapes.length) > OUTLINE_FORCED_RATIO_LIMIT)) {
|
|
1968
|
+
finalShapes = [fallbackShape];
|
|
1969
|
+
const forcedRatio = forcedCount2 / Math.max(1, shapes.length);
|
|
1970
|
+
debugWarn(`[Outline] \u8F6E\u5ED3\u788E\u7247\u5316(\u9762\u79EF=${maxArea.toFixed(2)}mm\xB2\uFF0C\u5F62\u72B6\u6570=${shapes.length}\uFF0C\u5F3A\u5236\u95ED\u5408\u6BD4=${forcedRatio.toFixed(2)})\uFF0C\u4F7F\u7528${fallbackType}\u515C\u5E95`);
|
|
1971
|
+
} else {
|
|
1972
|
+
const localTotalArea = shapes.reduce((acc, s) => acc + Math.abs(THREE2.ShapeUtils.area(s.getPoints())), 0);
|
|
1973
|
+
if (localTotalArea < bboxArea * OUTLINE_TOTAL_AREA_RATIO) {
|
|
1974
|
+
finalShapes = [fallbackShape];
|
|
1975
|
+
debugLog(`[Outline] \u603B\u9762\u79EF\u8FC7\u5C0F\uFF0C\u4F7F\u7528${fallbackType}\u515C\u5E95`);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
const usedBboxSolid = fallbackShape && finalShapes.length === 1 && finalShapes[0] === fallbackShape;
|
|
1980
|
+
usedBboxOutlineFallback = usedBboxSolid;
|
|
1981
|
+
this.renderOutlineBoard(finalShapes, !usedBboxSolid);
|
|
1982
|
+
hasOutline = true;
|
|
1983
|
+
}
|
|
1984
|
+
} else if (outlineStrokes.length > 0) {
|
|
1985
|
+
const strokeBboxShape = this.buildBBoxShapeFromSegments(outlineStrokes);
|
|
1986
|
+
if (strokeBboxShape) {
|
|
1987
|
+
const allPoints = strokeBboxShape.getPoints().map((p) => new THREE2.Vector3(p.x, p.y, 0));
|
|
1988
|
+
const bbox = new THREE2.Box3().setFromPoints(allPoints);
|
|
1989
|
+
this.outlineBbox = bbox.clone();
|
|
1990
|
+
debugLog(`[\u8F6E\u5ED3\u5C42] \u4F7F\u7528\u7EBF\u6BB5\u5916\u63A5\u77E9\u5F62\u4F5C\u4E3Abbox: ${bbox.getSize(new THREE2.Vector3()).x.toFixed(2)} x ${bbox.getSize(new THREE2.Vector3()).y.toFixed(2)}`);
|
|
1991
|
+
}
|
|
1992
|
+
} else {
|
|
1993
|
+
debugWarn("[3D-new] \u8F6E\u5ED3\u5C42\u89E3\u6790\u5931\u8D25\uFF08\u65E0\u5F62\u72B6\u65E0\u7EBF\u6BB5\uFF09\uFF0C\u5C1D\u8BD5\u4ECE\u5176\u4ED6\u5C42\u8BA1\u7B97\u5916\u63A5\u77E9\u5F62\u4F5C\u4E3A\u57FA\u6750...");
|
|
1994
|
+
const fallbackFilesForOutline = [...topMaskFiles, ...bottomMaskFiles, ...topCopperFiles, ...bottomCopperFiles];
|
|
1995
|
+
if (fallbackFilesForOutline.length > 0) {
|
|
1996
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1997
|
+
let hasValidBounds = false;
|
|
1998
|
+
for (const file of fallbackFilesForOutline) {
|
|
1999
|
+
try {
|
|
2000
|
+
const res = await GerberParser.parseFile(file, "#ffffff");
|
|
2001
|
+
if (res && res.data && res.data.vertices && res.data.vertices.length > 0) {
|
|
2002
|
+
let scale = 1;
|
|
2003
|
+
if (res.units === "in" || res.units === "inch") {
|
|
2004
|
+
scale = 25.4;
|
|
2005
|
+
}
|
|
2006
|
+
for (let i = 0; i < res.data.vertices.length; i += 3) {
|
|
2007
|
+
const x = res.data.vertices[i] * scale;
|
|
2008
|
+
const y = -res.data.vertices[i + 1] * scale;
|
|
2009
|
+
if (x < minX) minX = x;
|
|
2010
|
+
if (x > maxX) maxX = x;
|
|
2011
|
+
if (y < minY) minY = y;
|
|
2012
|
+
if (y > maxY) maxY = y;
|
|
2013
|
+
}
|
|
2014
|
+
hasValidBounds = true;
|
|
2015
|
+
}
|
|
2016
|
+
} catch (e) {
|
|
2017
|
+
debugWarn(`[3D-new] \u4ECE ${file.name} \u8BA1\u7B97\u8FB9\u754C\u5931\u8D25:`, e.message);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
if (hasValidBounds && isFinite(minX) && isFinite(maxX) && isFinite(minY) && isFinite(maxY)) {
|
|
2021
|
+
const width = maxX - minX;
|
|
2022
|
+
const height = maxY - minY;
|
|
2023
|
+
debugLog(`[3D-new] \u8F6E\u5ED3\u89E3\u6790\u5931\u8D25\u56DE\u9000\uFF1A\u8BA1\u7B97\u5F97\u5230\u5916\u63A5\u77E9\u5F62: ${width.toFixed(2)} x ${height.toFixed(2)}`);
|
|
2024
|
+
const padding = Math.max(width, height) * 0.02;
|
|
2025
|
+
const rectShape = new THREE2.Shape();
|
|
2026
|
+
rectShape.moveTo(minX - padding, minY - padding);
|
|
2027
|
+
rectShape.lineTo(maxX + padding, minY - padding);
|
|
2028
|
+
rectShape.lineTo(maxX + padding, maxY + padding);
|
|
2029
|
+
rectShape.lineTo(minX - padding, maxY + padding);
|
|
2030
|
+
rectShape.lineTo(minX - padding, minY - padding);
|
|
2031
|
+
this.renderOutlineBoard([rectShape], false);
|
|
2032
|
+
hasOutline = true;
|
|
2033
|
+
const bboxPoints = rectShape.getPoints().map((p) => new THREE2.Vector3(p.x, p.y, 0));
|
|
2034
|
+
this.outlineBbox = new THREE2.Box3().setFromPoints(bboxPoints);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
} else {
|
|
2039
|
+
debugWarn("[3D-new] \u672A\u627E\u5230\u8F6E\u5ED3\u5C42\u6587\u4EF6\uFF0C\u5C1D\u8BD5\u4ECE\u5176\u4ED6\u5C42\u8BA1\u7B97\u5916\u63A5\u77E9\u5F62\u4F5C\u4E3A\u57FA\u6750...");
|
|
2040
|
+
const fallbackFiles = [...topMaskFiles, ...bottomMaskFiles, ...topCopperFiles, ...bottomCopperFiles];
|
|
2041
|
+
if (fallbackFiles.length > 0) {
|
|
2042
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2043
|
+
let hasValidBounds = false;
|
|
2044
|
+
for (const file of fallbackFiles) {
|
|
2045
|
+
try {
|
|
2046
|
+
const res = await GerberParser.parseFile(file, "#ffffff");
|
|
2047
|
+
if (res && res.data && res.data.vertices && res.data.vertices.length > 0) {
|
|
2048
|
+
let scale = 1;
|
|
2049
|
+
if (res.units === "in" || res.units === "inch") {
|
|
2050
|
+
scale = 25.4;
|
|
2051
|
+
}
|
|
2052
|
+
for (let i = 0; i < res.data.vertices.length; i += 3) {
|
|
2053
|
+
const x = res.data.vertices[i] * scale;
|
|
2054
|
+
const y = -res.data.vertices[i + 1] * scale;
|
|
2055
|
+
if (x < minX) minX = x;
|
|
2056
|
+
if (x > maxX) maxX = x;
|
|
2057
|
+
if (y < minY) minY = y;
|
|
2058
|
+
if (y > maxY) maxY = y;
|
|
2059
|
+
}
|
|
2060
|
+
hasValidBounds = true;
|
|
2061
|
+
debugLog(`[3D-new] \u4ECE ${file.name} \u63D0\u53D6\u8FB9\u754C: minX=${minX.toFixed(2)}, maxX=${maxX.toFixed(2)}, minY=${minY.toFixed(2)}, maxY=${maxY.toFixed(2)}`);
|
|
2062
|
+
}
|
|
2063
|
+
} catch (e) {
|
|
2064
|
+
debugWarn(`[3D-new] \u4ECE ${file.name} \u8BA1\u7B97\u8FB9\u754C\u5931\u8D25:`, e.message);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
if (hasValidBounds && isFinite(minX) && isFinite(maxX) && isFinite(minY) && isFinite(maxY)) {
|
|
2068
|
+
const width = maxX - minX;
|
|
2069
|
+
const height = maxY - minY;
|
|
2070
|
+
const centerX = (minX + maxX) / 2;
|
|
2071
|
+
const centerY = (minY + maxY) / 2;
|
|
2072
|
+
debugLog(`[3D-new] \u8BA1\u7B97\u5F97\u5230\u5916\u63A5\u77E9\u5F62: ${width.toFixed(2)} x ${height.toFixed(2)}, \u4E2D\u5FC3=(${centerX.toFixed(2)}, ${centerY.toFixed(2)})`);
|
|
2073
|
+
const padding = Math.max(width, height) * 0.02;
|
|
2074
|
+
const rectShape = new THREE2.Shape();
|
|
2075
|
+
rectShape.moveTo(minX - padding, minY - padding);
|
|
2076
|
+
rectShape.lineTo(maxX + padding, minY - padding);
|
|
2077
|
+
rectShape.lineTo(maxX + padding, maxY + padding);
|
|
2078
|
+
rectShape.lineTo(minX - padding, maxY + padding);
|
|
2079
|
+
rectShape.lineTo(minX - padding, minY - padding);
|
|
2080
|
+
this.renderOutlineBoard([rectShape], false);
|
|
2081
|
+
hasOutline = true;
|
|
2082
|
+
} else {
|
|
2083
|
+
debugWarn("[3D-new] \u65E0\u6CD5\u8BA1\u7B97\u6709\u6548\u8FB9\u754C");
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
if (!hasOutline) {
|
|
2087
|
+
debugWarn("[3D-new] \u65E0\u6CD5\u4ECE\u4EFB\u4F55\u5C42\u63D0\u53D6\u8F6E\u5ED3\uFF0C\u8DF3\u8FC7\u57FA\u6750\u6E32\u67D3");
|
|
2088
|
+
BOARD_THICKNESS = 1.6;
|
|
2089
|
+
this.clearScene();
|
|
2090
|
+
if (topSilkFiles.length > 0) {
|
|
2091
|
+
fallbackLayerForCamera = { type: "GTO", files: topSilkFiles };
|
|
2092
|
+
} else if (topCopperFiles.length > 0) {
|
|
2093
|
+
fallbackLayerForCamera = { type: "GTL", files: topCopperFiles };
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
if (outlineStrokes.length > 0) {
|
|
2098
|
+
const MAX_STROKES_FOR_BORDER = 2e5;
|
|
2099
|
+
if (outlineStrokes.length > MAX_STROKES_FOR_BORDER) {
|
|
2100
|
+
debugWarn(`[Outline] \u7EBF\u6BB5\u6570\u91CF\u8FC7\u591A(${outlineStrokes.length})\uFF0C\u8DF3\u8FC7\u9ED1\u8272\u63CF\u8FB9\u4EE5\u907F\u514D\u6027\u80FD\u95EE\u9898`);
|
|
2101
|
+
} else if (usedBboxOutlineFallback) {
|
|
2102
|
+
BOARD_THICKNESS = BOARD_THICKNESS || 1.6;
|
|
2103
|
+
const zTop = BOARD_THICKNESS / 2 + 0.05;
|
|
2104
|
+
const zBottom = -BOARD_THICKNESS / 2 - 0.05 + 1e-3;
|
|
2105
|
+
this.renderOutlineStrokes(outlineStrokes, 0.1, COLORS.OUTLINE_EDGE, zTop);
|
|
2106
|
+
this.renderOutlineStrokes(outlineStrokes, 0.1, COLORS.OUTLINE_EDGE, zBottom);
|
|
2107
|
+
} else if (!hasOutline) {
|
|
2108
|
+
BOARD_THICKNESS = BOARD_THICKNESS || 1.6;
|
|
2109
|
+
const zTop = BOARD_THICKNESS / 2 + 0.05;
|
|
2110
|
+
const zBottom = -BOARD_THICKNESS / 2 - 0.05 + 1e-3;
|
|
2111
|
+
this.renderOutlineStrokes(outlineStrokes, 0.1, COLORS.OUTLINE_EDGE, zTop);
|
|
2112
|
+
this.renderOutlineStrokes(outlineStrokes, 0.1, COLORS.OUTLINE_EDGE, zBottom);
|
|
2113
|
+
hasOutline = true;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
const endTimeOutline = performance.now();
|
|
2117
|
+
console.log(`[\u6027\u80FD] \u8F6E\u5ED3\u5C42\u6E32\u67D3\u5B8C\u6210\uFF0C\u8017\u65F6: ${(endTimeOutline - startTimeOutline).toFixed(2)}ms`);
|
|
2118
|
+
console.log("[\u6027\u80FD] \u5F00\u59CB\u6E32\u67D3\u5176\u4ED6\u56FE\u5C42...");
|
|
2119
|
+
const startTimeOtherLayers = performance.now();
|
|
2120
|
+
console.log(
|
|
2121
|
+
`[3D-new] \u5F00\u59CB\u6E32\u67D3\u5176\u4ED6\u56FE\u5C42\uFF0C\u6587\u4EF6\u5206\u7EC4\u60C5\u51B5:`,
|
|
2122
|
+
`\u8F6E\u5ED3: ${outlineFiles.length}`,
|
|
2123
|
+
`\u9876\u94DC: ${topCopperFiles.length}`,
|
|
2124
|
+
`\u5185\u5C42\u94DC: ${innerCopperFiles.length}`,
|
|
2125
|
+
`\u5E95\u94DC: ${bottomCopperFiles.length}`,
|
|
2126
|
+
`\u9876\u963B\u710A: ${topMaskFiles.length}`,
|
|
2127
|
+
`\u5E95\u963B\u710A: ${bottomMaskFiles.length}`,
|
|
2128
|
+
`\u9876\u4E1D\u5370: ${topSilkFiles.length}`,
|
|
2129
|
+
`\u5E95\u4E1D\u5370: ${bottomSilkFiles.length}`,
|
|
2130
|
+
`\u94BB\u5B54: ${drillFiles.length}`
|
|
2131
|
+
);
|
|
2132
|
+
const parseLayer = async (file) => {
|
|
2133
|
+
const res = await GerberParser.parseFile(file, "#ffffff");
|
|
2134
|
+
return res;
|
|
2135
|
+
};
|
|
2136
|
+
const processLayerGroup = async (files, renderer, z, isTop) => {
|
|
2137
|
+
for (const file of files) {
|
|
2138
|
+
try {
|
|
2139
|
+
console.log(`[3D-new] \u6B63\u5728\u89E3\u6790\u6587\u4EF6: ${file.name}`);
|
|
2140
|
+
const res = await parseLayer(file);
|
|
2141
|
+
if (res && res.data) {
|
|
2142
|
+
console.log(`[3D-new] \u89E3\u6790\u6210\u529F: ${file.name}, \u5355\u4F4D: ${res.units}`);
|
|
2143
|
+
let scale = 1;
|
|
2144
|
+
if (res.units === "in" || res.units === "inch") {
|
|
2145
|
+
scale = 25.4;
|
|
2146
|
+
}
|
|
2147
|
+
renderer.call(this, [res], z, isTop, scale);
|
|
2148
|
+
} else {
|
|
2149
|
+
console.warn(`[3D-new] \u89E3\u6790\u5931\u8D25\u6216\u65E0\u6570\u636E: ${file.name}`);
|
|
2150
|
+
}
|
|
2151
|
+
} catch (e) {
|
|
2152
|
+
console.error(`[3D-new] \u89E3\u6790\u56FE\u5C42\u51FA\u9519: ${file.name}`, e);
|
|
2153
|
+
console.error(`[3D-new] \u9519\u8BEF\u5806\u6808:`, e.stack);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
};
|
|
2157
|
+
const halfThick = BOARD_THICKNESS / 2;
|
|
2158
|
+
const layerSpacing = 0.05;
|
|
2159
|
+
const zTopCu = halfThick + layerSpacing;
|
|
2160
|
+
const zBotCu = -halfThick - layerSpacing;
|
|
2161
|
+
await processLayerGroup(topCopperFiles, this.renderCopperLayer, zTopCu, true);
|
|
2162
|
+
innerCopperFiles.sort((a, b) => {
|
|
2163
|
+
const sa = getLayerType(a.name) || "";
|
|
2164
|
+
const sb = getLayerType(b.name) || "";
|
|
2165
|
+
const ma = sa.match(/^SIG(\d+)$/i);
|
|
2166
|
+
const mb = sb.match(/^SIG(\d+)$/i);
|
|
2167
|
+
return (ma ? parseInt(ma[1], 10) : 0) - (mb ? parseInt(mb[1], 10) : 0);
|
|
2168
|
+
});
|
|
2169
|
+
const nInner = innerCopperFiles.length;
|
|
2170
|
+
for (let i = 0; i < nInner; i++) {
|
|
2171
|
+
const t = (i + 1) / (nInner + 1);
|
|
2172
|
+
const zInner = zTopCu - t * (zTopCu - zBotCu);
|
|
2173
|
+
await processLayerGroup([innerCopperFiles[i]], this.renderCopperLayer, zInner, zInner >= 0);
|
|
2174
|
+
}
|
|
2175
|
+
await processLayerGroup(bottomCopperFiles, this.renderCopperLayer, zBotCu, false);
|
|
2176
|
+
await processLayerGroup(topMaskFiles, this.renderMaskLayer, halfThick + layerSpacing * 2, true);
|
|
2177
|
+
await processLayerGroup(bottomMaskFiles, this.renderMaskLayer, -halfThick - layerSpacing * 2, false);
|
|
2178
|
+
await processLayerGroup(topPasteFiles, this.renderPasteLayer, halfThick + layerSpacing * 2.5, true);
|
|
2179
|
+
await processLayerGroup(bottomPasteFiles, this.renderPasteLayer, -halfThick - layerSpacing * 2.5, false);
|
|
2180
|
+
await processLayerGroup(topSilkFiles, this.renderSilkscreenLayer, halfThick + layerSpacing * 3, true);
|
|
2181
|
+
await processLayerGroup(bottomSilkFiles, this.renderSilkscreenLayer, -halfThick - layerSpacing * 3, false);
|
|
2182
|
+
await this.renderPhoFiles(gerberFiles, halfThick, layerSpacing);
|
|
2183
|
+
let drillRefBbox = null;
|
|
2184
|
+
if (this.outlineBbox && !this.outlineBbox.isEmpty()) {
|
|
2185
|
+
drillRefBbox = this.outlineBbox.clone();
|
|
2186
|
+
} else if (this.baseGroup.children.length > 0) {
|
|
2187
|
+
drillRefBbox = new THREE2.Box3().setFromObject(this.baseGroup);
|
|
2188
|
+
}
|
|
2189
|
+
const expandedDrillRefBbox = drillRefBbox ? drillRefBbox.clone().expandByScalar(20) : null;
|
|
2190
|
+
for (const file of drillFiles) {
|
|
2191
|
+
try {
|
|
2192
|
+
const res = await parseLayer(file);
|
|
2193
|
+
if (res && res.data) {
|
|
2194
|
+
let scale = 1;
|
|
2195
|
+
if (res.units === "in" || res.units === "inch") {
|
|
2196
|
+
scale = 25.4;
|
|
2197
|
+
}
|
|
2198
|
+
if (expandedDrillRefBbox) {
|
|
2199
|
+
debugLog(`[Drill] \u5904\u7406 ${file.name}...`);
|
|
2200
|
+
const fixResult = this.fixDrillLayerAlignment(res.data, scale, drillRefBbox);
|
|
2201
|
+
scale = fixResult.scale;
|
|
2202
|
+
if (!fixResult.success || !fixResult.bbox) {
|
|
2203
|
+
debugWarn(`[Drill] ${file.name} \u4FEE\u6B63\u5931\u8D25\uFF0C\u8DF3\u8FC7\u6E32\u67D3`);
|
|
2204
|
+
continue;
|
|
2205
|
+
}
|
|
2206
|
+
const drillBbox = fixResult.bbox;
|
|
2207
|
+
if (!this.validateDrillBbox(drillBbox, drillRefBbox, expandedDrillRefBbox, file.name)) {
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
this.renderDrillLayer([res.data], scale);
|
|
2212
|
+
}
|
|
2213
|
+
} catch (e) {
|
|
2214
|
+
console.error(`Error processing drill ${file.name}:`, e);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
const endTimeOtherLayers = performance.now();
|
|
2218
|
+
console.log(`[\u6027\u80FD] \u5176\u4ED6\u56FE\u5C42\u6E32\u67D3\u5B8C\u6210\uFF0C\u8017\u65F6: ${(endTimeOtherLayers - startTimeOtherLayers).toFixed(2)}ms`);
|
|
2219
|
+
console.log("[\u6027\u80FD] \u5F00\u59CB\u76F8\u673A\u9002\u914D...");
|
|
2220
|
+
const startTimeCamera = performance.now();
|
|
2221
|
+
if (this.baseGroup.children.length > 0) {
|
|
2222
|
+
let finalBbox = null;
|
|
2223
|
+
if (this.outlineBbox) {
|
|
2224
|
+
if (this.outlineBbox.isEmpty()) {
|
|
2225
|
+
debugLog("[Camera] outlineBbox\u5B58\u5728\u4F46\u4E3A\u7A7A");
|
|
2226
|
+
} else {
|
|
2227
|
+
const size = this.outlineBbox.getSize(new THREE2.Vector3());
|
|
2228
|
+
const center = this.outlineBbox.getCenter(new THREE2.Vector3());
|
|
2229
|
+
debugLog(`[Camera] outlineBbox\u72B6\u6001: \u5C3A\u5BF8=${size.x.toFixed(2)} x ${size.y.toFixed(2)}, \u4E2D\u5FC3=(${center.x.toFixed(2)}, ${center.y.toFixed(2)})`);
|
|
2230
|
+
}
|
|
2231
|
+
} else {
|
|
2232
|
+
debugLog("[Camera] outlineBbox\u4E0D\u5B58\u5728");
|
|
2233
|
+
}
|
|
2234
|
+
if (this.outlineBbox && !this.outlineBbox.isEmpty()) {
|
|
2235
|
+
finalBbox = this.outlineBbox;
|
|
2236
|
+
const size = finalBbox.getSize(new THREE2.Vector3());
|
|
2237
|
+
const center = finalBbox.getCenter(new THREE2.Vector3());
|
|
2238
|
+
debugLog(`[Camera] \u4F7F\u7528\u8F6E\u5ED3\u5C42bbox\u8FDB\u884C\u81EA\u9002\u5E94: \u5C3A\u5BF8=${size.x.toFixed(2)} x ${size.y.toFixed(2)}, \u4E2D\u5FC3=(${center.x.toFixed(2)}, ${center.y.toFixed(2)})`);
|
|
2239
|
+
this.fitCameraToBbox(finalBbox);
|
|
2240
|
+
} else {
|
|
2241
|
+
let gtlBbox = null;
|
|
2242
|
+
let gblBbox = null;
|
|
2243
|
+
if (topCopperFiles.length > 0) {
|
|
2244
|
+
try {
|
|
2245
|
+
const res = await parseLayer(topCopperFiles[0]);
|
|
2246
|
+
if (res && res.data) {
|
|
2247
|
+
gtlBbox = this.calculateLayerBbox([res.data]);
|
|
2248
|
+
}
|
|
2249
|
+
} catch (e) {
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
if (bottomCopperFiles.length > 0) {
|
|
2253
|
+
try {
|
|
2254
|
+
const res = await parseLayer(bottomCopperFiles[0]);
|
|
2255
|
+
if (res && res.data) {
|
|
2256
|
+
gblBbox = this.calculateLayerBbox([res.data]);
|
|
2257
|
+
}
|
|
2258
|
+
} catch (e) {
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
const referenceBbox2 = gtlBbox && !gtlBbox.isEmpty() ? gtlBbox : gblBbox && !gblBbox.isEmpty() ? gblBbox : null;
|
|
2262
|
+
if (referenceBbox2) {
|
|
2263
|
+
finalBbox = referenceBbox2;
|
|
2264
|
+
const size = finalBbox.getSize(new THREE2.Vector3());
|
|
2265
|
+
const center = finalBbox.getCenter(new THREE2.Vector3());
|
|
2266
|
+
const layerName = gtlBbox && !gtlBbox.isEmpty() ? "GTL" : "GBL";
|
|
2267
|
+
debugLog(`[Camera] \u65E0\u8F6E\u5ED3\u5C42\uFF0C\u4F7F\u7528${layerName}\u5C42bbox\u8FDB\u884C\u81EA\u9002\u5E94: \u5C3A\u5BF8=${size.x.toFixed(2)} x ${size.y.toFixed(2)}, \u4E2D\u5FC3=(${center.x.toFixed(2)}, ${center.y.toFixed(2)})`);
|
|
2268
|
+
this.fitCameraToBbox(finalBbox);
|
|
2269
|
+
} else if (fallbackLayerForCamera) {
|
|
2270
|
+
try {
|
|
2271
|
+
const res = await parseLayer(fallbackLayerForCamera.files[0]);
|
|
2272
|
+
if (res && res.data) {
|
|
2273
|
+
const bbox = this.calculateLayerBbox([res.data]);
|
|
2274
|
+
if (bbox && !bbox.isEmpty()) {
|
|
2275
|
+
finalBbox = bbox;
|
|
2276
|
+
const size = finalBbox.getSize(new THREE2.Vector3());
|
|
2277
|
+
const center = finalBbox.getCenter(new THREE2.Vector3());
|
|
2278
|
+
debugLog(`[Camera] \u4F7F\u7528${fallbackLayerForCamera.type}\u5C42bbox\u8FDB\u884C\u81EA\u9002\u5E94: \u5C3A\u5BF8=${size.x.toFixed(2)} x ${size.y.toFixed(2)}, \u4E2D\u5FC3=(${center.x.toFixed(2)}, ${center.y.toFixed(2)})`);
|
|
2279
|
+
this.fitCameraToBbox(finalBbox);
|
|
2280
|
+
} else {
|
|
2281
|
+
debugLog("[Camera] \u672A\u627E\u5230\u6709\u6548bbox\uFF0C\u4F7F\u7528\u6240\u6709\u56FE\u5C42\u8FDB\u884C\u81EA\u9002\u5E94");
|
|
2282
|
+
this.fitCameraToObject(this.baseGroup);
|
|
2283
|
+
}
|
|
2284
|
+
} else {
|
|
2285
|
+
debugLog("[Camera] \u672A\u627E\u5230\u6709\u6548bbox\uFF0C\u4F7F\u7528\u6240\u6709\u56FE\u5C42\u8FDB\u884C\u81EA\u9002\u5E94");
|
|
2286
|
+
this.fitCameraToObject(this.baseGroup);
|
|
2287
|
+
}
|
|
2288
|
+
} catch (e) {
|
|
2289
|
+
debugLog("[Camera] \u672A\u627E\u5230\u6709\u6548bbox\uFF0C\u4F7F\u7528\u6240\u6709\u56FE\u5C42\u8FDB\u884C\u81EA\u9002\u5E94");
|
|
2290
|
+
this.fitCameraToObject(this.baseGroup);
|
|
2291
|
+
}
|
|
2292
|
+
} else {
|
|
2293
|
+
debugLog("[Camera] \u672A\u627E\u5230\u6709\u6548bbox\uFF0C\u4F7F\u7528\u6240\u6709\u56FE\u5C42\u8FDB\u884C\u81EA\u9002\u5E94");
|
|
2294
|
+
this.fitCameraToObject(this.baseGroup);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
const endTimeCamera = performance.now();
|
|
2298
|
+
console.log(`[\u6027\u80FD] \u76F8\u673A\u9002\u914D\u5B8C\u6210\uFF0C\u8017\u65F6: ${(endTimeCamera - startTimeCamera).toFixed(2)}ms`);
|
|
2299
|
+
if (this.renderer && this.renderer.domElement) {
|
|
2300
|
+
this.renderer.domElement.style.visibility = "visible";
|
|
2301
|
+
}
|
|
2302
|
+
const endTimeTotal = performance.now();
|
|
2303
|
+
const totalProcessTime = (endTimeTotal - startTimeTotal) / 1e3;
|
|
2304
|
+
console.log(`[\u6027\u80FD] ========== processFiles \u5B8C\u6210\uFF0C\u603B\u8017\u65F6: ${totalProcessTime.toFixed(2)}\u79D2 ==========`);
|
|
2305
|
+
console.log(`[\u6027\u80FD] \u8BE6\u7EC6\u8017\u65F6\uFF1A\u89E3\u538B=${((endTimeExtract - startTimeExtract) / 1e3).toFixed(2)}\u79D2, \u5206\u7C7B=${((endTimeClassify - startTimeClassify) / 1e3).toFixed(2)}\u79D2, \u8F6E\u5ED3=${((endTimeOutline - startTimeOutline) / 1e3).toFixed(2)}\u79D2, \u5176\u4ED6\u56FE\u5C42=${((endTimeOtherLayers - startTimeOtherLayers) / 1e3).toFixed(2)}\u79D2, \u76F8\u673A=${((endTimeCamera - startTimeCamera) / 1e3).toFixed(2)}\u79D2`);
|
|
2306
|
+
console.log(`[3D-new] \u6E32\u67D3\u5B8C\u6210\uFF0C\u573A\u666F\u4E2D\u5171\u6709 ${this.baseGroup.children.length} \u4E2A\u5BF9\u8C61`);
|
|
2307
|
+
} else {
|
|
2308
|
+
console.warn("[3D-new] \u6CA1\u6709\u6E32\u67D3\u4EFB\u4F55\u56FE\u5C42\uFF0CbaseGroup \u4E3A\u7A7A");
|
|
2309
|
+
if (this.renderer && this.renderer.domElement) {
|
|
2310
|
+
this.renderer.domElement.style.visibility = "visible";
|
|
2311
|
+
}
|
|
2312
|
+
const endTimeTotal = performance.now();
|
|
2313
|
+
const totalProcessTime = (endTimeTotal - startTimeTotal) / 1e3;
|
|
2314
|
+
console.log(`[\u6027\u80FD] ========== processFiles \u5B8C\u6210\uFF08\u65E0\u6E32\u67D3\uFF09\uFF0C\u603B\u8017\u65F6: ${totalProcessTime.toFixed(2)}\u79D2 ==========`);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
clearScene() {
|
|
2318
|
+
while (this.baseGroup.children.length) {
|
|
2319
|
+
const obj = this.baseGroup.children.pop();
|
|
2320
|
+
obj.geometry?.dispose?.();
|
|
2321
|
+
obj.material?.dispose?.();
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
renderOutlineBoard(shapes, showEdges = true) {
|
|
2325
|
+
this.clearScene();
|
|
2326
|
+
const allPointsForBbox = [];
|
|
2327
|
+
shapes.forEach((shape) => {
|
|
2328
|
+
const points = shape.getPoints();
|
|
2329
|
+
if (points && points.length > 0) {
|
|
2330
|
+
allPointsForBbox.push(...points.map((p) => new THREE2.Vector3(p.x, p.y, 0)));
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
if (allPointsForBbox.length > 0) {
|
|
2334
|
+
const bbox2 = new THREE2.Box3().setFromPoints(allPointsForBbox);
|
|
2335
|
+
this.outlineBbox = bbox2.clone();
|
|
2336
|
+
if (!this.outlineBbox.isEmpty()) {
|
|
2337
|
+
const size2 = this.outlineBbox.getSize(new THREE2.Vector3());
|
|
2338
|
+
const center = this.outlineBbox.getCenter(new THREE2.Vector3());
|
|
2339
|
+
debugLog(`[\u8F6E\u5ED3\u5C42] \u8BBE\u7F6EoutlineBbox: \u5C3A\u5BF8=${size2.x.toFixed(2)} x ${size2.y.toFixed(2)}, \u4E2D\u5FC3=(${center.x.toFixed(2)}, ${center.y.toFixed(2)})`);
|
|
2340
|
+
} else {
|
|
2341
|
+
debugWarn("[\u8F6E\u5ED3\u5C42] outlineBbox\u4E3A\u7A7A\uFF0C\u53EF\u80FD\u65E0\u6CD5\u6B63\u786E\u81EA\u9002\u5E94");
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
const validShapes = shapes.filter((s) => {
|
|
2345
|
+
const area = THREE2.ShapeUtils.area(s.getPoints());
|
|
2346
|
+
return Math.abs(area) > 1e-3;
|
|
2347
|
+
});
|
|
2348
|
+
if (validShapes.length === 0) {
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
const allPoints = [];
|
|
2352
|
+
validShapes.forEach((shape) => {
|
|
2353
|
+
allPoints.push(...shape.getPoints().map((p) => new THREE2.Vector3(p.x, p.y, 0)));
|
|
2354
|
+
});
|
|
2355
|
+
const bbox = new THREE2.Box3().setFromPoints(allPoints);
|
|
2356
|
+
const size = new THREE2.Vector3();
|
|
2357
|
+
bbox.getSize(size);
|
|
2358
|
+
const maxDim = Math.max(size.x, size.y);
|
|
2359
|
+
if (maxDim < 20) {
|
|
2360
|
+
BOARD_THICKNESS = 0.063 * 25.4;
|
|
2361
|
+
BOARD_THICKNESS = 1.6;
|
|
2362
|
+
} else {
|
|
2363
|
+
BOARD_THICKNESS = 1.6;
|
|
2364
|
+
}
|
|
2365
|
+
const material = this.getCachedMaterial(
|
|
2366
|
+
"outline-fill",
|
|
2367
|
+
() => new THREE2.MeshPhysicalMaterial({
|
|
2368
|
+
color: COLORS.OUTLINE,
|
|
2369
|
+
metalness: 0,
|
|
2370
|
+
roughness: 0.8,
|
|
2371
|
+
side: THREE2.DoubleSide
|
|
2372
|
+
})
|
|
2373
|
+
);
|
|
2374
|
+
const edgeMat = this.getCachedMaterial(
|
|
2375
|
+
"outline-edge",
|
|
2376
|
+
() => new THREE2.MeshBasicMaterial({ color: COLORS.OUTLINE_EDGE, side: THREE2.DoubleSide })
|
|
2377
|
+
);
|
|
2378
|
+
const createThickLineLoop = (points, z, width = 0.1) => {
|
|
2379
|
+
if (points.length < 2) return null;
|
|
2380
|
+
const first = points[0];
|
|
2381
|
+
const last = points[points.length - 1];
|
|
2382
|
+
const isClosed = Math.hypot(first.x - last.x, first.y - last.y) < 1e-4;
|
|
2383
|
+
const workingPoints = isClosed && points.length > 2 ? points.slice(0, -1) : points;
|
|
2384
|
+
if (workingPoints.length < 2) return null;
|
|
2385
|
+
const vertices = [];
|
|
2386
|
+
const indices = [];
|
|
2387
|
+
let baseIndex = 0;
|
|
2388
|
+
for (let i = 0; i < workingPoints.length; i++) {
|
|
2389
|
+
const p1 = workingPoints[i];
|
|
2390
|
+
const p2 = workingPoints[(i + 1) % workingPoints.length];
|
|
2391
|
+
const dx = p2.x - p1.x;
|
|
2392
|
+
const dy = p2.y - p1.y;
|
|
2393
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
2394
|
+
if (len < 1e-6) continue;
|
|
2395
|
+
const perpX = -dy / len * (width / 2);
|
|
2396
|
+
const perpY = dx / len * (width / 2);
|
|
2397
|
+
const p1x = p1.x + perpX;
|
|
2398
|
+
const p1y = p1.y + perpY;
|
|
2399
|
+
const p2x = p1.x - perpX;
|
|
2400
|
+
const p2y = p1.y - perpY;
|
|
2401
|
+
const p3x = p2.x - perpX;
|
|
2402
|
+
const p3y = p2.y - perpY;
|
|
2403
|
+
const p4x = p2.x + perpX;
|
|
2404
|
+
const p4y = p2.y + perpY;
|
|
2405
|
+
vertices.push(p1x, p1y, z);
|
|
2406
|
+
vertices.push(p2x, p2y, z);
|
|
2407
|
+
vertices.push(p3x, p3y, z);
|
|
2408
|
+
vertices.push(p4x, p4y, z);
|
|
2409
|
+
indices.push(baseIndex, baseIndex + 1, baseIndex + 2);
|
|
2410
|
+
indices.push(baseIndex, baseIndex + 2, baseIndex + 3);
|
|
2411
|
+
baseIndex += 4;
|
|
2412
|
+
}
|
|
2413
|
+
if (vertices.length === 0) return null;
|
|
2414
|
+
const geo = new THREE2.BufferGeometry();
|
|
2415
|
+
geo.setAttribute("position", new THREE2.Float32BufferAttribute(vertices, 3));
|
|
2416
|
+
geo.setIndex(indices);
|
|
2417
|
+
geo.computeVertexNormals();
|
|
2418
|
+
return new THREE2.Mesh(geo, edgeMat);
|
|
2419
|
+
};
|
|
2420
|
+
const triangulateWithEarcut = (shape) => {
|
|
2421
|
+
const points = shape.getPoints();
|
|
2422
|
+
if (points.length < 3) return null;
|
|
2423
|
+
const vertices = [];
|
|
2424
|
+
for (const pt of points) {
|
|
2425
|
+
vertices.push(pt.x, pt.y);
|
|
2426
|
+
}
|
|
2427
|
+
const holeIndices = [];
|
|
2428
|
+
if (shape.holes && shape.holes.length > 0) {
|
|
2429
|
+
for (const hole of shape.holes) {
|
|
2430
|
+
const holePts = hole.getPoints();
|
|
2431
|
+
if (holePts.length >= 3) {
|
|
2432
|
+
holeIndices.push(vertices.length / 2);
|
|
2433
|
+
for (const pt of holePts) {
|
|
2434
|
+
vertices.push(pt.x, pt.y);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
let triangleIndices;
|
|
2440
|
+
try {
|
|
2441
|
+
triangleIndices = earcut(vertices, holeIndices.length > 0 ? holeIndices : null, 2);
|
|
2442
|
+
} catch (err) {
|
|
2443
|
+
debugWarn("[Outline] earcut\u5931\u8D25:", err.message);
|
|
2444
|
+
return null;
|
|
2445
|
+
}
|
|
2446
|
+
if (!triangleIndices || triangleIndices.length === 0) {
|
|
2447
|
+
return null;
|
|
2448
|
+
}
|
|
2449
|
+
const topZ = BOARD_THICKNESS / 2;
|
|
2450
|
+
const bottomZ = -BOARD_THICKNESS / 2;
|
|
2451
|
+
const positions = [];
|
|
2452
|
+
const indices = [];
|
|
2453
|
+
const numVertices2D = vertices.length / 2;
|
|
2454
|
+
for (let i = 0; i < numVertices2D; i++) {
|
|
2455
|
+
const x = vertices[i * 2];
|
|
2456
|
+
const y = vertices[i * 2 + 1];
|
|
2457
|
+
positions.push(x, y, topZ);
|
|
2458
|
+
}
|
|
2459
|
+
for (let i = 0; i < numVertices2D; i++) {
|
|
2460
|
+
const x = vertices[i * 2];
|
|
2461
|
+
const y = vertices[i * 2 + 1];
|
|
2462
|
+
positions.push(x, y, bottomZ);
|
|
2463
|
+
}
|
|
2464
|
+
for (let i = 0; i < triangleIndices.length; i += 3) {
|
|
2465
|
+
indices.push(triangleIndices[i], triangleIndices[i + 1], triangleIndices[i + 2]);
|
|
2466
|
+
}
|
|
2467
|
+
for (let i = 0; i < triangleIndices.length; i += 3) {
|
|
2468
|
+
const a = triangleIndices[i] + numVertices2D;
|
|
2469
|
+
const b = triangleIndices[i + 1] + numVertices2D;
|
|
2470
|
+
const c = triangleIndices[i + 2] + numVertices2D;
|
|
2471
|
+
indices.push(a, c, b);
|
|
2472
|
+
}
|
|
2473
|
+
const outerPointCount = holeIndices.length > 0 ? holeIndices[0] : numVertices2D;
|
|
2474
|
+
for (let i = 0; i < outerPointCount; i++) {
|
|
2475
|
+
const next = (i + 1) % outerPointCount;
|
|
2476
|
+
const topA = i;
|
|
2477
|
+
const topB = next;
|
|
2478
|
+
const bottomA = i + numVertices2D;
|
|
2479
|
+
const bottomB = next + numVertices2D;
|
|
2480
|
+
indices.push(topA, bottomA, topB);
|
|
2481
|
+
indices.push(topB, bottomA, bottomB);
|
|
2482
|
+
}
|
|
2483
|
+
const geometry = new THREE2.BufferGeometry();
|
|
2484
|
+
geometry.setAttribute("position", new THREE2.Float32BufferAttribute(positions, 3));
|
|
2485
|
+
geometry.setIndex(indices);
|
|
2486
|
+
geometry.computeVertexNormals();
|
|
2487
|
+
return geometry;
|
|
2488
|
+
};
|
|
2489
|
+
const MAX_POINTS_FOR_EXTRUDE = 500;
|
|
2490
|
+
const MAX_POINTS_FOR_EARCUT = 5e3;
|
|
2491
|
+
let renderedCount = 0;
|
|
2492
|
+
let skippedComplexCount = 0;
|
|
2493
|
+
validShapes.forEach((shape) => {
|
|
2494
|
+
try {
|
|
2495
|
+
const points = shape.getPoints();
|
|
2496
|
+
if (points.length <= MAX_POINTS_FOR_EXTRUDE) {
|
|
2497
|
+
const geo = new THREE2.ExtrudeGeometry(shape, { depth: BOARD_THICKNESS, bevelEnabled: false });
|
|
2498
|
+
geo.translate(0, 0, -BOARD_THICKNESS / 2);
|
|
2499
|
+
const mesh = new THREE2.Mesh(geo, material);
|
|
2500
|
+
this.baseGroup.add(mesh);
|
|
2501
|
+
renderedCount++;
|
|
2502
|
+
} else if (points.length <= MAX_POINTS_FOR_EARCUT) {
|
|
2503
|
+
const geo = triangulateWithEarcut(shape);
|
|
2504
|
+
if (geo) {
|
|
2505
|
+
const mesh = new THREE2.Mesh(geo, material);
|
|
2506
|
+
this.baseGroup.add(mesh);
|
|
2507
|
+
renderedCount++;
|
|
2508
|
+
debugLog(`[Outline] \u4F7F\u7528earcut\u6E32\u67D3\u590D\u6742\u5F62\u72B6\uFF08${points.length}\u70B9\uFF09`);
|
|
2509
|
+
} else {
|
|
2510
|
+
skippedComplexCount++;
|
|
2511
|
+
}
|
|
2512
|
+
} else {
|
|
2513
|
+
debugWarn(`[Outline] \u5F62\u72B6\u8FC7\u4E8E\u590D\u6742\uFF08${points.length}\u70B9 > ${MAX_POINTS_FOR_EARCUT}\uFF09\uFF0C\u8DF3\u8FC7`);
|
|
2514
|
+
skippedComplexCount++;
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
if (showEdges && !shape.userData?.forcedClose) {
|
|
2518
|
+
const EDGE_Z_TOP = BOARD_THICKNESS / 2 + 0.05;
|
|
2519
|
+
const EDGE_Z_BOTTOM = -BOARD_THICKNESS / 2 - 0.05 + 1e-3;
|
|
2520
|
+
const pts2 = shape.getPoints();
|
|
2521
|
+
const thickTop = createThickLineLoop(pts2, EDGE_Z_TOP);
|
|
2522
|
+
const thickBottom = createThickLineLoop(pts2, EDGE_Z_BOTTOM);
|
|
2523
|
+
if (thickTop) this.baseGroup.add(thickTop);
|
|
2524
|
+
if (thickBottom) this.baseGroup.add(thickBottom);
|
|
2525
|
+
}
|
|
2526
|
+
if (showEdges && shape.holes && shape.holes.length > 0) {
|
|
2527
|
+
shape.holes.forEach((path) => {
|
|
2528
|
+
const EDGE_Z_TOP = BOARD_THICKNESS / 2 + 0.05;
|
|
2529
|
+
const EDGE_Z_BOTTOM = -BOARD_THICKNESS / 2 - 0.05 + 1e-3;
|
|
2530
|
+
const hPts2 = path.getPoints();
|
|
2531
|
+
const thickHoleTop = createThickLineLoop(hPts2, EDGE_Z_TOP);
|
|
2532
|
+
const thickHoleBottom = createThickLineLoop(hPts2, EDGE_Z_BOTTOM);
|
|
2533
|
+
if (thickHoleTop) this.baseGroup.add(thickHoleTop);
|
|
2534
|
+
if (thickHoleBottom) this.baseGroup.add(thickHoleBottom);
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
} catch (err) {
|
|
2538
|
+
debugWarn(`[Outline] \u6E32\u67D3\u5931\u8D25:`, err.message);
|
|
2539
|
+
skippedComplexCount++;
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
if (skippedComplexCount > 0) {
|
|
2543
|
+
debugWarn(`[Outline] \u8DF3\u8FC7\u4E86 ${skippedComplexCount} \u4E2A\u5F62\u72B6`);
|
|
2544
|
+
}
|
|
2545
|
+
if (renderedCount === 0 && validShapes.length > 0) {
|
|
2546
|
+
debugWarn("[Outline] \u6240\u6709\u5F62\u72B6\u90FD\u88AB\u8DF3\u8FC7\uFF0C\u4F7F\u7528bbox\u515C\u5E95");
|
|
2547
|
+
const bboxShape = this.buildBBoxShapeFromShapes(validShapes);
|
|
2548
|
+
if (bboxShape) {
|
|
2549
|
+
try {
|
|
2550
|
+
const geo = new THREE2.ExtrudeGeometry(bboxShape, { depth: BOARD_THICKNESS, bevelEnabled: false });
|
|
2551
|
+
geo.translate(0, 0, -BOARD_THICKNESS / 2);
|
|
2552
|
+
const mesh = new THREE2.Mesh(geo, material);
|
|
2553
|
+
this.baseGroup.add(mesh);
|
|
2554
|
+
} catch (err) {
|
|
2555
|
+
debugWarn("[Outline] bbox\u515C\u5E95\u4E5F\u5931\u8D25:", err.message);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
// 基于线段点集求凸包,生成单一 Shape 兜底
|
|
2561
|
+
buildHullShapeFromSegments(segments) {
|
|
2562
|
+
if (!segments || segments.length === 0) return null;
|
|
2563
|
+
const pts = [];
|
|
2564
|
+
for (const seg of segments) {
|
|
2565
|
+
pts.push(new THREE2.Vector2(seg.start.x, seg.start.y));
|
|
2566
|
+
pts.push(new THREE2.Vector2(seg.end.x, seg.end.y));
|
|
2567
|
+
}
|
|
2568
|
+
if (pts.length < 3) return null;
|
|
2569
|
+
pts.sort((a, b) => a.x === b.x ? a.y - b.y : a.x - b.x);
|
|
2570
|
+
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
2571
|
+
const lower = [];
|
|
2572
|
+
for (const p of pts) {
|
|
2573
|
+
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
|
|
2574
|
+
lower.pop();
|
|
2575
|
+
}
|
|
2576
|
+
lower.push(p);
|
|
2577
|
+
}
|
|
2578
|
+
const upper = [];
|
|
2579
|
+
for (let i = pts.length - 1; i >= 0; i--) {
|
|
2580
|
+
const p = pts[i];
|
|
2581
|
+
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
|
|
2582
|
+
upper.pop();
|
|
2583
|
+
}
|
|
2584
|
+
upper.push(p);
|
|
2585
|
+
}
|
|
2586
|
+
upper.pop();
|
|
2587
|
+
lower.pop();
|
|
2588
|
+
const hull = lower.concat(upper);
|
|
2589
|
+
if (hull.length < 3) return null;
|
|
2590
|
+
const shape = new THREE2.Shape();
|
|
2591
|
+
shape.moveTo(hull[0].x, hull[0].y);
|
|
2592
|
+
for (let i = 1; i < hull.length; i++) shape.lineTo(hull[i].x, hull[i].y);
|
|
2593
|
+
shape.closePath();
|
|
2594
|
+
return shape;
|
|
2595
|
+
}
|
|
2596
|
+
// 基于已存在的闭合 shapes 计算外接矩形,作为兜底
|
|
2597
|
+
buildBBoxShapeFromShapes(shapes) {
|
|
2598
|
+
if (!shapes || shapes.length === 0) return null;
|
|
2599
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
2600
|
+
for (const s of shapes) {
|
|
2601
|
+
const pts = s.getPoints();
|
|
2602
|
+
for (const p of pts) {
|
|
2603
|
+
if (p.x < minX) minX = p.x;
|
|
2604
|
+
if (p.x > maxX) maxX = p.x;
|
|
2605
|
+
if (p.y < minY) minY = p.y;
|
|
2606
|
+
if (p.y > maxY) maxY = p.y;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (!isFinite(minX) || !isFinite(maxX) || !isFinite(minY) || !isFinite(maxY)) return null;
|
|
2610
|
+
const shape = new THREE2.Shape();
|
|
2611
|
+
shape.moveTo(minX, minY);
|
|
2612
|
+
shape.lineTo(maxX, minY);
|
|
2613
|
+
shape.lineTo(maxX, maxY);
|
|
2614
|
+
shape.lineTo(minX, maxY);
|
|
2615
|
+
shape.closePath();
|
|
2616
|
+
return shape;
|
|
2617
|
+
}
|
|
2618
|
+
// 基于线段集合计算外接矩形,用于兜底渲染和相机适配
|
|
2619
|
+
buildBBoxShapeFromSegments(segments) {
|
|
2620
|
+
if (!segments || segments.length === 0) return null;
|
|
2621
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
2622
|
+
for (const seg of segments) {
|
|
2623
|
+
if (seg.start.x < minX) minX = seg.start.x;
|
|
2624
|
+
if (seg.start.x > maxX) maxX = seg.start.x;
|
|
2625
|
+
if (seg.start.y < minY) minY = seg.start.y;
|
|
2626
|
+
if (seg.start.y > maxY) maxY = seg.start.y;
|
|
2627
|
+
if (seg.end.x < minX) minX = seg.end.x;
|
|
2628
|
+
if (seg.end.x > maxX) maxX = seg.end.x;
|
|
2629
|
+
if (seg.end.y < minY) minY = seg.end.y;
|
|
2630
|
+
if (seg.end.y > maxY) maxY = seg.end.y;
|
|
2631
|
+
}
|
|
2632
|
+
if (!isFinite(minX) || !isFinite(maxX) || !isFinite(minY) || !isFinite(maxY)) return null;
|
|
2633
|
+
const shape = new THREE2.Shape();
|
|
2634
|
+
shape.moveTo(minX, minY);
|
|
2635
|
+
shape.lineTo(maxX, minY);
|
|
2636
|
+
shape.lineTo(maxX, maxY);
|
|
2637
|
+
shape.lineTo(minX, maxY);
|
|
2638
|
+
shape.closePath();
|
|
2639
|
+
return shape;
|
|
2640
|
+
}
|
|
2641
|
+
// 兜底:用线段直接画轮廓(G01/G02/G03 离散后),不做布尔运算
|
|
2642
|
+
renderOutlineStrokes(segments, lineWidth = 0.1, color = COLORS.OUTLINE_EDGE, zOverride = null) {
|
|
2643
|
+
if (!segments || segments.length === 0) return;
|
|
2644
|
+
const points = [];
|
|
2645
|
+
segments.forEach((seg) => {
|
|
2646
|
+
points.push(new THREE2.Vector3(seg.start.x, seg.start.y, 0));
|
|
2647
|
+
points.push(new THREE2.Vector3(seg.end.x, seg.end.y, 0));
|
|
2648
|
+
});
|
|
2649
|
+
if (points.length > 0) {
|
|
2650
|
+
const bbox = new THREE2.Box3().setFromPoints(points);
|
|
2651
|
+
if (!this.outlineBbox || this.outlineBbox.isEmpty()) {
|
|
2652
|
+
this.outlineBbox = bbox.clone();
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
const key = `outline-stroke-${color.toString(16)}-${lineWidth}`;
|
|
2656
|
+
const edgeMat = this.getCachedMaterial(
|
|
2657
|
+
key,
|
|
2658
|
+
() => new THREE2.MeshBasicMaterial({ color, side: THREE2.DoubleSide })
|
|
2659
|
+
);
|
|
2660
|
+
const LINE_WIDTH = lineWidth;
|
|
2661
|
+
const vertices = [];
|
|
2662
|
+
const indices = [];
|
|
2663
|
+
let baseIndex = 0;
|
|
2664
|
+
const z = typeof zOverride === "number" ? zOverride : BOARD_THICKNESS / 2 + 0.05;
|
|
2665
|
+
for (const seg of segments) {
|
|
2666
|
+
const x1 = seg.start.x;
|
|
2667
|
+
const y1 = seg.start.y;
|
|
2668
|
+
const x2 = seg.end.x;
|
|
2669
|
+
const y2 = seg.end.y;
|
|
2670
|
+
const dx = x2 - x1;
|
|
2671
|
+
const dy = y2 - y1;
|
|
2672
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
2673
|
+
if (len < 1e-6) continue;
|
|
2674
|
+
const perpX = -dy / len * (LINE_WIDTH / 2);
|
|
2675
|
+
const perpY = dx / len * (LINE_WIDTH / 2);
|
|
2676
|
+
const p1x = x1 + perpX;
|
|
2677
|
+
const p1y = y1 + perpY;
|
|
2678
|
+
const p2x = x1 - perpX;
|
|
2679
|
+
const p2y = y1 - perpY;
|
|
2680
|
+
const p3x = x2 - perpX;
|
|
2681
|
+
const p3y = y2 - perpY;
|
|
2682
|
+
const p4x = x2 + perpX;
|
|
2683
|
+
const p4y = y2 + perpY;
|
|
2684
|
+
vertices.push(p1x, p1y, z);
|
|
2685
|
+
vertices.push(p2x, p2y, z);
|
|
2686
|
+
vertices.push(p3x, p3y, z);
|
|
2687
|
+
vertices.push(p4x, p4y, z);
|
|
2688
|
+
indices.push(baseIndex, baseIndex + 1, baseIndex + 2);
|
|
2689
|
+
indices.push(baseIndex, baseIndex + 2, baseIndex + 3);
|
|
2690
|
+
baseIndex += 4;
|
|
2691
|
+
}
|
|
2692
|
+
if (vertices.length === 0) return;
|
|
2693
|
+
const geo = new THREE2.BufferGeometry();
|
|
2694
|
+
geo.setAttribute("position", new THREE2.Float32BufferAttribute(vertices, 3));
|
|
2695
|
+
geo.setIndex(indices);
|
|
2696
|
+
geo.computeVertexNormals();
|
|
2697
|
+
const mesh = new THREE2.Mesh(geo, edgeMat);
|
|
2698
|
+
this.baseGroup.add(mesh);
|
|
2699
|
+
}
|
|
2700
|
+
// ==================== OTHER LAYER RENDERING ====================
|
|
2701
|
+
renderCopperLayer(layers, z, isTop, scale = 1) {
|
|
2702
|
+
const allShapes = [];
|
|
2703
|
+
for (const layer of layers) {
|
|
2704
|
+
const webglData = layer.data || layer;
|
|
2705
|
+
if (webglData.lineStrips && webglData.vertices) {
|
|
2706
|
+
for (const strip of webglData.lineStrips) {
|
|
2707
|
+
if (strip.count < 2) continue;
|
|
2708
|
+
const pts = [];
|
|
2709
|
+
for (let j = 0; j < strip.count; j++) {
|
|
2710
|
+
const ptr = (strip.start + j) * 3;
|
|
2711
|
+
pts.push(new THREE2.Vector2(webglData.vertices[ptr], -webglData.vertices[ptr + 1]));
|
|
2712
|
+
}
|
|
2713
|
+
if (pts.length < 2) continue;
|
|
2714
|
+
const first = pts[0];
|
|
2715
|
+
const last = pts[pts.length - 1];
|
|
2716
|
+
const isClosed = first.distanceTo(last) < 1e-3;
|
|
2717
|
+
if (!isClosed) continue;
|
|
2718
|
+
if (pts.length > 2 && first.distanceTo(last) < 1e-9) {
|
|
2719
|
+
pts.pop();
|
|
2720
|
+
}
|
|
2721
|
+
if (pts.length < 3) continue;
|
|
2722
|
+
const shape = new THREE2.Shape();
|
|
2723
|
+
shape.moveTo(pts[0].x, pts[0].y);
|
|
2724
|
+
for (let i = 1; i < pts.length; i++) {
|
|
2725
|
+
shape.lineTo(pts[i].x, pts[i].y);
|
|
2726
|
+
}
|
|
2727
|
+
shape.closePath();
|
|
2728
|
+
allShapes.push(shape);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
if (allShapes.length === 0) return;
|
|
2733
|
+
const shapesToRender = allShapes;
|
|
2734
|
+
const shapesWithArea = shapesToRender.map((shape, idx) => {
|
|
2735
|
+
const pts = shape.getPoints();
|
|
2736
|
+
const area = Math.abs(THREE2.ShapeUtils.area(pts));
|
|
2737
|
+
return { shape, area, idx, pointCount: pts.length };
|
|
2738
|
+
});
|
|
2739
|
+
shapesWithArea.sort((a, b) => b.area - a.area);
|
|
2740
|
+
const maxArea = shapesWithArea[0]?.area || 0;
|
|
2741
|
+
const filteredShapes = shapesWithArea;
|
|
2742
|
+
if (filteredShapes.length === 0) return;
|
|
2743
|
+
const shapes = filteredShapes.map((s) => s.shape);
|
|
2744
|
+
const material = this.getCachedMaterial(
|
|
2745
|
+
isTop ? "copper-top" : "copper-bot",
|
|
2746
|
+
() => new THREE2.MeshPhysicalMaterial({
|
|
2747
|
+
color: COLORS.COPPER,
|
|
2748
|
+
metalness: 0.3,
|
|
2749
|
+
roughness: 0.5,
|
|
2750
|
+
side: THREE2.DoubleSide
|
|
2751
|
+
})
|
|
2752
|
+
);
|
|
2753
|
+
this.renderShapesInChunks(shapes, material, z, scale, 2e3, isTop ? "GTL" : "GBL");
|
|
2754
|
+
}
|
|
2755
|
+
renderMaskLayer(layers, z, isTop, scale = 1) {
|
|
2756
|
+
const shapes = this.collectStandardShapes(layers);
|
|
2757
|
+
if (shapes.length === 0) return;
|
|
2758
|
+
const material = this.getCachedMaterial(
|
|
2759
|
+
"mask-opening",
|
|
2760
|
+
() => new THREE2.MeshPhysicalMaterial({
|
|
2761
|
+
color: COLORS.GOLD,
|
|
2762
|
+
metalness: 0.8,
|
|
2763
|
+
roughness: 0.2,
|
|
2764
|
+
side: THREE2.DoubleSide
|
|
2765
|
+
})
|
|
2766
|
+
);
|
|
2767
|
+
this.renderShapesInChunks(shapes, material, z + 1e-3, scale);
|
|
2768
|
+
}
|
|
2769
|
+
renderPasteLayer(layers, z, isTop, scale = 1) {
|
|
2770
|
+
const shapes = this.collectStandardShapes(layers);
|
|
2771
|
+
if (shapes.length === 0) return;
|
|
2772
|
+
const material = this.getCachedMaterial(
|
|
2773
|
+
"paste",
|
|
2774
|
+
() => new THREE2.MeshPhysicalMaterial({
|
|
2775
|
+
color: COLORS.PASTE,
|
|
2776
|
+
// Gray color for solder paste
|
|
2777
|
+
metalness: 0.3,
|
|
2778
|
+
roughness: 0.7,
|
|
2779
|
+
side: THREE2.DoubleSide
|
|
2780
|
+
})
|
|
2781
|
+
);
|
|
2782
|
+
this.renderShapesInChunks(shapes, material, z, scale);
|
|
2783
|
+
}
|
|
2784
|
+
renderSilkscreenLayer(layers, z, isTop, scale = 1) {
|
|
2785
|
+
const openPolylines = [];
|
|
2786
|
+
const strokeLoops = [];
|
|
2787
|
+
const fillShapes = [];
|
|
2788
|
+
for (const layer of layers) {
|
|
2789
|
+
const webglData = layer.data || layer;
|
|
2790
|
+
if (webglData.lineStrips && webglData.vertices) {
|
|
2791
|
+
for (const strip of webglData.lineStrips) {
|
|
2792
|
+
if (strip.count < 2) continue;
|
|
2793
|
+
const pts = [];
|
|
2794
|
+
for (let j = 0; j < strip.count; j++) {
|
|
2795
|
+
const ptr = (strip.start + j) * 3;
|
|
2796
|
+
pts.push(new THREE2.Vector2(webglData.vertices[ptr], -webglData.vertices[ptr + 1]));
|
|
2797
|
+
}
|
|
2798
|
+
if (pts.length < 2) continue;
|
|
2799
|
+
const first = pts[0];
|
|
2800
|
+
const last = pts[pts.length - 1];
|
|
2801
|
+
const isClosed = first.distanceTo(last) < 1e-3;
|
|
2802
|
+
if (!isClosed) {
|
|
2803
|
+
openPolylines.push(pts);
|
|
2804
|
+
continue;
|
|
2805
|
+
}
|
|
2806
|
+
if (pts.length > 2 && first.distanceTo(last) < 1e-9) {
|
|
2807
|
+
pts.pop();
|
|
2808
|
+
}
|
|
2809
|
+
if (pts.length < 3) continue;
|
|
2810
|
+
if (strip.kind === "stroke") {
|
|
2811
|
+
strokeLoops.push(pts);
|
|
2812
|
+
continue;
|
|
2813
|
+
}
|
|
2814
|
+
if (strip.kind === "fill") {
|
|
2815
|
+
const shape2 = new THREE2.Shape();
|
|
2816
|
+
shape2.moveTo(pts[0].x, pts[0].y);
|
|
2817
|
+
for (let i = 1; i < pts.length; i++) shape2.lineTo(pts[i].x, pts[i].y);
|
|
2818
|
+
shape2.closePath();
|
|
2819
|
+
fillShapes.push(shape2);
|
|
2820
|
+
continue;
|
|
2821
|
+
}
|
|
2822
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
2823
|
+
for (const p of pts) {
|
|
2824
|
+
if (p.x < minX) minX = p.x;
|
|
2825
|
+
if (p.x > maxX) maxX = p.x;
|
|
2826
|
+
if (p.y < minY) minY = p.y;
|
|
2827
|
+
if (p.y > maxY) maxY = p.y;
|
|
2828
|
+
}
|
|
2829
|
+
const w = maxX - minX;
|
|
2830
|
+
const h = maxY - minY;
|
|
2831
|
+
const cx = (minX + maxX) / 2;
|
|
2832
|
+
const cy = (minY + maxY) / 2;
|
|
2833
|
+
const rx = w / 2;
|
|
2834
|
+
const ry = h / 2;
|
|
2835
|
+
let ellipseLike = false;
|
|
2836
|
+
if (rx > 1e-6 && ry > 1e-6) {
|
|
2837
|
+
let sum = 0;
|
|
2838
|
+
let sumSq = 0;
|
|
2839
|
+
let n = 0;
|
|
2840
|
+
for (const p of pts) {
|
|
2841
|
+
const nx = (p.x - cx) / rx;
|
|
2842
|
+
const ny = (p.y - cy) / ry;
|
|
2843
|
+
const r = Math.sqrt(nx * nx + ny * ny);
|
|
2844
|
+
if (!Number.isFinite(r)) continue;
|
|
2845
|
+
sum += r;
|
|
2846
|
+
sumSq += r * r;
|
|
2847
|
+
n++;
|
|
2848
|
+
}
|
|
2849
|
+
if (n >= 8) {
|
|
2850
|
+
const mean = sum / n;
|
|
2851
|
+
const varR = Math.max(0, sumSq / n - mean * mean);
|
|
2852
|
+
const std = Math.sqrt(varR);
|
|
2853
|
+
ellipseLike = mean > 0.9 && mean < 1.1 && std < 0.03;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
if (ellipseLike) {
|
|
2857
|
+
strokeLoops.push(pts);
|
|
2858
|
+
continue;
|
|
2859
|
+
}
|
|
2860
|
+
const shape = new THREE2.Shape();
|
|
2861
|
+
shape.moveTo(pts[0].x, pts[0].y);
|
|
2862
|
+
for (let i = 1; i < pts.length; i++) {
|
|
2863
|
+
shape.lineTo(pts[i].x, pts[i].y);
|
|
2864
|
+
}
|
|
2865
|
+
shape.closePath();
|
|
2866
|
+
fillShapes.push(shape);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
if (fillShapes.length === 0 && strokeLoops.length === 0 && openPolylines.length === 0) return;
|
|
2871
|
+
const material = this.getCachedMaterial(
|
|
2872
|
+
"silkscreen",
|
|
2873
|
+
() => new THREE2.MeshPhysicalMaterial({
|
|
2874
|
+
color: COLORS.SILKSCREEN,
|
|
2875
|
+
side: THREE2.DoubleSide
|
|
2876
|
+
})
|
|
2877
|
+
);
|
|
2878
|
+
if (fillShapes.length > 0) {
|
|
2879
|
+
const shapesWithHoles = this.processSilkscreenHoles(fillShapes);
|
|
2880
|
+
if (shapesWithHoles.length > 0) {
|
|
2881
|
+
this.renderShapesInChunks(shapesWithHoles, material, z, scale, 2e3, isTop ? "GTO" : "GBO");
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
const desiredWidthMm = 0.2;
|
|
2885
|
+
const widthInLayerUnits = desiredWidthMm / (scale || 1);
|
|
2886
|
+
const strokeShapes = [];
|
|
2887
|
+
const allStrokePolylines = openPolylines.concat(strokeLoops);
|
|
2888
|
+
for (const pts of allStrokePolylines) {
|
|
2889
|
+
if (pts.length < 2) continue;
|
|
2890
|
+
const first = pts[0];
|
|
2891
|
+
const last = pts[pts.length - 1];
|
|
2892
|
+
const isClosed = first.distanceTo(last) < 1e-3;
|
|
2893
|
+
const n = isClosed && pts.length > 2 && first.distanceTo(last) < 1e-9 ? pts.length - 1 : pts.length;
|
|
2894
|
+
const segCount = isClosed ? n : n - 1;
|
|
2895
|
+
for (let i = 0; i < segCount; i++) {
|
|
2896
|
+
const p1 = pts[i];
|
|
2897
|
+
const p2 = isClosed && i === n - 1 ? pts[0] : pts[i + 1];
|
|
2898
|
+
const dx = p2.x - p1.x;
|
|
2899
|
+
const dy = p2.y - p1.y;
|
|
2900
|
+
const len = Math.hypot(dx, dy);
|
|
2901
|
+
if (len < 1e-8) continue;
|
|
2902
|
+
const nx = -dy / len * (widthInLayerUnits / 2);
|
|
2903
|
+
const ny = dx / len * (widthInLayerUnits / 2);
|
|
2904
|
+
const s = new THREE2.Shape();
|
|
2905
|
+
s.moveTo(p1.x + nx, p1.y + ny);
|
|
2906
|
+
s.lineTo(p2.x + nx, p2.y + ny);
|
|
2907
|
+
s.lineTo(p2.x - nx, p2.y - ny);
|
|
2908
|
+
s.lineTo(p1.x - nx, p1.y - ny);
|
|
2909
|
+
s.closePath();
|
|
2910
|
+
strokeShapes.push(s);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
if (strokeShapes.length > 0) {
|
|
2914
|
+
const zOffset = isTop ? 1e-3 : -1e-3;
|
|
2915
|
+
this.renderShapesInChunks(strokeShapes, material, z + zOffset, scale, 2e3, isTop ? "GTO" : "GBO");
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
renderDrillLayer(layers, scale = 1) {
|
|
2919
|
+
const shapes = this.collectStandardShapes(layers);
|
|
2920
|
+
if (shapes.length === 0) {
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
const material = this.getCachedMaterial(
|
|
2924
|
+
"drill",
|
|
2925
|
+
() => new THREE2.MeshBasicMaterial({
|
|
2926
|
+
color: COLORS.DRILL,
|
|
2927
|
+
depthWrite: true,
|
|
2928
|
+
depthTest: true
|
|
2929
|
+
})
|
|
2930
|
+
);
|
|
2931
|
+
const extrusionSettings = {
|
|
2932
|
+
depth: BOARD_THICKNESS + 0.35,
|
|
2933
|
+
// Slightly longer than thickness for visibility, must exceed paste layer
|
|
2934
|
+
bevelEnabled: false
|
|
2935
|
+
};
|
|
2936
|
+
const geo = new THREE2.ExtrudeGeometry(shapes, extrusionSettings);
|
|
2937
|
+
geo.translate(0, 0, -BOARD_THICKNESS / 2 - 0.175);
|
|
2938
|
+
const mesh = new THREE2.Mesh(geo, material);
|
|
2939
|
+
mesh.scale.set(scale, scale, 1);
|
|
2940
|
+
mesh.renderOrder = 10;
|
|
2941
|
+
this.baseGroup.add(mesh);
|
|
2942
|
+
debugLog(`[Drill Layer] \u63D0\u53D6\u5230 ${shapes.length} \u4E2A\u94BB\u5B54\u5F62\u72B6\uFF0Cscale=${scale}`);
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* 专门渲染 .pho 文件的函数
|
|
2946
|
+
* 根据文件名前缀和数字识别图层类型,并调用对应的渲染函数
|
|
2947
|
+
*/
|
|
2948
|
+
async renderPhoFiles(gerberFiles, halfThick, layerSpacing) {
|
|
2949
|
+
const phoFiles = gerberFiles.filter((file) => {
|
|
2950
|
+
const fileNameOnly = file.name.split(/[/\\]/).pop() || "";
|
|
2951
|
+
const fileExtension = "." + fileNameOnly.split(".").pop().toUpperCase();
|
|
2952
|
+
return fileExtension === ".PHO";
|
|
2953
|
+
});
|
|
2954
|
+
if (phoFiles.length === 0) return;
|
|
2955
|
+
const buildPhoGroups = (files) => {
|
|
2956
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2957
|
+
const add = (prefix, num) => {
|
|
2958
|
+
if (!groups.has(prefix)) {
|
|
2959
|
+
groups.set(prefix, { min: num });
|
|
2960
|
+
} else {
|
|
2961
|
+
groups.get(prefix).min = Math.min(groups.get(prefix).min, num);
|
|
2962
|
+
}
|
|
2963
|
+
};
|
|
2964
|
+
files.forEach((f) => {
|
|
2965
|
+
const nameOnly = f.name.split(/[/\\]/).pop() || "";
|
|
2966
|
+
const upper = nameOnly.toUpperCase();
|
|
2967
|
+
let prefix = null;
|
|
2968
|
+
let num = 0;
|
|
2969
|
+
if (upper.startsWith("SST")) {
|
|
2970
|
+
prefix = "sst";
|
|
2971
|
+
const m = upper.match(/^SST(\d+)/);
|
|
2972
|
+
num = m ? parseInt(m[1], 10) : 0;
|
|
2973
|
+
} else if (upper.startsWith("SMD")) {
|
|
2974
|
+
prefix = "smd";
|
|
2975
|
+
const m = upper.match(/^SMD(\d+)/);
|
|
2976
|
+
num = m ? parseInt(m[1], 10) : 0;
|
|
2977
|
+
} else if (upper.startsWith("SM")) {
|
|
2978
|
+
prefix = "sm";
|
|
2979
|
+
const m = upper.match(/^SM(\d+)/);
|
|
2980
|
+
num = m ? parseInt(m[1], 10) : 0;
|
|
2981
|
+
} else if (upper.startsWith("ART")) {
|
|
2982
|
+
prefix = "art";
|
|
2983
|
+
const m = upper.match(/^ART(\d+)/);
|
|
2984
|
+
num = m ? parseInt(m[1], 10) : 0;
|
|
2985
|
+
} else if (upper.startsWith("SSB")) {
|
|
2986
|
+
prefix = "ssb";
|
|
2987
|
+
num = 0;
|
|
2988
|
+
} else if (upper.startsWith("DRL")) {
|
|
2989
|
+
prefix = "drl";
|
|
2990
|
+
num = 0;
|
|
2991
|
+
} else if (upper.startsWith("DD")) {
|
|
2992
|
+
prefix = "dd";
|
|
2993
|
+
num = 0;
|
|
2994
|
+
}
|
|
2995
|
+
if (prefix) add(prefix, num);
|
|
2996
|
+
});
|
|
2997
|
+
return groups;
|
|
2998
|
+
};
|
|
2999
|
+
const phoGroups = buildPhoGroups(phoFiles);
|
|
3000
|
+
const getPhoLayerType = (fileName) => {
|
|
3001
|
+
const fileNameOnly = fileName.split(/[/\\]/).pop() || "";
|
|
3002
|
+
const fileNameUpper = fileNameOnly.toUpperCase();
|
|
3003
|
+
if (fileNameUpper.startsWith("SST")) return "GTO";
|
|
3004
|
+
const pickByGroup = (prefix, topType, botType, defaultType = null) => {
|
|
3005
|
+
const m = fileNameUpper.match(new RegExp(`^${prefix}(\\d+)`));
|
|
3006
|
+
const num = m ? parseInt(m[1], 10) : 0;
|
|
3007
|
+
const g = phoGroups.get(prefix.toLowerCase());
|
|
3008
|
+
if (g) {
|
|
3009
|
+
return num === g.min ? topType : botType;
|
|
3010
|
+
}
|
|
3011
|
+
return defaultType || topType;
|
|
3012
|
+
};
|
|
3013
|
+
if (fileNameUpper.startsWith("SMD")) {
|
|
3014
|
+
return pickByGroup("SMD", "GTP", "GBP", "GTP");
|
|
3015
|
+
}
|
|
3016
|
+
if (fileNameUpper.startsWith("SM")) {
|
|
3017
|
+
return pickByGroup("SM", "GTS", "GBS", "GTS");
|
|
3018
|
+
}
|
|
3019
|
+
if (fileNameUpper.startsWith("ART")) {
|
|
3020
|
+
return pickByGroup("ART", "GTL", "GBL", "GTL");
|
|
3021
|
+
}
|
|
3022
|
+
if (fileNameUpper.startsWith("SSB")) return "GBO";
|
|
3023
|
+
if (fileNameUpper.startsWith("DRL")) return "DRL";
|
|
3024
|
+
return null;
|
|
3025
|
+
};
|
|
3026
|
+
for (const file of phoFiles) {
|
|
3027
|
+
try {
|
|
3028
|
+
const layerType = getPhoLayerType(file.name);
|
|
3029
|
+
if (!layerType) continue;
|
|
3030
|
+
const res = await GerberParser.parseFile(file, "#ffffff");
|
|
3031
|
+
if (!res || !res.data) continue;
|
|
3032
|
+
let scale = 1;
|
|
3033
|
+
if (res.units === "in" || res.units === "inch") {
|
|
3034
|
+
scale = 25.4;
|
|
3035
|
+
}
|
|
3036
|
+
switch (layerType) {
|
|
3037
|
+
case "GTL":
|
|
3038
|
+
this.renderCopperLayer([res.data], halfThick + layerSpacing, true, scale);
|
|
3039
|
+
break;
|
|
3040
|
+
case "GBL":
|
|
3041
|
+
this.renderCopperLayer([res.data], -halfThick - layerSpacing, false, scale);
|
|
3042
|
+
break;
|
|
3043
|
+
case "GTS":
|
|
3044
|
+
this.renderMaskLayer([res.data], halfThick + layerSpacing * 2, true, scale);
|
|
3045
|
+
break;
|
|
3046
|
+
case "GBS":
|
|
3047
|
+
this.renderMaskLayer([res.data], -halfThick - layerSpacing * 2, false, scale);
|
|
3048
|
+
break;
|
|
3049
|
+
case "GTP":
|
|
3050
|
+
this.renderPasteLayer([res.data], halfThick + layerSpacing * 2.5, true, scale);
|
|
3051
|
+
break;
|
|
3052
|
+
case "GBP":
|
|
3053
|
+
this.renderPasteLayer([res.data], -halfThick - layerSpacing * 2.5, false, scale);
|
|
3054
|
+
break;
|
|
3055
|
+
case "GTO":
|
|
3056
|
+
this.renderSilkscreenLayer([res], halfThick + layerSpacing * 3, true, scale);
|
|
3057
|
+
break;
|
|
3058
|
+
case "GBO":
|
|
3059
|
+
this.renderSilkscreenLayer([res], -halfThick - layerSpacing * 3, false, scale);
|
|
3060
|
+
break;
|
|
3061
|
+
case "DRL":
|
|
3062
|
+
this.renderDrillLayer([res.data], scale);
|
|
3063
|
+
break;
|
|
3064
|
+
}
|
|
3065
|
+
} catch (e) {
|
|
3066
|
+
console.error(`Error processing PHO file ${file.name}:`, e);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
// ==================== HELPERS ====================
|
|
3071
|
+
collectStandardShapes(layers) {
|
|
3072
|
+
const shapes = [];
|
|
3073
|
+
for (const layer of layers) {
|
|
3074
|
+
const webglData = layer.data || layer;
|
|
3075
|
+
if (webglData.lineStrips && webglData.vertices) {
|
|
3076
|
+
for (const strip of webglData.lineStrips) {
|
|
3077
|
+
if (strip.count < 2) continue;
|
|
3078
|
+
const points = [];
|
|
3079
|
+
for (let j = 0; j < strip.count; j++) {
|
|
3080
|
+
const ptr = (strip.start + j) * 3;
|
|
3081
|
+
points.push({
|
|
3082
|
+
x: webglData.vertices[ptr],
|
|
3083
|
+
y: -webglData.vertices[ptr + 1]
|
|
3084
|
+
// Y轴翻转
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
if (points.length < 2) continue;
|
|
3088
|
+
const first = points[0];
|
|
3089
|
+
const last = points[points.length - 1];
|
|
3090
|
+
const closeDist = Math.sqrt(
|
|
3091
|
+
Math.pow(first.x - last.x, 2) + Math.pow(first.y - last.y, 2)
|
|
3092
|
+
);
|
|
3093
|
+
const isClosed = closeDist < 1e-3;
|
|
3094
|
+
if (isClosed || points.length >= 4) {
|
|
3095
|
+
const shape = new THREE2.Shape();
|
|
3096
|
+
shape.moveTo(points[0].x, points[0].y);
|
|
3097
|
+
for (let j = 1; j < points.length; j++) {
|
|
3098
|
+
shape.lineTo(points[j].x, points[j].y);
|
|
3099
|
+
}
|
|
3100
|
+
if (!isClosed) {
|
|
3101
|
+
shape.lineTo(points[0].x, points[0].y);
|
|
3102
|
+
}
|
|
3103
|
+
const area = Math.abs(THREE2.ShapeUtils.area(shape.getPoints()));
|
|
3104
|
+
if (area > 1e-4) {
|
|
3105
|
+
shapes.push(shape);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
if (layer.children) {
|
|
3111
|
+
const segments = [];
|
|
3112
|
+
for (const child of layer.children) {
|
|
3113
|
+
if (child.type === "imagePath" && child.segments) {
|
|
3114
|
+
segments.push(...child.segments);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
if (segments.length > 0) {
|
|
3118
|
+
const stitched = this.buildShapesFromSegments(segments);
|
|
3119
|
+
shapes.push(...stitched);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
return shapes;
|
|
3124
|
+
}
|
|
3125
|
+
buildShapesFromSegments(segments) {
|
|
3126
|
+
if (segments.length === 0) return [];
|
|
3127
|
+
const pool = segments.map((s) => ({
|
|
3128
|
+
start: new THREE2.Vector2(s.start[0], -s.start[1]),
|
|
3129
|
+
// Flip Y for WebGL
|
|
3130
|
+
end: new THREE2.Vector2(s.end[0], -s.end[1]),
|
|
3131
|
+
// Flip Y
|
|
3132
|
+
original: s,
|
|
3133
|
+
used: false
|
|
3134
|
+
}));
|
|
3135
|
+
const shapes = [];
|
|
3136
|
+
const EPSILON = 0.01;
|
|
3137
|
+
const EPSILON_SQ = EPSILON * EPSILON;
|
|
3138
|
+
while (true) {
|
|
3139
|
+
const startSeg = pool.find((s) => !s.used);
|
|
3140
|
+
if (!startSeg) break;
|
|
3141
|
+
startSeg.used = true;
|
|
3142
|
+
const currentPath = new THREE2.Shape();
|
|
3143
|
+
currentPath.moveTo(startSeg.start.x, startSeg.start.y);
|
|
3144
|
+
this.addSegmentToPath(currentPath, startSeg);
|
|
3145
|
+
let currentPoint = startSeg.end;
|
|
3146
|
+
let loopClosed = false;
|
|
3147
|
+
let segmentCount = 0;
|
|
3148
|
+
const MAX_SEGMENTS = 1e4;
|
|
3149
|
+
while (!loopClosed && segmentCount < MAX_SEGMENTS) {
|
|
3150
|
+
segmentCount++;
|
|
3151
|
+
let nextSeg = null;
|
|
3152
|
+
let minDistSq = EPSILON_SQ;
|
|
3153
|
+
for (let i = 0; i < pool.length; i++) {
|
|
3154
|
+
if (pool[i].used) continue;
|
|
3155
|
+
const distSq = currentPoint.distanceToSquared(pool[i].start);
|
|
3156
|
+
if (distSq < minDistSq) {
|
|
3157
|
+
minDistSq = distSq;
|
|
3158
|
+
nextSeg = pool[i];
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
if (!nextSeg) {
|
|
3162
|
+
if (currentPoint.distanceToSquared(startSeg.start) < EPSILON_SQ) {
|
|
3163
|
+
loopClosed = true;
|
|
3164
|
+
} else {
|
|
3165
|
+
break;
|
|
3166
|
+
}
|
|
3167
|
+
} else {
|
|
3168
|
+
nextSeg.used = true;
|
|
3169
|
+
this.addSegmentToPath(currentPath, nextSeg);
|
|
3170
|
+
currentPoint = nextSeg.end;
|
|
3171
|
+
if (currentPoint.distanceToSquared(startSeg.start) < EPSILON_SQ) {
|
|
3172
|
+
loopClosed = true;
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
if (loopClosed) {
|
|
3177
|
+
shapes.push(currentPath);
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
return shapes;
|
|
3181
|
+
}
|
|
3182
|
+
addSegmentToPath(path, seg) {
|
|
3183
|
+
const s = seg.original;
|
|
3184
|
+
if (s.type === "line") {
|
|
3185
|
+
path.lineTo(seg.end.x, seg.end.y);
|
|
3186
|
+
} else if (s.type === "arc") {
|
|
3187
|
+
const cx = s.center[0];
|
|
3188
|
+
const cy = -s.center[1];
|
|
3189
|
+
const r = s.radius;
|
|
3190
|
+
path.lineTo(seg.end.x, seg.end.y);
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
// --- Silkscreen helper: attach inner contours as holes to keep text counters hollow ---
|
|
3194
|
+
processSilkscreenHoles(shapes) {
|
|
3195
|
+
const items = shapes.map((s) => {
|
|
3196
|
+
const points = s.getPoints();
|
|
3197
|
+
if (!points || points.length < 3) return null;
|
|
3198
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
3199
|
+
for (const p of points) {
|
|
3200
|
+
if (p.x < minX) minX = p.x;
|
|
3201
|
+
if (p.y < minY) minY = p.y;
|
|
3202
|
+
if (p.x > maxX) maxX = p.x;
|
|
3203
|
+
if (p.y > maxY) maxY = p.y;
|
|
3204
|
+
}
|
|
3205
|
+
const area = Math.abs(THREE2.ShapeUtils.area(points));
|
|
3206
|
+
if (!isFinite(area) || area <= 1e-6) return null;
|
|
3207
|
+
const areaInMm2 = area * 645.16;
|
|
3208
|
+
return { shape: s, points, minX, minY, maxX, maxY, area: areaInMm2, isHole: false };
|
|
3209
|
+
}).filter(Boolean);
|
|
3210
|
+
if (items.length === 0) return [];
|
|
3211
|
+
items.sort((a, b) => b.area - a.area);
|
|
3212
|
+
const roots = [];
|
|
3213
|
+
for (let i = 0; i < items.length; i++) {
|
|
3214
|
+
const child = items[i];
|
|
3215
|
+
let parent = null;
|
|
3216
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
3217
|
+
const potential = items[j];
|
|
3218
|
+
if (child.minX >= potential.minX && child.maxX <= potential.maxX && child.minY >= potential.minY && child.maxY <= potential.maxY) {
|
|
3219
|
+
if (this.isPointInPolygon(child.points[0], potential.points)) {
|
|
3220
|
+
parent = potential;
|
|
3221
|
+
break;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
if (parent) {
|
|
3226
|
+
const ratio = child.area / parent.area;
|
|
3227
|
+
if (ratio > 0.9) {
|
|
3228
|
+
parent = null;
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
if (parent) {
|
|
3232
|
+
const ratio = child.area / parent.area;
|
|
3233
|
+
let adaptiveMaxRatio;
|
|
3234
|
+
if (child.area > 50) {
|
|
3235
|
+
adaptiveMaxRatio = 0.45;
|
|
3236
|
+
} else if (child.area > 10) {
|
|
3237
|
+
adaptiveMaxRatio = 0.2;
|
|
3238
|
+
} else if (child.area > 1) {
|
|
3239
|
+
adaptiveMaxRatio = 0.65;
|
|
3240
|
+
} else {
|
|
3241
|
+
adaptiveMaxRatio = 0.9;
|
|
3242
|
+
}
|
|
3243
|
+
if (ratio < 5e-4) {
|
|
3244
|
+
roots.push(child.shape);
|
|
3245
|
+
continue;
|
|
3246
|
+
}
|
|
3247
|
+
if (ratio > adaptiveMaxRatio) {
|
|
3248
|
+
console.log(`[3D\u5B54\u6D1E\u62D2\u7EDD] \u2717 \u5F62\u72B6\u9762\u79EF=${child.area.toFixed(2)}mm\xB2, ratio=${ratio.toFixed(4)} > maxRatio=${adaptiveMaxRatio.toFixed(2)}, \u7236\u9762\u79EF=${parent.area.toFixed(2)}mm\xB2`);
|
|
3249
|
+
roots.push(child.shape);
|
|
3250
|
+
continue;
|
|
3251
|
+
}
|
|
3252
|
+
if (parent.isHole) {
|
|
3253
|
+
roots.push(child.shape);
|
|
3254
|
+
} else {
|
|
3255
|
+
parent.shape.holes.push(child.shape);
|
|
3256
|
+
child.isHole = true;
|
|
3257
|
+
}
|
|
3258
|
+
} else {
|
|
3259
|
+
roots.push(child.shape);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
return roots;
|
|
3263
|
+
}
|
|
3264
|
+
isPointInPolygon(point, vs) {
|
|
3265
|
+
const x = point.x, y = point.y;
|
|
3266
|
+
let inside = false;
|
|
3267
|
+
for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) {
|
|
3268
|
+
const xi = vs[i].x, yi = vs[i].y;
|
|
3269
|
+
const xj = vs[j].x, yj = vs[j].y;
|
|
3270
|
+
const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi || 1e-12) + xi;
|
|
3271
|
+
if (intersect) inside = !inside;
|
|
3272
|
+
}
|
|
3273
|
+
return inside;
|
|
3274
|
+
}
|
|
3275
|
+
renderShapesInChunks(shapes, material, zPosition, scale = 1, chunkSize = 2e3, layerType = null) {
|
|
3276
|
+
if (shapes.length === 0) return [];
|
|
3277
|
+
const meshes = [];
|
|
3278
|
+
let optimizedChunkSize = chunkSize;
|
|
3279
|
+
if (shapes.length > 2e4) {
|
|
3280
|
+
optimizedChunkSize = 200;
|
|
3281
|
+
} else if (shapes.length > 1e4) {
|
|
3282
|
+
optimizedChunkSize = 400;
|
|
3283
|
+
} else if (shapes.length > 5e3) {
|
|
3284
|
+
optimizedChunkSize = 800;
|
|
3285
|
+
}
|
|
3286
|
+
const useAsync = shapes.length > 3e3;
|
|
3287
|
+
if (useAsync) {
|
|
3288
|
+
this.renderShapesAsync(shapes, material, zPosition, scale, optimizedChunkSize, layerType);
|
|
3289
|
+
return [];
|
|
3290
|
+
} else {
|
|
3291
|
+
for (let i = 0; i < shapes.length; i += optimizedChunkSize) {
|
|
3292
|
+
const chunk = shapes.slice(i, i + optimizedChunkSize);
|
|
3293
|
+
const geometry = new THREE2.ShapeGeometry(chunk, 3);
|
|
3294
|
+
const mesh = new THREE2.Mesh(geometry, material);
|
|
3295
|
+
mesh.position.z = zPosition;
|
|
3296
|
+
mesh.scale.set(scale, scale, 1);
|
|
3297
|
+
if (layerType) {
|
|
3298
|
+
mesh.userData = { layerType };
|
|
3299
|
+
}
|
|
3300
|
+
this.baseGroup.add(mesh);
|
|
3301
|
+
meshes.push(mesh);
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
return meshes;
|
|
3305
|
+
}
|
|
3306
|
+
renderShapesAsync(shapes, material, zPosition, scale, chunkSize, layerType = null) {
|
|
3307
|
+
let index = 0;
|
|
3308
|
+
const totalChunks = Math.ceil(shapes.length / chunkSize);
|
|
3309
|
+
let renderedChunks = 0;
|
|
3310
|
+
const renderNextChunk = () => {
|
|
3311
|
+
if (index >= shapes.length) {
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
const chunksPerFrame = 5;
|
|
3315
|
+
for (let i = 0; i < chunksPerFrame && index < shapes.length; i++) {
|
|
3316
|
+
const chunk = shapes.slice(index, index + chunkSize);
|
|
3317
|
+
const geometry = new THREE2.ShapeGeometry(chunk, 3);
|
|
3318
|
+
const mesh = new THREE2.Mesh(geometry, material);
|
|
3319
|
+
mesh.position.z = zPosition;
|
|
3320
|
+
mesh.scale.set(scale, scale, 1);
|
|
3321
|
+
if (layerType) {
|
|
3322
|
+
mesh.userData = { layerType };
|
|
3323
|
+
}
|
|
3324
|
+
this.baseGroup.add(mesh);
|
|
3325
|
+
index += chunkSize;
|
|
3326
|
+
renderedChunks++;
|
|
3327
|
+
}
|
|
3328
|
+
if (index < shapes.length) {
|
|
3329
|
+
requestAnimationFrame(renderNextChunk);
|
|
3330
|
+
}
|
|
3331
|
+
};
|
|
3332
|
+
renderNextChunk();
|
|
3333
|
+
}
|
|
3334
|
+
getCachedMaterial(key, creator) {
|
|
3335
|
+
if (!this.materialCache.has(key)) {
|
|
3336
|
+
this.materialCache.set(key, creator());
|
|
3337
|
+
}
|
|
3338
|
+
return this.materialCache.get(key);
|
|
3339
|
+
}
|
|
3340
|
+
fitCameraToObject(object) {
|
|
3341
|
+
const box = new THREE2.Box3().setFromObject(object);
|
|
3342
|
+
if (box.isEmpty()) return;
|
|
3343
|
+
this.fitCameraToBbox(box);
|
|
3344
|
+
}
|
|
3345
|
+
fitCameraToBbox(box) {
|
|
3346
|
+
if (box.isEmpty()) return;
|
|
3347
|
+
const center = box.getCenter(new THREE2.Vector3());
|
|
3348
|
+
const size = box.getSize(new THREE2.Vector3());
|
|
3349
|
+
const maxDim = Math.max(size.x, size.y);
|
|
3350
|
+
const fov = this.camera.fov * (Math.PI / 180);
|
|
3351
|
+
let distance = maxDim / 2 / Math.tan(fov / 2);
|
|
3352
|
+
distance *= 1.1;
|
|
3353
|
+
this.camera.position.set(center.x, center.y, center.z + distance);
|
|
3354
|
+
this.camera.near = distance / 100;
|
|
3355
|
+
this.camera.far = distance * 100;
|
|
3356
|
+
this.camera.updateProjectionMatrix();
|
|
3357
|
+
this.controls.target.copy(center);
|
|
3358
|
+
this.controls.update();
|
|
3359
|
+
}
|
|
3360
|
+
/**
|
|
3361
|
+
* 检测并修正钻孔层的坐标缩放问题
|
|
3362
|
+
*
|
|
3363
|
+
* 常见问题:
|
|
3364
|
+
* - 10倍问题:mil被当作inch解析
|
|
3365
|
+
* - 25.4倍问题:inch被当作mm解析
|
|
3366
|
+
*
|
|
3367
|
+
* @param {Object} layerData - 钻孔层数据(包含vertices和lineStrips)
|
|
3368
|
+
* @param {number} scale - 单位转换因子(inch -> mm 时为25.4)
|
|
3369
|
+
* @param {THREE.Box3} referenceBbox - 参考层边界
|
|
3370
|
+
* @returns {{ scale: number, bbox: THREE.Box3|null, success: boolean }}
|
|
3371
|
+
*/
|
|
3372
|
+
fixDrillLayerAlignment(layerData, scale, referenceBbox) {
|
|
3373
|
+
if (!layerData?.vertices || !referenceBbox) {
|
|
3374
|
+
return { scale, bbox: null, success: false };
|
|
3375
|
+
}
|
|
3376
|
+
let drillBbox = this.calculateLayerBbox([layerData]);
|
|
3377
|
+
if (!drillBbox || drillBbox.isEmpty()) {
|
|
3378
|
+
return { scale, bbox: null, success: false };
|
|
3379
|
+
}
|
|
3380
|
+
drillBbox.min.multiplyScalar(scale);
|
|
3381
|
+
drillBbox.max.multiplyScalar(scale);
|
|
3382
|
+
const refSize = referenceBbox.getSize(new THREE2.Vector3());
|
|
3383
|
+
const tolerance = 0.1;
|
|
3384
|
+
const inRangeX = drillBbox.min.x >= referenceBbox.min.x - refSize.x * tolerance && drillBbox.max.x <= referenceBbox.max.x + refSize.x * tolerance;
|
|
3385
|
+
const inRangeY = drillBbox.min.y >= referenceBbox.min.y - refSize.y * tolerance && drillBbox.max.y <= referenceBbox.max.y + refSize.y * tolerance;
|
|
3386
|
+
if (inRangeX && inRangeY) {
|
|
3387
|
+
debugLog("[Drill] \u5750\u6807\u5DF2\u5728\u8303\u56F4\u5185\uFF0C\u65E0\u9700\u4FEE\u6B63");
|
|
3388
|
+
return { scale, bbox: drillBbox, success: true };
|
|
3389
|
+
}
|
|
3390
|
+
const drillSize = drillBbox.getSize(new THREE2.Vector3());
|
|
3391
|
+
const avgSizeRatio = (drillSize.x / refSize.x + drillSize.y / refSize.y) / 2;
|
|
3392
|
+
let scaleFix = 1;
|
|
3393
|
+
if (avgSizeRatio > 5 && avgSizeRatio < 15) {
|
|
3394
|
+
scaleFix = 10;
|
|
3395
|
+
} else if (avgSizeRatio > 20 && avgSizeRatio < 30) {
|
|
3396
|
+
scaleFix = 25.4;
|
|
3397
|
+
} else if (avgSizeRatio > 1.5) {
|
|
3398
|
+
scaleFix = avgSizeRatio;
|
|
3399
|
+
}
|
|
3400
|
+
if (scaleFix === 1) {
|
|
3401
|
+
debugLog("[Drill] \u672A\u68C0\u6D4B\u5230\u5E38\u89C1\u7F29\u653E\u95EE\u9898");
|
|
3402
|
+
return { scale, bbox: drillBbox, success: true };
|
|
3403
|
+
}
|
|
3404
|
+
debugLog(`[Drill] \u68C0\u6D4B\u5230${scaleFix.toFixed(1)}\u500D\u7F29\u653E\u95EE\u9898\uFF0C\u5F00\u59CB\u4FEE\u6B63...`);
|
|
3405
|
+
const { vertices, lineStrips } = layerData;
|
|
3406
|
+
if (lineStrips?.length > 0) {
|
|
3407
|
+
this.applyDrillScaleFix(vertices, lineStrips, scale, scaleFix);
|
|
3408
|
+
} else {
|
|
3409
|
+
debugWarn("[Drill] \u7F3A\u5C11lineStrips\uFF0C\u5B54\u5F84\u4F1A\u88AB\u7F29\u653E");
|
|
3410
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
3411
|
+
vertices[i] = vertices[i] * scale / scaleFix;
|
|
3412
|
+
vertices[i + 1] = vertices[i + 1] * scale / scaleFix;
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
drillBbox = this.calculateLayerBbox([layerData]);
|
|
3416
|
+
if (!drillBbox || drillBbox.isEmpty()) {
|
|
3417
|
+
return { scale: 1, bbox: null, success: false };
|
|
3418
|
+
}
|
|
3419
|
+
debugLog(`[Drill] \u4FEE\u6B63\u540Ebbox: (${drillBbox.min.x.toFixed(2)}, ${drillBbox.min.y.toFixed(2)}) - (${drillBbox.max.x.toFixed(2)}, ${drillBbox.max.y.toFixed(2)})`);
|
|
3420
|
+
return { scale: 1, bbox: drillBbox, success: true };
|
|
3421
|
+
}
|
|
3422
|
+
/**
|
|
3423
|
+
* 应用缩放修正,只移动孔位中心,保持孔径不变
|
|
3424
|
+
*
|
|
3425
|
+
* 原理:每个lineStrip代表一个钻孔圆,通过计算圆心位置并只平移顶点,
|
|
3426
|
+
* 可以实现"缩放位置但保持孔径"的效果。
|
|
3427
|
+
*
|
|
3428
|
+
* @param {Float32Array} vertices - 顶点数组
|
|
3429
|
+
* @param {Array} lineStrips - 线段描述数组(每个代表一个圆)
|
|
3430
|
+
* @param {number} scale - 单位转换因子(inch -> mm)
|
|
3431
|
+
* @param {number} scaleFix - 缩放修正因子(如10.0表示除以10)
|
|
3432
|
+
*/
|
|
3433
|
+
applyDrillScaleFix(vertices, lineStrips, scale, scaleFix) {
|
|
3434
|
+
let processedCount = 0;
|
|
3435
|
+
for (const strip of lineStrips) {
|
|
3436
|
+
if (strip.count < 3) continue;
|
|
3437
|
+
let sumX = 0, sumY = 0;
|
|
3438
|
+
for (let i = 0; i < strip.count; i++) {
|
|
3439
|
+
const idx = (strip.start + i) * 3;
|
|
3440
|
+
sumX += vertices[idx];
|
|
3441
|
+
sumY += vertices[idx + 1];
|
|
3442
|
+
}
|
|
3443
|
+
const centerX = sumX / strip.count * scale;
|
|
3444
|
+
const centerY = -sumY / strip.count * scale;
|
|
3445
|
+
const deltaX = centerX / scaleFix - centerX;
|
|
3446
|
+
const deltaY = centerY / scaleFix - centerY;
|
|
3447
|
+
for (let i = 0; i < strip.count; i++) {
|
|
3448
|
+
const idx = (strip.start + i) * 3;
|
|
3449
|
+
vertices[idx] = vertices[idx] * scale + deltaX;
|
|
3450
|
+
vertices[idx + 1] = vertices[idx + 1] * scale - deltaY;
|
|
3451
|
+
}
|
|
3452
|
+
processedCount++;
|
|
3453
|
+
}
|
|
3454
|
+
debugLog(`[Drill] \u5DF2\u4FEE\u6B63 ${processedCount} \u4E2A\u94BB\u5B54\uFF0C\u5B54\u5F84\u4FDD\u6301\u4E0D\u53D8`);
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* 验证钻孔层边界是否在合理范围内
|
|
3458
|
+
*
|
|
3459
|
+
* @param {THREE.Box3} drillBbox - 钻孔层边界
|
|
3460
|
+
* @param {THREE.Box3} referenceBbox - 参考层边界
|
|
3461
|
+
* @param {THREE.Box3} expandedReferenceBbox - 扩展后的参考层边界(含容差)
|
|
3462
|
+
* @param {string} fileName - 文件名(用于日志)
|
|
3463
|
+
* @returns {boolean} 是否通过验证
|
|
3464
|
+
*/
|
|
3465
|
+
validateDrillBbox(drillBbox, referenceBbox, expandedReferenceBbox, fileName) {
|
|
3466
|
+
if (!expandedReferenceBbox.intersectsBox(drillBbox)) {
|
|
3467
|
+
debugWarn(`[Drill] ${fileName} \u4E0E\u677F\u5B50\u8303\u56F4\u65E0\u4EA4\u96C6\uFF08\u5BB9\u951920mm\uFF09\uFF0C\u8DF3\u8FC7\u6E32\u67D3`);
|
|
3468
|
+
return false;
|
|
3469
|
+
}
|
|
3470
|
+
if (!referenceBbox) return true;
|
|
3471
|
+
const refSize = referenceBbox.getSize(new THREE2.Vector3());
|
|
3472
|
+
const drillSize = drillBbox.getSize(new THREE2.Vector3());
|
|
3473
|
+
const maxRatio = 1.3;
|
|
3474
|
+
const minRatio = 0.2;
|
|
3475
|
+
if (drillSize.x > refSize.x * maxRatio || drillSize.y > refSize.y * maxRatio) {
|
|
3476
|
+
debugWarn(`[Drill] ${fileName} \u94BB\u5B54\u8303\u56F4\u8FC7\u5927\uFF08>${maxRatio}x\uFF09\uFF0C\u8DF3\u8FC7\u6E32\u67D3`);
|
|
3477
|
+
return false;
|
|
3478
|
+
}
|
|
3479
|
+
if (drillSize.x < refSize.x * minRatio && drillSize.y < refSize.y * minRatio) {
|
|
3480
|
+
debugWarn(`[Drill] ${fileName} \u94BB\u5B54\u8303\u56F4\u8FC7\u5C0F\uFF08<${minRatio}x\uFF09\uFF0C\u8DF3\u8FC7\u6E32\u67D3`);
|
|
3481
|
+
return false;
|
|
3482
|
+
}
|
|
3483
|
+
const centerRef = referenceBbox.getCenter(new THREE2.Vector3());
|
|
3484
|
+
const centerDrill = drillBbox.getCenter(new THREE2.Vector3());
|
|
3485
|
+
const maxDim = Math.max(refSize.x, refSize.y);
|
|
3486
|
+
const centerThreshold = Math.max(maxDim * 0.15, 25);
|
|
3487
|
+
const centerDist = centerRef.distanceTo(centerDrill);
|
|
3488
|
+
const sizeRatioX = drillSize.x / Math.max(1e-9, refSize.x);
|
|
3489
|
+
const sizeRatioY = drillSize.y / Math.max(1e-9, refSize.y);
|
|
3490
|
+
const localized = Math.max(sizeRatioX, sizeRatioY) < 0.6;
|
|
3491
|
+
if (!localized && centerDist > centerThreshold) {
|
|
3492
|
+
debugWarn(`[Drill] ${fileName} \u4E2D\u5FC3\u504F\u79FB\u8FC7\u5927\uFF08${centerDist.toFixed(1)}mm\uFF09\uFF0C\u8DF3\u8FC7\u6E32\u67D3`);
|
|
3493
|
+
return false;
|
|
3494
|
+
}
|
|
3495
|
+
return true;
|
|
3496
|
+
}
|
|
3497
|
+
/**
|
|
3498
|
+
* 将线段拼接成THREE.Shape对象(用于多轮廓文件合并)
|
|
3499
|
+
* @param {Array<{start:{x,y}, end:{x,y}}>} segments 线段数组
|
|
3500
|
+
* @returns {Array<THREE.Shape>} 形状数组
|
|
3501
|
+
*/
|
|
3502
|
+
stitchSegmentsToShapes3D(segments) {
|
|
3503
|
+
if (!segments || segments.length === 0) return [];
|
|
3504
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
3505
|
+
for (const seg of segments) {
|
|
3506
|
+
minX = Math.min(minX, seg.start.x, seg.end.x);
|
|
3507
|
+
maxX = Math.max(maxX, seg.start.x, seg.end.x);
|
|
3508
|
+
minY = Math.min(minY, seg.start.y, seg.end.y);
|
|
3509
|
+
maxY = Math.max(maxY, seg.start.y, seg.end.y);
|
|
3510
|
+
}
|
|
3511
|
+
const boardSize = Math.max(maxX - minX, maxY - minY);
|
|
3512
|
+
const TOLERANCE = boardSize > 100 ? 0.2 : boardSize > 50 ? 0.1 : 0.05;
|
|
3513
|
+
const cellSize = TOLERANCE;
|
|
3514
|
+
const grid = /* @__PURE__ */ new Map();
|
|
3515
|
+
const getCell = (pt) => `${Math.floor(pt.x / cellSize)},${Math.floor(pt.y / cellSize)}`;
|
|
3516
|
+
const poolItems = segments.map((s) => ({ seg: s, used: false }));
|
|
3517
|
+
const addToGrid = (item) => {
|
|
3518
|
+
const k1 = getCell(item.seg.start);
|
|
3519
|
+
const k2 = getCell(item.seg.end);
|
|
3520
|
+
if (!grid.has(k1)) grid.set(k1, []);
|
|
3521
|
+
grid.get(k1).push(item);
|
|
3522
|
+
if (k1 !== k2) {
|
|
3523
|
+
if (!grid.has(k2)) grid.set(k2, []);
|
|
3524
|
+
grid.get(k2).push(item);
|
|
3525
|
+
}
|
|
3526
|
+
};
|
|
3527
|
+
poolItems.forEach(addToGrid);
|
|
3528
|
+
const distanceTo = (p1, p2) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
|
|
3529
|
+
const getCandidates = (pt) => {
|
|
3530
|
+
const cx = Math.floor(pt.x / cellSize);
|
|
3531
|
+
const cy = Math.floor(pt.y / cellSize);
|
|
3532
|
+
const results = /* @__PURE__ */ new Set();
|
|
3533
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
3534
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
3535
|
+
const k = `${cx + dx},${cy + dy}`;
|
|
3536
|
+
const list = grid.get(k);
|
|
3537
|
+
if (list) {
|
|
3538
|
+
for (const item of list) results.add(item);
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
return results;
|
|
3543
|
+
};
|
|
3544
|
+
const chains = [];
|
|
3545
|
+
for (const startItem of poolItems) {
|
|
3546
|
+
if (startItem.used) continue;
|
|
3547
|
+
startItem.used = true;
|
|
3548
|
+
const chain = [startItem.seg.start, startItem.seg.end];
|
|
3549
|
+
let tail = startItem.seg.end;
|
|
3550
|
+
let head = startItem.seg.start;
|
|
3551
|
+
let finding = true;
|
|
3552
|
+
while (finding) {
|
|
3553
|
+
finding = false;
|
|
3554
|
+
const candidates = getCandidates(tail);
|
|
3555
|
+
let bestNext = null;
|
|
3556
|
+
let minDst = Infinity;
|
|
3557
|
+
let isReverse = false;
|
|
3558
|
+
for (const item of candidates) {
|
|
3559
|
+
if (item.used) continue;
|
|
3560
|
+
const d1 = distanceTo(item.seg.start, tail);
|
|
3561
|
+
if (d1 < TOLERANCE && d1 < minDst) {
|
|
3562
|
+
minDst = d1;
|
|
3563
|
+
bestNext = item;
|
|
3564
|
+
isReverse = false;
|
|
3565
|
+
}
|
|
3566
|
+
const d2 = distanceTo(item.seg.end, tail);
|
|
3567
|
+
if (d2 < TOLERANCE && d2 < minDst) {
|
|
3568
|
+
minDst = d2;
|
|
3569
|
+
bestNext = item;
|
|
3570
|
+
isReverse = true;
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
if (bestNext) {
|
|
3574
|
+
bestNext.used = true;
|
|
3575
|
+
if (isReverse) {
|
|
3576
|
+
chain.push(bestNext.seg.start);
|
|
3577
|
+
tail = bestNext.seg.start;
|
|
3578
|
+
} else {
|
|
3579
|
+
chain.push(bestNext.seg.end);
|
|
3580
|
+
tail = bestNext.seg.end;
|
|
3581
|
+
}
|
|
3582
|
+
finding = true;
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
finding = true;
|
|
3586
|
+
while (finding) {
|
|
3587
|
+
finding = false;
|
|
3588
|
+
const candidates = getCandidates(head);
|
|
3589
|
+
let bestPrev = null;
|
|
3590
|
+
let minDst = Infinity;
|
|
3591
|
+
let isReverse = false;
|
|
3592
|
+
for (const item of candidates) {
|
|
3593
|
+
if (item.used) continue;
|
|
3594
|
+
const d1 = distanceTo(item.seg.end, head);
|
|
3595
|
+
if (d1 < TOLERANCE && d1 < minDst) {
|
|
3596
|
+
minDst = d1;
|
|
3597
|
+
bestPrev = item;
|
|
3598
|
+
isReverse = false;
|
|
3599
|
+
}
|
|
3600
|
+
const d2 = distanceTo(item.seg.start, head);
|
|
3601
|
+
if (d2 < TOLERANCE && d2 < minDst) {
|
|
3602
|
+
minDst = d2;
|
|
3603
|
+
bestPrev = item;
|
|
3604
|
+
isReverse = true;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
if (bestPrev) {
|
|
3608
|
+
bestPrev.used = true;
|
|
3609
|
+
if (isReverse) {
|
|
3610
|
+
chain.unshift(bestPrev.seg.end);
|
|
3611
|
+
head = bestPrev.seg.end;
|
|
3612
|
+
} else {
|
|
3613
|
+
chain.unshift(bestPrev.seg.start);
|
|
3614
|
+
head = bestPrev.seg.start;
|
|
3615
|
+
}
|
|
3616
|
+
finding = true;
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
chains.push(chain);
|
|
3620
|
+
}
|
|
3621
|
+
const shapes = [];
|
|
3622
|
+
const MIN_AREA = 0.01;
|
|
3623
|
+
for (const chain of chains) {
|
|
3624
|
+
if (chain.length < 3) continue;
|
|
3625
|
+
const headPt = chain[0];
|
|
3626
|
+
const tailPt = chain[chain.length - 1];
|
|
3627
|
+
const isClosed = distanceTo(headPt, tailPt) < TOLERANCE;
|
|
3628
|
+
if (!isClosed) continue;
|
|
3629
|
+
let area = 0;
|
|
3630
|
+
for (let i = 0; i < chain.length; i++) {
|
|
3631
|
+
const p1 = chain[i];
|
|
3632
|
+
const p2 = chain[(i + 1) % chain.length];
|
|
3633
|
+
area += p1.x * p2.y - p2.x * p1.y;
|
|
3634
|
+
}
|
|
3635
|
+
area = Math.abs(area) / 2;
|
|
3636
|
+
if (area < MIN_AREA) continue;
|
|
3637
|
+
const shape = new THREE2.Shape();
|
|
3638
|
+
shape.moveTo(chain[0].x, chain[0].y);
|
|
3639
|
+
for (let i = 1; i < chain.length; i++) {
|
|
3640
|
+
shape.lineTo(chain[i].x, chain[i].y);
|
|
3641
|
+
}
|
|
3642
|
+
shape.closePath();
|
|
3643
|
+
shape.userData = { forcedClose: false, area };
|
|
3644
|
+
shapes.push(shape);
|
|
3645
|
+
}
|
|
3646
|
+
return shapes;
|
|
3647
|
+
}
|
|
3648
|
+
calculateLayerBbox(layers) {
|
|
3649
|
+
const allPoints = [];
|
|
3650
|
+
for (const layer of layers) {
|
|
3651
|
+
if (layer.lineStrips && layer.vertices) {
|
|
3652
|
+
for (const strip of layer.lineStrips) {
|
|
3653
|
+
for (let j = 0; j < strip.count; j++) {
|
|
3654
|
+
const ptr = (strip.start + j) * 3;
|
|
3655
|
+
allPoints.push(new THREE2.Vector3(
|
|
3656
|
+
layer.vertices[ptr],
|
|
3657
|
+
-layer.vertices[ptr + 1],
|
|
3658
|
+
0
|
|
3659
|
+
));
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
if (layer.children) {
|
|
3664
|
+
for (const child of layer.children) {
|
|
3665
|
+
if (child.type === "imagePath" && child.segments) {
|
|
3666
|
+
for (const seg of child.segments) {
|
|
3667
|
+
allPoints.push(new THREE2.Vector3(seg.start[0], -seg.start[1], 0));
|
|
3668
|
+
allPoints.push(new THREE2.Vector3(seg.end[0], -seg.end[1], 0));
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
if (allPoints.length === 0) return null;
|
|
3675
|
+
return new THREE2.Box3().setFromPoints(allPoints);
|
|
3676
|
+
}
|
|
3677
|
+
};
|
|
3678
|
+
window.onload = () => new Viewer3D();
|
|
3679
|
+
//# sourceMappingURL=gerber-3d-entry-DEHDBOO2.js.map
|