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,553 @@
|
|
|
1
|
+
import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds copper pad meshes for the interactive 3D PCB scene.
|
|
5
|
+
*/
|
|
6
|
+
export class PcbScene3dPadFactory {
|
|
7
|
+
static #PAD_SHAPE_RECTANGULAR = 2
|
|
8
|
+
static #PAD_THICKNESS_MIL = 2.2
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds the pad mesh group for one scene.
|
|
12
|
+
* @param {any} THREE
|
|
13
|
+
* @param {any[]} pads
|
|
14
|
+
* @param {number} z
|
|
15
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
16
|
+
* @param {{ side?: 'top' | 'bottom', mirrorY?: boolean }} [options]
|
|
17
|
+
* @returns {any}
|
|
18
|
+
*/
|
|
19
|
+
static buildGroup(THREE, pads, z, normalizeBoardPoint, options = {}) {
|
|
20
|
+
const group = new THREE.Group()
|
|
21
|
+
const material = new THREE.MeshStandardMaterial({
|
|
22
|
+
color: 0xd9a61d,
|
|
23
|
+
roughness: 0.38,
|
|
24
|
+
metalness: 0.55,
|
|
25
|
+
side: THREE.DoubleSide
|
|
26
|
+
})
|
|
27
|
+
const geometryCache = new Map()
|
|
28
|
+
const side = PcbScene3dPadFactory.#normalizeSide(options?.side)
|
|
29
|
+
const mirrorY = Boolean(options?.mirrorY)
|
|
30
|
+
|
|
31
|
+
;(pads || []).forEach((pad) => {
|
|
32
|
+
if (!PcbScene3dPadFactory.#hasVisibleSurface(pad, side)) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const spec = PcbScene3dPadFactory.resolvePadSurfaceSpec(pad, side)
|
|
37
|
+
const geometry = PcbScene3dPadFactory.#resolveGeometry(
|
|
38
|
+
THREE,
|
|
39
|
+
geometryCache,
|
|
40
|
+
spec,
|
|
41
|
+
pad
|
|
42
|
+
)
|
|
43
|
+
const point = PcbScene3dPadFactory.#normalizePoint(
|
|
44
|
+
normalizeBoardPoint,
|
|
45
|
+
Number(pad?.x || 0),
|
|
46
|
+
Number(pad?.y || 0),
|
|
47
|
+
mirrorY
|
|
48
|
+
)
|
|
49
|
+
const root = new THREE.Group()
|
|
50
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
51
|
+
root.position.set(point.x, point.y, 0)
|
|
52
|
+
root.rotation.z = (Number(pad?.rotation || 0) * Math.PI) / 180
|
|
53
|
+
mesh.position.set(
|
|
54
|
+
spec.offsetX,
|
|
55
|
+
mirrorY ? -spec.offsetY : spec.offsetY,
|
|
56
|
+
z
|
|
57
|
+
)
|
|
58
|
+
if (geometry.type === 'CylinderGeometry') {
|
|
59
|
+
mesh.rotation.x = Math.PI / 2
|
|
60
|
+
}
|
|
61
|
+
root.add(mesh)
|
|
62
|
+
group.add(root)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return group
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolves the visible copper shape for one pad face.
|
|
70
|
+
* @param {{ sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number, holeDiameter?: number, holeShape?: number | null, holeSlotLength?: number | null, holeRotation?: number | null, shapeTop?: number, shapeMid?: number, shapeBottom?: number, hasRoundedRect?: boolean, roundedRectShapeTop?: number | null, roundedRectShapeBottom?: number | null, cornerRadiusTop?: number | null, cornerRadiusBottom?: number | null, offsetTopX?: number, offsetTopY?: number, offsetBottomX?: number, offsetBottomY?: number }} pad
|
|
71
|
+
* @param {'top' | 'bottom'} [side]
|
|
72
|
+
* @returns {{ width: number, height: number, kind: 'circle' | 'rect' | 'rounded-rect', radius: number, cornerRadius: number, offsetX: number, offsetY: number, hasHole: boolean, holeDiameter: number, holeSlotLength: number | null, holeRotation: number }}
|
|
73
|
+
*/
|
|
74
|
+
static resolvePadSurfaceSpec(pad, side = 'top') {
|
|
75
|
+
const normalizedSide = PcbScene3dPadFactory.#normalizeSide(side)
|
|
76
|
+
const size = PcbScene3dPadFactory.#resolvePadSurfaceSize(
|
|
77
|
+
pad,
|
|
78
|
+
normalizedSide
|
|
79
|
+
)
|
|
80
|
+
const cornerRadius = PcbScene3dPadFactory.#resolvePadCornerRadius(
|
|
81
|
+
pad,
|
|
82
|
+
size,
|
|
83
|
+
normalizedSide
|
|
84
|
+
)
|
|
85
|
+
const offset = PcbScene3dPadFactory.#resolvePadOffset(
|
|
86
|
+
pad,
|
|
87
|
+
normalizedSide
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
width: size.width,
|
|
92
|
+
height: size.height,
|
|
93
|
+
kind: PcbScene3dPadFactory.#resolvePadKind(
|
|
94
|
+
pad,
|
|
95
|
+
size,
|
|
96
|
+
cornerRadius,
|
|
97
|
+
normalizedSide
|
|
98
|
+
),
|
|
99
|
+
radius: Math.max(size.width, size.height) / 2,
|
|
100
|
+
cornerRadius,
|
|
101
|
+
offsetX: offset.x,
|
|
102
|
+
offsetY: offset.y,
|
|
103
|
+
hasHole: Number(pad?.holeDiameter || 0) > 0,
|
|
104
|
+
holeDiameter: Math.max(Number(pad?.holeDiameter || 0), 0),
|
|
105
|
+
holeSlotLength:
|
|
106
|
+
Number(pad?.holeSlotLength || 0) >
|
|
107
|
+
Number(pad?.holeDiameter || 0)
|
|
108
|
+
? Number(pad?.holeSlotLength || 0)
|
|
109
|
+
: null,
|
|
110
|
+
holeRotation: Number(pad?.holeRotation || 0)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolves the visible copper size for one pad face.
|
|
116
|
+
* @param {{ sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number, holeDiameter?: number }} pad
|
|
117
|
+
* @param {'top' | 'bottom'} side
|
|
118
|
+
* @returns {{ width: number, height: number }}
|
|
119
|
+
*/
|
|
120
|
+
static #resolvePadSurfaceSize(pad, side) {
|
|
121
|
+
const preferredWidth =
|
|
122
|
+
side === 'bottom'
|
|
123
|
+
? Number(pad?.sizeBottomX || 0)
|
|
124
|
+
: Number(pad?.sizeTopX || 0)
|
|
125
|
+
const preferredHeight =
|
|
126
|
+
side === 'bottom'
|
|
127
|
+
? Number(pad?.sizeBottomY || 0)
|
|
128
|
+
: Number(pad?.sizeTopY || 0)
|
|
129
|
+
const width =
|
|
130
|
+
Number(
|
|
131
|
+
preferredWidth ||
|
|
132
|
+
pad?.sizeMidX ||
|
|
133
|
+
(side === 'bottom' ? pad?.sizeTopX : pad?.sizeBottomX) ||
|
|
134
|
+
pad?.holeDiameter ||
|
|
135
|
+
0
|
|
136
|
+
) || 0
|
|
137
|
+
const height =
|
|
138
|
+
Number(
|
|
139
|
+
preferredHeight ||
|
|
140
|
+
pad?.sizeMidY ||
|
|
141
|
+
(side === 'bottom' ? pad?.sizeTopY : pad?.sizeBottomY) ||
|
|
142
|
+
pad?.holeDiameter ||
|
|
143
|
+
0
|
|
144
|
+
) || 0
|
|
145
|
+
const holeDiameter = Number(pad?.holeDiameter || 0)
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
width: Math.max(width, holeDiameter, 1),
|
|
149
|
+
height: Math.max(height, holeDiameter, 1)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolves the authored pad kind for one face.
|
|
155
|
+
* @param {{ shapeTop?: number, shapeMid?: number, shapeBottom?: number, hasRoundedRect?: boolean, roundedRectShapeTop?: number | null, roundedRectShapeBottom?: number | null }} pad
|
|
156
|
+
* @param {{ width: number, height: number }} size
|
|
157
|
+
* @param {number} cornerRadius
|
|
158
|
+
* @param {'top' | 'bottom'} side
|
|
159
|
+
* @returns {'circle' | 'rect' | 'rounded-rect'}
|
|
160
|
+
*/
|
|
161
|
+
static #resolvePadKind(pad, size, cornerRadius, side) {
|
|
162
|
+
if (PcbScene3dPadFactory.#isCircularPad(pad, size, side)) {
|
|
163
|
+
return 'circle'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (cornerRadius > 0.001) {
|
|
167
|
+
return 'rounded-rect'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return 'rect'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Returns true when one pad should render as a circular copper disc.
|
|
175
|
+
* @param {{ shapeTop?: number, shapeMid?: number, shapeBottom?: number, hasRoundedRect?: boolean, roundedRectShapeTop?: number | null, roundedRectShapeBottom?: number | null }} pad
|
|
176
|
+
* @param {{ width: number, height: number }} size
|
|
177
|
+
* @param {'top' | 'bottom'} side
|
|
178
|
+
* @returns {boolean}
|
|
179
|
+
*/
|
|
180
|
+
static #isCircularPad(pad, size, side) {
|
|
181
|
+
if (
|
|
182
|
+
PcbScene3dPadFactory.#resolvePadShape(pad, side) ===
|
|
183
|
+
PcbScene3dPadFactory.#PAD_SHAPE_RECTANGULAR
|
|
184
|
+
) {
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return Math.abs(Number(size.width) - Number(size.height)) < 0.001
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolves the effective pad shape code for one face, including any
|
|
193
|
+
* rounded-rect extension override when present.
|
|
194
|
+
* @param {{ shapeTop?: number, shapeMid?: number, shapeBottom?: number, hasRoundedRect?: boolean, roundedRectShapeTop?: number | null, roundedRectShapeBottom?: number | null }} pad
|
|
195
|
+
* @param {'top' | 'bottom'} side
|
|
196
|
+
* @returns {number}
|
|
197
|
+
*/
|
|
198
|
+
static #resolvePadShape(pad, side) {
|
|
199
|
+
if (side === 'bottom') {
|
|
200
|
+
if (
|
|
201
|
+
pad?.hasRoundedRect &&
|
|
202
|
+
Number.isInteger(pad?.roundedRectShapeBottom)
|
|
203
|
+
) {
|
|
204
|
+
return Number(pad.roundedRectShapeBottom)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return Number(
|
|
208
|
+
pad?.shapeBottom || pad?.shapeMid || pad?.shapeTop || 0
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (pad?.hasRoundedRect && Number.isInteger(pad?.roundedRectShapeTop)) {
|
|
213
|
+
return Number(pad.roundedRectShapeTop)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return Number(pad?.shapeTop || pad?.shapeMid || pad?.shapeBottom || 0)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Resolves the visible corner radius for one pad face.
|
|
221
|
+
* @param {{ shapeTop?: number, shapeMid?: number, shapeBottom?: number, hasRoundedRect?: boolean, roundedRectShapeTop?: number | null, roundedRectShapeBottom?: number | null, cornerRadiusTop?: number | null, cornerRadiusBottom?: number | null }} pad
|
|
222
|
+
* @param {{ width: number, height: number }} size
|
|
223
|
+
* @param {'top' | 'bottom'} side
|
|
224
|
+
* @returns {number}
|
|
225
|
+
*/
|
|
226
|
+
static #resolvePadCornerRadius(pad, size, side) {
|
|
227
|
+
const roundedRectCornerRadius =
|
|
228
|
+
PcbScene3dPadFactory.#resolveRoundedRectCornerRadius(
|
|
229
|
+
pad,
|
|
230
|
+
size,
|
|
231
|
+
side
|
|
232
|
+
)
|
|
233
|
+
if (roundedRectCornerRadius > 0) {
|
|
234
|
+
return roundedRectCornerRadius
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (PcbScene3dPadFactory.#resolvePadShape(pad, side) === 1) {
|
|
238
|
+
return Math.min(size.width, size.height) / 2
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return 0
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolves one optional explicit rounded-rect corner radius.
|
|
246
|
+
* @param {{ hasRoundedRect?: boolean, cornerRadiusTop?: number | null, cornerRadiusBottom?: number | null }} pad
|
|
247
|
+
* @param {{ width: number, height: number }} size
|
|
248
|
+
* @param {'top' | 'bottom'} side
|
|
249
|
+
* @returns {number}
|
|
250
|
+
*/
|
|
251
|
+
static #resolveRoundedRectCornerRadius(pad, size, side) {
|
|
252
|
+
const rawCornerRadius =
|
|
253
|
+
side === 'bottom'
|
|
254
|
+
? Number(pad?.cornerRadiusBottom)
|
|
255
|
+
: Number(pad?.cornerRadiusTop)
|
|
256
|
+
if (
|
|
257
|
+
pad?.hasRoundedRect &&
|
|
258
|
+
Number.isFinite(rawCornerRadius) &&
|
|
259
|
+
rawCornerRadius > 0
|
|
260
|
+
) {
|
|
261
|
+
return Math.min(size.width, size.height) * (rawCornerRadius / 100)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return 0
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Resolves the local copper offset for one face.
|
|
269
|
+
* @param {{ offsetTopX?: number, offsetTopY?: number, offsetBottomX?: number, offsetBottomY?: number }} pad
|
|
270
|
+
* @param {'top' | 'bottom'} side
|
|
271
|
+
* @returns {{ x: number, y: number }}
|
|
272
|
+
*/
|
|
273
|
+
static #resolvePadOffset(pad, side) {
|
|
274
|
+
if (side === 'bottom') {
|
|
275
|
+
return {
|
|
276
|
+
x: Number(pad?.offsetBottomX ?? pad?.offsetTopX ?? 0),
|
|
277
|
+
y: Number(pad?.offsetBottomY ?? pad?.offsetTopY ?? 0)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
x: Number(pad?.offsetTopX ?? pad?.offsetBottomX ?? 0),
|
|
283
|
+
y: Number(pad?.offsetTopY ?? pad?.offsetBottomY ?? 0)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Returns true when one pad has visible copper on the requested face.
|
|
289
|
+
* @param {{ sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number, holeDiameter?: number, hasTopSolderMaskOpening?: boolean, hasBottomSolderMaskOpening?: boolean }} pad
|
|
290
|
+
* @param {'top' | 'bottom'} side
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
static #hasVisibleSurface(pad, side) {
|
|
294
|
+
const maskOpening = PcbScene3dPadFactory.#resolveSolderMaskOpening(
|
|
295
|
+
pad,
|
|
296
|
+
side
|
|
297
|
+
)
|
|
298
|
+
if (maskOpening === false) {
|
|
299
|
+
return false
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const preferredSideHasSize = PcbScene3dPadFactory.#hasSideSize(
|
|
303
|
+
pad,
|
|
304
|
+
side
|
|
305
|
+
)
|
|
306
|
+
if (preferredSideHasSize) {
|
|
307
|
+
return true
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const alternateSideHasSize = PcbScene3dPadFactory.#hasSideSize(
|
|
311
|
+
pad,
|
|
312
|
+
side === 'bottom' ? 'top' : 'bottom'
|
|
313
|
+
)
|
|
314
|
+
const midHasSize =
|
|
315
|
+
Number(pad?.sizeMidX || 0) > 0 || Number(pad?.sizeMidY || 0) > 0
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
(!alternateSideHasSize && midHasSize) ||
|
|
319
|
+
(!alternateSideHasSize &&
|
|
320
|
+
!midHasSize &&
|
|
321
|
+
Number(pad?.holeDiameter || 0) > 0)
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Resolves an explicit side-specific solder-mask opening when available.
|
|
327
|
+
* @param {{ hasTopSolderMaskOpening?: boolean, hasBottomSolderMaskOpening?: boolean }} pad
|
|
328
|
+
* @param {'top' | 'bottom'} side
|
|
329
|
+
* @returns {boolean | null}
|
|
330
|
+
*/
|
|
331
|
+
static #resolveSolderMaskOpening(pad, side) {
|
|
332
|
+
const fieldName =
|
|
333
|
+
side === 'bottom'
|
|
334
|
+
? 'hasBottomSolderMaskOpening'
|
|
335
|
+
: 'hasTopSolderMaskOpening'
|
|
336
|
+
|
|
337
|
+
if (typeof pad?.[fieldName] === 'boolean') {
|
|
338
|
+
return pad[fieldName]
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return null
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Returns true when one face has an explicit copper size.
|
|
346
|
+
* @param {{ sizeTopX?: number, sizeTopY?: number, sizeBottomX?: number, sizeBottomY?: number }} pad
|
|
347
|
+
* @param {'top' | 'bottom'} side
|
|
348
|
+
* @returns {boolean}
|
|
349
|
+
*/
|
|
350
|
+
static #hasSideSize(pad, side) {
|
|
351
|
+
if (side === 'bottom') {
|
|
352
|
+
return (
|
|
353
|
+
Number(pad?.sizeBottomX || 0) > 0 ||
|
|
354
|
+
Number(pad?.sizeBottomY || 0) > 0
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return Number(pad?.sizeTopX || 0) > 0 || Number(pad?.sizeTopY || 0) > 0
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Normalizes one face selector to a supported side.
|
|
363
|
+
* @param {string | undefined} side
|
|
364
|
+
* @returns {'top' | 'bottom'}
|
|
365
|
+
*/
|
|
366
|
+
static #normalizeSide(side) {
|
|
367
|
+
return String(side || 'top').toLowerCase() === 'bottom'
|
|
368
|
+
? 'bottom'
|
|
369
|
+
: 'top'
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Normalizes one board point and optionally mirrors it for underside
|
|
374
|
+
* copper groups before the parent face flip rotates them below the board.
|
|
375
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
376
|
+
* @param {number} x
|
|
377
|
+
* @param {number} y
|
|
378
|
+
* @param {boolean} mirrorY
|
|
379
|
+
* @returns {{ x: number, y: number }}
|
|
380
|
+
*/
|
|
381
|
+
static #normalizePoint(normalizeBoardPoint, x, y, mirrorY) {
|
|
382
|
+
const point = normalizeBoardPoint(x, y)
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
x: point.x,
|
|
386
|
+
y: mirrorY ? -point.y : point.y
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Resolves or creates one reusable geometry for the pad spec.
|
|
392
|
+
* @param {any} THREE
|
|
393
|
+
* @param {Map<string, any>} geometryCache
|
|
394
|
+
* @param {any} pad
|
|
395
|
+
* @param {{ width: number, height: number, kind: 'circle' | 'rect' | 'rounded-rect', radius: number, cornerRadius: number, hasHole: boolean, holeDiameter: number, holeSlotLength: number | null, holeRotation: number }} spec
|
|
396
|
+
* @returns {any}
|
|
397
|
+
*/
|
|
398
|
+
static #resolveGeometry(THREE, geometryCache, spec, pad) {
|
|
399
|
+
const cacheKey = [
|
|
400
|
+
spec.kind,
|
|
401
|
+
spec.width.toFixed(4),
|
|
402
|
+
spec.height.toFixed(4),
|
|
403
|
+
spec.cornerRadius.toFixed(4),
|
|
404
|
+
spec.holeDiameter.toFixed(4),
|
|
405
|
+
Number(spec.holeSlotLength || 0).toFixed(4),
|
|
406
|
+
spec.holeRotation.toFixed(4)
|
|
407
|
+
].join(':')
|
|
408
|
+
const cached = geometryCache.get(cacheKey)
|
|
409
|
+
if (cached) {
|
|
410
|
+
return cached
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let geometry
|
|
414
|
+
if (spec.kind === 'circle' && !spec.hasHole) {
|
|
415
|
+
geometry = new THREE.CylinderGeometry(
|
|
416
|
+
spec.radius,
|
|
417
|
+
spec.radius,
|
|
418
|
+
PcbScene3dPadFactory.#PAD_THICKNESS_MIL,
|
|
419
|
+
28
|
|
420
|
+
)
|
|
421
|
+
} else {
|
|
422
|
+
const shape = PcbScene3dPadFactory.#buildOuterShape(THREE, spec)
|
|
423
|
+
const drillHole = PcbScene3dDrillPathFactory.buildPadHolePath(
|
|
424
|
+
THREE,
|
|
425
|
+
pad
|
|
426
|
+
)
|
|
427
|
+
if (drillHole) {
|
|
428
|
+
shape.holes.push(drillHole)
|
|
429
|
+
}
|
|
430
|
+
geometry = new THREE.ExtrudeGeometry(shape, {
|
|
431
|
+
depth: PcbScene3dPadFactory.#PAD_THICKNESS_MIL,
|
|
432
|
+
bevelEnabled: false,
|
|
433
|
+
curveSegments: 16,
|
|
434
|
+
steps: 1
|
|
435
|
+
})
|
|
436
|
+
geometry.translate?.(
|
|
437
|
+
0,
|
|
438
|
+
0,
|
|
439
|
+
-PcbScene3dPadFactory.#PAD_THICKNESS_MIL / 2
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
geometryCache.set(cacheKey, geometry)
|
|
444
|
+
return geometry
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Builds one pad outline shape for extrusion.
|
|
449
|
+
* @param {any} THREE
|
|
450
|
+
* @param {{ width: number, height: number, kind: 'circle' | 'rect' | 'rounded-rect', radius: number, cornerRadius: number }} spec
|
|
451
|
+
* @returns {any}
|
|
452
|
+
*/
|
|
453
|
+
static #buildOuterShape(THREE, spec) {
|
|
454
|
+
if (spec.kind === 'circle') {
|
|
455
|
+
const shape = new THREE.Shape()
|
|
456
|
+
shape.moveTo(spec.radius, 0)
|
|
457
|
+
shape.absarc(0, 0, spec.radius, 0, Math.PI, false)
|
|
458
|
+
shape.absarc(0, 0, spec.radius, Math.PI, Math.PI * 2, false)
|
|
459
|
+
shape.closePath()
|
|
460
|
+
return shape
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (spec.kind === 'rounded-rect') {
|
|
464
|
+
return PcbScene3dPadFactory.#buildRoundedRectShape(THREE, spec)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return PcbScene3dPadFactory.#buildRectShape(THREE, spec)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Builds one rectangular pad profile.
|
|
472
|
+
* @param {any} THREE
|
|
473
|
+
* @param {{ width: number, height: number }} spec
|
|
474
|
+
* @returns {any}
|
|
475
|
+
*/
|
|
476
|
+
static #buildRectShape(THREE, spec) {
|
|
477
|
+
const halfWidth = spec.width / 2
|
|
478
|
+
const halfHeight = spec.height / 2
|
|
479
|
+
const shape = new THREE.Shape()
|
|
480
|
+
|
|
481
|
+
shape.moveTo(-halfWidth, -halfHeight)
|
|
482
|
+
shape.lineTo(halfWidth, -halfHeight)
|
|
483
|
+
shape.lineTo(halfWidth, halfHeight)
|
|
484
|
+
shape.lineTo(-halfWidth, halfHeight)
|
|
485
|
+
shape.closePath()
|
|
486
|
+
return shape
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Builds one rounded rectangular pad profile.
|
|
491
|
+
* @param {any} THREE
|
|
492
|
+
* @param {{ width: number, height: number, cornerRadius: number }} spec
|
|
493
|
+
* @returns {any}
|
|
494
|
+
*/
|
|
495
|
+
static #buildRoundedRectShape(THREE, spec) {
|
|
496
|
+
const halfWidth = spec.width / 2
|
|
497
|
+
const halfHeight = spec.height / 2
|
|
498
|
+
const radius = Math.max(
|
|
499
|
+
0,
|
|
500
|
+
Math.min(spec.cornerRadius, halfWidth, halfHeight)
|
|
501
|
+
)
|
|
502
|
+
const shape = new THREE.Shape()
|
|
503
|
+
|
|
504
|
+
if (radius <= 0.001) {
|
|
505
|
+
shape.moveTo(-halfWidth, -halfHeight)
|
|
506
|
+
shape.lineTo(halfWidth, -halfHeight)
|
|
507
|
+
shape.lineTo(halfWidth, halfHeight)
|
|
508
|
+
shape.lineTo(-halfWidth, halfHeight)
|
|
509
|
+
shape.closePath()
|
|
510
|
+
return shape
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
shape.moveTo(-halfWidth + radius, -halfHeight)
|
|
514
|
+
shape.lineTo(halfWidth - radius, -halfHeight)
|
|
515
|
+
shape.absarc(
|
|
516
|
+
halfWidth - radius,
|
|
517
|
+
-halfHeight + radius,
|
|
518
|
+
radius,
|
|
519
|
+
-Math.PI / 2,
|
|
520
|
+
0,
|
|
521
|
+
false
|
|
522
|
+
)
|
|
523
|
+
shape.lineTo(halfWidth, halfHeight - radius)
|
|
524
|
+
shape.absarc(
|
|
525
|
+
halfWidth - radius,
|
|
526
|
+
halfHeight - radius,
|
|
527
|
+
radius,
|
|
528
|
+
0,
|
|
529
|
+
Math.PI / 2,
|
|
530
|
+
false
|
|
531
|
+
)
|
|
532
|
+
shape.lineTo(-halfWidth + radius, halfHeight)
|
|
533
|
+
shape.absarc(
|
|
534
|
+
-halfWidth + radius,
|
|
535
|
+
halfHeight - radius,
|
|
536
|
+
radius,
|
|
537
|
+
Math.PI / 2,
|
|
538
|
+
Math.PI,
|
|
539
|
+
false
|
|
540
|
+
)
|
|
541
|
+
shape.lineTo(-halfWidth, -halfHeight + radius)
|
|
542
|
+
shape.absarc(
|
|
543
|
+
-halfWidth + radius,
|
|
544
|
+
-halfHeight + radius,
|
|
545
|
+
radius,
|
|
546
|
+
Math.PI,
|
|
547
|
+
(Math.PI * 3) / 2,
|
|
548
|
+
false
|
|
549
|
+
)
|
|
550
|
+
shape.closePath()
|
|
551
|
+
return shape
|
|
552
|
+
}
|
|
553
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks the last requested 3D camera preset while the scene runtime starts.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dPresetState {
|
|
5
|
+
/** @type {string} */
|
|
6
|
+
#preset
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
this.#preset = 'isometric'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Stores one normalized preset name.
|
|
14
|
+
* @param {string} preset
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
set(preset) {
|
|
18
|
+
this.#preset = PcbScene3dPresetState.#normalize(preset)
|
|
19
|
+
return this.#preset
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns the last normalized preset name.
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
get() {
|
|
27
|
+
return this.#preset
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalizes one preset name to a supported camera preset.
|
|
32
|
+
* @param {string} preset
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
static #normalize(preset) {
|
|
36
|
+
const normalized = String(preset || 'isometric').toLowerCase()
|
|
37
|
+
if (
|
|
38
|
+
normalized === 'top' ||
|
|
39
|
+
normalized === 'bottom' ||
|
|
40
|
+
normalized === 'isometric' ||
|
|
41
|
+
normalized === 'reset'
|
|
42
|
+
) {
|
|
43
|
+
return normalized
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return 'isometric'
|
|
47
|
+
}
|
|
48
|
+
}
|