pcb-scene3d-viewer 1.2.2 → 1.3.1
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/README.md +14 -0
- package/docs/api.md +18 -0
- package/docs/circuitjson.md +6 -4
- package/docs/model-format.md +6 -0
- package/docs/release-notes-v1.3.0.md +38 -0
- package/docs/release-notes-v1.3.1.md +23 -0
- package/package.json +4 -2
- package/src/CircuitJsonCadModelAssetResolver.mjs +24 -10
- package/src/PcbAssemblyGltfModelMeshParser.mjs +3 -1
- package/src/PcbAssemblyModelMeshLoader.mjs +1 -1
- package/src/PcbAssemblyTextModelMeshParser.mjs +3 -1
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -11
- package/src/PcbScene3dBoardMaterialPalette.mjs +12 -0
- package/src/PcbScene3dCircuitJsonAdapter.mjs +33 -85
- package/src/PcbScene3dCircuitJsonCopperTextBuilder.mjs +385 -0
- package/src/PcbScene3dCircuitJsonDocumentationArtworkBuilder.mjs +123 -22
- package/src/PcbScene3dCircuitJsonGeometry.mjs +12 -0
- package/src/PcbScene3dCircuitJsonPadCorner.mjs +72 -0
- package/src/PcbScene3dCircuitJsonSilkscreenBuilder.mjs +140 -25
- package/src/PcbScene3dCircuitJsonSilkscreenDetailBuilder.mjs +21 -0
- package/src/PcbScene3dCircuitJsonSourceLayer.mjs +124 -0
- package/src/PcbScene3dCircuitJsonTraceRouteBuilder.mjs +6 -7
- package/src/PcbScene3dCopperDetailFilter.mjs +44 -5
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +1 -1
- package/src/PcbScene3dCopperFactory.mjs +23 -4
- package/src/PcbScene3dCopperTextFactory.mjs +105 -20
- package/src/PcbScene3dExternalModels.mjs +26 -0
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +16 -1
- package/src/PcbScene3dModelContent.mjs +32 -1
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +2 -4
- package/src/PcbScene3dSilkscreenCopperCutoutBuilder.mjs +588 -0
- package/src/PcbScene3dStepLoader.mjs +2 -1
- package/src/PcbScene3dStrokeCutoutBuilder.mjs +98 -0
- package/src/PcbScene3dViaFactory.mjs +193 -8
- package/src/PcbScene3dViaLayerSpan.mjs +126 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
|
|
2
|
+
import { PcbScene3dViaLayerSpan } from './PcbScene3dViaLayerSpan.mjs'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Builds annular via barrels for the interactive 3D PCB scene.
|
|
@@ -7,14 +8,16 @@ export class PcbScene3dViaFactory {
|
|
|
7
8
|
static #PAD_BARREL_OUTER_RADIUS_SCALE = 0.98
|
|
8
9
|
static #PAD_BARREL_MIN_WALL_MIL = 1.2
|
|
9
10
|
static #PAD_BARREL_WALL_FRACTION = 0.09
|
|
11
|
+
static #SURFACE_COPPER_DEPTH_MIL = 2
|
|
12
|
+
static #SURFACE_MASK_Z_OFFSET_MIL = 1.3
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Builds the via mesh group for one scene.
|
|
13
16
|
* @param {any} THREE
|
|
14
|
-
* @param {{ diameter?: number, holeDiameter?: number, x?: number, y?: number, barrelOnly?: boolean }[]} vias
|
|
17
|
+
* @param {{ diameter?: number, holeDiameter?: number, x?: number, y?: number, barrelOnly?: boolean, layers?: unknown[], fromLayer?: unknown, toLayer?: unknown, from_layer?: unknown, to_layer?: unknown }[]} vias
|
|
15
18
|
* @param {number} thicknessMil
|
|
16
19
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
17
|
-
* @param {{ material?: any }} [options]
|
|
20
|
+
* @param {{ material?: any, surfaceMaterial?: any }} [options]
|
|
18
21
|
* @returns {any}
|
|
19
22
|
*/
|
|
20
23
|
static buildGroup(
|
|
@@ -25,26 +28,48 @@ export class PcbScene3dViaFactory {
|
|
|
25
28
|
options = {}
|
|
26
29
|
) {
|
|
27
30
|
const group = new THREE.Group()
|
|
28
|
-
const
|
|
31
|
+
const copperMaterial = PcbScene3dViaFactory.#resolveMaterial(
|
|
32
|
+
THREE,
|
|
33
|
+
options
|
|
34
|
+
)
|
|
29
35
|
const geometryCache = new Map()
|
|
36
|
+
const surfaceGeometryCache = new Map()
|
|
30
37
|
|
|
31
38
|
;(vias || []).forEach((via) => {
|
|
39
|
+
const renderMode = PcbScene3dViaLayerSpan.renderMode(via)
|
|
40
|
+
if (!renderMode) return
|
|
41
|
+
|
|
32
42
|
const geometry = PcbScene3dViaFactory.#resolveGeometry(
|
|
33
43
|
THREE,
|
|
34
44
|
geometryCache,
|
|
35
45
|
via,
|
|
36
|
-
thicknessMil
|
|
46
|
+
thicknessMil,
|
|
47
|
+
renderMode
|
|
37
48
|
)
|
|
38
|
-
const mesh = new THREE.Mesh(geometry,
|
|
49
|
+
const mesh = new THREE.Mesh(geometry, copperMaterial)
|
|
39
50
|
const point = normalizeBoardPoint(
|
|
40
51
|
Number(via?.x || 0),
|
|
41
52
|
Number(via?.y || 0)
|
|
42
53
|
)
|
|
43
|
-
mesh.position.set(
|
|
54
|
+
mesh.position.set(
|
|
55
|
+
point.x,
|
|
56
|
+
point.y,
|
|
57
|
+
PcbScene3dViaFactory.#centerZ(renderMode, thicknessMil)
|
|
58
|
+
)
|
|
44
59
|
if (geometry.type === 'CylinderGeometry') {
|
|
45
60
|
mesh.rotation.x = Math.PI / 2
|
|
46
61
|
}
|
|
47
62
|
group.add(mesh)
|
|
63
|
+
PcbScene3dViaFactory.#appendMaskSurfaceMeshes(
|
|
64
|
+
THREE,
|
|
65
|
+
group,
|
|
66
|
+
surfaceGeometryCache,
|
|
67
|
+
via,
|
|
68
|
+
renderMode,
|
|
69
|
+
thicknessMil,
|
|
70
|
+
point,
|
|
71
|
+
options?.surfaceMaterial
|
|
72
|
+
)
|
|
48
73
|
})
|
|
49
74
|
|
|
50
75
|
return group
|
|
@@ -68,18 +93,152 @@ export class PcbScene3dViaFactory {
|
|
|
68
93
|
)
|
|
69
94
|
}
|
|
70
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Adds side-specific solder-mask rings above the copper via surface.
|
|
98
|
+
* @param {any} THREE Three.js namespace.
|
|
99
|
+
* @param {any} group Output group.
|
|
100
|
+
* @param {Map<string, any>} geometryCache Surface geometry cache.
|
|
101
|
+
* @param {object} via Via primitive.
|
|
102
|
+
* @param {'through' | 'top' | 'bottom'} renderMode Via geometry mode.
|
|
103
|
+
* @param {number} thicknessMil Board thickness in mil.
|
|
104
|
+
* @param {{ x: number, y: number }} point Normalized board point.
|
|
105
|
+
* @param {any | undefined} material Solder-mask material.
|
|
106
|
+
* @returns {void}
|
|
107
|
+
*/
|
|
108
|
+
static #appendMaskSurfaceMeshes(
|
|
109
|
+
THREE,
|
|
110
|
+
group,
|
|
111
|
+
geometryCache,
|
|
112
|
+
via,
|
|
113
|
+
renderMode,
|
|
114
|
+
thicknessMil,
|
|
115
|
+
point,
|
|
116
|
+
material
|
|
117
|
+
) {
|
|
118
|
+
if (!material) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const geometry = PcbScene3dViaFactory.#resolveSurfaceGeometry(
|
|
123
|
+
THREE,
|
|
124
|
+
geometryCache,
|
|
125
|
+
via
|
|
126
|
+
)
|
|
127
|
+
if (!geometry) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const side of ['top', 'bottom']) {
|
|
132
|
+
if (
|
|
133
|
+
!PcbScene3dViaFactory.#renderModeTouchesSide(
|
|
134
|
+
renderMode,
|
|
135
|
+
side
|
|
136
|
+
) ||
|
|
137
|
+
!PcbScene3dViaFactory.#isSideTented(via, side)
|
|
138
|
+
) {
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
143
|
+
mesh.position.set(
|
|
144
|
+
point.x,
|
|
145
|
+
point.y,
|
|
146
|
+
PcbScene3dViaFactory.#surfaceZ(side, thicknessMil)
|
|
147
|
+
)
|
|
148
|
+
group.add(mesh)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolves one reusable annular surface geometry.
|
|
154
|
+
* @param {any} THREE Three.js namespace.
|
|
155
|
+
* @param {Map<string, any>} geometryCache Surface geometry cache.
|
|
156
|
+
* @param {object} via Via primitive.
|
|
157
|
+
* @returns {any | null}
|
|
158
|
+
*/
|
|
159
|
+
static #resolveSurfaceGeometry(THREE, geometryCache, via) {
|
|
160
|
+
const diameter = Number(via?.diameter || 0)
|
|
161
|
+
const holeDiameter = Number(via?.holeDiameter || 0)
|
|
162
|
+
if (diameter <= 0 || holeDiameter < 0 || diameter <= holeDiameter) {
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const key = `${diameter.toFixed(4)}:${holeDiameter.toFixed(4)}`
|
|
167
|
+
const cached = geometryCache.get(key)
|
|
168
|
+
if (cached) {
|
|
169
|
+
return cached
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const shape = PcbScene3dViaFactory.#buildCircleShape(
|
|
173
|
+
THREE,
|
|
174
|
+
diameter / 2
|
|
175
|
+
)
|
|
176
|
+
if (holeDiameter > 0) {
|
|
177
|
+
shape.holes.push(
|
|
178
|
+
PcbScene3dViaFactory.#buildCirclePath(THREE, holeDiameter / 2)
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
const geometry = new THREE.ShapeGeometry(shape, 24)
|
|
182
|
+
geometryCache.set(key, geometry)
|
|
183
|
+
return geometry
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Checks whether one rendered via span reaches a board side.
|
|
188
|
+
* @param {'through' | 'top' | 'bottom'} renderMode Via geometry mode.
|
|
189
|
+
* @param {'top' | 'bottom'} side Board side.
|
|
190
|
+
* @returns {boolean}
|
|
191
|
+
*/
|
|
192
|
+
static #renderModeTouchesSide(renderMode, side) {
|
|
193
|
+
return renderMode === 'through' || renderMode === side
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Checks whether one via surface is tented on a board side.
|
|
198
|
+
* @param {object} via Via primitive.
|
|
199
|
+
* @param {'top' | 'bottom'} side Board side.
|
|
200
|
+
* @returns {boolean}
|
|
201
|
+
*/
|
|
202
|
+
static #isSideTented(via, side) {
|
|
203
|
+
const fieldName = side === 'bottom' ? 'isTentingBottom' : 'isTentingTop'
|
|
204
|
+
return via?.[fieldName] !== false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resolves the solder-mask surface Z above the exposed copper stack.
|
|
209
|
+
* @param {'top' | 'bottom'} side Board side.
|
|
210
|
+
* @param {number} thicknessMil Board thickness in mil.
|
|
211
|
+
* @returns {number}
|
|
212
|
+
*/
|
|
213
|
+
static #surfaceZ(side, thicknessMil) {
|
|
214
|
+
const distance =
|
|
215
|
+
Math.max(Number(thicknessMil) || 0, 0) / 2 +
|
|
216
|
+
PcbScene3dViaFactory.#SURFACE_MASK_Z_OFFSET_MIL
|
|
217
|
+
return side === 'bottom' ? -distance : distance
|
|
218
|
+
}
|
|
219
|
+
|
|
71
220
|
/**
|
|
72
221
|
* Resolves one reusable via geometry from the via drill spec.
|
|
73
222
|
* @param {any} THREE
|
|
74
223
|
* @param {Map<string, any>} geometryCache
|
|
75
224
|
* @param {{ diameter?: number, holeDiameter?: number, barrelOnly?: boolean }} via
|
|
76
225
|
* @param {number} thicknessMil
|
|
226
|
+
* @param {'through' | 'top' | 'bottom'} renderMode Via geometry mode.
|
|
77
227
|
* @returns {any}
|
|
78
228
|
*/
|
|
79
|
-
static #resolveGeometry(
|
|
229
|
+
static #resolveGeometry(
|
|
230
|
+
THREE,
|
|
231
|
+
geometryCache,
|
|
232
|
+
via,
|
|
233
|
+
thicknessMil,
|
|
234
|
+
renderMode
|
|
235
|
+
) {
|
|
80
236
|
const outerRadius = Math.max(Number(via?.diameter || 0) / 2, 1.2)
|
|
81
237
|
const holeDiameter = Math.max(Number(via?.holeDiameter || 0), 0)
|
|
82
|
-
const depth =
|
|
238
|
+
const depth = PcbScene3dViaFactory.#geometryDepth(
|
|
239
|
+
renderMode,
|
|
240
|
+
thicknessMil
|
|
241
|
+
)
|
|
83
242
|
const isBarrelOnly = Boolean(via?.barrelOnly)
|
|
84
243
|
const cacheKey = [
|
|
85
244
|
isBarrelOnly ? 'barrel' : 'annulus',
|
|
@@ -131,6 +290,32 @@ export class PcbScene3dViaFactory {
|
|
|
131
290
|
return geometry
|
|
132
291
|
}
|
|
133
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Resolves copper geometry depth without extending blind vias through-board.
|
|
295
|
+
* @param {'through' | 'top' | 'bottom'} renderMode Via geometry mode.
|
|
296
|
+
* @param {number} thicknessMil Board thickness in mil.
|
|
297
|
+
* @returns {number}
|
|
298
|
+
*/
|
|
299
|
+
static #geometryDepth(renderMode, thicknessMil) {
|
|
300
|
+
if (renderMode !== 'through') {
|
|
301
|
+
return PcbScene3dViaFactory.#SURFACE_COPPER_DEPTH_MIL
|
|
302
|
+
}
|
|
303
|
+
return Math.max(Number(thicknessMil) || 0, 0) + 2
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Resolves the world-space Z center for one via geometry mode.
|
|
308
|
+
* @param {'through' | 'top' | 'bottom'} renderMode Via geometry mode.
|
|
309
|
+
* @param {number} thicknessMil Board thickness in mil.
|
|
310
|
+
* @returns {number}
|
|
311
|
+
*/
|
|
312
|
+
static #centerZ(renderMode, thicknessMil) {
|
|
313
|
+
const halfThickness = Math.max(Number(thicknessMil) || 0, 0) / 2
|
|
314
|
+
if (renderMode === 'top') return halfThickness
|
|
315
|
+
if (renderMode === 'bottom') return -halfThickness
|
|
316
|
+
return 0
|
|
317
|
+
}
|
|
318
|
+
|
|
134
319
|
/**
|
|
135
320
|
* Builds a visible copper sleeve for through-hole pads.
|
|
136
321
|
* @param {any} THREE
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { PcbScene3dCircuitJsonLayer } from './PcbScene3dCircuitJsonLayer.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves authored via layer spans for surface-aware scene geometry.
|
|
5
|
+
*/
|
|
6
|
+
export class PcbScene3dViaLayerSpan {
|
|
7
|
+
/**
|
|
8
|
+
* Preserves an authored span in normalized scene-detail fields.
|
|
9
|
+
* Explicit layer lists take precedence over legacy default endpoints.
|
|
10
|
+
* @param {object} via CircuitJSON via or normalized scene via.
|
|
11
|
+
* @returns {{ layers: unknown[], fromLayer: unknown | null, toLayer: unknown | null }}
|
|
12
|
+
*/
|
|
13
|
+
static fields(via) {
|
|
14
|
+
const span = PcbScene3dViaLayerSpan.#resolve(via)
|
|
15
|
+
return {
|
|
16
|
+
layers: span.layers,
|
|
17
|
+
fromLayer: span.fromLayer,
|
|
18
|
+
toLayer: span.toLayer
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the outer board faces reached by one via.
|
|
24
|
+
* @param {object} via CircuitJSON via or normalized scene via.
|
|
25
|
+
* @returns {('top' | 'bottom')[]}
|
|
26
|
+
*/
|
|
27
|
+
static surfaceSides(via) {
|
|
28
|
+
const sides = PcbScene3dViaLayerSpan.#resolve(via)
|
|
29
|
+
.layers.map((layer) =>
|
|
30
|
+
PcbScene3dCircuitJsonLayer.surfaceSide(layer)
|
|
31
|
+
)
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
return [...new Set(sides)]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns true when one via reaches the requested board face.
|
|
38
|
+
* Vias without authored span metadata retain legacy through-board behavior.
|
|
39
|
+
* @param {object} via CircuitJSON via or normalized scene via.
|
|
40
|
+
* @param {'top' | 'bottom'} side Board face.
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
static reachesSide(via, side) {
|
|
44
|
+
const span = PcbScene3dViaLayerSpan.#resolve(via)
|
|
45
|
+
if (!span.hasAuthoredSpan) return true
|
|
46
|
+
|
|
47
|
+
return span.layers.some(
|
|
48
|
+
(layer) => PcbScene3dCircuitJsonLayer.surfaceSide(layer) === side
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolves the physical surface geometry mode for one via.
|
|
54
|
+
* Inner-only vias have no outer-surface geometry and return null.
|
|
55
|
+
* @param {object} via CircuitJSON via or normalized scene via.
|
|
56
|
+
* @returns {'through' | 'top' | 'bottom' | null}
|
|
57
|
+
*/
|
|
58
|
+
static renderMode(via) {
|
|
59
|
+
const span = PcbScene3dViaLayerSpan.#resolve(via)
|
|
60
|
+
if (!span.hasAuthoredSpan) return 'through'
|
|
61
|
+
|
|
62
|
+
const sides = span.layers
|
|
63
|
+
.map((layer) => PcbScene3dCircuitJsonLayer.surfaceSide(layer))
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
const reachesTop = sides.includes('top')
|
|
66
|
+
const reachesBottom = sides.includes('bottom')
|
|
67
|
+
if (reachesTop && reachesBottom) return 'through'
|
|
68
|
+
if (reachesTop) return 'top'
|
|
69
|
+
if (reachesBottom) return 'bottom'
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolves one via span while retaining whether it was explicitly authored.
|
|
75
|
+
* @param {object} via CircuitJSON via or normalized scene via.
|
|
76
|
+
* @returns {{ layers: unknown[], fromLayer: unknown | null, toLayer: unknown | null, hasAuthoredSpan: boolean }}
|
|
77
|
+
*/
|
|
78
|
+
static #resolve(via) {
|
|
79
|
+
const explicitLayers = PcbScene3dViaLayerSpan.#explicitLayers(
|
|
80
|
+
via?.layers
|
|
81
|
+
)
|
|
82
|
+
const fromLayer =
|
|
83
|
+
explicitLayers[0] ??
|
|
84
|
+
via?.fromLayer ??
|
|
85
|
+
via?.from_layer ??
|
|
86
|
+
via?.layer ??
|
|
87
|
+
null
|
|
88
|
+
const toLayer =
|
|
89
|
+
explicitLayers[explicitLayers.length - 1] ??
|
|
90
|
+
via?.toLayer ??
|
|
91
|
+
via?.to_layer ??
|
|
92
|
+
via?.layer ??
|
|
93
|
+
null
|
|
94
|
+
const layers = explicitLayers.length
|
|
95
|
+
? [...explicitLayers]
|
|
96
|
+
: [fromLayer, toLayer].filter(
|
|
97
|
+
(layer, index, values) =>
|
|
98
|
+
layer !== null && values.indexOf(layer) === index
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
layers,
|
|
103
|
+
fromLayer,
|
|
104
|
+
toLayer,
|
|
105
|
+
hasAuthoredSpan: layers.length > 0
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Normalizes an explicit via layer list without interpreting layer names.
|
|
111
|
+
* @param {unknown} layers CircuitJSON layer list.
|
|
112
|
+
* @returns {unknown[]}
|
|
113
|
+
*/
|
|
114
|
+
static #explicitLayers(layers) {
|
|
115
|
+
const values = Array.isArray(layers)
|
|
116
|
+
? layers
|
|
117
|
+
: typeof layers === 'string'
|
|
118
|
+
? layers.split(',')
|
|
119
|
+
: []
|
|
120
|
+
return values
|
|
121
|
+
.map((layer) => (typeof layer === 'string' ? layer.trim() : layer))
|
|
122
|
+
.filter(
|
|
123
|
+
(layer) => layer !== undefined && layer !== null && layer !== ''
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|