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.
Files changed (65) hide show
  1. package/AGENTS.md +67 -0
  2. package/COMMERCIAL-LICENSE.md +15 -0
  3. package/CONTRIBUTING.md +14 -0
  4. package/LICENSE +19 -0
  5. package/LICENSES/AGPL-3.0-or-later.txt +235 -0
  6. package/LICENSES/CC-BY-SA-4.0.txt +170 -0
  7. package/LICENSES/LGPL-2.1-or-later.txt +176 -0
  8. package/LICENSES/LicenseRef-PolyForm-Noncommercial-1.0.0.txt +131 -0
  9. package/NOTICE.md +36 -0
  10. package/README.md +128 -0
  11. package/REUSE.toml +16 -0
  12. package/docs/api.md +148 -0
  13. package/docs/circuitjson.md +190 -0
  14. package/docs/model-format.md +117 -0
  15. package/docs/testing.md +23 -0
  16. package/package.json +65 -0
  17. package/spec/library-scope.md +36 -0
  18. package/src/PcbModelArchiveExporter.mjs +320 -0
  19. package/src/PcbScene3dArcUtils.mjs +27 -0
  20. package/src/PcbScene3dBoardAssemblyPlacement.mjs +36 -0
  21. package/src/PcbScene3dBoardAssemblyPresentation.mjs +859 -0
  22. package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +537 -0
  23. package/src/PcbScene3dBoardMaterialPalette.mjs +40 -0
  24. package/src/PcbScene3dBoardShapeFactory.mjs +895 -0
  25. package/src/PcbScene3dBoardSolderMaskFactory.mjs +613 -0
  26. package/src/PcbScene3dCameraRig.mjs +168 -0
  27. package/src/PcbScene3dCircuitJsonAdapter.mjs +545 -0
  28. package/src/PcbScene3dController.mjs +956 -0
  29. package/src/PcbScene3dCopperDetailFilter.mjs +490 -0
  30. package/src/PcbScene3dCopperFactory.mjs +559 -0
  31. package/src/PcbScene3dCopperTextFactory.mjs +534 -0
  32. package/src/PcbScene3dCutoutGeometryFilter.mjs +873 -0
  33. package/src/PcbScene3dDetailCoordinateNormalizer.mjs +65 -0
  34. package/src/PcbScene3dDrillCutoutFilter.mjs +224 -0
  35. package/src/PcbScene3dDrillPathFactory.mjs +362 -0
  36. package/src/PcbScene3dDrillVoidFactory.mjs +268 -0
  37. package/src/PcbScene3dExternalModelLoadOrder.mjs +54 -0
  38. package/src/PcbScene3dExternalModels.mjs +968 -0
  39. package/src/PcbScene3dFallbackVisibility.mjs +82 -0
  40. package/src/PcbScene3dInteractionHints.mjs +56 -0
  41. package/src/PcbScene3dMountRig.mjs +53 -0
  42. package/src/PcbScene3dOutlineBuilder.mjs +210 -0
  43. package/src/PcbScene3dPadFactory.mjs +553 -0
  44. package/src/PcbScene3dPresetState.mjs +48 -0
  45. package/src/PcbScene3dRenderGroupVisibility.mjs +134 -0
  46. package/src/PcbScene3dRuntime.mjs +996 -0
  47. package/src/PcbScene3dRuntimeBoardMeshes.mjs +99 -0
  48. package/src/PcbScene3dSelectionStyler.mjs +252 -0
  49. package/src/PcbScene3dShapePathFactory.mjs +220 -0
  50. package/src/PcbScene3dShellRenderer.mjs +131 -0
  51. package/src/PcbScene3dSilkscreenFactory.mjs +854 -0
  52. package/src/PcbScene3dSilkscreenStrokeWidthResolver.mjs +81 -0
  53. package/src/PcbScene3dStepLoader.mjs +611 -0
  54. package/src/PcbScene3dStrokeFont.mjs +671 -0
  55. package/src/PcbScene3dStrokeGeometryBuilder.mjs +322 -0
  56. package/src/PcbScene3dText.mjs +99 -0
  57. package/src/PcbScene3dTrueTypeTextFactory.mjs +885 -0
  58. package/src/PcbScene3dViaFactory.mjs +176 -0
  59. package/src/PcbScene3dViewCompensation.mjs +109 -0
  60. package/src/PcbScene3dViewScale.mjs +24 -0
  61. package/src/PcbScene3dViewportResize.mjs +35 -0
  62. package/src/PcbScene3dWorkerClient.mjs +123 -0
  63. package/src/index.mjs +1 -0
  64. package/src/scene3d.mjs +44 -0
  65. 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
+ }