pcb-scene3d-viewer 1.3.0 → 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
CHANGED
|
@@ -28,6 +28,12 @@ surface-aware blind/buried vias, collision-safe model assets, and a consistent
|
|
|
28
28
|
light FR-4 substrate edge. See the
|
|
29
29
|
[1.3.0 release notes](docs/release-notes-v1.3.0.md).
|
|
30
30
|
|
|
31
|
+
Version 1.3.1 renders side-specific tented Gerber vias without recoloring the
|
|
32
|
+
plated drill wall. Covered surfaces receive a solder-mask ring above the copper
|
|
33
|
+
annulus, mixed top/bottom states remain independent, and fully open Gerber vias
|
|
34
|
+
retain the exposed-copper path. See the
|
|
35
|
+
[1.3.1 release notes](docs/release-notes-v1.3.1.md).
|
|
36
|
+
|
|
31
37
|
## CircuitJSON 1.1 convergence
|
|
32
38
|
|
|
33
39
|
Version 1.2.2 accepts the common document and prepared-context shapes returned
|
|
@@ -136,6 +142,7 @@ const controller = new PcbScene3dController(viewportNode, document)
|
|
|
136
142
|
|
|
137
143
|
- [API](docs/api.md)
|
|
138
144
|
- [CircuitJSON usage](docs/circuitjson.md)
|
|
145
|
+
- [1.3.1 release notes](docs/release-notes-v1.3.1.md)
|
|
139
146
|
- [1.2.2 release notes](docs/release-notes-v1.2.2.md)
|
|
140
147
|
- [1.3.0 release notes](docs/release-notes-v1.3.0.md)
|
|
141
148
|
- [1.2.1 release notes](docs/release-notes-v1.2.1.md)
|
package/docs/model-format.md
CHANGED
|
@@ -220,3 +220,9 @@ Rounded SMT pads set `hasRoundedRect`, the side-specific
|
|
|
220
220
|
Route-derived vias use the same `detail.vias` shape as standalone vias, and
|
|
221
221
|
surface route segments use `layerId: 1` for top copper or `layerId: 32` for
|
|
222
222
|
bottom copper.
|
|
223
|
+
|
|
224
|
+
Via surfaces can set `isTentingTop` and `isTentingBottom` independently. A
|
|
225
|
+
truthy field adds a solder-mask ring on that board surface while the plated
|
|
226
|
+
through-hole barrel remains copper. A mixed via therefore renders one covered
|
|
227
|
+
annulus and one exposed annulus; a via with both fields explicitly false stays
|
|
228
|
+
on the exposed-copper path. The source toolkit owns this classification.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# pcb-scene3d-viewer 1.3.1
|
|
2
|
+
|
|
3
|
+
Version 1.3.1 renders source-classified via solder mask as a surface treatment
|
|
4
|
+
without replacing the plated copper barrel material.
|
|
5
|
+
|
|
6
|
+
## Via rendering
|
|
7
|
+
|
|
8
|
+
- Gerber vias with either `isTentingTop` or `isTentingBottom` use the covered
|
|
9
|
+
via rendering path; both fields explicitly false retain the exposed path.
|
|
10
|
+
- Each tented board surface receives its own solder-mask ring above the copper
|
|
11
|
+
annulus. Mixed top/bottom tenting is preserved.
|
|
12
|
+
- The mask ring keeps the authored drill opening clear and leaves the plated
|
|
13
|
+
through-hole wall copper-colored.
|
|
14
|
+
- Blind and buried surface reachability remains authoritative, so mask geometry
|
|
15
|
+
is added only where a via actually reaches the corresponding board surface.
|
|
16
|
+
|
|
17
|
+
## Compatibility and verification
|
|
18
|
+
|
|
19
|
+
- Existing CircuitJSON default-tenting and explicit-opening behavior is
|
|
20
|
+
unchanged.
|
|
21
|
+
- Scene, controller, runtime, export, and package entrypoints are unchanged.
|
|
22
|
+
- Tests cover fully tented, mixed, and fully open Gerber classification plus
|
|
23
|
+
copper-barrel and one-sided mask-ring material routing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pcb-scene3d-viewer",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pcb",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"docs/release-notes-v1.2.1.md",
|
|
38
38
|
"docs/release-notes-v1.2.2.md",
|
|
39
39
|
"docs/release-notes-v1.3.0.md",
|
|
40
|
+
"docs/release-notes-v1.3.1.md",
|
|
40
41
|
"docs/model-format.md",
|
|
41
42
|
"docs/testing.md",
|
|
42
43
|
"spec",
|
|
@@ -266,24 +266,37 @@ export class PcbScene3dCopperDetailFilter {
|
|
|
266
266
|
* @returns {any[]}
|
|
267
267
|
*/
|
|
268
268
|
static #filterExposedVias(vias, sceneDescription) {
|
|
269
|
+
if (PcbScene3dCopperDetailFilter.#isGerberScene(sceneDescription)) {
|
|
270
|
+
return (vias || []).filter(
|
|
271
|
+
(via) =>
|
|
272
|
+
via?.isTentingTop === false &&
|
|
273
|
+
via?.isTentingBottom === false
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
269
277
|
return (vias || []).filter((via) =>
|
|
270
278
|
PcbScene3dCopperDetailFilter.#isViaExplicitlyOpen(via)
|
|
271
279
|
)
|
|
272
280
|
}
|
|
273
281
|
|
|
274
282
|
/**
|
|
275
|
-
* Keeps KiCad via annuli that are covered by solder mask.
|
|
283
|
+
* Keeps KiCad and Gerber via annuli that are covered by solder mask.
|
|
276
284
|
* @param {any[] | undefined} vias Via list.
|
|
277
285
|
* @param {object} sceneDescription 3D scene description.
|
|
278
286
|
* @returns {any[]}
|
|
279
287
|
*/
|
|
280
288
|
static #filterMaskCoveredVias(vias, sceneDescription) {
|
|
281
|
-
if (
|
|
289
|
+
if (
|
|
290
|
+
!PcbScene3dCopperDetailFilter.#isKiCadScene(sceneDescription) &&
|
|
291
|
+
!PcbScene3dCopperDetailFilter.#isGerberScene(sceneDescription)
|
|
292
|
+
) {
|
|
282
293
|
return []
|
|
283
294
|
}
|
|
284
295
|
|
|
285
|
-
return (vias || []).filter(
|
|
286
|
-
|
|
296
|
+
return (vias || []).filter((via) =>
|
|
297
|
+
PcbScene3dCopperDetailFilter.#isGerberScene(sceneDescription)
|
|
298
|
+
? PcbScene3dCopperDetailFilter.#isViaExplicitlyTented(via)
|
|
299
|
+
: !PcbScene3dCopperDetailFilter.#isViaExplicitlyOpen(via)
|
|
287
300
|
)
|
|
288
301
|
}
|
|
289
302
|
|
|
@@ -296,6 +309,15 @@ export class PcbScene3dCopperDetailFilter {
|
|
|
296
309
|
return via?.isTentingTop === false || via?.isTentingBottom === false
|
|
297
310
|
}
|
|
298
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Checks whether one via explicitly carries mask on either board side.
|
|
314
|
+
* @param {object} via Via primitive.
|
|
315
|
+
* @returns {boolean}
|
|
316
|
+
*/
|
|
317
|
+
static #isViaExplicitlyTented(via) {
|
|
318
|
+
return via?.isTentingTop === true || via?.isTentingBottom === true
|
|
319
|
+
}
|
|
320
|
+
|
|
299
321
|
/**
|
|
300
322
|
* Appends copper barrels for through-hole pads with copper annuli.
|
|
301
323
|
* @param {any[]} vias Visible via list.
|
|
@@ -487,6 +509,19 @@ export class PcbScene3dCopperDetailFilter {
|
|
|
487
509
|
)
|
|
488
510
|
}
|
|
489
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Checks whether one scene was built from Gerber fabrication artwork.
|
|
514
|
+
* @param {object} sceneDescription Scene description.
|
|
515
|
+
* @returns {boolean}
|
|
516
|
+
*/
|
|
517
|
+
static #isGerberScene(sceneDescription) {
|
|
518
|
+
return (
|
|
519
|
+
String(sceneDescription?.sourceFormat || '')
|
|
520
|
+
.trim()
|
|
521
|
+
.toLowerCase() === 'gerber'
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
|
|
490
525
|
/**
|
|
491
526
|
* Checks whether one text primitive belongs to a solder-mask layer.
|
|
492
527
|
* @param {object} text Text primitive.
|
|
@@ -130,7 +130,7 @@ export class PcbScene3dCopperDetailGroupBuilder {
|
|
|
130
130
|
sceneDescription?.board?.thicknessMil,
|
|
131
131
|
normalizePoint,
|
|
132
132
|
{
|
|
133
|
-
|
|
133
|
+
surfaceMaterial: PcbScene3dMaskCoveredCopperMaterial.build(
|
|
134
134
|
THREE,
|
|
135
135
|
PcbScene3dCopperDetailGroupBuilder.#coveredCopperMaterialOptions(
|
|
136
136
|
sceneDescription
|
|
@@ -9,6 +9,7 @@ export class PcbScene3dViaFactory {
|
|
|
9
9
|
static #PAD_BARREL_MIN_WALL_MIL = 1.2
|
|
10
10
|
static #PAD_BARREL_WALL_FRACTION = 0.09
|
|
11
11
|
static #SURFACE_COPPER_DEPTH_MIL = 2
|
|
12
|
+
static #SURFACE_MASK_Z_OFFSET_MIL = 1.3
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Builds the via mesh group for one scene.
|
|
@@ -16,7 +17,7 @@ export class PcbScene3dViaFactory {
|
|
|
16
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
|
|
17
18
|
* @param {number} thicknessMil
|
|
18
19
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
19
|
-
* @param {{ material?: any }} [options]
|
|
20
|
+
* @param {{ material?: any, surfaceMaterial?: any }} [options]
|
|
20
21
|
* @returns {any}
|
|
21
22
|
*/
|
|
22
23
|
static buildGroup(
|
|
@@ -27,8 +28,12 @@ export class PcbScene3dViaFactory {
|
|
|
27
28
|
options = {}
|
|
28
29
|
) {
|
|
29
30
|
const group = new THREE.Group()
|
|
30
|
-
const
|
|
31
|
+
const copperMaterial = PcbScene3dViaFactory.#resolveMaterial(
|
|
32
|
+
THREE,
|
|
33
|
+
options
|
|
34
|
+
)
|
|
31
35
|
const geometryCache = new Map()
|
|
36
|
+
const surfaceGeometryCache = new Map()
|
|
32
37
|
|
|
33
38
|
;(vias || []).forEach((via) => {
|
|
34
39
|
const renderMode = PcbScene3dViaLayerSpan.renderMode(via)
|
|
@@ -41,7 +46,7 @@ export class PcbScene3dViaFactory {
|
|
|
41
46
|
thicknessMil,
|
|
42
47
|
renderMode
|
|
43
48
|
)
|
|
44
|
-
const mesh = new THREE.Mesh(geometry,
|
|
49
|
+
const mesh = new THREE.Mesh(geometry, copperMaterial)
|
|
45
50
|
const point = normalizeBoardPoint(
|
|
46
51
|
Number(via?.x || 0),
|
|
47
52
|
Number(via?.y || 0)
|
|
@@ -55,6 +60,16 @@ export class PcbScene3dViaFactory {
|
|
|
55
60
|
mesh.rotation.x = Math.PI / 2
|
|
56
61
|
}
|
|
57
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
|
+
)
|
|
58
73
|
})
|
|
59
74
|
|
|
60
75
|
return group
|
|
@@ -78,6 +93,130 @@ export class PcbScene3dViaFactory {
|
|
|
78
93
|
)
|
|
79
94
|
}
|
|
80
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
|
+
|
|
81
220
|
/**
|
|
82
221
|
* Resolves one reusable via geometry from the via drill spec.
|
|
83
222
|
* @param {any} THREE
|