pcb-scene3d-viewer 1.0.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/AGENTS.md +67 -0
- package/COMMERCIAL-LICENSE.md +15 -0
- package/CONTRIBUTING.md +14 -0
- package/LICENSE +19 -0
- package/LICENSES/AGPL-3.0-or-later.txt +235 -0
- package/LICENSES/CC-BY-SA-4.0.txt +170 -0
- package/LICENSES/LGPL-2.1-or-later.txt +176 -0
- package/LICENSES/LicenseRef-PolyForm-Noncommercial-1.0.0.txt +131 -0
- package/NOTICE.md +36 -0
- package/README.md +128 -0
- package/REUSE.toml +16 -0
- package/docs/api.md +148 -0
- package/docs/circuitjson.md +190 -0
- package/docs/model-format.md +117 -0
- package/docs/testing.md +23 -0
- package/package.json +65 -0
- package/spec/library-scope.md +36 -0
- package/src/PcbModelArchiveExporter.mjs +320 -0
- package/src/PcbScene3dArcUtils.mjs +27 -0
- package/src/PcbScene3dBoardAssemblyPlacement.mjs +36 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +859 -0
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +537 -0
- package/src/PcbScene3dBoardMaterialPalette.mjs +40 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +895 -0
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +613 -0
- package/src/PcbScene3dCameraRig.mjs +168 -0
- package/src/PcbScene3dCircuitJsonAdapter.mjs +545 -0
- package/src/PcbScene3dController.mjs +956 -0
- package/src/PcbScene3dCopperDetailFilter.mjs +490 -0
- package/src/PcbScene3dCopperFactory.mjs +559 -0
- package/src/PcbScene3dCopperTextFactory.mjs +534 -0
- package/src/PcbScene3dCutoutGeometryFilter.mjs +873 -0
- package/src/PcbScene3dDetailCoordinateNormalizer.mjs +65 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +224 -0
- package/src/PcbScene3dDrillPathFactory.mjs +362 -0
- package/src/PcbScene3dDrillVoidFactory.mjs +268 -0
- package/src/PcbScene3dExternalModelLoadOrder.mjs +54 -0
- package/src/PcbScene3dExternalModels.mjs +968 -0
- package/src/PcbScene3dFallbackVisibility.mjs +82 -0
- package/src/PcbScene3dInteractionHints.mjs +56 -0
- package/src/PcbScene3dMountRig.mjs +53 -0
- package/src/PcbScene3dOutlineBuilder.mjs +210 -0
- package/src/PcbScene3dPadFactory.mjs +553 -0
- package/src/PcbScene3dPresetState.mjs +48 -0
- package/src/PcbScene3dRenderGroupVisibility.mjs +134 -0
- package/src/PcbScene3dRuntime.mjs +996 -0
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +99 -0
- package/src/PcbScene3dSelectionStyler.mjs +252 -0
- package/src/PcbScene3dShapePathFactory.mjs +220 -0
- package/src/PcbScene3dShellRenderer.mjs +131 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +854 -0
- package/src/PcbScene3dSilkscreenStrokeWidthResolver.mjs +81 -0
- package/src/PcbScene3dStepLoader.mjs +611 -0
- package/src/PcbScene3dStrokeFont.mjs +671 -0
- package/src/PcbScene3dStrokeGeometryBuilder.mjs +322 -0
- package/src/PcbScene3dText.mjs +99 -0
- package/src/PcbScene3dTrueTypeTextFactory.mjs +885 -0
- package/src/PcbScene3dViaFactory.mjs +176 -0
- package/src/PcbScene3dViewCompensation.mjs +109 -0
- package/src/PcbScene3dViewScale.mjs +24 -0
- package/src/PcbScene3dViewportResize.mjs +35 -0
- package/src/PcbScene3dWorkerClient.mjs +123 -0
- package/src/index.mjs +1 -0
- package/src/scene3d.mjs +44 -0
- package/src/styles/scene3d.css +295 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { PcbScene3dBoardShapeFactory } from './PcbScene3dBoardShapeFactory.mjs'
|
|
2
|
+
import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds open drill-interior surfaces for board-assembly substrates.
|
|
6
|
+
*/
|
|
7
|
+
export class PcbScene3dDrillVoidFactory {
|
|
8
|
+
static #DEFAULT_INTERIOR_COLOR = 0xf4f0ea
|
|
9
|
+
static #INTERIOR_RADIUS_SCALE = 0.86
|
|
10
|
+
static #INTERIOR_SEGMENTS = 18
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds open-ended circular drill interiors without capping apertures.
|
|
14
|
+
* @param {any} THREE
|
|
15
|
+
* @param {{ pads?: any[], vias?: any[] }} [detail]
|
|
16
|
+
* @param {number} [topZ]
|
|
17
|
+
* @param {number} [bottomZ]
|
|
18
|
+
* @param {(x: number, y: number) => { x: number, y: number }} [normalizeBoardPoint]
|
|
19
|
+
* @param {{ enabled?: boolean, color?: number, board?: object }} [options]
|
|
20
|
+
* @returns {any}
|
|
21
|
+
*/
|
|
22
|
+
static buildGroup(
|
|
23
|
+
THREE,
|
|
24
|
+
detail = {},
|
|
25
|
+
topZ = 0,
|
|
26
|
+
bottomZ = 0,
|
|
27
|
+
normalizeBoardPoint = (x, y) => ({ x, y }),
|
|
28
|
+
options = {}
|
|
29
|
+
) {
|
|
30
|
+
const group = new THREE.Group()
|
|
31
|
+
group.name = 'drill-voids'
|
|
32
|
+
if (!options?.enabled) {
|
|
33
|
+
return group
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const material = PcbScene3dDrillVoidFactory.#buildMaterial(
|
|
37
|
+
THREE,
|
|
38
|
+
options
|
|
39
|
+
)
|
|
40
|
+
const geometryCache = new Map()
|
|
41
|
+
const depth = Math.max(Math.abs(Number(topZ) - Number(bottomZ)), 1)
|
|
42
|
+
const centerZ = (Number(topZ || 0) + Number(bottomZ || 0)) / 2
|
|
43
|
+
const platedDrillKeys =
|
|
44
|
+
PcbScene3dDrillVoidFactory.#buildPlatedDrillKeySet(detail)
|
|
45
|
+
const edgeDrillKeys = PcbScene3dDrillVoidFactory.#buildEdgeDrillKeySet(
|
|
46
|
+
THREE,
|
|
47
|
+
detail,
|
|
48
|
+
normalizeBoardPoint,
|
|
49
|
+
options
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
PcbScene3dDrillPathFactory.resolveBoardDrillSpecs(detail).forEach(
|
|
53
|
+
(drillSpec) => {
|
|
54
|
+
if (
|
|
55
|
+
platedDrillKeys.has(
|
|
56
|
+
PcbScene3dDrillVoidFactory.#drillKey(drillSpec)
|
|
57
|
+
) ||
|
|
58
|
+
edgeDrillKeys.has(
|
|
59
|
+
PcbScene3dDrillVoidFactory.#drillKey(drillSpec)
|
|
60
|
+
)
|
|
61
|
+
) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mesh = PcbScene3dDrillVoidFactory.#buildInteriorMesh(
|
|
66
|
+
THREE,
|
|
67
|
+
geometryCache,
|
|
68
|
+
drillSpec,
|
|
69
|
+
depth,
|
|
70
|
+
centerZ,
|
|
71
|
+
material,
|
|
72
|
+
normalizeBoardPoint
|
|
73
|
+
)
|
|
74
|
+
if (mesh) {
|
|
75
|
+
group.add(mesh)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return group
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Builds a lookup for circular drills already carved into the board edge.
|
|
85
|
+
* @param {any} THREE
|
|
86
|
+
* @param {{ pads?: any[], vias?: any[] }} detail
|
|
87
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
88
|
+
* @param {{ board?: object }} options
|
|
89
|
+
* @returns {Set<string>}
|
|
90
|
+
*/
|
|
91
|
+
static #buildEdgeDrillKeySet(THREE, detail, normalizeBoardPoint, options) {
|
|
92
|
+
return new Set(
|
|
93
|
+
PcbScene3dBoardShapeFactory.resolveCircularEdgeDrills(
|
|
94
|
+
THREE,
|
|
95
|
+
options?.board,
|
|
96
|
+
detail,
|
|
97
|
+
normalizeBoardPoint
|
|
98
|
+
).map((drillSpec) =>
|
|
99
|
+
PcbScene3dDrillVoidFactory.#drillKey(drillSpec)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Builds a lookup for drill holes that already have copper barrel geometry.
|
|
106
|
+
* @param {{ pads?: any[], vias?: any[] }} detail
|
|
107
|
+
* @returns {Set<string>}
|
|
108
|
+
*/
|
|
109
|
+
static #buildPlatedDrillKeySet(detail) {
|
|
110
|
+
const platedDrills = new Set()
|
|
111
|
+
|
|
112
|
+
for (const via of detail?.vias || []) {
|
|
113
|
+
if (PcbScene3dDrillVoidFactory.#hasViaCopperAnnulus(via)) {
|
|
114
|
+
platedDrills.add(PcbScene3dDrillVoidFactory.#drillKey(via))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const pad of detail?.pads || []) {
|
|
119
|
+
if (PcbScene3dDrillVoidFactory.#hasPadCopperAnnulus(pad)) {
|
|
120
|
+
platedDrills.add(PcbScene3dDrillVoidFactory.#drillKey(pad))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return platedDrills
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Checks whether one via has a copper annulus around its drill.
|
|
129
|
+
* @param {any} via Via primitive.
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
static #hasViaCopperAnnulus(via) {
|
|
133
|
+
const holeDiameter = Number(via?.holeDiameter || 0)
|
|
134
|
+
return (
|
|
135
|
+
holeDiameter > 0 &&
|
|
136
|
+
Number(via?.diameter || 0) > holeDiameter + 0.001
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Checks whether one pad has copper larger than its drill aperture.
|
|
142
|
+
* @param {any} pad Pad primitive.
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
static #hasPadCopperAnnulus(pad) {
|
|
146
|
+
const holeDiameter = Number(pad?.holeDiameter || 0)
|
|
147
|
+
if (holeDiameter <= 0) {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return [
|
|
152
|
+
pad?.sizeTopX,
|
|
153
|
+
pad?.sizeTopY,
|
|
154
|
+
pad?.sizeMidX,
|
|
155
|
+
pad?.sizeMidY,
|
|
156
|
+
pad?.sizeBottomX,
|
|
157
|
+
pad?.sizeBottomY
|
|
158
|
+
].some((size) => Number(size || 0) > holeDiameter + 0.001)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Builds a stable lookup key for one circular drill.
|
|
163
|
+
* @param {{ x?: number, y?: number, holeDiameter?: number, diameter?: number }} drill
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
static #drillKey(drill) {
|
|
167
|
+
return [
|
|
168
|
+
Number(drill?.x || 0).toFixed(4),
|
|
169
|
+
Number(drill?.y || 0).toFixed(4),
|
|
170
|
+
Number(drill?.holeDiameter || drill?.diameter || 0).toFixed(4)
|
|
171
|
+
].join(':')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Builds the shared drill-interior material.
|
|
176
|
+
* @param {any} THREE
|
|
177
|
+
* @param {{ color?: number }} options
|
|
178
|
+
* @returns {any}
|
|
179
|
+
*/
|
|
180
|
+
static #buildMaterial(THREE, options) {
|
|
181
|
+
return new THREE.MeshStandardMaterial({
|
|
182
|
+
color: Number.isInteger(options?.color)
|
|
183
|
+
? options.color
|
|
184
|
+
: PcbScene3dDrillVoidFactory.#DEFAULT_INTERIOR_COLOR,
|
|
185
|
+
roughness: 0.82,
|
|
186
|
+
metalness: 0,
|
|
187
|
+
side: THREE.DoubleSide
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Builds one open circular drill-interior mesh.
|
|
193
|
+
* @param {any} THREE
|
|
194
|
+
* @param {Map<string, any>} geometryCache
|
|
195
|
+
* @param {{ x: number, y: number, diameter: number, slotLength?: number | null }} drillSpec
|
|
196
|
+
* @param {number} depth
|
|
197
|
+
* @param {number} centerZ
|
|
198
|
+
* @param {any} material
|
|
199
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
200
|
+
* @returns {any | null}
|
|
201
|
+
*/
|
|
202
|
+
static #buildInteriorMesh(
|
|
203
|
+
THREE,
|
|
204
|
+
geometryCache,
|
|
205
|
+
drillSpec,
|
|
206
|
+
depth,
|
|
207
|
+
centerZ,
|
|
208
|
+
material,
|
|
209
|
+
normalizeBoardPoint
|
|
210
|
+
) {
|
|
211
|
+
if (
|
|
212
|
+
Number(drillSpec?.slotLength || 0) >
|
|
213
|
+
Number(drillSpec?.diameter || 0) + 0.001
|
|
214
|
+
) {
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const point = normalizeBoardPoint(
|
|
219
|
+
Number(drillSpec?.x || 0),
|
|
220
|
+
Number(drillSpec?.y || 0)
|
|
221
|
+
)
|
|
222
|
+
const mesh = new THREE.Mesh(
|
|
223
|
+
PcbScene3dDrillVoidFactory.#resolveGeometry(
|
|
224
|
+
THREE,
|
|
225
|
+
geometryCache,
|
|
226
|
+
Number(drillSpec?.diameter || 0),
|
|
227
|
+
depth
|
|
228
|
+
),
|
|
229
|
+
material
|
|
230
|
+
)
|
|
231
|
+
mesh.name = 'drill-void-interior'
|
|
232
|
+
mesh.position.set(point.x, point.y, centerZ)
|
|
233
|
+
mesh.rotation.x = Math.PI / 2
|
|
234
|
+
return mesh
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Resolves a reusable open-cylinder geometry for a circular drill.
|
|
239
|
+
* @param {any} THREE
|
|
240
|
+
* @param {Map<string, any>} geometryCache
|
|
241
|
+
* @param {number} diameter
|
|
242
|
+
* @param {number} depth
|
|
243
|
+
* @returns {any}
|
|
244
|
+
*/
|
|
245
|
+
static #resolveGeometry(THREE, geometryCache, diameter, depth) {
|
|
246
|
+
const radius = Math.max(
|
|
247
|
+
(Math.max(Number(diameter || 0), 1) / 2) *
|
|
248
|
+
PcbScene3dDrillVoidFactory.#INTERIOR_RADIUS_SCALE,
|
|
249
|
+
0.6
|
|
250
|
+
)
|
|
251
|
+
const cacheKey = [radius.toFixed(4), depth.toFixed(4)].join(':')
|
|
252
|
+
const cached = geometryCache.get(cacheKey)
|
|
253
|
+
if (cached) {
|
|
254
|
+
return cached
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const geometry = new THREE.CylinderGeometry(
|
|
258
|
+
radius,
|
|
259
|
+
radius,
|
|
260
|
+
depth,
|
|
261
|
+
PcbScene3dDrillVoidFactory.#INTERIOR_SEGMENTS,
|
|
262
|
+
1,
|
|
263
|
+
true
|
|
264
|
+
)
|
|
265
|
+
geometryCache.set(cacheKey, geometry)
|
|
266
|
+
return geometry
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orders external 3D model placements so visible progress starts quickly.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dExternalModelLoadOrder {
|
|
5
|
+
/**
|
|
6
|
+
* Sorts placements by estimated import cost while preserving stable ties.
|
|
7
|
+
* @param {any[]} placements External model placements.
|
|
8
|
+
* @returns {any[]}
|
|
9
|
+
*/
|
|
10
|
+
static sort(placements) {
|
|
11
|
+
if (!Array.isArray(placements)) {
|
|
12
|
+
return []
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return placements
|
|
16
|
+
.map((placement, index) => ({
|
|
17
|
+
placement,
|
|
18
|
+
index,
|
|
19
|
+
weight: PcbScene3dExternalModelLoadOrder.#resolveWeight(
|
|
20
|
+
placement
|
|
21
|
+
)
|
|
22
|
+
}))
|
|
23
|
+
.sort((left, right) => {
|
|
24
|
+
if (left.weight !== right.weight) {
|
|
25
|
+
return left.weight - right.weight
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return left.index - right.index
|
|
29
|
+
})
|
|
30
|
+
.map((entry) => entry.placement)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolves one placement's import-cost estimate.
|
|
35
|
+
* @param {{ externalModel?: { payloadText?: string, file?: { size?: number } } }} placement External model placement.
|
|
36
|
+
* @returns {number}
|
|
37
|
+
*/
|
|
38
|
+
static #resolveWeight(placement) {
|
|
39
|
+
const model = placement?.externalModel || {}
|
|
40
|
+
const payloadLength =
|
|
41
|
+
typeof model.payloadText === 'string' ? model.payloadText.length : 0
|
|
42
|
+
const fileSize = Number(model.file?.size || 0)
|
|
43
|
+
|
|
44
|
+
if (payloadLength > 0) {
|
|
45
|
+
return payloadLength
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (Number.isFinite(fileSize) && fileSize > 0) {
|
|
49
|
+
return fileSize
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return Number.POSITIVE_INFINITY
|
|
53
|
+
}
|
|
54
|
+
}
|