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.
- package/README.md +7 -0
- package/docs/api.md +18 -0
- package/docs/circuitjson.md +6 -4
- package/docs/release-notes-v1.3.0.md +38 -0
- package/package.json +3 -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 +5 -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 +51 -5
- 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(
|
|
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(
|
|
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 =
|
|
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
|
+
}
|