pcb-scene3d-viewer 1.1.48 → 1.1.49
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/package.json +2 -1
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +16 -0
- package/src/PcbScene3dCopperFactory.mjs +64 -93
- package/src/PcbScene3dCopperFillAreaClipper.mjs +726 -0
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +530 -8
- package/src/PcbScene3dCopperFillPolygonBoolean.mjs +235 -0
- package/src/PcbScene3dCopperPrismBuilder.mjs +92 -0
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +2 -1
- package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +64 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import earcut from 'earcut'
|
|
2
2
|
import { PcbAssemblyFillGeometryResolver } from './PcbAssemblyFillGeometryResolver.mjs'
|
|
3
|
+
import { PcbScene3dCopperFillAreaClipper } from './PcbScene3dCopperFillAreaClipper.mjs'
|
|
4
|
+
import { PcbScene3dCopperFillPolygonBoolean } from './PcbScene3dCopperFillPolygonBoolean.mjs'
|
|
3
5
|
import { PcbScene3dCopperOcclusionClipper } from './PcbScene3dCopperOcclusionClipper.mjs'
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -18,6 +20,7 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
18
20
|
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
19
21
|
* @param {any} material Copper material.
|
|
20
22
|
* @param {{ x: number, y: number }[][]} [cutouts] Optional normalized overlay cutouts.
|
|
23
|
+
* @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean }} [options] Optional mesh options.
|
|
21
24
|
* @returns {any | null}
|
|
22
25
|
*/
|
|
23
26
|
static build(
|
|
@@ -28,22 +31,41 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
28
31
|
normalizeBoardPoint,
|
|
29
32
|
mirrorY,
|
|
30
33
|
material,
|
|
31
|
-
cutouts = []
|
|
34
|
+
cutouts = [],
|
|
35
|
+
options = {}
|
|
32
36
|
) {
|
|
33
37
|
const positions = []
|
|
34
38
|
const halfThickness = Math.max(Number(thickness || 0), 0.001) / 2
|
|
35
39
|
const bottomZ = Number(z || 0) - halfThickness
|
|
36
40
|
const topZ = Number(z || 0) + halfThickness
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
PcbScene3dCopperFillMeshBuilder.#
|
|
42
|
+
if (
|
|
43
|
+
PcbScene3dCopperFillMeshBuilder.#shouldClipContainedFillOverlaps(
|
|
44
|
+
options
|
|
45
|
+
)
|
|
46
|
+
) {
|
|
47
|
+
PcbScene3dCopperFillMeshBuilder.#appendClippedFillPositions(
|
|
48
|
+
THREE,
|
|
40
49
|
positions,
|
|
41
|
-
|
|
50
|
+
fills,
|
|
42
51
|
bottomZ,
|
|
43
52
|
topZ,
|
|
44
53
|
normalizeBoardPoint,
|
|
45
|
-
mirrorY
|
|
54
|
+
mirrorY,
|
|
55
|
+
options
|
|
46
56
|
)
|
|
57
|
+
} else {
|
|
58
|
+
for (const fill of fills || []) {
|
|
59
|
+
PcbScene3dCopperFillMeshBuilder.#appendFillPositions(
|
|
60
|
+
positions,
|
|
61
|
+
fill,
|
|
62
|
+
bottomZ,
|
|
63
|
+
topZ,
|
|
64
|
+
normalizeBoardPoint,
|
|
65
|
+
mirrorY,
|
|
66
|
+
options
|
|
67
|
+
)
|
|
68
|
+
}
|
|
47
69
|
}
|
|
48
70
|
|
|
49
71
|
if (!positions.length) {
|
|
@@ -93,6 +115,7 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
93
115
|
* @param {number} topZ Upper Z.
|
|
94
116
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
95
117
|
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
118
|
+
* @param {{ surfaceOnly?: boolean }} options Mesh options.
|
|
96
119
|
* @returns {void}
|
|
97
120
|
*/
|
|
98
121
|
static #appendFillPositions(
|
|
@@ -101,7 +124,8 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
101
124
|
bottomZ,
|
|
102
125
|
topZ,
|
|
103
126
|
normalizeBoardPoint,
|
|
104
|
-
mirrorY
|
|
127
|
+
mirrorY,
|
|
128
|
+
options
|
|
105
129
|
) {
|
|
106
130
|
for (const loops of PcbAssemblyFillGeometryResolver.resolveAll(fill)) {
|
|
107
131
|
PcbScene3dCopperFillMeshBuilder.#appendLoopSetPositions(
|
|
@@ -110,11 +134,216 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
110
134
|
bottomZ,
|
|
111
135
|
topZ,
|
|
112
136
|
normalizeBoardPoint,
|
|
113
|
-
mirrorY
|
|
137
|
+
mirrorY,
|
|
138
|
+
options
|
|
114
139
|
)
|
|
115
140
|
}
|
|
116
141
|
}
|
|
117
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Appends fill positions while removing duplicate overlap surfaces.
|
|
145
|
+
* @param {any} THREE Three.js namespace.
|
|
146
|
+
* @param {number[]} positions Position buffer.
|
|
147
|
+
* @param {object[]} fills Filled copper primitives.
|
|
148
|
+
* @param {number} bottomZ Lower Z.
|
|
149
|
+
* @param {number} topZ Upper Z.
|
|
150
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
151
|
+
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
152
|
+
* @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean }} options Mesh options.
|
|
153
|
+
* @returns {void}
|
|
154
|
+
*/
|
|
155
|
+
static #appendClippedFillPositions(
|
|
156
|
+
THREE,
|
|
157
|
+
positions,
|
|
158
|
+
fills,
|
|
159
|
+
bottomZ,
|
|
160
|
+
topZ,
|
|
161
|
+
normalizeBoardPoint,
|
|
162
|
+
mirrorY,
|
|
163
|
+
options
|
|
164
|
+
) {
|
|
165
|
+
const loopSets = PcbScene3dCopperFillMeshBuilder.#resolveLoopSets(
|
|
166
|
+
fills,
|
|
167
|
+
normalizeBoardPoint,
|
|
168
|
+
mirrorY
|
|
169
|
+
)
|
|
170
|
+
const emittedPolygons = []
|
|
171
|
+
const emittedLoopSets = []
|
|
172
|
+
|
|
173
|
+
for (const loopSet of loopSets) {
|
|
174
|
+
const remainingLoopSets =
|
|
175
|
+
PcbScene3dCopperFillPolygonBoolean.resolveRemainingLoopSets(
|
|
176
|
+
loopSet,
|
|
177
|
+
emittedPolygons
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if (remainingLoopSets) {
|
|
181
|
+
for (const remainingLoopSet of remainingLoopSets) {
|
|
182
|
+
PcbScene3dCopperFillMeshBuilder.#appendNormalizedLoopSetPositions(
|
|
183
|
+
positions,
|
|
184
|
+
remainingLoopSet,
|
|
185
|
+
bottomZ,
|
|
186
|
+
topZ,
|
|
187
|
+
options
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
PcbScene3dCopperFillMeshBuilder.#appendTriangleClippedLoopSet(
|
|
192
|
+
THREE,
|
|
193
|
+
positions,
|
|
194
|
+
loopSet,
|
|
195
|
+
emittedLoopSets,
|
|
196
|
+
bottomZ,
|
|
197
|
+
topZ,
|
|
198
|
+
options
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
emittedLoopSets.push(loopSet)
|
|
203
|
+
emittedPolygons.push(
|
|
204
|
+
...PcbScene3dCopperFillPolygonBoolean.resolveNormalizedPolygons(
|
|
205
|
+
loopSet
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Appends one loop set using the older triangle clipper fallback.
|
|
213
|
+
* @param {any} THREE Three.js namespace.
|
|
214
|
+
* @param {number[]} positions Position buffer.
|
|
215
|
+
* @param {{ outer: number[][], holes: number[][][], bounds: object }} loopSet Candidate loop set.
|
|
216
|
+
* @param {{ outer: number[][], holes: number[][][], bounds: object }[]} emittedLoopSets Already emitted loop sets.
|
|
217
|
+
* @param {number} bottomZ Lower Z.
|
|
218
|
+
* @param {number} topZ Upper Z.
|
|
219
|
+
* @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean }} options Mesh options.
|
|
220
|
+
* @returns {void}
|
|
221
|
+
*/
|
|
222
|
+
static #appendTriangleClippedLoopSet(
|
|
223
|
+
THREE,
|
|
224
|
+
positions,
|
|
225
|
+
loopSet,
|
|
226
|
+
emittedLoopSets,
|
|
227
|
+
bottomZ,
|
|
228
|
+
topZ,
|
|
229
|
+
options
|
|
230
|
+
) {
|
|
231
|
+
if (
|
|
232
|
+
emittedLoopSets.some((emittedLoopSet) =>
|
|
233
|
+
PcbScene3dCopperFillMeshBuilder.#containsLoopSet(
|
|
234
|
+
emittedLoopSet,
|
|
235
|
+
loopSet
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const loopPositions = []
|
|
243
|
+
PcbScene3dCopperFillMeshBuilder.#appendNormalizedLoopSetPositions(
|
|
244
|
+
loopPositions,
|
|
245
|
+
loopSet,
|
|
246
|
+
bottomZ,
|
|
247
|
+
topZ,
|
|
248
|
+
options
|
|
249
|
+
)
|
|
250
|
+
PcbScene3dCopperFillMeshBuilder.#appendPositions(
|
|
251
|
+
positions,
|
|
252
|
+
PcbScene3dCopperFillMeshBuilder.#clipPositionsAgainstLoopSets(
|
|
253
|
+
THREE,
|
|
254
|
+
loopPositions,
|
|
255
|
+
PcbScene3dCopperFillMeshBuilder.#resolveClipLoopSets(
|
|
256
|
+
loopSet,
|
|
257
|
+
emittedLoopSets
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolves earlier loop sets that can actually overlap a candidate.
|
|
265
|
+
* @param {{ outer: number[][], holes: number[][][], bounds: object }} candidate Candidate loop set.
|
|
266
|
+
* @param {{ outer: number[][], holes: number[][][], bounds: object }[]} loopSets Emitted loop sets.
|
|
267
|
+
* @returns {{ outer: number[][], holes: number[][][], bounds: object }[]}
|
|
268
|
+
*/
|
|
269
|
+
static #resolveClipLoopSets(candidate, loopSets) {
|
|
270
|
+
return loopSets.filter(
|
|
271
|
+
(loopSet) =>
|
|
272
|
+
PcbScene3dCopperFillMeshBuilder.#boundsOverlap(
|
|
273
|
+
candidate.bounds,
|
|
274
|
+
loopSet.bounds
|
|
275
|
+
) &&
|
|
276
|
+
!PcbScene3dCopperFillMeshBuilder.#isInsideAnyHole(
|
|
277
|
+
candidate,
|
|
278
|
+
loopSet
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Appends a potentially large position array without using argument spread.
|
|
285
|
+
* @param {number[]} target Target position buffer.
|
|
286
|
+
* @param {number[]} source Source positions.
|
|
287
|
+
* @returns {void}
|
|
288
|
+
*/
|
|
289
|
+
static #appendPositions(target, source) {
|
|
290
|
+
for (const value of source) {
|
|
291
|
+
target.push(value)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Clips position triangles against already-emitted fill loop sets.
|
|
297
|
+
* @param {any} THREE Three.js namespace.
|
|
298
|
+
* @param {number[]} positions Candidate triangle positions.
|
|
299
|
+
* @param {{ outer: number[][], holes: number[][][] }[]} loopSets Filled areas already represented.
|
|
300
|
+
* @returns {number[]}
|
|
301
|
+
*/
|
|
302
|
+
static #clipPositionsAgainstLoopSets(THREE, positions, loopSets) {
|
|
303
|
+
if (!positions.length || !loopSets.length) {
|
|
304
|
+
return positions
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const geometry = new THREE.BufferGeometry()
|
|
308
|
+
geometry.setAttribute(
|
|
309
|
+
'position',
|
|
310
|
+
new THREE.Float32BufferAttribute(positions, 3)
|
|
311
|
+
)
|
|
312
|
+
const mesh = new THREE.Mesh(geometry)
|
|
313
|
+
const clippedMesh = PcbScene3dCopperFillAreaClipper.filter(
|
|
314
|
+
THREE,
|
|
315
|
+
mesh,
|
|
316
|
+
PcbScene3dCopperFillMeshBuilder.#loopSetsToFills(loopSets),
|
|
317
|
+
(x, y) => ({ x, y }),
|
|
318
|
+
false
|
|
319
|
+
)
|
|
320
|
+
if (!clippedMesh) {
|
|
321
|
+
return []
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return Array.from(clippedMesh.geometry.attributes.position.array)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Converts normalized loop sets back to fill primitives for shared clipping.
|
|
329
|
+
* @param {{ outer: number[][], holes: number[][][] }[]} loopSets Loop sets.
|
|
330
|
+
* @returns {object[]}
|
|
331
|
+
*/
|
|
332
|
+
static #loopSetsToFills(loopSets) {
|
|
333
|
+
return loopSets.map((loopSet) => ({
|
|
334
|
+
points: loopSet.outer.map((point) => ({
|
|
335
|
+
x: point[0],
|
|
336
|
+
y: point[1]
|
|
337
|
+
})),
|
|
338
|
+
holes: (loopSet.holes || []).map((hole) =>
|
|
339
|
+
hole.map((point) => ({
|
|
340
|
+
x: point[0],
|
|
341
|
+
y: point[1]
|
|
342
|
+
}))
|
|
343
|
+
)
|
|
344
|
+
}))
|
|
345
|
+
}
|
|
346
|
+
|
|
118
347
|
/**
|
|
119
348
|
* Appends one fill island loop set to a shared position buffer.
|
|
120
349
|
* @param {number[]} positions Position buffer.
|
|
@@ -123,6 +352,7 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
123
352
|
* @param {number} topZ Upper Z.
|
|
124
353
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
125
354
|
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
355
|
+
* @param {{ surfaceOnly?: boolean }} options Mesh options.
|
|
126
356
|
* @returns {void}
|
|
127
357
|
*/
|
|
128
358
|
static #appendLoopSetPositions(
|
|
@@ -131,7 +361,8 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
131
361
|
bottomZ,
|
|
132
362
|
topZ,
|
|
133
363
|
normalizeBoardPoint,
|
|
134
|
-
mirrorY
|
|
364
|
+
mirrorY,
|
|
365
|
+
options
|
|
135
366
|
) {
|
|
136
367
|
const outer = PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
|
|
137
368
|
loops.outer,
|
|
@@ -154,6 +385,33 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
154
385
|
return
|
|
155
386
|
}
|
|
156
387
|
|
|
388
|
+
PcbScene3dCopperFillMeshBuilder.#appendNormalizedLoopSetPositions(
|
|
389
|
+
positions,
|
|
390
|
+
{ outer, holes },
|
|
391
|
+
bottomZ,
|
|
392
|
+
topZ,
|
|
393
|
+
options
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Appends one already-normalized fill island loop set.
|
|
399
|
+
* @param {number[]} positions Position buffer.
|
|
400
|
+
* @param {{ outer: number[][], holes: number[][][] }} loops Fill loop set.
|
|
401
|
+
* @param {number} bottomZ Lower Z.
|
|
402
|
+
* @param {number} topZ Upper Z.
|
|
403
|
+
* @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean }} options Mesh options.
|
|
404
|
+
* @returns {void}
|
|
405
|
+
*/
|
|
406
|
+
static #appendNormalizedLoopSetPositions(
|
|
407
|
+
positions,
|
|
408
|
+
loops,
|
|
409
|
+
bottomZ,
|
|
410
|
+
topZ,
|
|
411
|
+
options
|
|
412
|
+
) {
|
|
413
|
+
const outer = loops.outer
|
|
414
|
+
const holes = loops.holes || []
|
|
157
415
|
const { points, flat, holeIndexes } =
|
|
158
416
|
PcbScene3dCopperFillMeshBuilder.#flattenLoops(outer, holes)
|
|
159
417
|
const triangles = earcut(flat, holeIndexes, 2)
|
|
@@ -168,6 +426,10 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
168
426
|
topZ,
|
|
169
427
|
false
|
|
170
428
|
)
|
|
429
|
+
if (PcbScene3dCopperFillMeshBuilder.#isSurfaceOnly(options)) {
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
|
|
171
433
|
PcbScene3dCopperFillMeshBuilder.#appendSurfaceTriangles(
|
|
172
434
|
positions,
|
|
173
435
|
points,
|
|
@@ -191,6 +453,132 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
191
453
|
}
|
|
192
454
|
}
|
|
193
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Resolves normalized fill loop sets.
|
|
458
|
+
* @param {object[]} fills Filled copper primitives.
|
|
459
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
460
|
+
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
461
|
+
* @returns {{ outer: number[][], holes: number[][][], bounds: object }[]}
|
|
462
|
+
*/
|
|
463
|
+
static #resolveLoopSets(fills, normalizeBoardPoint, mirrorY) {
|
|
464
|
+
return (fills || []).flatMap((fill) =>
|
|
465
|
+
PcbAssemblyFillGeometryResolver.resolveAll(fill)
|
|
466
|
+
.map((loops) =>
|
|
467
|
+
PcbScene3dCopperFillMeshBuilder.#normalizeLoopSet(
|
|
468
|
+
loops,
|
|
469
|
+
normalizeBoardPoint,
|
|
470
|
+
mirrorY
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
.filter(Boolean)
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Normalizes one fill loop set.
|
|
479
|
+
* @param {{ outer: number[][], holes: number[][][] }} loops Source loops.
|
|
480
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
481
|
+
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
482
|
+
* @returns {{ outer: number[][], holes: number[][][], bounds: object } | null}
|
|
483
|
+
*/
|
|
484
|
+
static #normalizeLoopSet(loops, normalizeBoardPoint, mirrorY) {
|
|
485
|
+
const outer = PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
|
|
486
|
+
loops.outer,
|
|
487
|
+
normalizeBoardPoint,
|
|
488
|
+
mirrorY
|
|
489
|
+
)
|
|
490
|
+
if (!PcbScene3dCopperFillMeshBuilder.#isValidLoop(outer)) {
|
|
491
|
+
return null
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const holes = (loops.holes || [])
|
|
495
|
+
.map((loop) =>
|
|
496
|
+
PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
|
|
497
|
+
loop,
|
|
498
|
+
normalizeBoardPoint,
|
|
499
|
+
mirrorY
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
.filter((loop) =>
|
|
503
|
+
PcbScene3dCopperFillMeshBuilder.#isValidLoop(loop)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
outer,
|
|
508
|
+
holes,
|
|
509
|
+
bounds: PcbScene3dCopperFillMeshBuilder.#loopBounds(outer)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Resolves later fill loops fully contained by an earlier loop set.
|
|
515
|
+
* @param {{ outer: number[][], holes: number[][][], bounds: object }} loopSet Current loop set.
|
|
516
|
+
* @param {{ outer: number[][], holes: number[][][], bounds: object }[]} loopSets All loop sets.
|
|
517
|
+
* @param {number} currentIndex Current loop-set index.
|
|
518
|
+
* @returns {number[][][]}
|
|
519
|
+
*/
|
|
520
|
+
static #containedLaterLoops(loopSet, loopSets, currentIndex) {
|
|
521
|
+
return loopSets
|
|
522
|
+
.slice(currentIndex + 1)
|
|
523
|
+
.filter((candidate) =>
|
|
524
|
+
PcbScene3dCopperFillMeshBuilder.#containsLoopSet(
|
|
525
|
+
loopSet,
|
|
526
|
+
candidate
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
.map((candidate) => candidate.outer)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Checks whether one loop set contains another fill loop.
|
|
534
|
+
* @param {{ outer: number[][], holes: number[][][], bounds: object }} outerSet Outer loop set.
|
|
535
|
+
* @param {{ outer: number[][], bounds: object }} candidate Candidate loop set.
|
|
536
|
+
* @returns {boolean}
|
|
537
|
+
*/
|
|
538
|
+
static #containsLoopSet(outerSet, candidate) {
|
|
539
|
+
const representative =
|
|
540
|
+
PcbScene3dCopperFillMeshBuilder.#representativePoint(
|
|
541
|
+
candidate.outer
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
return (
|
|
545
|
+
PcbScene3dCopperFillMeshBuilder.#boundsContainBounds(
|
|
546
|
+
outerSet.bounds,
|
|
547
|
+
candidate.bounds
|
|
548
|
+
) &&
|
|
549
|
+
candidate.outer.every((point) =>
|
|
550
|
+
PcbScene3dCopperFillMeshBuilder.#pointInPolygon(
|
|
551
|
+
point,
|
|
552
|
+
outerSet.outer
|
|
553
|
+
)
|
|
554
|
+
) &&
|
|
555
|
+
!outerSet.holes.some((hole) =>
|
|
556
|
+
PcbScene3dCopperFillMeshBuilder.#pointInPolygon(
|
|
557
|
+
representative,
|
|
558
|
+
hole
|
|
559
|
+
)
|
|
560
|
+
)
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Checks whether a fill should be represented by only its visible face.
|
|
566
|
+
* @param {{ surfaceOnly?: boolean } | undefined} options Mesh options.
|
|
567
|
+
* @returns {boolean}
|
|
568
|
+
*/
|
|
569
|
+
static #isSurfaceOnly(options) {
|
|
570
|
+
return options?.surfaceOnly === true
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Checks whether contained fill overlaps should be removed before meshing.
|
|
575
|
+
* @param {{ clipContainedFillOverlaps?: boolean } | undefined} options Mesh options.
|
|
576
|
+
* @returns {boolean}
|
|
577
|
+
*/
|
|
578
|
+
static #shouldClipContainedFillOverlaps(options) {
|
|
579
|
+
return options?.clipContainedFillOverlaps === true
|
|
580
|
+
}
|
|
581
|
+
|
|
194
582
|
/**
|
|
195
583
|
* Normalizes one loop into local copper-side coordinates.
|
|
196
584
|
* @param {number[][]} loop Source loop.
|
|
@@ -376,6 +764,140 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
376
764
|
)
|
|
377
765
|
}
|
|
378
766
|
|
|
767
|
+
/**
|
|
768
|
+
* Computes axis-aligned bounds for one loop.
|
|
769
|
+
* @param {number[][]} loop Candidate loop.
|
|
770
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number }}
|
|
771
|
+
*/
|
|
772
|
+
static #loopBounds(loop) {
|
|
773
|
+
return (loop || []).reduce(
|
|
774
|
+
(bounds, point) => ({
|
|
775
|
+
minX: Math.min(bounds.minX, Number(point?.[0])),
|
|
776
|
+
minY: Math.min(bounds.minY, Number(point?.[1])),
|
|
777
|
+
maxX: Math.max(bounds.maxX, Number(point?.[0])),
|
|
778
|
+
maxY: Math.max(bounds.maxY, Number(point?.[1]))
|
|
779
|
+
}),
|
|
780
|
+
{
|
|
781
|
+
minX: Infinity,
|
|
782
|
+
minY: Infinity,
|
|
783
|
+
maxX: -Infinity,
|
|
784
|
+
maxY: -Infinity
|
|
785
|
+
}
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Checks whether one bounds fully contains another.
|
|
791
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} outer Outer bounds.
|
|
792
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} inner Inner bounds.
|
|
793
|
+
* @returns {boolean}
|
|
794
|
+
*/
|
|
795
|
+
static #boundsContainBounds(outer, inner) {
|
|
796
|
+
return (
|
|
797
|
+
Number.isFinite(
|
|
798
|
+
outer.minX +
|
|
799
|
+
outer.minY +
|
|
800
|
+
outer.maxX +
|
|
801
|
+
outer.maxY +
|
|
802
|
+
inner.minX +
|
|
803
|
+
inner.minY +
|
|
804
|
+
inner.maxX +
|
|
805
|
+
inner.maxY
|
|
806
|
+
) &&
|
|
807
|
+
inner.minX >= outer.minX - 0.001 &&
|
|
808
|
+
inner.maxX <= outer.maxX + 0.001 &&
|
|
809
|
+
inner.minY >= outer.minY - 0.001 &&
|
|
810
|
+
inner.maxY <= outer.maxY + 0.001
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Checks whether one candidate loop set is fully inside a hole.
|
|
816
|
+
* @param {{ outer: number[][], bounds: object }} candidate Candidate loop set.
|
|
817
|
+
* @param {{ holes: number[][][] }} loopSet Previous loop set.
|
|
818
|
+
* @returns {boolean}
|
|
819
|
+
*/
|
|
820
|
+
static #isInsideAnyHole(candidate, loopSet) {
|
|
821
|
+
return (loopSet.holes || []).some((hole) => {
|
|
822
|
+
const holeBounds = PcbScene3dCopperFillMeshBuilder.#loopBounds(hole)
|
|
823
|
+
|
|
824
|
+
return (
|
|
825
|
+
PcbScene3dCopperFillMeshBuilder.#boundsContainBounds(
|
|
826
|
+
holeBounds,
|
|
827
|
+
candidate.bounds
|
|
828
|
+
) &&
|
|
829
|
+
candidate.outer.every((point) =>
|
|
830
|
+
PcbScene3dCopperFillMeshBuilder.#pointInPolygon(point, hole)
|
|
831
|
+
)
|
|
832
|
+
)
|
|
833
|
+
})
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Checks whether two bounds overlap.
|
|
838
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} left Left bounds.
|
|
839
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} right Right bounds.
|
|
840
|
+
* @returns {boolean}
|
|
841
|
+
*/
|
|
842
|
+
static #boundsOverlap(left, right) {
|
|
843
|
+
return !(
|
|
844
|
+
left.maxX < right.minX - 0.001 ||
|
|
845
|
+
left.minX > right.maxX + 0.001 ||
|
|
846
|
+
left.maxY < right.minY - 0.001 ||
|
|
847
|
+
left.minY > right.maxY + 0.001
|
|
848
|
+
)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Resolves a representative point for one loop.
|
|
853
|
+
* @param {number[][]} loop Candidate loop.
|
|
854
|
+
* @returns {number[]}
|
|
855
|
+
*/
|
|
856
|
+
static #representativePoint(loop) {
|
|
857
|
+
const total = (loop || []).reduce(
|
|
858
|
+
(sum, point) => [
|
|
859
|
+
sum[0] + Number(point?.[0]),
|
|
860
|
+
sum[1] + Number(point?.[1])
|
|
861
|
+
],
|
|
862
|
+
[0, 0]
|
|
863
|
+
)
|
|
864
|
+
const count = Math.max((loop || []).length, 1)
|
|
865
|
+
return [total[0] / count, total[1] / count]
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Checks whether a point is inside a polygon loop.
|
|
870
|
+
* @param {number[]} point Candidate point.
|
|
871
|
+
* @param {number[][]} polygon Polygon loop.
|
|
872
|
+
* @returns {boolean}
|
|
873
|
+
*/
|
|
874
|
+
static #pointInPolygon(point, polygon) {
|
|
875
|
+
let inside = false
|
|
876
|
+
|
|
877
|
+
for (
|
|
878
|
+
let index = 0, previousIndex = polygon.length - 1;
|
|
879
|
+
index < polygon.length;
|
|
880
|
+
previousIndex = index, index += 1
|
|
881
|
+
) {
|
|
882
|
+
const current = polygon[index]
|
|
883
|
+
const previous = polygon[previousIndex]
|
|
884
|
+
const currentY = Number(current?.[1])
|
|
885
|
+
const previousY = Number(previous?.[1])
|
|
886
|
+
const pointY = Number(point?.[1])
|
|
887
|
+
const intersects =
|
|
888
|
+
currentY > pointY !== previousY > pointY &&
|
|
889
|
+
Number(point?.[0]) <
|
|
890
|
+
((Number(previous?.[0]) - Number(current?.[0])) *
|
|
891
|
+
(pointY - currentY)) /
|
|
892
|
+
(previousY - currentY) +
|
|
893
|
+
Number(current?.[0])
|
|
894
|
+
|
|
895
|
+
if (intersects) inside = !inside
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return inside
|
|
899
|
+
}
|
|
900
|
+
|
|
379
901
|
/**
|
|
380
902
|
* Computes signed loop area.
|
|
381
903
|
* @param {number[][]} loop Candidate loop.
|