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.
Files changed (32) hide show
  1. package/README.md +7 -0
  2. package/docs/api.md +18 -0
  3. package/docs/circuitjson.md +6 -4
  4. package/docs/release-notes-v1.3.0.md +38 -0
  5. package/package.json +3 -2
  6. package/src/CircuitJsonCadModelAssetResolver.mjs +24 -10
  7. package/src/PcbAssemblyGltfModelMeshParser.mjs +3 -1
  8. package/src/PcbAssemblyModelMeshLoader.mjs +1 -1
  9. package/src/PcbAssemblyTextModelMeshParser.mjs +3 -1
  10. package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -11
  11. package/src/PcbScene3dBoardMaterialPalette.mjs +12 -0
  12. package/src/PcbScene3dCircuitJsonAdapter.mjs +33 -85
  13. package/src/PcbScene3dCircuitJsonCopperTextBuilder.mjs +385 -0
  14. package/src/PcbScene3dCircuitJsonDocumentationArtworkBuilder.mjs +123 -22
  15. package/src/PcbScene3dCircuitJsonGeometry.mjs +12 -0
  16. package/src/PcbScene3dCircuitJsonPadCorner.mjs +72 -0
  17. package/src/PcbScene3dCircuitJsonSilkscreenBuilder.mjs +140 -25
  18. package/src/PcbScene3dCircuitJsonSilkscreenDetailBuilder.mjs +21 -0
  19. package/src/PcbScene3dCircuitJsonSourceLayer.mjs +124 -0
  20. package/src/PcbScene3dCircuitJsonTraceRouteBuilder.mjs +6 -7
  21. package/src/PcbScene3dCopperDetailFilter.mjs +5 -1
  22. package/src/PcbScene3dCopperFactory.mjs +23 -4
  23. package/src/PcbScene3dCopperTextFactory.mjs +105 -20
  24. package/src/PcbScene3dExternalModels.mjs +26 -0
  25. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +16 -1
  26. package/src/PcbScene3dModelContent.mjs +32 -1
  27. package/src/PcbScene3dRuntimeBoardMeshes.mjs +2 -4
  28. package/src/PcbScene3dSilkscreenCopperCutoutBuilder.mjs +588 -0
  29. package/src/PcbScene3dStepLoader.mjs +2 -1
  30. package/src/PcbScene3dStrokeCutoutBuilder.mjs +98 -0
  31. package/src/PcbScene3dViaFactory.mjs +51 -5
  32. package/src/PcbScene3dViaLayerSpan.mjs +126 -0
@@ -1,6 +1,7 @@
1
1
  import { PcbScene3dStrokeFont } from './PcbScene3dStrokeFont.mjs'
2
2
  import { PcbScene3dCutoutGeometryFilter } from './PcbScene3dCutoutGeometryFilter.mjs'
3
3
  import { PcbScene3dStrokeGeometryBuilder } from './PcbScene3dStrokeGeometryBuilder.mjs'
4
+ import { PcbScene3dStrokeCutoutBuilder } from './PcbScene3dStrokeCutoutBuilder.mjs'
4
5
 
5
6
  /**
6
7
  * Builds KiCad copper text as widened stroke meshes for the 3D PCB scene.
@@ -11,6 +12,33 @@ export class PcbScene3dCopperTextFactory {
11
12
  static #FIRST_LINE_HEIGHT_RATIO = 1.17
12
13
  static #STROKE_BASELINE_FUDGE_RATIO = 0.052
13
14
 
15
+ /**
16
+ * Builds exact round-capped stroke polygons for one copper text primitive.
17
+ * @param {object} text Copper text primitive.
18
+ * @param {{ glyphYUp?: boolean, alignmentStrokeWidth?: number }} [options] Text geometry options. `alignmentStrokeWidth` keeps paired text with different stroke widths on one source-authored anchor.
19
+ * @returns {{ x: number, y: number }[][]}
20
+ */
21
+ static strokeCutouts(text, options = {}) {
22
+ const width = PcbScene3dCopperTextFactory.#textStrokeWidth(text)
23
+ return PcbScene3dCopperTextFactory.#textStrokes(
24
+ text,
25
+ Boolean(options?.glyphYUp),
26
+ options?.alignmentStrokeWidth
27
+ ).flatMap((stroke) => {
28
+ const cutouts = []
29
+ for (let index = 1; index < stroke.length; index += 1) {
30
+ const cutout = PcbScene3dStrokeCutoutBuilder.build(
31
+ stroke[index - 1],
32
+ stroke[index],
33
+ width,
34
+ { minWidth: 1 }
35
+ )
36
+ if (cutout.length >= 3) cutouts.push(cutout)
37
+ }
38
+ return cutouts
39
+ })
40
+ }
41
+
14
42
  /**
15
43
  * Builds one side-specific copper text group.
16
44
  * @param {any} THREE
@@ -171,9 +199,10 @@ export class PcbScene3dCopperTextFactory {
171
199
  * Builds all KiCad stroke-font point lists for one text primitive.
172
200
  * @param {object} text
173
201
  * @param {boolean} glyphYUp
202
+ * @param {number | undefined} alignmentStrokeWidth Stroke width used only for anchor adjustment.
174
203
  * @returns {{ x: number, y: number }[][]}
175
204
  */
176
- static #textStrokes(text, glyphYUp) {
205
+ static #textStrokes(text, glyphYUp, alignmentStrokeWidth) {
177
206
  const lines = String(text?.value ?? text?.text ?? '').split('\n')
178
207
  const lineSpacing = PcbScene3dCopperTextFactory.#textLineSpacing(text)
179
208
 
@@ -184,7 +213,8 @@ export class PcbScene3dCopperTextFactory {
184
213
  index,
185
214
  lines.length,
186
215
  lineSpacing,
187
- glyphYUp
216
+ glyphYUp,
217
+ alignmentStrokeWidth
188
218
  )
189
219
  )
190
220
  }
@@ -197,6 +227,7 @@ export class PcbScene3dCopperTextFactory {
197
227
  * @param {number} lineCount
198
228
  * @param {number} lineSpacing
199
229
  * @param {boolean} glyphYUp
230
+ * @param {number | undefined} alignmentStrokeWidth Stroke width used only for anchor adjustment.
200
231
  * @returns {{ x: number, y: number }[][]}
201
232
  */
202
233
  static #textLineStrokes(
@@ -205,7 +236,8 @@ export class PcbScene3dCopperTextFactory {
205
236
  index,
206
237
  lineCount,
207
238
  lineSpacing,
208
- glyphYUp
239
+ glyphYUp,
240
+ alignmentStrokeWidth
209
241
  ) {
210
242
  const sizeX = PcbScene3dCopperTextFactory.#textWidth(text)
211
243
  const sizeY = PcbScene3dCopperTextFactory.#textHeight(text)
@@ -215,12 +247,17 @@ export class PcbScene3dCopperTextFactory {
215
247
  sizeX,
216
248
  sizeY
217
249
  })
218
- const x = PcbScene3dCopperTextFactory.#textLineX(text, layout.width)
250
+ const x = PcbScene3dCopperTextFactory.#textLineX(
251
+ text,
252
+ layout.width,
253
+ alignmentStrokeWidth
254
+ )
219
255
  const y = PcbScene3dCopperTextFactory.#textLineY(
220
256
  text,
221
257
  index,
222
258
  lineCount,
223
- lineSpacing
259
+ lineSpacing,
260
+ alignmentStrokeWidth
224
261
  )
225
262
 
226
263
  return layout.strokes.map((stroke) =>
@@ -293,11 +330,14 @@ export class PcbScene3dCopperTextFactory {
293
330
  * Resolves line origin from KiCad horizontal justification.
294
331
  * @param {object} text
295
332
  * @param {number} lineWidth
333
+ * @param {number | undefined} alignmentStrokeWidth Stroke width used only for anchor adjustment.
296
334
  * @returns {number}
297
335
  */
298
- static #textLineX(text, lineWidth) {
299
- const fudge =
300
- PcbScene3dCopperTextFactory.#textStrokeHorizontalFudge(text)
336
+ static #textLineX(text, lineWidth, alignmentStrokeWidth) {
337
+ const fudge = PcbScene3dCopperTextFactory.#textStrokeHorizontalFudge(
338
+ text,
339
+ alignmentStrokeWidth
340
+ )
301
341
 
302
342
  if (text?.hAlign === 'left') {
303
343
  return Number(text?.x || 0) + fudge
@@ -316,9 +356,16 @@ export class PcbScene3dCopperTextFactory {
316
356
  * @param {number} index
317
357
  * @param {number} lineCount
318
358
  * @param {number} lineSpacing
359
+ * @param {number | undefined} alignmentStrokeWidth Stroke width used only for anchor adjustment.
319
360
  * @returns {number}
320
361
  */
321
- static #textLineY(text, index, lineCount, lineSpacing) {
362
+ static #textLineY(
363
+ text,
364
+ index,
365
+ lineCount,
366
+ lineSpacing,
367
+ alignmentStrokeWidth
368
+ ) {
322
369
  const height = PcbScene3dCopperTextFactory.#textHeight(text)
323
370
  const blockHeight =
324
371
  height * PcbScene3dCopperTextFactory.#FIRST_LINE_HEIGHT_RATIO +
@@ -326,7 +373,10 @@ export class PcbScene3dCopperTextFactory {
326
373
  let baseline =
327
374
  Number(text?.y || 0) +
328
375
  height -
329
- PcbScene3dCopperTextFactory.#textStrokeBaselineFudge(text)
376
+ PcbScene3dCopperTextFactory.#textStrokeBaselineFudge(
377
+ text,
378
+ alignmentStrokeWidth
379
+ )
330
380
 
331
381
  if (text?.vAlign === 'bottom') {
332
382
  baseline -= blockHeight
@@ -354,24 +404,46 @@ export class PcbScene3dCopperTextFactory {
354
404
  /**
355
405
  * Resolves KiCad's small horizontal text adjustment.
356
406
  * @param {object} text
407
+ * @param {number | undefined} alignmentStrokeWidth Stroke width used only for anchor adjustment.
357
408
  * @returns {number}
358
409
  */
359
- static #textStrokeHorizontalFudge(text) {
360
- return PcbScene3dCopperTextFactory.#textStrokeWidth(text) / 1.52
410
+ static #textStrokeHorizontalFudge(text, alignmentStrokeWidth) {
411
+ return (
412
+ PcbScene3dCopperTextFactory.#alignmentStrokeWidth(
413
+ text,
414
+ alignmentStrokeWidth
415
+ ) / 1.52
416
+ )
361
417
  }
362
418
 
363
419
  /**
364
420
  * Resolves KiCad's small baseline text adjustment.
365
421
  * @param {object} text
422
+ * @param {number | undefined} alignmentStrokeWidth Stroke width used only for anchor adjustment.
366
423
  * @returns {number}
367
424
  */
368
- static #textStrokeBaselineFudge(text) {
425
+ static #textStrokeBaselineFudge(text, alignmentStrokeWidth) {
369
426
  return (
370
- PcbScene3dCopperTextFactory.#textStrokeWidth(text) *
371
- PcbScene3dCopperTextFactory.#STROKE_BASELINE_FUDGE_RATIO
427
+ PcbScene3dCopperTextFactory.#alignmentStrokeWidth(
428
+ text,
429
+ alignmentStrokeWidth
430
+ ) * PcbScene3dCopperTextFactory.#STROKE_BASELINE_FUDGE_RATIO
372
431
  )
373
432
  }
374
433
 
434
+ /**
435
+ * Resolves the stroke width used for source-anchor alignment adjustments.
436
+ * @param {object} text Copper text primitive.
437
+ * @param {number | undefined} alignmentStrokeWidth Optional paired-text reference width.
438
+ * @returns {number}
439
+ */
440
+ static #alignmentStrokeWidth(text, alignmentStrokeWidth) {
441
+ const candidate = Number(alignmentStrokeWidth)
442
+ return Number.isFinite(candidate) && candidate > 0
443
+ ? candidate
444
+ : PcbScene3dCopperTextFactory.#textStrokeWidth(text)
445
+ }
446
+
375
447
  /**
376
448
  * Applies KiCad text rotation and mirrored text transforms.
377
449
  * @param {object} text
@@ -389,15 +461,15 @@ export class PcbScene3dCopperTextFactory {
389
461
  : point
390
462
 
391
463
  if (text?.mirrored) {
392
- const rotated = PcbScene3dCopperTextFactory.#rotatePoint(
464
+ const mirroredPoint = PcbScene3dCopperTextFactory.#mirrorPointX(
393
465
  sourcePoint,
466
+ origin
467
+ )
468
+ return PcbScene3dCopperTextFactory.#rotatePoint(
469
+ mirroredPoint,
394
470
  origin,
395
471
  Number(text?.rotation || 0)
396
472
  )
397
- return {
398
- x: origin.x - (rotated.x - origin.x),
399
- y: rotated.y
400
- }
401
473
  }
402
474
 
403
475
  return PcbScene3dCopperTextFactory.#rotatePoint(
@@ -407,6 +479,19 @@ export class PcbScene3dCopperTextFactory {
407
479
  )
408
480
  }
409
481
 
482
+ /**
483
+ * Mirrors one stroke-font point across the text anchor's local Y axis.
484
+ * @param {{ x: number, y: number }} point
485
+ * @param {{ x: number, y: number }} origin
486
+ * @returns {{ x: number, y: number }}
487
+ */
488
+ static #mirrorPointX(point, origin) {
489
+ return {
490
+ x: origin.x - (Number(point?.x || 0) - origin.x),
491
+ y: Number(point?.y || 0)
492
+ }
493
+ }
494
+
410
495
  /**
411
496
  * Mirrors one stroke-font point across the text anchor's local X axis.
412
497
  * @param {{ x: number, y: number }} point
@@ -84,6 +84,15 @@ export class PcbScene3dExternalModels {
84
84
  await PcbScene3dExternalModels.#yieldToMainThread()
85
85
  }
86
86
  } catch (error) {
87
+ if (
88
+ PcbScene3dExternalModels.#isDeferredContentError(
89
+ placement,
90
+ error,
91
+ modelLoaderOptions
92
+ )
93
+ ) {
94
+ continue
95
+ }
87
96
  diagnostics.push(
88
97
  'Could not load external model for ' +
89
98
  String(placement?.designator || 'component') +
@@ -101,6 +110,23 @@ export class PcbScene3dExternalModels {
101
110
  return diagnostics
102
111
  }
103
112
 
113
+ /**
114
+ * Returns whether a failure represents an intentionally deferred model.
115
+ * @param {object} placement External-model placement.
116
+ * @param {unknown} error Model loading failure.
117
+ * @param {object} modelLoaderOptions Scoped model loading options.
118
+ * @returns {boolean}
119
+ */
120
+ static #isDeferredContentError(placement, error, modelLoaderOptions) {
121
+ const model = placement?.externalModel
122
+ return Boolean(
123
+ model &&
124
+ PcbScene3dModelContent.isUnavailableError(error) &&
125
+ !PcbScene3dModelContent.hasLocal(model) &&
126
+ !PcbScene3dModelContent.canFetch(modelLoaderOptions)
127
+ )
128
+ }
129
+
104
130
  /**
105
131
  * Applies the active view mirror compensation to loaded model geometry.
106
132
  * @param {any} externalModelsGroup Root group containing placed models.
@@ -16,7 +16,7 @@ export class PcbScene3dMaskCoveredCopperSideGroupBuilder {
16
16
  /**
17
17
  * Builds one side group from prepared mask-covered copper meshes.
18
18
  * @param {any} THREE Three.js namespace.
19
- * @param {{ trackMesh?: any | null, arcMesh?: any | null, fillMesh?: any | null, z?: number, mirrorY?: boolean }} options
19
+ * @param {{ trackMesh?: any | null, arcMesh?: any | null, fillMesh?: any | null, textMesh?: any | null, z?: number, mirrorY?: boolean }} options
20
20
  * @returns {any}
21
21
  */
22
22
  static build(
@@ -25,6 +25,7 @@ export class PcbScene3dMaskCoveredCopperSideGroupBuilder {
25
25
  trackMesh = null,
26
26
  arcMesh = null,
27
27
  fillMesh = null,
28
+ textMesh = null,
28
29
  z = 0,
29
30
  mirrorY = false
30
31
  } = {}
@@ -87,6 +88,20 @@ export class PcbScene3dMaskCoveredCopperSideGroupBuilder {
87
88
  .#FILL_RENDER_ORDER
88
89
  }
89
90
  )
91
+ PcbScene3dMaskCoveredCopperSideGroupBuilder.#addCompressedMesh(
92
+ group,
93
+ textMesh,
94
+ 'mask-covered-copper-text',
95
+ z,
96
+ {
97
+ copperBlend:
98
+ PcbScene3dMaskCoveredCopperSideGroupBuilder
99
+ .#TRACK_COPPER_BLEND,
100
+ renderOrder:
101
+ PcbScene3dMaskCoveredCopperSideGroupBuilder
102
+ .#TRACK_RENDER_ORDER
103
+ }
104
+ )
90
105
 
91
106
  if (mirrorY && group.children.length) {
92
107
  group.rotation.x = Math.PI
@@ -1,6 +1,8 @@
1
1
  import { PcbScene3dModelIdentity } from './PcbScene3dModelIdentity.mjs'
2
2
  import { PcbScene3dModelFetchPolicy } from './PcbScene3dModelFetchPolicy.mjs'
3
3
 
4
+ const CONTENT_UNAVAILABLE_ERROR = 'ERR_MODEL_CONTENT_UNAVAILABLE'
5
+
4
6
  /**
5
7
  * Reads external-model payloads through one explicit local/network policy.
6
8
  */
@@ -23,6 +25,35 @@ export class PcbScene3dModelContent {
23
25
  return PcbScene3dModelFetchPolicy.canFetch(options)
24
26
  }
25
27
 
28
+ /**
29
+ * Creates a typed missing-content error for deferred model references.
30
+ * @param {string} [label] Human-readable format label.
31
+ * @returns {Error}
32
+ */
33
+ static unavailableError(label = 'Model') {
34
+ const error = new Error(label + ' model content is not available.')
35
+ Object.defineProperty(error, 'code', {
36
+ configurable: false,
37
+ enumerable: true,
38
+ value: CONTENT_UNAVAILABLE_ERROR,
39
+ writable: false
40
+ })
41
+ return error
42
+ }
43
+
44
+ /**
45
+ * Returns whether a failure is the typed missing-content condition.
46
+ * @param {unknown} error Candidate failure.
47
+ * @returns {boolean}
48
+ */
49
+ static isUnavailableError(error) {
50
+ try {
51
+ return error?.code === CONTENT_UNAVAILABLE_ERROR
52
+ } catch {
53
+ return false
54
+ }
55
+ }
56
+
26
57
  /**
27
58
  * Reads one model as bytes from text, binary data, files, or opted-in URLs.
28
59
  * @param {unknown} model External model metadata.
@@ -155,7 +186,7 @@ export class PcbScene3dModelContent {
155
186
  ''
156
187
  ).trim()
157
188
  if (!url || !PcbScene3dModelFetchPolicy.canFetch(options)) {
158
- throw new Error(label + ' model content is not available.')
189
+ throw PcbScene3dModelContent.unavailableError(label)
159
190
  }
160
191
 
161
192
  const cache =
@@ -124,10 +124,8 @@ export class PcbScene3dRuntimeBoardMeshes {
124
124
  metalness: 0.08,
125
125
  visible: generatedBodyVisible
126
126
  }
127
- const edgeColor = Number(board.edgeColor)
128
- const resolvedEdgeColor = Number.isInteger(edgeColor)
129
- ? edgeColor
130
- : 0xc9ca78
127
+ const resolvedEdgeColor =
128
+ PcbScene3dBoardMaterialPalette.resolveEdgeColor(board)
131
129
  const surfaceColor = hasBoardAssemblyModel
132
130
  ? resolvedEdgeColor
133
131
  : PcbScene3dBoardMaterialPalette.resolveBoardSurfaceColor(board, {