pcb-scene3d-viewer 1.2.2 → 1.3.0

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 (32) hide show
  1. package/README.md +7 -0
  2. package/docs/api.md +18 -0
  3. package/docs/circuitjson.md +6 -4
  4. package/docs/release-notes-v1.3.0.md +38 -0
  5. package/package.json +3 -2
  6. package/src/CircuitJsonCadModelAssetResolver.mjs +24 -10
  7. package/src/PcbAssemblyGltfModelMeshParser.mjs +3 -1
  8. package/src/PcbAssemblyModelMeshLoader.mjs +1 -1
  9. package/src/PcbAssemblyTextModelMeshParser.mjs +3 -1
  10. package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -11
  11. package/src/PcbScene3dBoardMaterialPalette.mjs +12 -0
  12. package/src/PcbScene3dCircuitJsonAdapter.mjs +33 -85
  13. package/src/PcbScene3dCircuitJsonCopperTextBuilder.mjs +385 -0
  14. package/src/PcbScene3dCircuitJsonDocumentationArtworkBuilder.mjs +123 -22
  15. package/src/PcbScene3dCircuitJsonGeometry.mjs +12 -0
  16. package/src/PcbScene3dCircuitJsonPadCorner.mjs +72 -0
  17. package/src/PcbScene3dCircuitJsonSilkscreenBuilder.mjs +140 -25
  18. package/src/PcbScene3dCircuitJsonSilkscreenDetailBuilder.mjs +21 -0
  19. package/src/PcbScene3dCircuitJsonSourceLayer.mjs +124 -0
  20. package/src/PcbScene3dCircuitJsonTraceRouteBuilder.mjs +6 -7
  21. package/src/PcbScene3dCopperDetailFilter.mjs +5 -1
  22. package/src/PcbScene3dCopperFactory.mjs +23 -4
  23. package/src/PcbScene3dCopperTextFactory.mjs +105 -20
  24. package/src/PcbScene3dExternalModels.mjs +26 -0
  25. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +16 -1
  26. package/src/PcbScene3dModelContent.mjs +32 -1
  27. package/src/PcbScene3dRuntimeBoardMeshes.mjs +2 -4
  28. package/src/PcbScene3dSilkscreenCopperCutoutBuilder.mjs +588 -0
  29. package/src/PcbScene3dStepLoader.mjs +2 -1
  30. package/src/PcbScene3dStrokeCutoutBuilder.mjs +98 -0
  31. package/src/PcbScene3dViaFactory.mjs +51 -5
  32. 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,11 +8,12 @@ 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
10
12
 
11
13
  /**
12
14
  * Builds the via mesh group for one scene.
13
15
  * @param {any} THREE
14
- * @param {{ diameter?: number, holeDiameter?: number, x?: number, y?: number, barrelOnly?: boolean }[]} vias
16
+ * @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
17
  * @param {number} thicknessMil
16
18
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
17
19
  * @param {{ material?: any }} [options]
@@ -29,18 +31,26 @@ export class PcbScene3dViaFactory {
29
31
  const geometryCache = new Map()
30
32
 
31
33
  ;(vias || []).forEach((via) => {
34
+ const renderMode = PcbScene3dViaLayerSpan.renderMode(via)
35
+ if (!renderMode) return
36
+
32
37
  const geometry = PcbScene3dViaFactory.#resolveGeometry(
33
38
  THREE,
34
39
  geometryCache,
35
40
  via,
36
- thicknessMil
41
+ thicknessMil,
42
+ renderMode
37
43
  )
38
44
  const mesh = new THREE.Mesh(geometry, material)
39
45
  const point = normalizeBoardPoint(
40
46
  Number(via?.x || 0),
41
47
  Number(via?.y || 0)
42
48
  )
43
- mesh.position.set(point.x, point.y, 0)
49
+ mesh.position.set(
50
+ point.x,
51
+ point.y,
52
+ PcbScene3dViaFactory.#centerZ(renderMode, thicknessMil)
53
+ )
44
54
  if (geometry.type === 'CylinderGeometry') {
45
55
  mesh.rotation.x = Math.PI / 2
46
56
  }
@@ -74,12 +84,22 @@ export class PcbScene3dViaFactory {
74
84
  * @param {Map<string, any>} geometryCache
75
85
  * @param {{ diameter?: number, holeDiameter?: number, barrelOnly?: boolean }} via
76
86
  * @param {number} thicknessMil
87
+ * @param {'through' | 'top' | 'bottom'} renderMode Via geometry mode.
77
88
  * @returns {any}
78
89
  */
79
- static #resolveGeometry(THREE, geometryCache, via, thicknessMil) {
90
+ static #resolveGeometry(
91
+ THREE,
92
+ geometryCache,
93
+ via,
94
+ thicknessMil,
95
+ renderMode
96
+ ) {
80
97
  const outerRadius = Math.max(Number(via?.diameter || 0) / 2, 1.2)
81
98
  const holeDiameter = Math.max(Number(via?.holeDiameter || 0), 0)
82
- const depth = thicknessMil + 2
99
+ const depth = PcbScene3dViaFactory.#geometryDepth(
100
+ renderMode,
101
+ thicknessMil
102
+ )
83
103
  const isBarrelOnly = Boolean(via?.barrelOnly)
84
104
  const cacheKey = [
85
105
  isBarrelOnly ? 'barrel' : 'annulus',
@@ -131,6 +151,32 @@ export class PcbScene3dViaFactory {
131
151
  return geometry
132
152
  }
133
153
 
154
+ /**
155
+ * Resolves copper geometry depth without extending blind vias through-board.
156
+ * @param {'through' | 'top' | 'bottom'} renderMode Via geometry mode.
157
+ * @param {number} thicknessMil Board thickness in mil.
158
+ * @returns {number}
159
+ */
160
+ static #geometryDepth(renderMode, thicknessMil) {
161
+ if (renderMode !== 'through') {
162
+ return PcbScene3dViaFactory.#SURFACE_COPPER_DEPTH_MIL
163
+ }
164
+ return Math.max(Number(thicknessMil) || 0, 0) + 2
165
+ }
166
+
167
+ /**
168
+ * Resolves the world-space Z center for one via geometry mode.
169
+ * @param {'through' | 'top' | 'bottom'} renderMode Via geometry mode.
170
+ * @param {number} thicknessMil Board thickness in mil.
171
+ * @returns {number}
172
+ */
173
+ static #centerZ(renderMode, thicknessMil) {
174
+ const halfThickness = Math.max(Number(thicknessMil) || 0, 0) / 2
175
+ if (renderMode === 'top') return halfThickness
176
+ if (renderMode === 'bottom') return -halfThickness
177
+ return 0
178
+ }
179
+
134
180
  /**
135
181
  * Builds a visible copper sleeve for through-hole pads.
136
182
  * @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
+ }