pcb-scene3d-viewer 1.1.10 → 1.1.11
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 +1 -1
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +362 -0
- package/src/PcbScene3dCopperFactory.mjs +52 -70
- package/src/PcbScene3dCopperLayerFilter.mjs +48 -0
- package/src/PcbScene3dCopperOcclusionClipper.mjs +86 -0
- package/src/PcbScene3dCopperTextFactory.mjs +23 -6
- package/src/PcbScene3dCutoutGeometryFilter.mjs +25 -28
- package/src/PcbScene3dDrillCutoutFilter.mjs +123 -0
- package/src/PcbScene3dGeometryFaceRefiner.mjs +218 -0
- package/src/PcbScene3dMaskCoveredCopperMaterial.mjs +1 -4
- package/src/PcbScene3dPadFactory.mjs +160 -3
- package/src/PcbScene3dPadSurfaceStack.mjs +155 -0
- package/src/PcbScene3dPolygonOverlap.mjs +283 -0
- package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +212 -0
- package/src/PcbScene3dShapeHoleMerger.mjs +476 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +174 -119
- package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +241 -0
- package/src/PcbScene3dTerminalCutoutClassifier.mjs +92 -0
- package/src/PcbScene3dTrueTypeTextFactory.mjs +12 -5
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { PcbScene3dBoardMaterialPalette } from './PcbScene3dBoardMaterialPalette.mjs'
|
|
2
2
|
import { PcbScene3dCopperDetailFilter } from './PcbScene3dCopperDetailFilter.mjs'
|
|
3
3
|
import { PcbScene3dCopperFactory } from './PcbScene3dCopperFactory.mjs'
|
|
4
|
+
import { PcbScene3dPadFactory } from './PcbScene3dPadFactory.mjs'
|
|
4
5
|
import { PcbScene3dViaFactory } from './PcbScene3dViaFactory.mjs'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Builds deferred copper-detail groups for the 3D runtime.
|
|
8
9
|
*/
|
|
9
10
|
export class PcbScene3dCopperDetailGroupBuilder {
|
|
11
|
+
static #CIRCLE_SEGMENTS = 64
|
|
12
|
+
static #ROUNDED_CORNER_SEGMENTS = 12
|
|
13
|
+
|
|
10
14
|
/**
|
|
11
15
|
* Builds visible exposed and mask-covered copper detail.
|
|
12
16
|
* @param {any} THREE Three.js namespace.
|
|
@@ -68,6 +72,10 @@ export class PcbScene3dCopperDetailGroupBuilder {
|
|
|
68
72
|
sceneDescription?.boardAssemblyModel
|
|
69
73
|
)
|
|
70
74
|
}
|
|
75
|
+
),
|
|
76
|
+
occlusionCutouts:
|
|
77
|
+
PcbScene3dCopperDetailGroupBuilder.#resolveCoveredCopperOcclusions(
|
|
78
|
+
sceneDescription?.detail
|
|
71
79
|
)
|
|
72
80
|
}
|
|
73
81
|
)
|
|
@@ -117,4 +125,358 @@ export class PcbScene3dCopperDetailGroupBuilder {
|
|
|
117
125
|
normalizePoint
|
|
118
126
|
)
|
|
119
127
|
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Resolves opaque silkscreen fill polygons that should hide covered copper.
|
|
131
|
+
* @param {{ top?: object, bottom?: object } | undefined} silkscreen Scene silkscreen detail.
|
|
132
|
+
* @returns {{ top: { x: number, y: number }[][], bottom: { x: number, y: number }[][] }}
|
|
133
|
+
*/
|
|
134
|
+
static #resolveSilkscreenFillOcclusions(silkscreen) {
|
|
135
|
+
return {
|
|
136
|
+
top: PcbScene3dCopperDetailGroupBuilder.#resolveSideFillOcclusions(
|
|
137
|
+
silkscreen?.top
|
|
138
|
+
),
|
|
139
|
+
bottom: PcbScene3dCopperDetailGroupBuilder.#resolveSideFillOcclusions(
|
|
140
|
+
silkscreen?.bottom
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolves fill contours on one silkscreen side.
|
|
147
|
+
* @param {{ fills?: any[] } | undefined} side Side-specific silkscreen detail.
|
|
148
|
+
* @returns {{ x: number, y: number }[][]}
|
|
149
|
+
*/
|
|
150
|
+
static #resolveSideFillOcclusions(side) {
|
|
151
|
+
return (Array.isArray(side?.fills) ? side.fills : [])
|
|
152
|
+
.map((fill) =>
|
|
153
|
+
Array.isArray(fill?.points) && fill.points.length >= 3
|
|
154
|
+
? fill.points
|
|
155
|
+
: PcbScene3dCopperDetailGroupBuilder.#resolveBoxFillPoints(
|
|
156
|
+
fill
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
.map((points) =>
|
|
160
|
+
points
|
|
161
|
+
.map((point) => ({
|
|
162
|
+
x: Number(point?.x),
|
|
163
|
+
y: Number(point?.y)
|
|
164
|
+
}))
|
|
165
|
+
.filter(
|
|
166
|
+
(point) =>
|
|
167
|
+
Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
.filter((points) => points.length >= 3)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolves a rectangular fill as a polygon contour.
|
|
175
|
+
* @param {{ x1?: number, y1?: number, x2?: number, y2?: number } | undefined} fill Fill record.
|
|
176
|
+
* @returns {{ x: number, y: number }[]}
|
|
177
|
+
*/
|
|
178
|
+
static #resolveBoxFillPoints(fill) {
|
|
179
|
+
const x1 = Number(fill?.x1)
|
|
180
|
+
const y1 = Number(fill?.y1)
|
|
181
|
+
const x2 = Number(fill?.x2)
|
|
182
|
+
const y2 = Number(fill?.y2)
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
!Number.isFinite(x1) ||
|
|
186
|
+
!Number.isFinite(y1) ||
|
|
187
|
+
!Number.isFinite(x2) ||
|
|
188
|
+
!Number.isFinite(y2)
|
|
189
|
+
) {
|
|
190
|
+
return []
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return [
|
|
194
|
+
{ x: x1, y: y1 },
|
|
195
|
+
{ x: x2, y: y1 },
|
|
196
|
+
{ x: x2, y: y2 },
|
|
197
|
+
{ x: x1, y: y2 }
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Resolves opaque geometry that should hide mask-covered copper.
|
|
203
|
+
* @param {object | undefined} detail Scene detail.
|
|
204
|
+
* @returns {{ top: { x: number, y: number }[][], bottom: { x: number, y: number }[][] }}
|
|
205
|
+
*/
|
|
206
|
+
static #resolveCoveredCopperOcclusions(detail) {
|
|
207
|
+
const silkscreenOcclusions =
|
|
208
|
+
PcbScene3dCopperDetailGroupBuilder.#resolveSilkscreenFillOcclusions(
|
|
209
|
+
detail?.silkscreen
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
top: silkscreenOcclusions.top.concat(
|
|
214
|
+
PcbScene3dCopperDetailGroupBuilder.#resolvePadSurfaceOcclusions(
|
|
215
|
+
detail?.pads,
|
|
216
|
+
'top'
|
|
217
|
+
)
|
|
218
|
+
),
|
|
219
|
+
bottom: silkscreenOcclusions.bottom.concat(
|
|
220
|
+
PcbScene3dCopperDetailGroupBuilder.#resolvePadSurfaceOcclusions(
|
|
221
|
+
detail?.pads,
|
|
222
|
+
'bottom'
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resolves exposed pad faces on one side as occlusion polygons.
|
|
230
|
+
* @param {object[] | undefined} pads Pad detail rows.
|
|
231
|
+
* @param {'top' | 'bottom'} side Board side.
|
|
232
|
+
* @returns {{ x: number, y: number }[][]}
|
|
233
|
+
*/
|
|
234
|
+
static #resolvePadSurfaceOcclusions(pads, side) {
|
|
235
|
+
return (Array.isArray(pads) ? pads : [])
|
|
236
|
+
.filter((pad) =>
|
|
237
|
+
PcbScene3dCopperDetailGroupBuilder.#hasVisiblePadSurface(
|
|
238
|
+
pad,
|
|
239
|
+
side
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
.map((pad) =>
|
|
243
|
+
PcbScene3dCopperDetailGroupBuilder.#resolvePadSurfacePolygon(
|
|
244
|
+
pad,
|
|
245
|
+
side
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
.filter((points) => points.length >= 3)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Checks whether one pad face has exposed copper on a side.
|
|
253
|
+
* @param {object} pad Pad detail row.
|
|
254
|
+
* @param {'top' | 'bottom'} side Board side.
|
|
255
|
+
* @returns {boolean}
|
|
256
|
+
*/
|
|
257
|
+
static #hasVisiblePadSurface(pad, side) {
|
|
258
|
+
if (
|
|
259
|
+
PcbScene3dCopperDetailGroupBuilder.#resolveSolderMaskOpening(
|
|
260
|
+
pad,
|
|
261
|
+
side
|
|
262
|
+
) === false
|
|
263
|
+
) {
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (PcbScene3dCopperDetailGroupBuilder.#hasSideSize(pad, side)) {
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const oppositeSide = side === 'bottom' ? 'top' : 'bottom'
|
|
272
|
+
if (
|
|
273
|
+
PcbScene3dCopperDetailGroupBuilder.#hasSideSize(pad, oppositeSide)
|
|
274
|
+
) {
|
|
275
|
+
return false
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
Number(pad?.sizeMidX || 0) > 0 ||
|
|
280
|
+
Number(pad?.sizeMidY || 0) > 0 ||
|
|
281
|
+
Number(pad?.holeDiameter || 0) > 0
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resolves one side-specific solder mask opening flag.
|
|
287
|
+
* @param {object} pad Pad detail row.
|
|
288
|
+
* @param {'top' | 'bottom'} side Board side.
|
|
289
|
+
* @returns {boolean | null}
|
|
290
|
+
*/
|
|
291
|
+
static #resolveSolderMaskOpening(pad, side) {
|
|
292
|
+
const fieldName =
|
|
293
|
+
side === 'bottom'
|
|
294
|
+
? 'hasBottomSolderMaskOpening'
|
|
295
|
+
: 'hasTopSolderMaskOpening'
|
|
296
|
+
|
|
297
|
+
return typeof pad?.[fieldName] === 'boolean' ? pad[fieldName] : null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Checks whether one pad has an explicit copper size for a side.
|
|
302
|
+
* @param {object} pad Pad detail row.
|
|
303
|
+
* @param {'top' | 'bottom'} side Board side.
|
|
304
|
+
* @returns {boolean}
|
|
305
|
+
*/
|
|
306
|
+
static #hasSideSize(pad, side) {
|
|
307
|
+
return side === 'bottom'
|
|
308
|
+
? Number(pad?.sizeBottomX || 0) > 0 ||
|
|
309
|
+
Number(pad?.sizeBottomY || 0) > 0
|
|
310
|
+
: Number(pad?.sizeTopX || 0) > 0 || Number(pad?.sizeTopY || 0) > 0
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Resolves one pad surface as a board-coordinate polygon.
|
|
315
|
+
* @param {object} pad Pad detail row.
|
|
316
|
+
* @param {'top' | 'bottom'} side Board side.
|
|
317
|
+
* @returns {{ x: number, y: number }[]}
|
|
318
|
+
*/
|
|
319
|
+
static #resolvePadSurfacePolygon(pad, side) {
|
|
320
|
+
const spec = PcbScene3dPadFactory.resolvePadSurfaceSpec(pad, side)
|
|
321
|
+
const center = {
|
|
322
|
+
x: Number(pad?.x || 0) + spec.offsetX,
|
|
323
|
+
y: Number(pad?.y || 0) + spec.offsetY
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return PcbScene3dCopperDetailGroupBuilder.#transformPoints(
|
|
327
|
+
PcbScene3dCopperDetailGroupBuilder.#buildPadLocalPoints(spec),
|
|
328
|
+
center,
|
|
329
|
+
Number(pad?.rotation || 0)
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Builds local points for one pad face.
|
|
335
|
+
* @param {{ width: number, height: number, kind: string, cornerRadius: number }} spec Pad surface spec.
|
|
336
|
+
* @returns {{ x: number, y: number }[]}
|
|
337
|
+
*/
|
|
338
|
+
static #buildPadLocalPoints(spec) {
|
|
339
|
+
if (spec.kind === 'circle') {
|
|
340
|
+
return PcbScene3dCopperDetailGroupBuilder.#buildCirclePoints(
|
|
341
|
+
Math.max(spec.width, spec.height) / 2
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (spec.kind === 'rounded-rect' && Number(spec.cornerRadius) > 0) {
|
|
346
|
+
return PcbScene3dCopperDetailGroupBuilder.#buildRoundedRectPoints(
|
|
347
|
+
spec.width,
|
|
348
|
+
spec.height,
|
|
349
|
+
spec.cornerRadius
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return PcbScene3dCopperDetailGroupBuilder.#buildRectPoints(
|
|
354
|
+
spec.width,
|
|
355
|
+
spec.height
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Builds a circular polygon centered at the origin.
|
|
361
|
+
* @param {number} radius Circle radius.
|
|
362
|
+
* @returns {{ x: number, y: number }[]}
|
|
363
|
+
*/
|
|
364
|
+
static #buildCirclePoints(radius) {
|
|
365
|
+
return Array.from(
|
|
366
|
+
{ length: PcbScene3dCopperDetailGroupBuilder.#CIRCLE_SEGMENTS },
|
|
367
|
+
(_unused, index) => {
|
|
368
|
+
const angle =
|
|
369
|
+
(Math.PI * 2 * index) /
|
|
370
|
+
PcbScene3dCopperDetailGroupBuilder.#CIRCLE_SEGMENTS
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
x: Math.cos(angle) * radius,
|
|
374
|
+
y: Math.sin(angle) * radius
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Builds a rectangle polygon centered at the origin.
|
|
382
|
+
* @param {number} width Rectangle width.
|
|
383
|
+
* @param {number} height Rectangle height.
|
|
384
|
+
* @returns {{ x: number, y: number }[]}
|
|
385
|
+
*/
|
|
386
|
+
static #buildRectPoints(width, height) {
|
|
387
|
+
const halfWidth = Number(width || 0) / 2
|
|
388
|
+
const halfHeight = Number(height || 0) / 2
|
|
389
|
+
|
|
390
|
+
return [
|
|
391
|
+
{ x: -halfWidth, y: -halfHeight },
|
|
392
|
+
{ x: halfWidth, y: -halfHeight },
|
|
393
|
+
{ x: halfWidth, y: halfHeight },
|
|
394
|
+
{ x: -halfWidth, y: halfHeight }
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Builds a rounded rectangle polygon centered at the origin.
|
|
400
|
+
* @param {number} width Rectangle width.
|
|
401
|
+
* @param {number} height Rectangle height.
|
|
402
|
+
* @param {number} radius Corner radius.
|
|
403
|
+
* @returns {{ x: number, y: number }[]}
|
|
404
|
+
*/
|
|
405
|
+
static #buildRoundedRectPoints(width, height, radius) {
|
|
406
|
+
const halfWidth = Number(width || 0) / 2
|
|
407
|
+
const halfHeight = Number(height || 0) / 2
|
|
408
|
+
const cornerRadius = Math.min(
|
|
409
|
+
Math.max(Number(radius || 0), 0),
|
|
410
|
+
halfWidth,
|
|
411
|
+
halfHeight
|
|
412
|
+
)
|
|
413
|
+
const corners = [
|
|
414
|
+
{ x: halfWidth - cornerRadius, y: halfHeight - cornerRadius },
|
|
415
|
+
{ x: -halfWidth + cornerRadius, y: halfHeight - cornerRadius },
|
|
416
|
+
{ x: -halfWidth + cornerRadius, y: -halfHeight + cornerRadius },
|
|
417
|
+
{ x: halfWidth - cornerRadius, y: -halfHeight + cornerRadius }
|
|
418
|
+
]
|
|
419
|
+
const angleRanges = [
|
|
420
|
+
[0, Math.PI / 2],
|
|
421
|
+
[Math.PI / 2, Math.PI],
|
|
422
|
+
[Math.PI, (Math.PI * 3) / 2],
|
|
423
|
+
[(Math.PI * 3) / 2, Math.PI * 2]
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
return corners.flatMap((corner, cornerIndex) =>
|
|
427
|
+
PcbScene3dCopperDetailGroupBuilder.#buildCornerPoints(
|
|
428
|
+
corner,
|
|
429
|
+
cornerRadius,
|
|
430
|
+
angleRanges[cornerIndex]
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Builds sampled points for one rounded corner.
|
|
437
|
+
* @param {{ x: number, y: number }} center Corner arc center.
|
|
438
|
+
* @param {number} radius Corner radius.
|
|
439
|
+
* @param {number[]} angleRange Start and end angle.
|
|
440
|
+
* @returns {{ x: number, y: number }[]}
|
|
441
|
+
*/
|
|
442
|
+
static #buildCornerPoints(center, radius, angleRange) {
|
|
443
|
+
const [startAngle, endAngle] = angleRange
|
|
444
|
+
|
|
445
|
+
return Array.from(
|
|
446
|
+
{
|
|
447
|
+
length:
|
|
448
|
+
PcbScene3dCopperDetailGroupBuilder
|
|
449
|
+
.#ROUNDED_CORNER_SEGMENTS + 1
|
|
450
|
+
},
|
|
451
|
+
(_unused, index) => {
|
|
452
|
+
const ratio =
|
|
453
|
+
index /
|
|
454
|
+
PcbScene3dCopperDetailGroupBuilder.#ROUNDED_CORNER_SEGMENTS
|
|
455
|
+
const angle = startAngle + (endAngle - startAngle) * ratio
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
x: center.x + Math.cos(angle) * radius,
|
|
459
|
+
y: center.y + Math.sin(angle) * radius
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Rotates and translates local points into board coordinates.
|
|
467
|
+
* @param {{ x: number, y: number }[]} points Local points.
|
|
468
|
+
* @param {{ x: number, y: number }} center Board center.
|
|
469
|
+
* @param {number} rotationDeg Rotation in degrees.
|
|
470
|
+
* @returns {{ x: number, y: number }[]}
|
|
471
|
+
*/
|
|
472
|
+
static #transformPoints(points, center, rotationDeg) {
|
|
473
|
+
const angle = (Number(rotationDeg || 0) * Math.PI) / 180
|
|
474
|
+
const cos = Math.cos(angle)
|
|
475
|
+
const sin = Math.sin(angle)
|
|
476
|
+
|
|
477
|
+
return points.map((point) => ({
|
|
478
|
+
x: center.x + point.x * cos - point.y * sin,
|
|
479
|
+
y: center.y + point.x * sin + point.y * cos
|
|
480
|
+
}))
|
|
481
|
+
}
|
|
120
482
|
}
|
|
@@ -3,14 +3,14 @@ import { PcbScene3dPadFactory } from './PcbScene3dPadFactory.mjs'
|
|
|
3
3
|
import { PcbScene3dCopperTextFactory } from './PcbScene3dCopperTextFactory.mjs'
|
|
4
4
|
import { PcbScene3dMaskCoveredCopperMaterial } from './PcbScene3dMaskCoveredCopperMaterial.mjs'
|
|
5
5
|
import { PcbScene3dGeometryZCompressor } from './PcbScene3dGeometryZCompressor.mjs'
|
|
6
|
+
import { PcbScene3dCopperOcclusionClipper } from './PcbScene3dCopperOcclusionClipper.mjs'
|
|
7
|
+
import { PcbScene3dCopperLayerFilter } from './PcbScene3dCopperLayerFilter.mjs'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Builds copper-detail meshes for the interactive 3D PCB scene.
|
|
9
11
|
*/
|
|
10
12
|
export class PcbScene3dCopperFactory {
|
|
11
13
|
static #ARC_SEGMENT_DEGREES = 3
|
|
12
|
-
static #TOP_COPPER_LAYER_ID = 1
|
|
13
|
-
static #BOTTOM_COPPER_LAYER_ID = 32
|
|
14
14
|
static #FULL_CIRCLE_EPSILON = 0.001
|
|
15
15
|
static #ROUND_CAP_SEGMENTS = 16
|
|
16
16
|
static #COPPER_THICKNESS_MIL = 2.2
|
|
@@ -46,11 +46,11 @@ export class PcbScene3dCopperFactory {
|
|
|
46
46
|
const topGroup = PcbScene3dCopperFactory.#buildSideGroup(
|
|
47
47
|
THREE,
|
|
48
48
|
{
|
|
49
|
-
tracks:
|
|
49
|
+
tracks: PcbScene3dCopperLayerFilter.tracks(
|
|
50
50
|
detail?.tracks,
|
|
51
51
|
'top'
|
|
52
52
|
),
|
|
53
|
-
arcs:
|
|
53
|
+
arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'top'),
|
|
54
54
|
pads: detail?.pads || [],
|
|
55
55
|
vias: detail?.vias || [],
|
|
56
56
|
copperTexts: detail?.copperTexts || []
|
|
@@ -63,14 +63,11 @@ export class PcbScene3dCopperFactory {
|
|
|
63
63
|
const bottomGroup = PcbScene3dCopperFactory.#buildSideGroup(
|
|
64
64
|
THREE,
|
|
65
65
|
{
|
|
66
|
-
tracks:
|
|
66
|
+
tracks: PcbScene3dCopperLayerFilter.tracks(
|
|
67
67
|
detail?.tracks,
|
|
68
68
|
'bottom'
|
|
69
69
|
),
|
|
70
|
-
arcs:
|
|
71
|
-
detail?.arcs,
|
|
72
|
-
'bottom'
|
|
73
|
-
),
|
|
70
|
+
arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'bottom'),
|
|
74
71
|
pads: detail?.pads || [],
|
|
75
72
|
vias: detail?.vias || [],
|
|
76
73
|
copperTexts: detail?.copperTexts || []
|
|
@@ -98,7 +95,7 @@ export class PcbScene3dCopperFactory {
|
|
|
98
95
|
* @param {number} topZ
|
|
99
96
|
* @param {number} bottomZ
|
|
100
97
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
101
|
-
* @param {{ solderMaskColor?: number }} [options]
|
|
98
|
+
* @param {{ solderMaskColor?: number, occlusionCutouts?: { top?: { x: number, y: number }[][], bottom?: { x: number, y: number }[][] } }} [options]
|
|
102
99
|
* @returns {any}
|
|
103
100
|
*/
|
|
104
101
|
static buildMaskCoveredGroup(
|
|
@@ -117,33 +114,40 @@ export class PcbScene3dCopperFactory {
|
|
|
117
114
|
const topGroup = PcbScene3dCopperFactory.#buildMaskCoveredSideGroup(
|
|
118
115
|
THREE,
|
|
119
116
|
{
|
|
120
|
-
tracks:
|
|
117
|
+
tracks: PcbScene3dCopperLayerFilter.tracks(
|
|
121
118
|
detail?.tracks,
|
|
122
119
|
'top'
|
|
123
120
|
),
|
|
124
|
-
arcs:
|
|
121
|
+
arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'top')
|
|
125
122
|
},
|
|
126
123
|
Math.abs(Number(topZ || 0)),
|
|
127
124
|
normalizeBoardPoint,
|
|
128
125
|
false,
|
|
129
|
-
material
|
|
126
|
+
material,
|
|
127
|
+
PcbScene3dCopperOcclusionClipper.normalizeCutouts(
|
|
128
|
+
options?.occlusionCutouts?.top,
|
|
129
|
+
normalizeBoardPoint,
|
|
130
|
+
false
|
|
131
|
+
)
|
|
130
132
|
)
|
|
131
133
|
const bottomGroup = PcbScene3dCopperFactory.#buildMaskCoveredSideGroup(
|
|
132
134
|
THREE,
|
|
133
135
|
{
|
|
134
|
-
tracks:
|
|
136
|
+
tracks: PcbScene3dCopperLayerFilter.tracks(
|
|
135
137
|
detail?.tracks,
|
|
136
138
|
'bottom'
|
|
137
139
|
),
|
|
138
|
-
arcs:
|
|
139
|
-
detail?.arcs,
|
|
140
|
-
'bottom'
|
|
141
|
-
)
|
|
140
|
+
arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'bottom')
|
|
142
141
|
},
|
|
143
142
|
Math.abs(Number(bottomZ || 0)),
|
|
144
143
|
normalizeBoardPoint,
|
|
145
144
|
true,
|
|
146
|
-
material
|
|
145
|
+
material,
|
|
146
|
+
PcbScene3dCopperOcclusionClipper.normalizeCutouts(
|
|
147
|
+
options?.occlusionCutouts?.bottom,
|
|
148
|
+
normalizeBoardPoint,
|
|
149
|
+
true
|
|
150
|
+
)
|
|
147
151
|
)
|
|
148
152
|
|
|
149
153
|
if (topGroup.children.length) {
|
|
@@ -238,6 +242,7 @@ export class PcbScene3dCopperFactory {
|
|
|
238
242
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
239
243
|
* @param {boolean} mirrorY
|
|
240
244
|
* @param {any} material Shared covered-trace material.
|
|
245
|
+
* @param {{ x: number, y: number }[][]} occlusionCutouts Silkscreen ink polygons covering this copper.
|
|
241
246
|
* @returns {any}
|
|
242
247
|
*/
|
|
243
248
|
static #buildMaskCoveredSideGroup(
|
|
@@ -246,7 +251,8 @@ export class PcbScene3dCopperFactory {
|
|
|
246
251
|
z,
|
|
247
252
|
normalizeBoardPoint,
|
|
248
253
|
mirrorY,
|
|
249
|
-
material
|
|
254
|
+
material,
|
|
255
|
+
occlusionCutouts
|
|
250
256
|
) {
|
|
251
257
|
const group = new THREE.Group()
|
|
252
258
|
const trackMesh = PcbScene3dCopperFactory.#buildTrackMesh(
|
|
@@ -255,7 +261,8 @@ export class PcbScene3dCopperFactory {
|
|
|
255
261
|
z,
|
|
256
262
|
normalizeBoardPoint,
|
|
257
263
|
mirrorY,
|
|
258
|
-
material
|
|
264
|
+
material,
|
|
265
|
+
occlusionCutouts
|
|
259
266
|
)
|
|
260
267
|
const arcMesh = PcbScene3dCopperFactory.#buildArcMesh(
|
|
261
268
|
THREE,
|
|
@@ -263,7 +270,8 @@ export class PcbScene3dCopperFactory {
|
|
|
263
270
|
z,
|
|
264
271
|
normalizeBoardPoint,
|
|
265
272
|
mirrorY,
|
|
266
|
-
material
|
|
273
|
+
material,
|
|
274
|
+
occlusionCutouts
|
|
267
275
|
)
|
|
268
276
|
|
|
269
277
|
if (trackMesh) {
|
|
@@ -306,6 +314,7 @@ export class PcbScene3dCopperFactory {
|
|
|
306
314
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
307
315
|
* @param {boolean} mirrorY
|
|
308
316
|
* @param {any | null} [material] Optional material override.
|
|
317
|
+
* @param {{ x: number, y: number }[][]} [cutouts] Optional geometry cutouts.
|
|
309
318
|
* @returns {any | null}
|
|
310
319
|
*/
|
|
311
320
|
static #buildTrackMesh(
|
|
@@ -314,7 +323,8 @@ export class PcbScene3dCopperFactory {
|
|
|
314
323
|
z,
|
|
315
324
|
normalizeBoardPoint,
|
|
316
325
|
mirrorY,
|
|
317
|
-
material = null
|
|
326
|
+
material = null,
|
|
327
|
+
cutouts = []
|
|
318
328
|
) {
|
|
319
329
|
const positions = []
|
|
320
330
|
|
|
@@ -344,7 +354,8 @@ export class PcbScene3dCopperFactory {
|
|
|
344
354
|
return PcbScene3dCopperFactory.#buildStrokeMesh(
|
|
345
355
|
THREE,
|
|
346
356
|
positions,
|
|
347
|
-
material
|
|
357
|
+
material,
|
|
358
|
+
cutouts
|
|
348
359
|
)
|
|
349
360
|
}
|
|
350
361
|
|
|
@@ -356,6 +367,7 @@ export class PcbScene3dCopperFactory {
|
|
|
356
367
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
357
368
|
* @param {boolean} mirrorY
|
|
358
369
|
* @param {any | null} [material] Optional material override.
|
|
370
|
+
* @param {{ x: number, y: number }[][]} [cutouts] Optional geometry cutouts.
|
|
359
371
|
* @returns {any | null}
|
|
360
372
|
*/
|
|
361
373
|
static #buildArcMesh(
|
|
@@ -364,7 +376,8 @@ export class PcbScene3dCopperFactory {
|
|
|
364
376
|
z,
|
|
365
377
|
normalizeBoardPoint,
|
|
366
378
|
mirrorY,
|
|
367
|
-
material = null
|
|
379
|
+
material = null,
|
|
380
|
+
cutouts = []
|
|
368
381
|
) {
|
|
369
382
|
const positions = []
|
|
370
383
|
|
|
@@ -387,7 +400,8 @@ export class PcbScene3dCopperFactory {
|
|
|
387
400
|
return PcbScene3dCopperFactory.#buildStrokeMesh(
|
|
388
401
|
THREE,
|
|
389
402
|
positions,
|
|
390
|
-
material
|
|
403
|
+
material,
|
|
404
|
+
cutouts
|
|
391
405
|
)
|
|
392
406
|
}
|
|
393
407
|
|
|
@@ -396,9 +410,10 @@ export class PcbScene3dCopperFactory {
|
|
|
396
410
|
* @param {any} THREE
|
|
397
411
|
* @param {number[]} positions
|
|
398
412
|
* @param {any | null} [material] Optional material override.
|
|
413
|
+
* @param {{ x: number, y: number }[][]} [cutouts] Optional geometry cutouts.
|
|
399
414
|
* @returns {any | null}
|
|
400
415
|
*/
|
|
401
|
-
static #buildStrokeMesh(THREE, positions, material = null) {
|
|
416
|
+
static #buildStrokeMesh(THREE, positions, material = null, cutouts = []) {
|
|
402
417
|
if (!positions.length) {
|
|
403
418
|
return null
|
|
404
419
|
}
|
|
@@ -408,10 +423,18 @@ export class PcbScene3dCopperFactory {
|
|
|
408
423
|
'position',
|
|
409
424
|
new THREE.Float32BufferAttribute(positions, 3)
|
|
410
425
|
)
|
|
411
|
-
|
|
426
|
+
const clippedGeometry = PcbScene3dCopperOcclusionClipper.filter(
|
|
427
|
+
THREE,
|
|
428
|
+
geometry,
|
|
429
|
+
cutouts
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
if (!clippedGeometry) {
|
|
433
|
+
return null
|
|
434
|
+
}
|
|
412
435
|
|
|
413
436
|
return new THREE.Mesh(
|
|
414
|
-
|
|
437
|
+
clippedGeometry,
|
|
415
438
|
material || PcbScene3dCopperFactory.#buildMaterial(THREE)
|
|
416
439
|
)
|
|
417
440
|
}
|
|
@@ -433,47 +456,6 @@ export class PcbScene3dCopperFactory {
|
|
|
433
456
|
})
|
|
434
457
|
}
|
|
435
458
|
|
|
436
|
-
/**
|
|
437
|
-
* Filters one track list to one outer copper face.
|
|
438
|
-
* @param {any[] | undefined} tracks
|
|
439
|
-
* @param {'top' | 'bottom'} side
|
|
440
|
-
* @returns {any[]}
|
|
441
|
-
*/
|
|
442
|
-
static #filterTracks(tracks, side) {
|
|
443
|
-
return (tracks || []).filter((track) =>
|
|
444
|
-
PcbScene3dCopperFactory.#matchesCopperLayer(track, side)
|
|
445
|
-
)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Filters one arc list to one outer copper face.
|
|
450
|
-
* @param {any[] | undefined} arcs
|
|
451
|
-
* @param {'top' | 'bottom'} side
|
|
452
|
-
* @returns {any[]}
|
|
453
|
-
*/
|
|
454
|
-
static #filterArcs(arcs, side) {
|
|
455
|
-
return (arcs || []).filter((arc) =>
|
|
456
|
-
PcbScene3dCopperFactory.#matchesCopperLayer(arc, side)
|
|
457
|
-
)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Returns true when one primitive belongs to the requested outer copper
|
|
462
|
-
* face.
|
|
463
|
-
* @param {{ layerId?: number, layerCode?: number }} primitive
|
|
464
|
-
* @param {'top' | 'bottom'} side
|
|
465
|
-
* @returns {boolean}
|
|
466
|
-
*/
|
|
467
|
-
static #matchesCopperLayer(primitive, side) {
|
|
468
|
-
const layerId = Number(
|
|
469
|
-
primitive?.layerId ?? primitive?.layerCode ?? NaN
|
|
470
|
-
)
|
|
471
|
-
|
|
472
|
-
return side === 'bottom'
|
|
473
|
-
? layerId === PcbScene3dCopperFactory.#BOTTOM_COPPER_LAYER_ID
|
|
474
|
-
: layerId === PcbScene3dCopperFactory.#TOP_COPPER_LAYER_ID
|
|
475
|
-
}
|
|
476
|
-
|
|
477
459
|
/**
|
|
478
460
|
* Appends one widened track quad as two triangles.
|
|
479
461
|
* @param {number[]} positions
|