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,385 @@
1
+ import { CircuitJsonUnits } from 'circuitjson-toolkit'
2
+ import { PcbScene3dCircuitJsonLayer } from './PcbScene3dCircuitJsonLayer.mjs'
3
+ import { PcbScene3dCircuitJsonSourceLayer } from './PcbScene3dCircuitJsonSourceLayer.mjs'
4
+ import { PcbScene3dCopperTextFactory } from './PcbScene3dCopperTextFactory.mjs'
5
+ import { PcbScene3dPreparedPolygon } from './PcbScene3dPreparedPolygon.mjs'
6
+
7
+ const TEXT_TYPES = [
8
+ 'pcb_copper_text',
9
+ 'pcb_note_text',
10
+ 'pcb_fabrication_note_text'
11
+ ]
12
+
13
+ /**
14
+ * Projects CircuitJSON text with outer-copper provenance into scene copper
15
+ * primitives while resolving matching solder-mask openings.
16
+ */
17
+ export class PcbScene3dCircuitJsonCopperTextBuilder {
18
+ /**
19
+ * Builds visible and mask-covered copper text primitives.
20
+ * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
21
+ * @returns {object[]}
22
+ */
23
+ static build(index) {
24
+ const texts = TEXT_TYPES.flatMap(
25
+ (type) => index.elementsByType.get(type) || []
26
+ ).filter(PcbScene3dCircuitJsonCopperTextBuilder.#isVisibleText)
27
+ const maskIndex =
28
+ PcbScene3dCircuitJsonCopperTextBuilder.#buildMaskIndex(
29
+ texts.filter((text) =>
30
+ PcbScene3dCircuitJsonCopperTextBuilder.#maskSide(text)
31
+ )
32
+ )
33
+
34
+ return texts
35
+ .map((text) =>
36
+ PcbScene3dCircuitJsonCopperTextBuilder.#buildText(
37
+ text,
38
+ maskIndex
39
+ )
40
+ )
41
+ .filter(Boolean)
42
+ }
43
+
44
+ /**
45
+ * Indexes projected mask geometry by side and exact rendered text.
46
+ * @param {object[]} maskTexts Solder-mask source text rows.
47
+ * @returns {Map<string, object[]>}
48
+ */
49
+ static #buildMaskIndex(maskTexts) {
50
+ const index = new Map()
51
+ maskTexts.forEach((maskText) => {
52
+ const side =
53
+ PcbScene3dCircuitJsonCopperTextBuilder.#maskSide(maskText)
54
+ const geometry =
55
+ PcbScene3dCircuitJsonCopperTextBuilder.#textGeometry(maskText)
56
+ const key = PcbScene3dCircuitJsonCopperTextBuilder.#maskIndexKey(
57
+ side,
58
+ geometry.value
59
+ )
60
+ if (!index.has(key)) index.set(key, [])
61
+ index.get(key).push(geometry)
62
+ })
63
+ return index
64
+ }
65
+
66
+ /**
67
+ * Builds a collision-safe mask lookup key.
68
+ * @param {'top' | 'bottom'} side Board side.
69
+ * @param {unknown} value Rendered text value.
70
+ * @returns {string}
71
+ */
72
+ static #maskIndexKey(side, value) {
73
+ return JSON.stringify([side, String(value ?? '')])
74
+ }
75
+
76
+ /**
77
+ * Returns pre-indexed mask candidates for one copper text primitive.
78
+ * @param {Map<string, object[]>} maskIndex Mask geometry index.
79
+ * @param {'top' | 'bottom'} side Copper side.
80
+ * @param {object} geometry Copper text geometry.
81
+ * @returns {object[]}
82
+ */
83
+ static #maskCandidates(maskIndex, side, geometry) {
84
+ return (
85
+ maskIndex.get(
86
+ PcbScene3dCircuitJsonCopperTextBuilder.#maskIndexKey(
87
+ side,
88
+ geometry?.value
89
+ )
90
+ ) || []
91
+ )
92
+ }
93
+
94
+ /**
95
+ * Builds one copper text primitive.
96
+ * @param {object} text CircuitJSON text element.
97
+ * @param {Map<string, object[]>} maskIndex Indexed solder-mask geometry.
98
+ * @returns {object | null}
99
+ */
100
+ static #buildText(text, maskIndex) {
101
+ const side = PcbScene3dCircuitJsonCopperTextBuilder.#copperSide(text)
102
+ if (!side) return null
103
+
104
+ const geometry =
105
+ PcbScene3dCircuitJsonCopperTextBuilder.#textGeometry(text)
106
+ const explicitOpening =
107
+ PcbScene3dCircuitJsonCopperTextBuilder.#explicitMaskOpening(text)
108
+ const solderMaskOpening =
109
+ explicitOpening ??
110
+ PcbScene3dCircuitJsonCopperTextBuilder.#maskCandidates(
111
+ maskIndex,
112
+ side,
113
+ geometry
114
+ ).some((maskGeometry) =>
115
+ PcbScene3dCircuitJsonCopperTextBuilder.#maskCoversCopperText(
116
+ geometry,
117
+ maskGeometry
118
+ )
119
+ )
120
+
121
+ return {
122
+ sourceId: PcbScene3dCircuitJsonCopperTextBuilder.#sourceId(text),
123
+ sourceType: String(text?.source_type || text?.sourceType || ''),
124
+ ...geometry,
125
+ layer: side === 'bottom' ? 'B.Cu' : 'F.Cu',
126
+ side: side === 'bottom' ? 'back' : 'front',
127
+ layerId: PcbScene3dCircuitJsonLayer.layerId(side),
128
+ hasSolderMask: !solderMaskOpening,
129
+ solderMaskOpening
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Builds viewer text geometry in mils.
135
+ * @param {object} text CircuitJSON text element.
136
+ * @returns {object}
137
+ */
138
+ static #textGeometry(text) {
139
+ const position =
140
+ PcbScene3dCircuitJsonCopperTextBuilder.#textPosition(text)
141
+ const alignment =
142
+ PcbScene3dCircuitJsonCopperTextBuilder.#textAlignment(text)
143
+
144
+ return {
145
+ x: CircuitJsonUnits.mmToMil(position.x, 0),
146
+ y: CircuitJsonUnits.mmToMil(position.y, 0),
147
+ value: String(text?.text ?? text?.value ?? ''),
148
+ rotation: Number(text?.ccw_rotation ?? text?.rotation ?? 0),
149
+ mirrored:
150
+ text?.is_mirrored === true ||
151
+ text?.is_mirrored_from_top_view === true ||
152
+ text?.mirrored === true,
153
+ hAlign: alignment.hAlign,
154
+ vAlign: alignment.vAlign,
155
+ sizeX: CircuitJsonUnits.mmToMil(
156
+ text?.font_width ??
157
+ text?.fontWidth ??
158
+ text?.font_size ??
159
+ text?.fontSize,
160
+ 1
161
+ ),
162
+ sizeY: CircuitJsonUnits.mmToMil(
163
+ text?.font_height ??
164
+ text?.fontHeight ??
165
+ text?.font_size ??
166
+ text?.fontSize,
167
+ 1
168
+ ),
169
+ thickness: CircuitJsonUnits.mmToMil(
170
+ text?.stroke_width ?? text?.strokeWidth,
171
+ 0.12
172
+ )
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Resolves the text insertion point in millimeters.
178
+ * @param {object} text CircuitJSON text element.
179
+ * @returns {{ x: number, y: number }}
180
+ */
181
+ static #textPosition(text) {
182
+ const position = text?.anchor_position || text?.position || text
183
+ const x = Number(position?.x)
184
+ const y = Number(position?.y)
185
+ return {
186
+ x: Number.isFinite(x) ? x : 0,
187
+ y: Number.isFinite(y) ? y : 0
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Resolves viewer alignment from canonical anchor metadata.
193
+ * @param {object} text CircuitJSON text element.
194
+ * @returns {{ hAlign: 'left' | 'center' | 'right', vAlign: 'top' | 'center' | 'bottom' }}
195
+ */
196
+ static #textAlignment(text) {
197
+ const value = String(
198
+ text?.source_anchor_alignment || text?.anchor_alignment || ''
199
+ )
200
+ .trim()
201
+ .toLowerCase()
202
+ .replaceAll('-', '_')
203
+ return {
204
+ hAlign: value.includes('left')
205
+ ? 'left'
206
+ : value.includes('right')
207
+ ? 'right'
208
+ : 'center',
209
+ vAlign: value.includes('bottom')
210
+ ? 'bottom'
211
+ : value.includes('center') || value.includes('middle')
212
+ ? 'center'
213
+ : 'top'
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Checks whether a canonical text row is visible.
219
+ * @param {object} text CircuitJSON text element.
220
+ * @returns {boolean}
221
+ */
222
+ static #isVisibleText(text) {
223
+ return text?.is_hidden !== true && text?.isHidden !== true
224
+ }
225
+
226
+ /**
227
+ * Resolves a copper source layer to an outer board side.
228
+ * @param {object} text CircuitJSON text element.
229
+ * @returns {'top' | 'bottom' | null}
230
+ */
231
+ static #copperSide(text) {
232
+ if (String(text?.type || '') === 'pcb_copper_text') {
233
+ return PcbScene3dCircuitJsonLayer.surfaceSide(text?.layer)
234
+ }
235
+ return PcbScene3dCircuitJsonSourceLayer.outerCopperSide(text)
236
+ }
237
+
238
+ /**
239
+ * Resolves a solder-mask source layer to an outer board side.
240
+ * @param {object} text CircuitJSON text element.
241
+ * @returns {'top' | 'bottom' | null}
242
+ */
243
+ static #maskSide(text) {
244
+ return PcbScene3dCircuitJsonSourceLayer.solderMaskSide(text)
245
+ }
246
+
247
+ /**
248
+ * Resolves explicit solder-mask coverage when provided.
249
+ * @param {object} text CircuitJSON text element.
250
+ * @returns {boolean | null} True when open, false when covered, or null.
251
+ */
252
+ static #explicitMaskOpening(text) {
253
+ const value =
254
+ text?.is_covered_with_solder_mask ?? text?.covered_with_solder_mask
255
+ if (typeof value === 'boolean') return !value
256
+ if (value === undefined || value === null || value === '') return null
257
+ const normalized = String(value).trim().toLowerCase()
258
+ if (normalized === 'true') return false
259
+ if (normalized === 'false') return true
260
+ return null
261
+ }
262
+
263
+ /**
264
+ * Checks whether one mask text is a compatible geometric cover.
265
+ * @param {object} copperGeometry Projected copper text geometry.
266
+ * @param {object} maskGeometry Projected mask text geometry.
267
+ * @returns {boolean}
268
+ */
269
+ static #maskCoversCopperText(copperGeometry, maskGeometry) {
270
+ if (
271
+ !PcbScene3dCircuitJsonCopperTextBuilder.#compatiblePlacement(
272
+ copperGeometry,
273
+ maskGeometry
274
+ )
275
+ ) {
276
+ return false
277
+ }
278
+
279
+ return PcbScene3dCircuitJsonCopperTextBuilder.#strokeGeometryCovers(
280
+ copperGeometry,
281
+ maskGeometry
282
+ )
283
+ }
284
+
285
+ /**
286
+ * Checks non-size fields required for paired source text.
287
+ * @param {object} copper Copper geometry.
288
+ * @param {object} mask Mask geometry.
289
+ * @returns {boolean}
290
+ */
291
+ static #compatiblePlacement(copper, mask) {
292
+ return (
293
+ String(copper?.value || '') === String(mask?.value || '') &&
294
+ PcbScene3dCircuitJsonCopperTextBuilder.#near(copper?.x, mask?.x) &&
295
+ PcbScene3dCircuitJsonCopperTextBuilder.#near(copper?.y, mask?.y) &&
296
+ PcbScene3dCircuitJsonCopperTextBuilder.#nearRotation(
297
+ copper?.rotation,
298
+ mask?.rotation
299
+ ) &&
300
+ Boolean(copper?.mirrored) === Boolean(mask?.mirrored) &&
301
+ copper?.hAlign === mask?.hAlign &&
302
+ copper?.vAlign === mask?.vAlign
303
+ )
304
+ }
305
+
306
+ /**
307
+ * Checks whether mask stroke polygons fully cover copper stroke polygons.
308
+ * @param {object} copper Copper geometry.
309
+ * @param {object} mask Mask geometry.
310
+ * @returns {boolean}
311
+ */
312
+ static #strokeGeometryCovers(copper, mask) {
313
+ if (
314
+ PcbScene3dCircuitJsonCopperTextBuilder.#near(
315
+ copper?.sizeX,
316
+ mask?.sizeX
317
+ ) &&
318
+ PcbScene3dCircuitJsonCopperTextBuilder.#near(
319
+ copper?.sizeY,
320
+ mask?.sizeY
321
+ ) &&
322
+ Number(mask?.thickness) + 0.001 >= Number(copper?.thickness)
323
+ ) {
324
+ return true
325
+ }
326
+
327
+ const copperCutouts = PcbScene3dCopperTextFactory.strokeCutouts(copper)
328
+ const maskCutouts = PcbScene3dCopperTextFactory.strokeCutouts(mask, {
329
+ alignmentStrokeWidth: copper?.thickness
330
+ })
331
+ if (
332
+ !copperCutouts.length ||
333
+ copperCutouts.length !== maskCutouts.length
334
+ ) {
335
+ return false
336
+ }
337
+
338
+ const preparedMasks = maskCutouts.map(
339
+ (cutout) => new PcbScene3dPreparedPolygon(cutout)
340
+ )
341
+ // Glyph segments retain source order and each capsule is convex, so
342
+ // vertex containment proves full coverage without sampling or unions.
343
+ return copperCutouts.every((cutout, index) =>
344
+ cutout.every((point) =>
345
+ preparedMasks[index].containsPointOrBoundary(point)
346
+ )
347
+ )
348
+ }
349
+
350
+ /**
351
+ * Checks two projected coordinates with float-conversion tolerance.
352
+ * @param {unknown} left First value.
353
+ * @param {unknown} right Second value.
354
+ * @returns {boolean}
355
+ */
356
+ static #near(left, right) {
357
+ return Math.abs(Number(left) - Number(right)) <= 0.001
358
+ }
359
+
360
+ /**
361
+ * Checks two degree rotations modulo one full turn.
362
+ * @param {unknown} left First rotation.
363
+ * @param {unknown} right Second rotation.
364
+ * @returns {boolean}
365
+ */
366
+ static #nearRotation(left, right) {
367
+ const delta = Math.abs(Number(left) - Number(right)) % 360
368
+ return Math.min(delta, 360 - delta) <= 0.001
369
+ }
370
+
371
+ /**
372
+ * Resolves a stable source identifier.
373
+ * @param {object} text CircuitJSON text element.
374
+ * @returns {string}
375
+ */
376
+ static #sourceId(text) {
377
+ return String(
378
+ text?.pcb_copper_text_id ||
379
+ text?.pcb_note_text_id ||
380
+ text?.pcb_fabrication_note_text_id ||
381
+ text?.id ||
382
+ ''
383
+ )
384
+ }
385
+ }
@@ -1,5 +1,6 @@
1
1
  import { CircuitJsonUnits } from 'circuitjson-toolkit'
2
2
  import { PcbScene3dCircuitJsonLayer } from './PcbScene3dCircuitJsonLayer.mjs'
3
+ import { PcbScene3dCircuitJsonSourceLayer } from './PcbScene3dCircuitJsonSourceLayer.mjs'
3
4
 
4
5
  const CIRCLE_SEGMENTS = 32
5
6
 
@@ -12,33 +13,39 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
12
13
  * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
13
14
  * @param {object} top Top-side silkscreen detail.
14
15
  * @param {object} bottom Bottom-side silkscreen detail.
16
+ * @param {{ sourceLayerFilter?: 'all' | 'silkscreen' | 'non-silkscreen' }} [options] Source-layer filter.
15
17
  * @returns {void}
16
18
  */
17
- static append(index, top, bottom) {
19
+ static append(index, top, bottom, options = {}) {
18
20
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#appendRectOutlines(
19
21
  index,
20
22
  top,
21
- bottom
23
+ bottom,
24
+ options
22
25
  )
23
26
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#appendCircleOutlines(
24
27
  index,
25
28
  top,
26
- bottom
29
+ bottom,
30
+ options
27
31
  )
28
32
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#appendPointOutlines(
29
33
  index,
30
34
  top,
31
- bottom
35
+ bottom,
36
+ options
32
37
  )
33
38
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#appendLines(
34
39
  index,
35
40
  top,
36
- bottom
41
+ bottom,
42
+ options
37
43
  )
38
44
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#appendPaths(
39
45
  index,
40
46
  top,
41
- bottom
47
+ bottom,
48
+ options
42
49
  )
43
50
  }
44
51
 
@@ -47,15 +54,24 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
47
54
  * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
48
55
  * @param {object} top Top-side silkscreen detail.
49
56
  * @param {object} bottom Bottom-side silkscreen detail.
57
+ * @param {{ sourceLayerFilter?: string }} options Source-layer filter.
50
58
  * @returns {void}
51
59
  */
52
- static #appendRectOutlines(index, top, bottom) {
60
+ static #appendRectOutlines(index, top, bottom, options) {
53
61
  const specs = [
54
62
  {
55
63
  type: 'pcb_note_rect',
56
64
  fallbackPrefix: 'pcb_note_rect',
57
65
  idFields: ['pcb_note_rect_id', 'note_rect_id']
58
66
  },
67
+ {
68
+ type: 'pcb_fabrication_note_rect',
69
+ fallbackPrefix: 'pcb_fabrication_note_rect',
70
+ idFields: [
71
+ 'pcb_fabrication_note_rect_id',
72
+ 'fabrication_note_rect_id'
73
+ ]
74
+ },
59
75
  {
60
76
  type: 'pcb_courtyard_rect',
61
77
  fallbackPrefix: 'pcb_courtyard_rect',
@@ -66,6 +82,14 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
66
82
  specs.forEach((spec) => {
67
83
  ;(index.elementsByType.get(spec.type) || []).forEach(
68
84
  (element, elementIndex) => {
85
+ if (
86
+ !PcbScene3dCircuitJsonDocumentationArtworkBuilder.#matchesSourceLayer(
87
+ element,
88
+ options
89
+ )
90
+ ) {
91
+ return
92
+ }
69
93
  const points =
70
94
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#rectanglePoints(
71
95
  element
@@ -88,11 +112,20 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
88
112
  * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
89
113
  * @param {object} top Top-side silkscreen detail.
90
114
  * @param {object} bottom Bottom-side silkscreen detail.
115
+ * @param {{ sourceLayerFilter?: string }} options Source-layer filter.
91
116
  * @returns {void}
92
117
  */
93
- static #appendCircleOutlines(index, top, bottom) {
118
+ static #appendCircleOutlines(index, top, bottom, options) {
94
119
  ;(index.elementsByType.get('pcb_courtyard_circle') || []).forEach(
95
120
  (circle, circleIndex) => {
121
+ if (
122
+ !PcbScene3dCircuitJsonDocumentationArtworkBuilder.#matchesSourceLayer(
123
+ circle,
124
+ options
125
+ )
126
+ ) {
127
+ return
128
+ }
96
129
  const points =
97
130
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#circlePoints(
98
131
  circle
@@ -120,11 +153,20 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
120
153
  * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
121
154
  * @param {object} top Top-side silkscreen detail.
122
155
  * @param {object} bottom Bottom-side silkscreen detail.
156
+ * @param {{ sourceLayerFilter?: string }} options Source-layer filter.
123
157
  * @returns {void}
124
158
  */
125
- static #appendPointOutlines(index, top, bottom) {
159
+ static #appendPointOutlines(index, top, bottom, options) {
126
160
  ;(index.elementsByType.get('pcb_courtyard_outline') || []).forEach(
127
161
  (outline, outlineIndex) => {
162
+ if (
163
+ !PcbScene3dCircuitJsonDocumentationArtworkBuilder.#matchesSourceLayer(
164
+ outline,
165
+ options
166
+ )
167
+ ) {
168
+ return
169
+ }
128
170
  const points =
129
171
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#pointList(
130
172
  outline?.outline || outline?.points
@@ -152,11 +194,20 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
152
194
  * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
153
195
  * @param {object} top Top-side silkscreen detail.
154
196
  * @param {object} bottom Bottom-side silkscreen detail.
197
+ * @param {{ sourceLayerFilter?: string }} options Source-layer filter.
155
198
  * @returns {void}
156
199
  */
157
- static #appendLines(index, top, bottom) {
200
+ static #appendLines(index, top, bottom, options) {
158
201
  ;(index.elementsByType.get('pcb_note_line') || []).forEach(
159
202
  (line, lineIndex) => {
203
+ if (
204
+ !PcbScene3dCircuitJsonDocumentationArtworkBuilder.#matchesSourceLayer(
205
+ line,
206
+ options
207
+ )
208
+ ) {
209
+ return
210
+ }
160
211
  const start =
161
212
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#lineStart(
162
213
  line
@@ -200,9 +251,10 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
200
251
  * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
201
252
  * @param {object} top Top-side silkscreen detail.
202
253
  * @param {object} bottom Bottom-side silkscreen detail.
254
+ * @param {{ sourceLayerFilter?: string }} options Source-layer filter.
203
255
  * @returns {void}
204
256
  */
205
- static #appendPaths(index, top, bottom) {
257
+ static #appendPaths(index, top, bottom, options) {
206
258
  const specs = [
207
259
  {
208
260
  type: 'pcb_note_path',
@@ -222,6 +274,14 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
222
274
  specs.forEach((spec) => {
223
275
  ;(index.elementsByType.get(spec.type) || []).forEach(
224
276
  (path, pathIndex) => {
277
+ if (
278
+ !PcbScene3dCircuitJsonDocumentationArtworkBuilder.#matchesSourceLayer(
279
+ path,
280
+ options
281
+ )
282
+ ) {
283
+ return
284
+ }
225
285
  const target =
226
286
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#sideDetail(
227
287
  path?.layer,
@@ -232,6 +292,12 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
232
292
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#array(
233
293
  path?.route || path?.points
234
294
  )
295
+ .map((point) =>
296
+ PcbScene3dCircuitJsonDocumentationArtworkBuilder.#point(
297
+ point
298
+ )
299
+ )
300
+ .filter(Boolean)
235
301
  const sourceId =
236
302
  PcbScene3dCircuitJsonDocumentationArtworkBuilder.#sourceId(
237
303
  path,
@@ -239,18 +305,17 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
239
305
  spec.fallbackPrefix,
240
306
  pathIndex
241
307
  )
308
+ const fillPoints =
309
+ PcbScene3dCircuitJsonDocumentationArtworkBuilder.#distinctClosingPoint(
310
+ route
311
+ )
312
+ if (path?.fill === true && fillPoints.length >= 3) {
313
+ target.fills.push({ sourceId, points: fillPoints })
314
+ return
315
+ }
242
316
  for (let index = 0; index < route.length - 1; index += 1) {
243
- const start =
244
- PcbScene3dCircuitJsonDocumentationArtworkBuilder.#point(
245
- route[index]
246
- )
247
- const end =
248
- PcbScene3dCircuitJsonDocumentationArtworkBuilder.#point(
249
- route[index + 1]
250
- )
251
- if (!start || !end) {
252
- continue
253
- }
317
+ const start = route[index]
318
+ const end = route[index + 1]
254
319
  target.tracks.push({
255
320
  sourceId,
256
321
  x1: start.x,
@@ -300,6 +365,11 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
300
365
  element
301
366
  )
302
367
 
368
+ if (element?.fill === true) {
369
+ target.fills.push({ sourceId, points })
370
+ return
371
+ }
372
+
303
373
  for (let index = 0; index < points.length; index += 1) {
304
374
  const start = points[index]
305
375
  const end = points[(index + 1) % points.length]
@@ -565,6 +635,37 @@ export class PcbScene3dCircuitJsonDocumentationArtworkBuilder {
565
635
  )
566
636
  }
567
637
 
638
+ /**
639
+ * Applies the requested source-layer classification filter.
640
+ * @param {object} element CircuitJSON element.
641
+ * @param {{ sourceLayerFilter?: string }} options Source-layer filter.
642
+ * @returns {boolean}
643
+ */
644
+ static #matchesSourceLayer(element, options) {
645
+ const filter = String(options?.sourceLayerFilter || 'all')
646
+ if (filter === 'silkscreen') {
647
+ return PcbScene3dCircuitJsonSourceLayer.isSilkscreen(element)
648
+ }
649
+ if (filter === 'non-silkscreen') {
650
+ return !PcbScene3dCircuitJsonSourceLayer.isSilkscreen(element)
651
+ }
652
+ return true
653
+ }
654
+
655
+ /**
656
+ * Removes a duplicate closing point while preserving route order.
657
+ * @param {{ x: number, y: number }[]} points Route points.
658
+ * @returns {{ x: number, y: number }[]}
659
+ */
660
+ static #distinctClosingPoint(points) {
661
+ if (points.length < 2) return points
662
+ const first = points[0]
663
+ const last = points[points.length - 1]
664
+ return first.x === last.x && first.y === last.y
665
+ ? points.slice(0, -1)
666
+ : points
667
+ }
668
+
568
669
  /**
569
670
  * Converts one point from millimeters to mils.
570
671
  * @param {unknown} point Candidate point.
@@ -16,6 +16,18 @@ const DRILL_QUALITY_POINTS = {
16
16
  * Resolves direct CircuitJSON board geometry, cutouts, and drill metadata.
17
17
  */
18
18
  export class PcbScene3dCircuitJsonGeometry {
19
+ /**
20
+ * Resolves plated-hole copper dimensions in viewer mils.
21
+ * @param {object} geometry Shared drilled-primitive geometry.
22
+ * @returns {{ width: number, height: number }}
23
+ */
24
+ static platedHoleOuterSize(geometry) {
25
+ return {
26
+ width: CircuitJsonUnits.mmToMil(geometry?.width, 1),
27
+ height: CircuitJsonUnits.mmToMil(geometry?.height, 1)
28
+ }
29
+ }
30
+
19
31
  /**
20
32
  * Builds the render-model board from a `pcb_panel` or `pcb_board` element.
21
33
  * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.