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
@@ -0,0 +1,72 @@
1
+ import { CircuitJsonUnits } from 'circuitjson-toolkit'
2
+
3
+ const RECTANGULAR_PAD_SHAPE = 2
4
+
5
+ /**
6
+ * Normalizes CircuitJSON rounded-pad metadata for scene pad faces.
7
+ */
8
+ export class PcbScene3dCircuitJsonPadCorner {
9
+ /**
10
+ * Builds rounded-rectangle metadata for one or both pad faces.
11
+ * @param {object} pad Pad element.
12
+ * @param {{ width: number, height: number }} size Pad copper size in mils.
13
+ * @param {boolean | null} isBottom Bottom side, or null for both sides.
14
+ * @returns {object}
15
+ */
16
+ static metadata(pad, size, isBottom) {
17
+ if (
18
+ !String(pad?.shape || '').endsWith('pill') &&
19
+ !(PcbScene3dCircuitJsonPadCorner.#radius(pad) > 0)
20
+ ) {
21
+ return {}
22
+ }
23
+ const cornerRadius =
24
+ PcbScene3dCircuitJsonPadCorner.#cornerRadiusPercent(pad, size)
25
+ const hasTop = isBottom !== true
26
+ const hasBottom = isBottom !== false
27
+ return {
28
+ hasRoundedRect: true,
29
+ roundedRectShapeTop: hasTop ? RECTANGULAR_PAD_SHAPE : null,
30
+ roundedRectShapeBottom: hasBottom ? RECTANGULAR_PAD_SHAPE : null,
31
+ cornerRadiusTop: hasTop ? cornerRadius : null,
32
+ cornerRadiusBottom: hasBottom ? cornerRadius : null
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Resolves a corner radius as a percentage of the shortest pad side.
38
+ * @param {object} pad Pad element.
39
+ * @param {{ width: number, height: number }} size Pad copper size in mils.
40
+ * @returns {number}
41
+ */
42
+ static #cornerRadiusPercent(pad, size) {
43
+ const width = CircuitJsonUnits.optionalLength(pad?.width)
44
+ const height = CircuitJsonUnits.optionalLength(pad?.height)
45
+ const radius = PcbScene3dCircuitJsonPadCorner.#radius(pad)
46
+ if (width > 0 && height > 0 && radius > 0) {
47
+ return Math.min((radius / Math.min(width, height)) * 100, 50)
48
+ }
49
+
50
+ const shortestSide = Math.min(Number(size.width), Number(size.height))
51
+ const radiusMil = CircuitJsonUnits.mmToMil(radius, 0)
52
+ if (shortestSide > 0 && radiusMil > 0) {
53
+ return Math.min((radiusMil / shortestSide) * 100, 50)
54
+ }
55
+
56
+ return 50
57
+ }
58
+
59
+ /**
60
+ * Resolves canonical and compatibility corner-radius fields.
61
+ * @param {object} pad Pad element.
62
+ * @returns {number}
63
+ */
64
+ static #radius(pad) {
65
+ return CircuitJsonUnits.optionalLength(
66
+ pad?.radius ??
67
+ pad?.corner_radius ??
68
+ pad?.rect_border_radius ??
69
+ pad?.cornerRadius
70
+ )
71
+ }
72
+ }
@@ -1,6 +1,7 @@
1
1
  import { CircuitJsonUnits } from 'circuitjson-toolkit'
2
2
  import { PcbScene3dCircuitJsonLayer } from './PcbScene3dCircuitJsonLayer.mjs'
3
3
  import { PcbScene3dCircuitJsonDocumentationArtworkBuilder } from './PcbScene3dCircuitJsonDocumentationArtworkBuilder.mjs'
4
+ import { PcbScene3dCircuitJsonSourceLayer } from './PcbScene3dCircuitJsonSourceLayer.mjs'
4
5
 
5
6
  const CURVE_SEGMENTS = 32
6
7
  const CAP_SEGMENTS = 16
@@ -21,6 +22,12 @@ export class PcbScene3dCircuitJsonSilkscreenBuilder {
21
22
 
22
23
  PcbScene3dCircuitJsonSilkscreenBuilder.#appendLines(index, top, bottom)
23
24
  PcbScene3dCircuitJsonSilkscreenBuilder.#appendPaths(index, top, bottom)
25
+ PcbScene3dCircuitJsonDocumentationArtworkBuilder.append(
26
+ index,
27
+ top,
28
+ bottom,
29
+ { sourceLayerFilter: 'silkscreen' }
30
+ )
24
31
  PcbScene3dCircuitJsonSilkscreenBuilder.#appendCircles(
25
32
  index,
26
33
  top,
@@ -36,18 +43,32 @@ export class PcbScene3dCircuitJsonSilkscreenBuilder {
36
43
  top,
37
44
  bottom
38
45
  )
46
+ PcbScene3dCircuitJsonSilkscreenBuilder.#appendTexts(
47
+ (index.elementsByType.get('pcb_note_text') || []).filter(
48
+ PcbScene3dCircuitJsonSourceLayer.isSilkscreen
49
+ ),
50
+ top,
51
+ bottom
52
+ )
39
53
  if (options?.showPcbNotes === true) {
40
54
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.append(
41
55
  index,
42
56
  top,
43
- bottom
57
+ bottom,
58
+ { sourceLayerFilter: 'non-silkscreen' }
44
59
  )
45
60
  PcbScene3dCircuitJsonSilkscreenBuilder.#appendTexts(
46
61
  [
47
62
  ...(index.elementsByType.get('pcb_note_text') || []),
48
63
  ...(index.elementsByType.get('pcb_fabrication_note_text') ||
49
64
  [])
50
- ],
65
+ ].filter(
66
+ (text) =>
67
+ !PcbScene3dCircuitJsonSourceLayer.isSilkscreen(text) &&
68
+ !PcbScene3dCircuitJsonSourceLayer.isCopperOrSolderMask(
69
+ text
70
+ )
71
+ ),
51
72
  top,
52
73
  bottom
53
74
  )
@@ -92,34 +113,107 @@ export class PcbScene3dCircuitJsonSilkscreenBuilder {
92
113
  */
93
114
  static #appendPaths(index, top, bottom) {
94
115
  ;(index.elementsByType.get('pcb_silkscreen_path') || []).forEach(
95
- (path) => {
116
+ (path, pathIndex) => {
96
117
  const target =
97
118
  PcbScene3dCircuitJsonSilkscreenBuilder.#sideDetail(
98
119
  path?.layer,
99
120
  top,
100
121
  bottom
101
122
  )
102
- const route = PcbScene3dCircuitJsonSilkscreenBuilder.#array(
103
- path?.route
123
+ PcbScene3dCircuitJsonSilkscreenBuilder.#appendPath(
124
+ target,
125
+ path,
126
+ PcbScene3dCircuitJsonSilkscreenBuilder.#sourceId(
127
+ path,
128
+ ['pcb_silkscreen_path_id', 'silkscreen_path_id'],
129
+ 'pcb_silkscreen_path',
130
+ pathIndex
131
+ )
104
132
  )
105
- for (let index = 0; index < route.length - 1; index += 1) {
106
- const start = route[index]
107
- const end = route[index + 1]
108
- target.tracks.push({
109
- x1: CircuitJsonUnits.mmToMil(start?.x, 0),
110
- y1: CircuitJsonUnits.mmToMil(start?.y, 0),
111
- x2: CircuitJsonUnits.mmToMil(end?.x, 0),
112
- y2: CircuitJsonUnits.mmToMil(end?.y, 0),
113
- width: CircuitJsonUnits.mmToMil(
114
- path?.stroke_width,
115
- 0.12
116
- )
117
- })
118
- }
119
133
  }
120
134
  )
121
135
  }
122
136
 
137
+ /**
138
+ * Appends one filled polygon or stroke path to a side detail container.
139
+ * @param {object} target Side-specific silkscreen detail.
140
+ * @param {object} path CircuitJSON path element.
141
+ * @param {string} sourceId Stable source ID.
142
+ * @returns {void}
143
+ */
144
+ static #appendPath(target, path, sourceId) {
145
+ const points = PcbScene3dCircuitJsonSilkscreenBuilder.#pathPoints(path)
146
+ if (points.length < 2) {
147
+ return
148
+ }
149
+
150
+ const fillPoints =
151
+ PcbScene3dCircuitJsonSilkscreenBuilder.#distinctPathPoints(points)
152
+ if (path?.fill === true && fillPoints.length >= 3) {
153
+ target.fills.push({
154
+ sourceId,
155
+ points: fillPoints
156
+ })
157
+ return
158
+ }
159
+
160
+ const width =
161
+ PcbScene3dCircuitJsonSilkscreenBuilder.#pathStrokeWidth(path)
162
+ for (let index = 0; index < points.length - 1; index += 1) {
163
+ const start = points[index]
164
+ const end = points[index + 1]
165
+ target.tracks.push({
166
+ sourceId,
167
+ x1: start.x,
168
+ y1: start.y,
169
+ x2: end.x,
170
+ y2: end.y,
171
+ width
172
+ })
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Converts one path route into valid millimeter-to-mil points.
178
+ * @param {object} path CircuitJSON path element.
179
+ * @returns {{ x: number, y: number }[]}
180
+ */
181
+ static #pathPoints(path) {
182
+ return PcbScene3dCircuitJsonSilkscreenBuilder.#array(
183
+ path?.route || path?.points
184
+ )
185
+ .map((point) =>
186
+ PcbScene3dCircuitJsonSilkscreenBuilder.#point(point)
187
+ )
188
+ .filter(Boolean)
189
+ }
190
+
191
+ /**
192
+ * Removes a duplicate closing point from one polygon route.
193
+ * @param {{ x: number, y: number }[]} points Route points.
194
+ * @returns {{ x: number, y: number }[]}
195
+ */
196
+ static #distinctPathPoints(points) {
197
+ if (points.length < 2) return points
198
+ const first = points[0]
199
+ const last = points[points.length - 1]
200
+ return first.x === last.x && first.y === last.y
201
+ ? points.slice(0, -1)
202
+ : points
203
+ }
204
+
205
+ /**
206
+ * Resolves a positive stroke width with the standard silkscreen fallback.
207
+ * @param {object} path CircuitJSON path element.
208
+ * @returns {number}
209
+ */
210
+ static #pathStrokeWidth(path) {
211
+ const width = Number(path?.stroke_width ?? path?.strokeWidth)
212
+ return Number.isFinite(width) && width > 0
213
+ ? CircuitJsonUnits.mmToMil(width, 0)
214
+ : CircuitJsonUnits.mmToMil(0.12, 0)
215
+ }
216
+
123
217
  /**
124
218
  * Appends circular silkscreen strokes as full-circle arcs.
125
219
  * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
@@ -246,6 +340,7 @@ export class PcbScene3dCircuitJsonSilkscreenBuilder {
246
340
  */
247
341
  static #appendTexts(texts, top, bottom) {
248
342
  texts.forEach((text) => {
343
+ if (text?.is_hidden === true || text?.isHidden === true) return
249
344
  const target = PcbScene3dCircuitJsonSilkscreenBuilder.#sideDetail(
250
345
  text?.layer,
251
346
  top,
@@ -265,8 +360,22 @@ export class PcbScene3dCircuitJsonSilkscreenBuilder {
265
360
  static #textPrimitive(text) {
266
361
  const position =
267
362
  PcbScene3dCircuitJsonSilkscreenBuilder.#textPosition(text)
268
- const size = CircuitJsonUnits.mmToMil(
269
- text?.font_size ?? text?.fontSize ?? text?.height ?? text?.size,
363
+ const sizeX = CircuitJsonUnits.mmToMil(
364
+ text?.font_width ??
365
+ text?.fontWidth ??
366
+ text?.font_size ??
367
+ text?.fontSize ??
368
+ text?.height ??
369
+ text?.size,
370
+ 1
371
+ )
372
+ const sizeY = CircuitJsonUnits.mmToMil(
373
+ text?.font_height ??
374
+ text?.fontHeight ??
375
+ text?.font_size ??
376
+ text?.fontSize ??
377
+ text?.height ??
378
+ text?.size,
270
379
  1
271
380
  )
272
381
  const strokeWidth = CircuitJsonUnits.mmToMil(
@@ -283,13 +392,17 @@ export class PcbScene3dCircuitJsonSilkscreenBuilder {
283
392
  x: position.x,
284
393
  y: position.y,
285
394
  rotation: Number(text?.ccw_rotation ?? text?.rotation ?? 0),
286
- sizeX: size,
287
- sizeY: size,
395
+ sizeX,
396
+ sizeY,
288
397
  width: strokeWidth,
289
398
  strokeWidth,
290
399
  thickness: strokeWidth,
291
400
  hAlign: alignment.hAlign,
292
- vAlign: alignment.vAlign
401
+ vAlign: alignment.vAlign,
402
+ mirrored:
403
+ text?.is_mirrored === true ||
404
+ text?.is_mirrored_from_top_view === true ||
405
+ text?.mirrored === true
293
406
  }
294
407
  }
295
408
 
@@ -315,7 +428,9 @@ export class PcbScene3dCircuitJsonSilkscreenBuilder {
315
428
  * @returns {{ hAlign: 'left' | 'center' | 'right', vAlign: 'top' | 'center' | 'bottom' }}
316
429
  */
317
430
  static #textAlignment(text) {
318
- const value = String(text?.anchor_alignment || '')
431
+ const value = String(
432
+ text?.source_anchor_alignment || text?.anchor_alignment || ''
433
+ )
319
434
  .trim()
320
435
  .toLowerCase()
321
436
  .replaceAll('-', '_')
@@ -0,0 +1,21 @@
1
+ import { PcbScene3dCircuitJsonSilkscreenBuilder } from './PcbScene3dCircuitJsonSilkscreenBuilder.mjs'
2
+ import { PcbScene3dSilkscreenCopperCutoutBuilder } from './PcbScene3dSilkscreenCopperCutoutBuilder.mjs'
3
+
4
+ /**
5
+ * Builds complete CircuitJSON silkscreen detail with surface keepouts.
6
+ */
7
+ export class PcbScene3dCircuitJsonSilkscreenDetailBuilder {
8
+ /**
9
+ * Builds and clips side-specific silkscreen detail.
10
+ * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
11
+ * @param {{ pads?: object[], vias?: object[], tracks?: object[], fills?: object[], polygons?: object[], copperTexts?: object[] }} detail Scene detail.
12
+ * @param {{ showPcbNotes?: boolean }} [options] Builder options.
13
+ * @returns {{ top: object, bottom: object }}
14
+ */
15
+ static build(index, detail, options = {}) {
16
+ return PcbScene3dSilkscreenCopperCutoutBuilder.apply(
17
+ PcbScene3dCircuitJsonSilkscreenBuilder.build(index, options),
18
+ detail
19
+ )
20
+ }
21
+ }
@@ -0,0 +1,124 @@
1
+ const OUTER_COPPER_LAYER_SIDES = new Map([
2
+ ['f.cu', 'top'],
3
+ ['front copper', 'top'],
4
+ ['top copper', 'top'],
5
+ ['top layer', 'top'],
6
+ ['b.cu', 'bottom'],
7
+ ['back copper', 'bottom'],
8
+ ['bottom copper', 'bottom'],
9
+ ['bottom layer', 'bottom']
10
+ ])
11
+
12
+ const SOLDER_MASK_LAYER_SIDES = new Map([
13
+ ['f.mask', 'top'],
14
+ ['front mask', 'top'],
15
+ ['top mask', 'top'],
16
+ ['front solder', 'top'],
17
+ ['top solder', 'top'],
18
+ ['front solder mask', 'top'],
19
+ ['top solder mask', 'top'],
20
+ ['b.mask', 'bottom'],
21
+ ['back mask', 'bottom'],
22
+ ['bottom mask', 'bottom'],
23
+ ['back solder', 'bottom'],
24
+ ['bottom solder', 'bottom'],
25
+ ['back solder mask', 'bottom'],
26
+ ['bottom solder mask', 'bottom']
27
+ ])
28
+
29
+ /**
30
+ * Classifies source-layer metadata retained beside canonical CircuitJSON rows.
31
+ */
32
+ export class PcbScene3dCircuitJsonSourceLayer {
33
+ /**
34
+ * Returns whether an element originated on a silkscreen layer.
35
+ * @param {object} element CircuitJSON element.
36
+ * @returns {boolean}
37
+ */
38
+ static isSilkscreen(element) {
39
+ return PcbScene3dCircuitJsonSourceLayer.#normalizedName(
40
+ element
41
+ ).includes('silk')
42
+ }
43
+
44
+ /**
45
+ * Resolves an outer-copper source layer to its board side.
46
+ * @param {object} element CircuitJSON element.
47
+ * @returns {'top' | 'bottom' | null}
48
+ */
49
+ static outerCopperSide(element) {
50
+ return (
51
+ OUTER_COPPER_LAYER_SIDES.get(
52
+ PcbScene3dCircuitJsonSourceLayer.#normalizedName(element)
53
+ ) || null
54
+ )
55
+ }
56
+
57
+ /**
58
+ * Returns whether an element originated on an outer copper layer.
59
+ * @param {object} element CircuitJSON element.
60
+ * @returns {boolean}
61
+ */
62
+ static isOuterCopper(element) {
63
+ return (
64
+ PcbScene3dCircuitJsonSourceLayer.outerCopperSide(element) !== null
65
+ )
66
+ }
67
+
68
+ /**
69
+ * Resolves a solder-mask source layer to its board side.
70
+ * @param {object} element CircuitJSON element.
71
+ * @returns {'top' | 'bottom' | null}
72
+ */
73
+ static solderMaskSide(element) {
74
+ return (
75
+ SOLDER_MASK_LAYER_SIDES.get(
76
+ PcbScene3dCircuitJsonSourceLayer.#normalizedName(element)
77
+ ) || null
78
+ )
79
+ }
80
+
81
+ /**
82
+ * Returns whether an element originated on a solder-mask layer.
83
+ * @param {object} element CircuitJSON element.
84
+ * @returns {boolean}
85
+ */
86
+ static isSolderMask(element) {
87
+ return PcbScene3dCircuitJsonSourceLayer.solderMaskSide(element) !== null
88
+ }
89
+
90
+ /**
91
+ * Returns whether an element belongs to copper or its mask aperture data.
92
+ * @param {object} element CircuitJSON element.
93
+ * @returns {boolean}
94
+ */
95
+ static isCopperOrSolderMask(element) {
96
+ return (
97
+ PcbScene3dCircuitJsonSourceLayer.isOuterCopper(element) ||
98
+ PcbScene3dCircuitJsonSourceLayer.isSolderMask(element)
99
+ )
100
+ }
101
+
102
+ /**
103
+ * Resolves a normalized source-layer name from one CircuitJSON element.
104
+ * @param {object} element CircuitJSON element.
105
+ * @returns {string}
106
+ */
107
+ static #normalizedName(element) {
108
+ return PcbScene3dCircuitJsonSourceLayer.#name(
109
+ element?.source_layer ?? element?.sourceLayer
110
+ ).toLowerCase()
111
+ }
112
+
113
+ /**
114
+ * Resolves a string layer name from scalar or object metadata.
115
+ * @param {unknown} layer Source-layer metadata.
116
+ * @returns {string}
117
+ */
118
+ static #name(layer) {
119
+ if (layer && typeof layer === 'object') {
120
+ return String(layer.name ?? layer.layer ?? '').trim()
121
+ }
122
+ return String(layer ?? '').trim()
123
+ }
124
+ }
@@ -1,5 +1,6 @@
1
1
  import { CircuitJsonUnits } from 'circuitjson-toolkit'
2
2
  import { PcbScene3dCircuitJsonLayer } from './PcbScene3dCircuitJsonLayer.mjs'
3
+ import { PcbScene3dViaLayerSpan } from './PcbScene3dViaLayerSpan.mjs'
3
4
 
4
5
  /**
5
6
  * Converts CircuitJSON trace routes into scene copper tracks and vias.
@@ -114,7 +115,8 @@ export class PcbScene3dCircuitJsonTraceRouteBuilder {
114
115
  0
115
116
  ),
116
117
  isTentingTop: isTented,
117
- isTentingBottom: isTented
118
+ isTentingBottom: isTented,
119
+ ...PcbScene3dViaLayerSpan.fields(via)
118
120
  }
119
121
  })
120
122
  }
@@ -172,7 +174,8 @@ export class PcbScene3dCircuitJsonTraceRouteBuilder {
172
174
  diameter,
173
175
  holeDiameter,
174
176
  isTentingTop: isTented,
175
- isTentingBottom: isTented
177
+ isTentingBottom: isTented,
178
+ ...PcbScene3dViaLayerSpan.fields(via)
176
179
  }
177
180
  }
178
181
 
@@ -290,11 +293,7 @@ export class PcbScene3dCircuitJsonTraceRouteBuilder {
290
293
  * @returns {boolean}
291
294
  */
292
295
  static #touchesSurface(via) {
293
- return Boolean(
294
- PcbScene3dCircuitJsonLayer.surfaceSide(via?.from_layer) ||
295
- PcbScene3dCircuitJsonLayer.surfaceSide(via?.to_layer) ||
296
- PcbScene3dCircuitJsonLayer.surfaceSide(via?.layer)
297
- )
296
+ return PcbScene3dViaLayerSpan.surfaceSides(via).length > 0
298
297
  }
299
298
 
300
299
  /**
@@ -93,7 +93,11 @@ export class PcbScene3dCopperDetailFilter {
93
93
  detail.polygons,
94
94
  defaultCovered
95
95
  ),
96
- copperTexts: [],
96
+ copperTexts:
97
+ PcbScene3dCopperDetailFilter.#filterMaskCoveredPrimitives(
98
+ detail.copperTexts,
99
+ defaultCovered
100
+ ),
97
101
  vias: []
98
102
  }
99
103
  }
@@ -100,7 +100,7 @@ export class PcbScene3dCopperFactory {
100
100
  /**
101
101
  * Builds top and bottom copper detail that is covered by solder mask.
102
102
  * @param {any} THREE
103
- * @param {{ tracks?: any[], arcs?: any[], fills?: any[], polygons?: any[] }} detail Mask-covered detail.
103
+ * @param {{ tracks?: any[], arcs?: any[], fills?: any[], polygons?: any[], copperTexts?: any[] }} detail Mask-covered detail.
104
104
  * @param {number} topZ
105
105
  * @param {number} bottomZ
106
106
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
@@ -146,7 +146,8 @@ export class PcbScene3dCopperFactory {
146
146
  fills: PcbScene3dCopperLayerFilter.fills(
147
147
  [...(detail?.fills || []), ...(detail?.polygons || [])],
148
148
  'top'
149
- )
149
+ ),
150
+ copperTexts: detail?.copperTexts || []
150
151
  },
151
152
  Math.abs(Number(topZ || 0)),
152
153
  normalizeBoardPoint,
@@ -166,7 +167,8 @@ export class PcbScene3dCopperFactory {
166
167
  fills: PcbScene3dCopperLayerFilter.fills(
167
168
  [...(detail?.fills || []), ...(detail?.polygons || [])],
168
169
  'bottom'
169
- )
170
+ ),
171
+ copperTexts: detail?.copperTexts || []
170
172
  },
171
173
  Math.abs(Number(bottomZ || 0)),
172
174
  normalizeBoardPoint,
@@ -267,7 +269,7 @@ export class PcbScene3dCopperFactory {
267
269
  /**
268
270
  * Builds one side of the mask-covered copper relief.
269
271
  * @param {any} THREE
270
- * @param {{ tracks?: any[], arcs?: any[], fills?: any[] }} detail Mask-covered detail.
272
+ * @param {{ tracks?: any[], arcs?: any[], fills?: any[], copperTexts?: any[] }} detail Mask-covered detail.
271
273
  * @param {number} z
272
274
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
273
275
  * @param {boolean} mirrorY
@@ -353,10 +355,27 @@ export class PcbScene3dCopperFactory {
353
355
  coverageContext
354
356
  }
355
357
  )
358
+ const textGroup = PcbScene3dCopperTextFactory.buildGroup(
359
+ THREE,
360
+ detail?.copperTexts || [],
361
+ z,
362
+ normalizeBoardPoint,
363
+ {
364
+ glyphYUp: PcbScene3dCopperFactory.#usesYUpGlyphs(options),
365
+ side: mirrorY ? 'bottom' : 'top',
366
+ mirrorY,
367
+ materialColor: PcbScene3dMaskCoveredCopperMaterial.resolveColor(
368
+ options?.solderMaskColor
369
+ ),
370
+ drillCutouts: occlusionCutouts
371
+ }
372
+ )
373
+ const textMesh = textGroup.children[0] || null
356
374
  return PcbScene3dMaskCoveredCopperSideGroupBuilder.build(THREE, {
357
375
  trackMesh,
358
376
  arcMesh,
359
377
  fillMesh,
378
+ textMesh,
360
379
  z,
361
380
  mirrorY
362
381
  })