pcb-scene3d-viewer 1.1.2 → 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/PcbScene3dArcUtils.mjs +18 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +3 -0
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +2 -2
- package/src/PcbScene3dCameraRig.mjs +16 -1
- package/src/PcbScene3dCopperDetailFilter.mjs +73 -1
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +482 -0
- package/src/PcbScene3dCopperFactory.mjs +499 -85
- package/src/PcbScene3dCopperLayerFilter.mjs +48 -0
- package/src/PcbScene3dCopperOcclusionClipper.mjs +86 -0
- package/src/PcbScene3dCopperTextFactory.mjs +26 -6
- package/src/PcbScene3dCutoutGeometryFilter.mjs +25 -28
- package/src/PcbScene3dDrillCutoutFilter.mjs +123 -0
- package/src/PcbScene3dGeometryFaceRefiner.mjs +218 -0
- package/src/PcbScene3dGeometryZCompressor.mjs +48 -0
- package/src/PcbScene3dMaskCoveredCopperMaterial.mjs +72 -0
- package/src/PcbScene3dMaterialFinish.mjs +49 -0
- package/src/PcbScene3dPadFactory.mjs +160 -3
- package/src/PcbScene3dPadSurfaceStack.mjs +155 -0
- package/src/PcbScene3dPolygonOverlap.mjs +283 -0
- package/src/PcbScene3dRuntime.mjs +11 -64
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +8 -3
- package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +212 -0
- package/src/PcbScene3dShapeHoleMerger.mjs +476 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +193 -126
- package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +241 -0
- package/src/PcbScene3dStrokeGeometryBuilder.mjs +7 -6
- package/src/PcbScene3dTerminalCutoutClassifier.mjs +92 -0
- package/src/PcbScene3dTrueTypeTextFactory.mjs +16 -4
package/package.json
CHANGED
|
@@ -4,6 +4,24 @@
|
|
|
4
4
|
export class PcbScene3dArcUtils {
|
|
5
5
|
static #FULL_CIRCLE_EPSILON = 0.001
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Resolves one arc record's sweep, preserving explicit long-arc sweeps.
|
|
9
|
+
* @param {{ startAngle?: number, endAngle?: number, sweepAngle?: number }} arc
|
|
10
|
+
* Arc record.
|
|
11
|
+
* @returns {number}
|
|
12
|
+
*/
|
|
13
|
+
static resolveArcSweepDelta(arc) {
|
|
14
|
+
const sweepAngle = Number(arc?.sweepAngle)
|
|
15
|
+
if (Number.isFinite(sweepAngle)) {
|
|
16
|
+
return sweepAngle
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return PcbScene3dArcUtils.resolveSweepDelta(
|
|
20
|
+
Number(arc?.startAngle || 0),
|
|
21
|
+
Number(arc?.endAngle || 0)
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
/**
|
|
8
26
|
* Normalizes one PCB arc delta to the intended short wrapped sweep.
|
|
9
27
|
* @param {number} startAngle Start angle in degrees.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PcbScene3dBoardMaterialPalette } from './PcbScene3dBoardMaterialPalette.mjs'
|
|
2
|
+
import { PcbScene3dMaterialFinish } from './PcbScene3dMaterialFinish.mjs'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Applies display-only material adjustments to full-board assembly models.
|
|
@@ -155,6 +156,7 @@ export class PcbScene3dBoardAssemblyPresentation {
|
|
|
155
156
|
object?.material,
|
|
156
157
|
surfaceColor
|
|
157
158
|
)
|
|
159
|
+
PcbScene3dMaterialFinish.applySemiMatteSolderMask(object?.material)
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
/**
|
|
@@ -192,6 +194,7 @@ export class PcbScene3dBoardAssemblyPresentation {
|
|
|
192
194
|
surfaceMaterial,
|
|
193
195
|
surfaceColor
|
|
194
196
|
)
|
|
197
|
+
PcbScene3dMaterialFinish.applySemiMatteSolderMask(surfaceMaterial)
|
|
195
198
|
PcbScene3dBoardAssemblyPresentation.#applyMaterialColor(
|
|
196
199
|
edgeMaterial,
|
|
197
200
|
edgeColor
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PcbScene3dBoardMaterialPalette } from './PcbScene3dBoardMaterialPalette.mjs'
|
|
2
2
|
import { PcbScene3dCutoutGeometryFilter } from './PcbScene3dCutoutGeometryFilter.mjs'
|
|
3
3
|
import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
|
|
4
|
+
import { PcbScene3dMaterialFinish } from './PcbScene3dMaterialFinish.mjs'
|
|
4
5
|
import { PcbScene3dOutlineBuilder } from './PcbScene3dOutlineBuilder.mjs'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -602,8 +603,7 @@ export class PcbScene3dBoardSolderMaskFactory {
|
|
|
602
603
|
color: PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
|
|
603
604
|
hasBoardAssemblyModel: true
|
|
604
605
|
}),
|
|
605
|
-
|
|
606
|
-
metalness: 0.08,
|
|
606
|
+
...PcbScene3dMaterialFinish.semiMatteSolderMaskProperties(),
|
|
607
607
|
polygonOffset: true,
|
|
608
608
|
polygonOffsetFactor: -1,
|
|
609
609
|
polygonOffsetUnits: -1,
|
|
@@ -48,7 +48,7 @@ export class PcbScene3dCameraRig {
|
|
|
48
48
|
return {
|
|
49
49
|
radius,
|
|
50
50
|
target,
|
|
51
|
-
up:
|
|
51
|
+
up: PcbScene3dCameraRig.#bottomUpVector(sceneDescription),
|
|
52
52
|
position: {
|
|
53
53
|
x: target.x,
|
|
54
54
|
y: target.y,
|
|
@@ -165,4 +165,19 @@ export class PcbScene3dCameraRig {
|
|
|
165
165
|
z: Number(controls?.target?.z || 0)
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Resolves screen-up for bottom views from the scene coordinate contract.
|
|
171
|
+
* @param {{ coordinateSystem?: string }} sceneDescription Scene metadata.
|
|
172
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
173
|
+
*/
|
|
174
|
+
static #bottomUpVector(sceneDescription) {
|
|
175
|
+
const coordinateSystem = String(
|
|
176
|
+
sceneDescription?.coordinateSystem || ''
|
|
177
|
+
).toLowerCase()
|
|
178
|
+
|
|
179
|
+
return coordinateSystem === 'kicad-3d-y-up'
|
|
180
|
+
? { x: 0, y: 1, z: 0 }
|
|
181
|
+
: { x: 0, y: -1, z: 0 }
|
|
182
|
+
}
|
|
168
183
|
}
|
|
@@ -39,6 +39,41 @@ export class PcbScene3dCopperDetailFilter {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Resolves trace-like copper that should be visible through solder mask.
|
|
44
|
+
* @param {object} sceneDescription 3D scene description.
|
|
45
|
+
* @returns {{ tracks: any[], arcs: any[], copperTexts: any[], vias: any[] }}
|
|
46
|
+
*/
|
|
47
|
+
static resolveCoveredByMask(sceneDescription) {
|
|
48
|
+
const detail = sceneDescription?.detail || {}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
!PcbScene3dCopperDetailFilter.#usesRealisticMasking(
|
|
52
|
+
sceneDescription
|
|
53
|
+
)
|
|
54
|
+
) {
|
|
55
|
+
return { tracks: [], arcs: [], copperTexts: [], vias: [] }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const defaultCovered =
|
|
59
|
+
PcbScene3dCopperDetailFilter.#usesDefaultCoveredCopper(
|
|
60
|
+
sceneDescription
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
tracks: PcbScene3dCopperDetailFilter.#filterMaskCoveredPrimitives(
|
|
65
|
+
detail.tracks,
|
|
66
|
+
defaultCovered
|
|
67
|
+
),
|
|
68
|
+
arcs: PcbScene3dCopperDetailFilter.#filterMaskCoveredPrimitives(
|
|
69
|
+
detail.arcs,
|
|
70
|
+
defaultCovered
|
|
71
|
+
),
|
|
72
|
+
copperTexts: [],
|
|
73
|
+
vias: []
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
/**
|
|
43
78
|
* Checks whether standalone via annuli should be rendered.
|
|
44
79
|
* @param {object} sceneDescription 3D scene description.
|
|
@@ -101,6 +136,24 @@ export class PcbScene3dCopperDetailFilter {
|
|
|
101
136
|
)
|
|
102
137
|
}
|
|
103
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Checks whether tracks are considered mask-covered without per-primitive
|
|
141
|
+
* mask metadata.
|
|
142
|
+
* @param {object} sceneDescription 3D scene description.
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
static #usesDefaultCoveredCopper(sceneDescription) {
|
|
146
|
+
const sourceFormat = String(sceneDescription?.sourceFormat || '')
|
|
147
|
+
.trim()
|
|
148
|
+
.toLowerCase()
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
sourceFormat === 'altium' ||
|
|
152
|
+
sourceFormat === 'kicad' ||
|
|
153
|
+
sceneDescription?.coordinateSystem === 'kicad-3d-y-up'
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
104
157
|
/**
|
|
105
158
|
* Checks whether parsed copper detail carries explicit solder-mask data.
|
|
106
159
|
* @param {object | undefined} detail Scene detail.
|
|
@@ -132,6 +185,25 @@ export class PcbScene3dCopperDetailFilter {
|
|
|
132
185
|
)
|
|
133
186
|
}
|
|
134
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Keeps trace-like copper primitives that are covered by solder mask.
|
|
190
|
+
* @param {any[] | undefined} primitives Copper primitive list.
|
|
191
|
+
* @param {boolean} defaultCovered Whether missing metadata means covered.
|
|
192
|
+
* @returns {any[]}
|
|
193
|
+
*/
|
|
194
|
+
static #filterMaskCoveredPrimitives(primitives, defaultCovered) {
|
|
195
|
+
return (primitives || []).filter((primitive) => {
|
|
196
|
+
if (PcbScene3dCopperDetailFilter.#hasMaskOpening(primitive)) {
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
defaultCovered ||
|
|
202
|
+
PcbScene3dCopperDetailFilter.#hasMaskMetadata(primitive)
|
|
203
|
+
)
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
135
207
|
/**
|
|
136
208
|
* Keeps vias only when they are explicitly not tented.
|
|
137
209
|
* @param {any[] | undefined} vias Via list.
|
|
@@ -243,7 +315,7 @@ export class PcbScene3dCopperDetailFilter {
|
|
|
243
315
|
* @returns {boolean}
|
|
244
316
|
*/
|
|
245
317
|
static #hasMaskOpening(primitive, maskMatcher = null) {
|
|
246
|
-
if (primitive?.hasSolderMask ===
|
|
318
|
+
if (primitive?.hasSolderMask === false) {
|
|
247
319
|
return true
|
|
248
320
|
}
|
|
249
321
|
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { PcbScene3dBoardMaterialPalette } from './PcbScene3dBoardMaterialPalette.mjs'
|
|
2
|
+
import { PcbScene3dCopperDetailFilter } from './PcbScene3dCopperDetailFilter.mjs'
|
|
3
|
+
import { PcbScene3dCopperFactory } from './PcbScene3dCopperFactory.mjs'
|
|
4
|
+
import { PcbScene3dPadFactory } from './PcbScene3dPadFactory.mjs'
|
|
5
|
+
import { PcbScene3dViaFactory } from './PcbScene3dViaFactory.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds deferred copper-detail groups for the 3D runtime.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbScene3dCopperDetailGroupBuilder {
|
|
11
|
+
static #CIRCLE_SEGMENTS = 64
|
|
12
|
+
static #ROUNDED_CORNER_SEGMENTS = 12
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds visible exposed and mask-covered copper detail.
|
|
16
|
+
* @param {any} THREE Three.js namespace.
|
|
17
|
+
* @param {object} sceneDescription Scene description.
|
|
18
|
+
* @param {number} topZ Top copper center Z.
|
|
19
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizePoint
|
|
20
|
+
* @returns {any}
|
|
21
|
+
*/
|
|
22
|
+
static build(THREE, sceneDescription, topZ, normalizePoint) {
|
|
23
|
+
const group = new THREE.Group()
|
|
24
|
+
const coveredGroup =
|
|
25
|
+
PcbScene3dCopperDetailGroupBuilder.#buildCoveredGroup(
|
|
26
|
+
THREE,
|
|
27
|
+
sceneDescription,
|
|
28
|
+
topZ,
|
|
29
|
+
normalizePoint
|
|
30
|
+
)
|
|
31
|
+
const exposedGroup =
|
|
32
|
+
PcbScene3dCopperDetailGroupBuilder.#buildExposedGroup(
|
|
33
|
+
THREE,
|
|
34
|
+
sceneDescription,
|
|
35
|
+
topZ,
|
|
36
|
+
normalizePoint
|
|
37
|
+
)
|
|
38
|
+
const viaGroup = PcbScene3dCopperDetailGroupBuilder.#buildViaGroup(
|
|
39
|
+
THREE,
|
|
40
|
+
sceneDescription,
|
|
41
|
+
normalizePoint
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
;[coveredGroup, exposedGroup, viaGroup]
|
|
45
|
+
.filter((child) => child.children.length)
|
|
46
|
+
.forEach((child) => group.add(child))
|
|
47
|
+
|
|
48
|
+
return group
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds traces covered by solder mask.
|
|
53
|
+
* @param {any} THREE Three.js namespace.
|
|
54
|
+
* @param {object} sceneDescription Scene description.
|
|
55
|
+
* @param {number} topZ Top copper center Z.
|
|
56
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizePoint
|
|
57
|
+
* @returns {any}
|
|
58
|
+
*/
|
|
59
|
+
static #buildCoveredGroup(THREE, sceneDescription, topZ, normalizePoint) {
|
|
60
|
+
return PcbScene3dCopperFactory.buildMaskCoveredGroup(
|
|
61
|
+
THREE,
|
|
62
|
+
PcbScene3dCopperDetailFilter.resolveCoveredByMask(sceneDescription),
|
|
63
|
+
topZ,
|
|
64
|
+
-topZ,
|
|
65
|
+
normalizePoint,
|
|
66
|
+
{
|
|
67
|
+
solderMaskColor:
|
|
68
|
+
PcbScene3dBoardMaterialPalette.resolveSurfaceColor(
|
|
69
|
+
sceneDescription?.board,
|
|
70
|
+
{
|
|
71
|
+
hasBoardAssemblyModel: Boolean(
|
|
72
|
+
sceneDescription?.boardAssemblyModel
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
),
|
|
76
|
+
occlusionCutouts:
|
|
77
|
+
PcbScene3dCopperDetailGroupBuilder.#resolveCoveredCopperOcclusions(
|
|
78
|
+
sceneDescription?.detail
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Builds exposed copper detail.
|
|
86
|
+
* @param {any} THREE Three.js namespace.
|
|
87
|
+
* @param {object} sceneDescription Scene description.
|
|
88
|
+
* @param {number} topZ Top copper center Z.
|
|
89
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizePoint
|
|
90
|
+
* @returns {any}
|
|
91
|
+
*/
|
|
92
|
+
static #buildExposedGroup(THREE, sceneDescription, topZ, normalizePoint) {
|
|
93
|
+
return PcbScene3dCopperFactory.buildGroup(
|
|
94
|
+
THREE,
|
|
95
|
+
PcbScene3dCopperDetailFilter.resolve(sceneDescription),
|
|
96
|
+
topZ,
|
|
97
|
+
-topZ,
|
|
98
|
+
normalizePoint,
|
|
99
|
+
{ coordinateSystem: sceneDescription?.coordinateSystem }
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Builds exposed via and through-hole barrel detail.
|
|
105
|
+
* @param {any} THREE Three.js namespace.
|
|
106
|
+
* @param {object} sceneDescription Scene description.
|
|
107
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizePoint
|
|
108
|
+
* @returns {any}
|
|
109
|
+
*/
|
|
110
|
+
static #buildViaGroup(THREE, sceneDescription, normalizePoint) {
|
|
111
|
+
if (
|
|
112
|
+
!PcbScene3dCopperDetailFilter.shouldRenderStandaloneVias(
|
|
113
|
+
sceneDescription
|
|
114
|
+
)
|
|
115
|
+
) {
|
|
116
|
+
return new THREE.Group()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return PcbScene3dViaFactory.buildGroup(
|
|
120
|
+
THREE,
|
|
121
|
+
PcbScene3dCopperDetailFilter.resolveStandaloneVias(
|
|
122
|
+
sceneDescription
|
|
123
|
+
),
|
|
124
|
+
sceneDescription?.board?.thicknessMil,
|
|
125
|
+
normalizePoint
|
|
126
|
+
)
|
|
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
|
+
}
|
|
482
|
+
}
|