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.
- package/README.md +7 -0
- package/docs/api.md +18 -0
- package/docs/circuitjson.md +6 -4
- package/docs/release-notes-v1.3.0.md +38 -0
- package/package.json +3 -2
- package/src/CircuitJsonCadModelAssetResolver.mjs +24 -10
- package/src/PcbAssemblyGltfModelMeshParser.mjs +3 -1
- package/src/PcbAssemblyModelMeshLoader.mjs +1 -1
- package/src/PcbAssemblyTextModelMeshParser.mjs +3 -1
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -11
- package/src/PcbScene3dBoardMaterialPalette.mjs +12 -0
- package/src/PcbScene3dCircuitJsonAdapter.mjs +33 -85
- package/src/PcbScene3dCircuitJsonCopperTextBuilder.mjs +385 -0
- package/src/PcbScene3dCircuitJsonDocumentationArtworkBuilder.mjs +123 -22
- package/src/PcbScene3dCircuitJsonGeometry.mjs +12 -0
- package/src/PcbScene3dCircuitJsonPadCorner.mjs +72 -0
- package/src/PcbScene3dCircuitJsonSilkscreenBuilder.mjs +140 -25
- package/src/PcbScene3dCircuitJsonSilkscreenDetailBuilder.mjs +21 -0
- package/src/PcbScene3dCircuitJsonSourceLayer.mjs +124 -0
- package/src/PcbScene3dCircuitJsonTraceRouteBuilder.mjs +6 -7
- package/src/PcbScene3dCopperDetailFilter.mjs +5 -1
- package/src/PcbScene3dCopperFactory.mjs +23 -4
- package/src/PcbScene3dCopperTextFactory.mjs +105 -20
- package/src/PcbScene3dExternalModels.mjs +26 -0
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +16 -1
- package/src/PcbScene3dModelContent.mjs +32 -1
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +2 -4
- package/src/PcbScene3dSilkscreenCopperCutoutBuilder.mjs +588 -0
- package/src/PcbScene3dStepLoader.mjs +2 -1
- package/src/PcbScene3dStrokeCutoutBuilder.mjs +98 -0
- package/src/PcbScene3dViaFactory.mjs +51 -5
- 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
|
-
|
|
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.
|