pcb-scene3d-viewer 1.1.49 → 1.2.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 (63) hide show
  1. package/README.md +46 -21
  2. package/docs/api.md +109 -8
  3. package/docs/circuitjson.md +143 -29
  4. package/docs/model-format.md +50 -7
  5. package/docs/release-notes-v1.2.0.md +122 -0
  6. package/docs/testing.md +30 -1
  7. package/package.json +6 -3
  8. package/spec/library-scope.md +10 -2
  9. package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
  10. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
  11. package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
  12. package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
  13. package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
  14. package/src/PcbModelArchiveExporter.mjs +28 -112
  15. package/src/PcbModelArchiveSourceBundle.mjs +326 -0
  16. package/src/PcbScene3dAabbIndex.mjs +464 -0
  17. package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -1
  18. package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
  19. package/src/PcbScene3dBoardMaterialPalette.mjs +29 -0
  20. package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
  21. package/src/PcbScene3dBoardSolderMaskFactory.mjs +65 -39
  22. package/src/PcbScene3dCircuitJsonAdapter.mjs +151 -48
  23. package/src/PcbScene3dCircuitJsonDrillDetail.mjs +31 -0
  24. package/src/PcbScene3dCircuitJsonGeometry.mjs +184 -33
  25. package/src/PcbScene3dCircuitJsonInput.mjs +132 -0
  26. package/src/PcbScene3dCircuitJsonModelAsset.mjs +40 -0
  27. package/src/PcbScene3dController.mjs +40 -34
  28. package/src/PcbScene3dCopperFactory.mjs +36 -22
  29. package/src/PcbScene3dCopperFillAreaClipper.mjs +133 -235
  30. package/src/PcbScene3dCopperFillCoverageContext.mjs +204 -0
  31. package/src/PcbScene3dCopperFillLoopSetResolver.mjs +192 -0
  32. package/src/PcbScene3dCopperFillMeshBuilder.mjs +77 -295
  33. package/src/PcbScene3dCopperTextFactory.mjs +12 -4
  34. package/src/PcbScene3dCutoutCircleDetector.mjs +34 -17
  35. package/src/PcbScene3dCutoutGeometryFilter.mjs +104 -269
  36. package/src/PcbScene3dCutoutGridIndex.mjs +184 -0
  37. package/src/PcbScene3dDeferredModelFinalizer.mjs +52 -0
  38. package/src/PcbScene3dDescriptorSafeRecord.mjs +38 -0
  39. package/src/PcbScene3dDrillCutoutFilter.mjs +149 -143
  40. package/src/PcbScene3dDrillPathFactory.mjs +86 -16
  41. package/src/PcbScene3dDrillVoidFactory.mjs +35 -10
  42. package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
  43. package/src/PcbScene3dExternalModels.mjs +23 -24
  44. package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
  45. package/src/PcbScene3dGeometryZCompressor.mjs +4 -2
  46. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +18 -4
  47. package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +75 -10
  48. package/src/PcbScene3dModelContent.mjs +236 -0
  49. package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
  50. package/src/PcbScene3dModelIdentity.mjs +106 -0
  51. package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
  52. package/src/PcbScene3dPreparedPolygon.mjs +709 -0
  53. package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
  54. package/src/PcbScene3dRuntime.mjs +45 -45
  55. package/src/PcbScene3dRuntimeBoardMeshes.mjs +85 -1
  56. package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
  57. package/src/PcbScene3dShellRenderer.mjs +75 -6
  58. package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
  59. package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
  60. package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
  61. package/src/PcbScene3dStepLoader.mjs +11 -10
  62. package/src/PcbScene3dText.mjs +1 -1
  63. package/src/PcbScene3dTriangleVertexQueryBounds.mjs +302 -0
@@ -2,6 +2,7 @@
2
2
  * Builds reusable circular and slotted drill paths for the 3D PCB scene.
3
3
  */
4
4
  export class PcbScene3dDrillPathFactory {
5
+ static #PAD_HOLE_SHAPE_RECT = 1
5
6
  static #PAD_HOLE_SHAPE_SLOT = 2
6
7
  static #ARC_SEGMENTS = 12
7
8
 
@@ -34,7 +35,7 @@ export class PcbScene3dDrillPathFactory {
34
35
  /**
35
36
  * Resolves deduped board-space drill specs from pads and vias.
36
37
  * @param {{ pads?: any[], vias?: any[] }} detail
37
- * @returns {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }[]}
38
+ * @returns {{ x: number, y: number, diameter: number, width?: number, height?: number, shape?: 'circle' | 'pill' | 'rect', slotLength?: number | null, rotationDeg?: number | null }[]}
38
39
  */
39
40
  static resolveBoardDrillSpecs(detail) {
40
41
  const seen = new Set()
@@ -64,7 +65,7 @@ export class PcbScene3dDrillPathFactory {
64
65
  /**
65
66
  * Builds one local pad drill path centered on the pad origin.
66
67
  * @param {any} THREE
67
- * @param {{ holeDiameter?: number, holeShape?: number | null, holeSlotLength?: number | null, holeRotation?: number | null, rotation?: number | null }} pad
68
+ * @param {{ holeDiameter?: number, holeWidth?: number, holeHeight?: number, holeShape?: number | null, holeSlotLength?: number | null, holeRotation?: number | null, rotation?: number | null }} pad
68
69
  * @returns {any | null}
69
70
  */
70
71
  static buildPadHolePath(THREE, pad) {
@@ -73,7 +74,14 @@ export class PcbScene3dDrillPathFactory {
73
74
  ? PcbScene3dDrillPathFactory.buildDrillPath(THREE, {
74
75
  ...drillSpec,
75
76
  x: 0,
76
- y: 0
77
+ y: 0,
78
+ rotationDeg:
79
+ drillSpec.shape === 'circle'
80
+ ? 0
81
+ : PcbScene3dDrillPathFactory.#normalizeAngle(
82
+ Number(drillSpec.rotationDeg || 0) -
83
+ Number(pad?.rotation || 0)
84
+ )
77
85
  })
78
86
  : null
79
87
  }
@@ -94,6 +102,9 @@ export class PcbScene3dDrillPathFactory {
94
102
  x: 0,
95
103
  y: 0,
96
104
  diameter: holeDiameter,
105
+ width: holeDiameter,
106
+ height: holeDiameter,
107
+ shape: 'circle',
97
108
  slotLength: null,
98
109
  rotationDeg: 0
99
110
  })
@@ -102,7 +113,7 @@ export class PcbScene3dDrillPathFactory {
102
113
  /**
103
114
  * Builds one drill path from a normalized drill descriptor.
104
115
  * @param {any} THREE
105
- * @param {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }} drillSpec
116
+ * @param {{ x: number, y: number, diameter: number, width?: number, height?: number, shape?: 'circle' | 'pill' | 'rect', slotLength?: number | null, rotationDeg?: number | null }} drillSpec
106
117
  * @returns {any | null}
107
118
  */
108
119
  static buildDrillPath(THREE, drillSpec) {
@@ -111,6 +122,10 @@ export class PcbScene3dDrillPathFactory {
111
122
  return null
112
123
  }
113
124
 
125
+ if (drillSpec?.shape === 'rect') {
126
+ return PcbScene3dDrillPathFactory.#buildRectPath(THREE, drillSpec)
127
+ }
128
+
114
129
  if (Number(drillSpec?.slotLength || 0) > diameter + 0.001) {
115
130
  return PcbScene3dDrillPathFactory.#buildSlotPath(THREE, drillSpec)
116
131
  }
@@ -121,7 +136,7 @@ export class PcbScene3dDrillPathFactory {
121
136
  /**
122
137
  * Resolves board-space via drill specs from normalized via detail.
123
138
  * @param {{ x?: number, y?: number, holeDiameter?: number }[]} vias
124
- * @returns {{ x: number, y: number, diameter: number, slotLength: null, rotationDeg: 0 }[]}
139
+ * @returns {{ x: number, y: number, diameter: number, width: number, height: number, shape: 'circle', slotLength: null, rotationDeg: 0 }[]}
125
140
  */
126
141
  static #resolveViaDrillSpecs(vias) {
127
142
  return (vias || [])
@@ -135,6 +150,9 @@ export class PcbScene3dDrillPathFactory {
135
150
  x: Number(via?.x || 0),
136
151
  y: Number(via?.y || 0),
137
152
  diameter,
153
+ width: diameter,
154
+ height: diameter,
155
+ shape: 'circle',
138
156
  slotLength: null,
139
157
  rotationDeg: 0
140
158
  }
@@ -145,7 +163,7 @@ export class PcbScene3dDrillPathFactory {
145
163
  /**
146
164
  * Resolves board-space pad drill specs from normalized pad detail.
147
165
  * @param {any[]} pads
148
- * @returns {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }[]}
166
+ * @returns {{ x: number, y: number, diameter: number, width?: number, height?: number, shape?: 'circle' | 'pill' | 'rect', slotLength?: number | null, rotationDeg?: number | null }[]}
149
167
  */
150
168
  static #resolvePadDrillSpecs(pads) {
151
169
  return (pads || [])
@@ -161,11 +179,10 @@ export class PcbScene3dDrillPathFactory {
161
179
  x: Number(pad?.x || 0),
162
180
  y: Number(pad?.y || 0),
163
181
  rotationDeg:
164
- drillSpec.slotLength === null
182
+ drillSpec.shape === 'circle'
165
183
  ? 0
166
184
  : PcbScene3dDrillPathFactory.#normalizeAngle(
167
- Number(pad?.rotation || 0) +
168
- Number(drillSpec.rotationDeg || 0)
185
+ Number(drillSpec.rotationDeg || 0)
169
186
  )
170
187
  }
171
188
  })
@@ -174,8 +191,8 @@ export class PcbScene3dDrillPathFactory {
174
191
 
175
192
  /**
176
193
  * Resolves one pad-local drill spec when the pad is through-hole.
177
- * @param {{ holeDiameter?: number, holeShape?: number | null, holeSlotLength?: number | null, holeRotation?: number | null }} pad
178
- * @returns {{ diameter: number, slotLength?: number | null, rotationDeg?: number | null } | null}
194
+ * @param {{ holeDiameter?: number, holeWidth?: number, holeHeight?: number, holeShape?: number | null, holeSlotLength?: number | null, holeRotation?: number | null }} pad
195
+ * @returns {{ diameter: number, width: number, height: number, shape: 'circle' | 'pill' | 'rect', slotLength?: number | null, rotationDeg?: number | null } | null}
179
196
  */
180
197
  static #resolvePadDrillSpec(pad) {
181
198
  const diameter = Number(pad?.holeDiameter || 0)
@@ -183,20 +200,70 @@ export class PcbScene3dDrillPathFactory {
183
200
  return null
184
201
  }
185
202
 
203
+ const holeShape = Number(pad?.holeShape)
204
+ const shape =
205
+ holeShape === PcbScene3dDrillPathFactory.#PAD_HOLE_SHAPE_RECT
206
+ ? 'rect'
207
+ : holeShape === PcbScene3dDrillPathFactory.#PAD_HOLE_SHAPE_SLOT
208
+ ? 'pill'
209
+ : 'circle'
210
+ const width = Number(pad?.holeWidth || diameter)
211
+ const height = Number(pad?.holeHeight || diameter)
186
212
  const slotLength =
187
- Number(pad?.holeShape) ===
188
- PcbScene3dDrillPathFactory.#PAD_HOLE_SHAPE_SLOT &&
189
- Number(pad?.holeSlotLength || 0) > diameter
213
+ shape === 'pill' && Number(pad?.holeSlotLength || 0) > diameter
190
214
  ? Number(pad?.holeSlotLength || 0)
191
215
  : null
192
216
 
193
217
  return {
194
218
  diameter,
219
+ width,
220
+ height,
221
+ shape,
195
222
  slotLength,
196
- rotationDeg: Number(pad?.holeRotation || 0)
223
+ rotationDeg: Number(pad?.holeRotation ?? pad?.rotation ?? 0)
197
224
  }
198
225
  }
199
226
 
227
+ /**
228
+ * Builds one rotated rectangular drill path.
229
+ * @param {any} THREE
230
+ * @param {{ x: number, y: number, diameter: number, width?: number, height?: number, rotationDeg?: number | null }} drillSpec
231
+ * @returns {any}
232
+ */
233
+ static #buildRectPath(THREE, drillSpec) {
234
+ const halfWidth = Math.max(
235
+ Number(drillSpec?.width || drillSpec?.diameter || 0) / 2,
236
+ 0.6
237
+ )
238
+ const halfHeight = Math.max(
239
+ Number(drillSpec?.height || drillSpec?.diameter || 0) / 2,
240
+ 0.6
241
+ )
242
+ const rotationRad = (Number(drillSpec.rotationDeg || 0) * Math.PI) / 180
243
+ const centerX = Number(drillSpec.x || 0)
244
+ const centerY = Number(drillSpec.y || 0)
245
+ const points = [
246
+ { x: -halfWidth, y: -halfHeight },
247
+ { x: halfWidth, y: -halfHeight },
248
+ { x: halfWidth, y: halfHeight },
249
+ { x: -halfWidth, y: halfHeight }
250
+ ].map((point) =>
251
+ PcbScene3dDrillPathFactory.#rotateAndTranslatePoint(
252
+ point,
253
+ rotationRad,
254
+ centerX,
255
+ centerY
256
+ )
257
+ )
258
+ const path = new THREE.Path()
259
+ path.moveTo(points[0].x, points[0].y)
260
+ for (let index = 1; index < points.length; index += 1) {
261
+ path.lineTo(points[index].x, points[index].y)
262
+ }
263
+ path.closePath()
264
+ return path
265
+ }
266
+
200
267
  /**
201
268
  * Builds one circular drill path.
202
269
  * @param {any} THREE
@@ -219,7 +286,7 @@ export class PcbScene3dDrillPathFactory {
219
286
  /**
220
287
  * Builds one slotted drill path as a rotated rounded rectangle.
221
288
  * @param {any} THREE
222
- * @param {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }} drillSpec
289
+ * @param {{ x: number, y: number, diameter: number, width?: number, height?: number, shape?: string, slotLength?: number | null, rotationDeg?: number | null }} drillSpec
223
290
  * @returns {any}
224
291
  */
225
292
  static #buildSlotPath(THREE, drillSpec) {
@@ -345,6 +412,9 @@ export class PcbScene3dDrillPathFactory {
345
412
  Number(drillSpec.x || 0).toFixed(4),
346
413
  Number(drillSpec.y || 0).toFixed(4),
347
414
  Number(drillSpec.diameter || 0).toFixed(4),
415
+ String(drillSpec.shape || 'circle'),
416
+ Number(drillSpec.width || drillSpec.diameter || 0).toFixed(4),
417
+ Number(drillSpec.height || drillSpec.diameter || 0).toFixed(4),
348
418
  Number(drillSpec.slotLength || 0).toFixed(4),
349
419
  Number(drillSpec.rotationDeg || 0).toFixed(4)
350
420
  ].join(':')
@@ -1,4 +1,5 @@
1
1
  import { PcbScene3dBoardShapeFactory } from './PcbScene3dBoardShapeFactory.mjs'
2
+ import { PcbScene3dBoardMaterialPalette } from './PcbScene3dBoardMaterialPalette.mjs'
2
3
  import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
3
4
 
4
5
  /**
@@ -16,7 +17,7 @@ export class PcbScene3dDrillVoidFactory {
16
17
  * @param {number} [topZ]
17
18
  * @param {number} [bottomZ]
18
19
  * @param {(x: number, y: number) => { x: number, y: number }} [normalizeBoardPoint]
19
- * @param {{ enabled?: boolean, color?: number, board?: object }} [options]
20
+ * @param {{ enabled?: boolean, color?: number, board?: object, hasBoardAssemblyModel?: boolean, sourceFormat?: string }} [options]
20
21
  * @returns {any}
21
22
  */
22
23
  static buildGroup(
@@ -50,7 +51,7 @@ export class PcbScene3dDrillVoidFactory {
50
51
  PcbScene3dDrillPathFactory.resolveBoardDrillSpecs(detail).forEach(
51
52
  (drillSpec) => {
52
53
  if (
53
- PcbScene3dDrillVoidFactory.#isSlottedDrill(drillSpec) ||
54
+ PcbScene3dDrillVoidFactory.#isNonCircularDrill(drillSpec) ||
54
55
  edgeDrillKeys.has(
55
56
  PcbScene3dDrillVoidFactory.#drillKey(drillSpec)
56
57
  )
@@ -113,14 +114,12 @@ export class PcbScene3dDrillVoidFactory {
113
114
  /**
114
115
  * Builds the shared drill-interior material.
115
116
  * @param {any} THREE
116
- * @param {{ color?: number }} options
117
+ * @param {{ color?: number, board?: object, hasBoardAssemblyModel?: boolean, sourceFormat?: string }} options
117
118
  * @returns {any}
118
119
  */
119
120
  static #buildInteriorMaterial(THREE, options) {
120
121
  return new THREE.MeshStandardMaterial({
121
- color: Number.isInteger(options?.color)
122
- ? options.color
123
- : PcbScene3dDrillVoidFactory.#DEFAULT_INTERIOR_COLOR,
122
+ color: PcbScene3dDrillVoidFactory.#resolveInteriorColor(options),
124
123
  roughness: 0.82,
125
124
  metalness: 0,
126
125
  side: THREE.DoubleSide
@@ -128,14 +127,40 @@ export class PcbScene3dDrillVoidFactory {
128
127
  }
129
128
 
130
129
  /**
131
- * Checks whether one drill is a routed slot.
132
- * @param {{ diameter?: number, slotLength?: number | null }} drillSpec Drill spec.
130
+ * Resolves the drill-interior display color.
131
+ * @param {{ color?: number, board?: object, hasBoardAssemblyModel?: boolean, sourceFormat?: string }} options
132
+ * @returns {number}
133
+ */
134
+ static #resolveInteriorColor(options) {
135
+ if (Number.isInteger(options?.color)) {
136
+ return options.color
137
+ }
138
+
139
+ if (options?.board) {
140
+ return PcbScene3dBoardMaterialPalette.resolveBoardSurfaceColor(
141
+ options.board,
142
+ {
143
+ hasBoardAssemblyModel: Boolean(
144
+ options?.hasBoardAssemblyModel
145
+ ),
146
+ sourceFormat: options?.sourceFormat
147
+ }
148
+ )
149
+ }
150
+
151
+ return PcbScene3dDrillVoidFactory.#DEFAULT_INTERIOR_COLOR
152
+ }
153
+
154
+ /**
155
+ * Checks whether one drill needs its non-circular substrate wall directly.
156
+ * @param {{ diameter?: number, shape?: string, slotLength?: number | null }} drillSpec Drill spec.
133
157
  * @returns {boolean}
134
158
  */
135
- static #isSlottedDrill(drillSpec) {
159
+ static #isNonCircularDrill(drillSpec) {
136
160
  return (
161
+ drillSpec?.shape === 'rect' ||
137
162
  Number(drillSpec?.slotLength || 0) >
138
- Number(drillSpec?.diameter || 0) + 0.001
163
+ Number(drillSpec?.diameter || 0) + 0.001
139
164
  )
140
165
  }
141
166