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.
Files changed (35) hide show
  1. package/README.md +14 -0
  2. package/docs/api.md +18 -0
  3. package/docs/circuitjson.md +6 -4
  4. package/docs/model-format.md +6 -0
  5. package/docs/release-notes-v1.3.0.md +38 -0
  6. package/docs/release-notes-v1.3.1.md +23 -0
  7. package/package.json +4 -2
  8. package/src/CircuitJsonCadModelAssetResolver.mjs +24 -10
  9. package/src/PcbAssemblyGltfModelMeshParser.mjs +3 -1
  10. package/src/PcbAssemblyModelMeshLoader.mjs +1 -1
  11. package/src/PcbAssemblyTextModelMeshParser.mjs +3 -1
  12. package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -11
  13. package/src/PcbScene3dBoardMaterialPalette.mjs +12 -0
  14. package/src/PcbScene3dCircuitJsonAdapter.mjs +33 -85
  15. package/src/PcbScene3dCircuitJsonCopperTextBuilder.mjs +385 -0
  16. package/src/PcbScene3dCircuitJsonDocumentationArtworkBuilder.mjs +123 -22
  17. package/src/PcbScene3dCircuitJsonGeometry.mjs +12 -0
  18. package/src/PcbScene3dCircuitJsonPadCorner.mjs +72 -0
  19. package/src/PcbScene3dCircuitJsonSilkscreenBuilder.mjs +140 -25
  20. package/src/PcbScene3dCircuitJsonSilkscreenDetailBuilder.mjs +21 -0
  21. package/src/PcbScene3dCircuitJsonSourceLayer.mjs +124 -0
  22. package/src/PcbScene3dCircuitJsonTraceRouteBuilder.mjs +6 -7
  23. package/src/PcbScene3dCopperDetailFilter.mjs +44 -5
  24. package/src/PcbScene3dCopperDetailGroupBuilder.mjs +1 -1
  25. package/src/PcbScene3dCopperFactory.mjs +23 -4
  26. package/src/PcbScene3dCopperTextFactory.mjs +105 -20
  27. package/src/PcbScene3dExternalModels.mjs +26 -0
  28. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +16 -1
  29. package/src/PcbScene3dModelContent.mjs +32 -1
  30. package/src/PcbScene3dRuntimeBoardMeshes.mjs +2 -4
  31. package/src/PcbScene3dSilkscreenCopperCutoutBuilder.mjs +588 -0
  32. package/src/PcbScene3dStepLoader.mjs +2 -1
  33. package/src/PcbScene3dStrokeCutoutBuilder.mjs +98 -0
  34. package/src/PcbScene3dViaFactory.mjs +193 -8
  35. 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 material = PcbScene3dViaFactory.#resolveMaterial(THREE, options)
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, material)
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(point.x, point.y, 0)
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(THREE, geometryCache, via, thicknessMil) {
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 = thicknessMil + 2
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
+ }