pcb-scene3d-viewer 1.0.1
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/AGENTS.md +67 -0
- package/COMMERCIAL-LICENSE.md +15 -0
- package/CONTRIBUTING.md +14 -0
- package/LICENSE +19 -0
- package/LICENSES/AGPL-3.0-or-later.txt +235 -0
- package/LICENSES/CC-BY-SA-4.0.txt +170 -0
- package/LICENSES/LGPL-2.1-or-later.txt +176 -0
- package/LICENSES/LicenseRef-PolyForm-Noncommercial-1.0.0.txt +131 -0
- package/NOTICE.md +36 -0
- package/README.md +128 -0
- package/REUSE.toml +16 -0
- package/docs/api.md +148 -0
- package/docs/circuitjson.md +190 -0
- package/docs/model-format.md +117 -0
- package/docs/testing.md +23 -0
- package/package.json +65 -0
- package/spec/library-scope.md +36 -0
- package/src/PcbModelArchiveExporter.mjs +320 -0
- package/src/PcbScene3dArcUtils.mjs +27 -0
- package/src/PcbScene3dBoardAssemblyPlacement.mjs +36 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +859 -0
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +537 -0
- package/src/PcbScene3dBoardMaterialPalette.mjs +40 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +895 -0
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +613 -0
- package/src/PcbScene3dCameraRig.mjs +168 -0
- package/src/PcbScene3dCircuitJsonAdapter.mjs +545 -0
- package/src/PcbScene3dController.mjs +956 -0
- package/src/PcbScene3dCopperDetailFilter.mjs +490 -0
- package/src/PcbScene3dCopperFactory.mjs +559 -0
- package/src/PcbScene3dCopperTextFactory.mjs +534 -0
- package/src/PcbScene3dCutoutGeometryFilter.mjs +873 -0
- package/src/PcbScene3dDetailCoordinateNormalizer.mjs +65 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +224 -0
- package/src/PcbScene3dDrillPathFactory.mjs +362 -0
- package/src/PcbScene3dDrillVoidFactory.mjs +268 -0
- package/src/PcbScene3dExternalModelLoadOrder.mjs +54 -0
- package/src/PcbScene3dExternalModels.mjs +968 -0
- package/src/PcbScene3dFallbackVisibility.mjs +82 -0
- package/src/PcbScene3dInteractionHints.mjs +56 -0
- package/src/PcbScene3dMountRig.mjs +53 -0
- package/src/PcbScene3dOutlineBuilder.mjs +210 -0
- package/src/PcbScene3dPadFactory.mjs +553 -0
- package/src/PcbScene3dPresetState.mjs +48 -0
- package/src/PcbScene3dRenderGroupVisibility.mjs +134 -0
- package/src/PcbScene3dRuntime.mjs +996 -0
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +99 -0
- package/src/PcbScene3dSelectionStyler.mjs +252 -0
- package/src/PcbScene3dShapePathFactory.mjs +220 -0
- package/src/PcbScene3dShellRenderer.mjs +131 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +854 -0
- package/src/PcbScene3dSilkscreenStrokeWidthResolver.mjs +81 -0
- package/src/PcbScene3dStepLoader.mjs +611 -0
- package/src/PcbScene3dStrokeFont.mjs +671 -0
- package/src/PcbScene3dStrokeGeometryBuilder.mjs +322 -0
- package/src/PcbScene3dText.mjs +99 -0
- package/src/PcbScene3dTrueTypeTextFactory.mjs +885 -0
- package/src/PcbScene3dViaFactory.mjs +176 -0
- package/src/PcbScene3dViewCompensation.mjs +109 -0
- package/src/PcbScene3dViewScale.mjs +24 -0
- package/src/PcbScene3dViewportResize.mjs +35 -0
- package/src/PcbScene3dWorkerClient.mjs +123 -0
- package/src/index.mjs +1 -0
- package/src/scene3d.mjs +44 -0
- package/src/styles/scene3d.css +295 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CircuitJsonDocument,
|
|
3
|
+
CircuitJsonIndexer,
|
|
4
|
+
CircuitJsonUnits
|
|
5
|
+
} from 'circuitjson-toolkit'
|
|
6
|
+
|
|
7
|
+
const TOP_LAYER_ID = 1
|
|
8
|
+
const BOTTOM_LAYER_ID = 32
|
|
9
|
+
const DEFAULT_BOARD_WIDTH_MM = 25.4
|
|
10
|
+
const DEFAULT_BOARD_HEIGHT_MM = 25.4
|
|
11
|
+
const DEFAULT_BOARD_THICKNESS_MM = 1.6
|
|
12
|
+
const DEFAULT_COMPONENT_HEIGHT_MIL = 60
|
|
13
|
+
const RECTANGULAR_PAD_SHAPE = 2
|
|
14
|
+
const CIRCULAR_PAD_SHAPE = 1
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converts serialized CircuitJSON element arrays into the viewer render model.
|
|
18
|
+
*/
|
|
19
|
+
export class PcbScene3dCircuitJsonAdapter {
|
|
20
|
+
/**
|
|
21
|
+
* Returns true when a value is a serialized CircuitJSON model.
|
|
22
|
+
* @param {unknown} value Candidate model.
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
static isCircuitJsonModel(value) {
|
|
26
|
+
return CircuitJsonDocument.isModel(value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns true when a model should bypass format-specific scene builders.
|
|
31
|
+
* @param {unknown} value Candidate model.
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
static isDirectCircuitJsonModel(value) {
|
|
35
|
+
if (!PcbScene3dCircuitJsonAdapter.isCircuitJsonModel(value)) {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (String(value?.sourceFormat || '') === 'circuitjson') {
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return !PcbScene3dCircuitJsonAdapter.#hasCompatibilityModel(value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds the internal render model used by the Three.js runtime.
|
|
48
|
+
* @param {object[]} circuitJson Serialized CircuitJSON model.
|
|
49
|
+
* @returns {object}
|
|
50
|
+
*/
|
|
51
|
+
static build(circuitJson) {
|
|
52
|
+
CircuitJsonDocument.assertModel(circuitJson)
|
|
53
|
+
const index = CircuitJsonIndexer.index(circuitJson)
|
|
54
|
+
const board = PcbScene3dCircuitJsonAdapter.#buildBoard(index)
|
|
55
|
+
const detail = PcbScene3dCircuitJsonAdapter.#buildDetail(index)
|
|
56
|
+
const components = PcbScene3dCircuitJsonAdapter.#buildComponents(
|
|
57
|
+
index,
|
|
58
|
+
board
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
sourceFormat: 'circuitjson',
|
|
63
|
+
coordinateSystem: 'circuitjson-mm',
|
|
64
|
+
board,
|
|
65
|
+
components,
|
|
66
|
+
externalPlacements: [],
|
|
67
|
+
boardAssemblyModel: null,
|
|
68
|
+
detail
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Builds the render-model board from a `pcb_board` element.
|
|
74
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
75
|
+
* @returns {object}
|
|
76
|
+
*/
|
|
77
|
+
static #buildBoard(index) {
|
|
78
|
+
const boardElement = index.elementsByType.get('pcb_board')?.[0] || {}
|
|
79
|
+
const widthMil = CircuitJsonUnits.mmToMil(
|
|
80
|
+
boardElement.width,
|
|
81
|
+
DEFAULT_BOARD_WIDTH_MM
|
|
82
|
+
)
|
|
83
|
+
const heightMil = CircuitJsonUnits.mmToMil(
|
|
84
|
+
boardElement.height,
|
|
85
|
+
DEFAULT_BOARD_HEIGHT_MM
|
|
86
|
+
)
|
|
87
|
+
const thicknessMil = CircuitJsonUnits.mmToMil(
|
|
88
|
+
boardElement.thickness,
|
|
89
|
+
DEFAULT_BOARD_THICKNESS_MM
|
|
90
|
+
)
|
|
91
|
+
const center = CircuitJsonUnits.pointMmToMil(boardElement.center || {})
|
|
92
|
+
const minX = center.x - widthMil / 2
|
|
93
|
+
const minY = center.y - heightMil / 2
|
|
94
|
+
const segments = PcbScene3dCircuitJsonAdapter.#buildBoardSegments(
|
|
95
|
+
boardElement,
|
|
96
|
+
{
|
|
97
|
+
minX,
|
|
98
|
+
minY,
|
|
99
|
+
widthMil,
|
|
100
|
+
heightMil
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
widthMil,
|
|
106
|
+
heightMil,
|
|
107
|
+
thicknessMil,
|
|
108
|
+
minX,
|
|
109
|
+
minY,
|
|
110
|
+
centerX: center.x,
|
|
111
|
+
centerY: center.y,
|
|
112
|
+
segments,
|
|
113
|
+
surfaceColor: null,
|
|
114
|
+
edgeColor: null
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Returns true for parser-produced arrays that already carry a legacy
|
|
120
|
+
* renderer model and must keep using their format-specific scene builder.
|
|
121
|
+
* @param {unknown} value Candidate model.
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
static #hasCompatibilityModel(value) {
|
|
125
|
+
return Boolean(value?.pcb || value?.schematic || value?.bom)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Builds closed board line segments from outline points or rectangle size.
|
|
130
|
+
* @param {object} boardElement CircuitJSON board element.
|
|
131
|
+
* @param {{ minX: number, minY: number, widthMil: number, heightMil: number }} fallback Fallback rectangle.
|
|
132
|
+
* @returns {object[]}
|
|
133
|
+
*/
|
|
134
|
+
static #buildBoardSegments(boardElement, fallback) {
|
|
135
|
+
const outlinePoints = Array.isArray(boardElement?.outline)
|
|
136
|
+
? boardElement.outline
|
|
137
|
+
.map((point) => CircuitJsonUnits.pointMmToMil(point))
|
|
138
|
+
.filter((point) => Number.isFinite(point.x + point.y))
|
|
139
|
+
: []
|
|
140
|
+
const points =
|
|
141
|
+
outlinePoints.length >= 3
|
|
142
|
+
? outlinePoints
|
|
143
|
+
: [
|
|
144
|
+
{ x: fallback.minX, y: fallback.minY },
|
|
145
|
+
{
|
|
146
|
+
x: fallback.minX + fallback.widthMil,
|
|
147
|
+
y: fallback.minY
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
x: fallback.minX + fallback.widthMil,
|
|
151
|
+
y: fallback.minY + fallback.heightMil
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
x: fallback.minX,
|
|
155
|
+
y: fallback.minY + fallback.heightMil
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
return points.map((point, index) => {
|
|
160
|
+
const next = points[(index + 1) % points.length]
|
|
161
|
+
return {
|
|
162
|
+
type: 'line',
|
|
163
|
+
x1: point.x,
|
|
164
|
+
y1: point.y,
|
|
165
|
+
x2: next.x,
|
|
166
|
+
y2: next.y
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Builds component fallback bodies from `pcb_component` elements.
|
|
173
|
+
* @param {{ elementsByType: Map<string, object[]>, sourceComponentById: Map<string, object> }} index CircuitJSON index.
|
|
174
|
+
* @param {{ centerX: number, centerY: number, thicknessMil: number }} board Render board.
|
|
175
|
+
* @returns {object[]}
|
|
176
|
+
*/
|
|
177
|
+
static #buildComponents(index, board) {
|
|
178
|
+
return (index.elementsByType.get('pcb_component') || []).map(
|
|
179
|
+
(component, componentIndex) => {
|
|
180
|
+
const sourceComponent = index.sourceComponentById.get(
|
|
181
|
+
String(component?.source_component_id || '')
|
|
182
|
+
)
|
|
183
|
+
const center = CircuitJsonUnits.pointMmToMil(
|
|
184
|
+
component?.center || {
|
|
185
|
+
x: component?.x,
|
|
186
|
+
y: component?.y
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
const width = CircuitJsonUnits.mmToMil(component?.width, 2)
|
|
190
|
+
const depth = CircuitJsonUnits.mmToMil(component?.height, 1.2)
|
|
191
|
+
const height = CircuitJsonUnits.mmToMil(
|
|
192
|
+
component?.component_height,
|
|
193
|
+
DEFAULT_COMPONENT_HEIGHT_MIL / 39.37007874015748
|
|
194
|
+
)
|
|
195
|
+
const designator =
|
|
196
|
+
PcbScene3dCircuitJsonAdapter.#componentDesignator(
|
|
197
|
+
component,
|
|
198
|
+
sourceComponent,
|
|
199
|
+
componentIndex
|
|
200
|
+
)
|
|
201
|
+
const mountSide = PcbScene3dCircuitJsonAdapter.#layerSide(
|
|
202
|
+
component?.layer
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
designator,
|
|
207
|
+
mountSide,
|
|
208
|
+
rotationDeg: Number(component?.rotation || 0),
|
|
209
|
+
positionMil: {
|
|
210
|
+
x: center.x - board.centerX,
|
|
211
|
+
y: center.y - board.centerY,
|
|
212
|
+
z: board.thicknessMil / 2 + height / 2
|
|
213
|
+
},
|
|
214
|
+
boardPositionMil: {
|
|
215
|
+
x: center.x,
|
|
216
|
+
y: center.y,
|
|
217
|
+
z: 0
|
|
218
|
+
},
|
|
219
|
+
pattern: String(
|
|
220
|
+
sourceComponent?.ftype ||
|
|
221
|
+
sourceComponent?.name ||
|
|
222
|
+
component?.pcb_component_id ||
|
|
223
|
+
'CircuitJSON component'
|
|
224
|
+
),
|
|
225
|
+
source: 'circuitjson',
|
|
226
|
+
body: {
|
|
227
|
+
family: 'chip',
|
|
228
|
+
sizeMil: {
|
|
229
|
+
width: Math.max(width, 20),
|
|
230
|
+
depth: Math.max(depth, 20),
|
|
231
|
+
height: Math.max(height, 10)
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
externalModel: null
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Resolves a display designator for one CircuitJSON component.
|
|
242
|
+
* @param {object} component PCB component element.
|
|
243
|
+
* @param {object | undefined} sourceComponent Source component element.
|
|
244
|
+
* @param {number} componentIndex Component index.
|
|
245
|
+
* @returns {string}
|
|
246
|
+
*/
|
|
247
|
+
static #componentDesignator(component, sourceComponent, componentIndex) {
|
|
248
|
+
return String(
|
|
249
|
+
sourceComponent?.name ||
|
|
250
|
+
component?.name ||
|
|
251
|
+
component?.source_component_id ||
|
|
252
|
+
component?.pcb_component_id ||
|
|
253
|
+
`C${componentIndex + 1}`
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Builds all core board detail primitives.
|
|
259
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
260
|
+
* @returns {object}
|
|
261
|
+
*/
|
|
262
|
+
static #buildDetail(index) {
|
|
263
|
+
return {
|
|
264
|
+
pads: [
|
|
265
|
+
...PcbScene3dCircuitJsonAdapter.#buildSmtPads(index),
|
|
266
|
+
...PcbScene3dCircuitJsonAdapter.#buildPlatedHoles(index),
|
|
267
|
+
...PcbScene3dCircuitJsonAdapter.#buildNonPlatedHoles(index)
|
|
268
|
+
],
|
|
269
|
+
tracks: PcbScene3dCircuitJsonAdapter.#buildTracks(index),
|
|
270
|
+
arcs: [],
|
|
271
|
+
fills: [],
|
|
272
|
+
vias: PcbScene3dCircuitJsonAdapter.#buildVias(index),
|
|
273
|
+
polygons: [],
|
|
274
|
+
copperTexts: [],
|
|
275
|
+
embeddedFonts: [],
|
|
276
|
+
silkscreen: PcbScene3dCircuitJsonAdapter.#buildSilkscreen(index)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Builds SMT pad detail primitives.
|
|
282
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
283
|
+
* @returns {object[]}
|
|
284
|
+
*/
|
|
285
|
+
static #buildSmtPads(index) {
|
|
286
|
+
return (index.elementsByType.get('pcb_smtpad') || []).map((pad) => {
|
|
287
|
+
const side = PcbScene3dCircuitJsonAdapter.#layerSide(pad?.layer)
|
|
288
|
+
const size = PcbScene3dCircuitJsonAdapter.#padSize(pad)
|
|
289
|
+
const isBottom = side === 'bottom'
|
|
290
|
+
const padDetail = {
|
|
291
|
+
x: CircuitJsonUnits.mmToMil(pad?.x, 0),
|
|
292
|
+
y: CircuitJsonUnits.mmToMil(pad?.y, 0),
|
|
293
|
+
rotation: Number(pad?.ccw_rotation || 0),
|
|
294
|
+
shapeTop: isBottom
|
|
295
|
+
? 0
|
|
296
|
+
: PcbScene3dCircuitJsonAdapter.#padShape(pad),
|
|
297
|
+
shapeMid: 0,
|
|
298
|
+
shapeBottom: isBottom
|
|
299
|
+
? PcbScene3dCircuitJsonAdapter.#padShape(pad)
|
|
300
|
+
: 0,
|
|
301
|
+
sizeTopX: isBottom ? 0 : size.width,
|
|
302
|
+
sizeTopY: isBottom ? 0 : size.height,
|
|
303
|
+
sizeMidX: 0,
|
|
304
|
+
sizeMidY: 0,
|
|
305
|
+
sizeBottomX: isBottom ? size.width : 0,
|
|
306
|
+
sizeBottomY: isBottom ? size.height : 0,
|
|
307
|
+
holeDiameter: 0,
|
|
308
|
+
hasTopSolderMaskOpening: !isBottom,
|
|
309
|
+
hasBottomSolderMaskOpening: isBottom
|
|
310
|
+
}
|
|
311
|
+
return padDetail
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Builds plated-through-hole pad detail primitives.
|
|
317
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
318
|
+
* @returns {object[]}
|
|
319
|
+
*/
|
|
320
|
+
static #buildPlatedHoles(index) {
|
|
321
|
+
return (index.elementsByType.get('pcb_plated_hole') || []).map(
|
|
322
|
+
(hole) => {
|
|
323
|
+
const size =
|
|
324
|
+
PcbScene3dCircuitJsonAdapter.#platedHoleOuterSize(hole)
|
|
325
|
+
const holeDiameter = CircuitJsonUnits.mmToMil(
|
|
326
|
+
hole?.hole_diameter || hole?.hole_width,
|
|
327
|
+
0
|
|
328
|
+
)
|
|
329
|
+
return {
|
|
330
|
+
x: CircuitJsonUnits.mmToMil(hole?.x, 0),
|
|
331
|
+
y: CircuitJsonUnits.mmToMil(hole?.y, 0),
|
|
332
|
+
rotation: Number(hole?.ccw_rotation || 0),
|
|
333
|
+
shapeTop: PcbScene3dCircuitJsonAdapter.#padShape(hole),
|
|
334
|
+
shapeMid: PcbScene3dCircuitJsonAdapter.#padShape(hole),
|
|
335
|
+
shapeBottom: PcbScene3dCircuitJsonAdapter.#padShape(hole),
|
|
336
|
+
sizeTopX: size.width,
|
|
337
|
+
sizeTopY: size.height,
|
|
338
|
+
sizeMidX: size.width,
|
|
339
|
+
sizeMidY: size.height,
|
|
340
|
+
sizeBottomX: size.width,
|
|
341
|
+
sizeBottomY: size.height,
|
|
342
|
+
holeDiameter,
|
|
343
|
+
hasTopSolderMaskOpening: true,
|
|
344
|
+
hasBottomSolderMaskOpening: true
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Builds drill-only hole detail primitives.
|
|
352
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
353
|
+
* @returns {object[]}
|
|
354
|
+
*/
|
|
355
|
+
static #buildNonPlatedHoles(index) {
|
|
356
|
+
return (index.elementsByType.get('pcb_hole') || []).map((hole) => {
|
|
357
|
+
const diameter = CircuitJsonUnits.mmToMil(
|
|
358
|
+
hole?.hole_diameter || hole?.hole_width,
|
|
359
|
+
0
|
|
360
|
+
)
|
|
361
|
+
return {
|
|
362
|
+
x: CircuitJsonUnits.mmToMil(hole?.x, 0),
|
|
363
|
+
y: CircuitJsonUnits.mmToMil(hole?.y, 0),
|
|
364
|
+
rotation: Number(hole?.ccw_rotation || 0),
|
|
365
|
+
shapeTop: 0,
|
|
366
|
+
shapeMid: 0,
|
|
367
|
+
shapeBottom: 0,
|
|
368
|
+
sizeTopX: 0,
|
|
369
|
+
sizeTopY: 0,
|
|
370
|
+
sizeMidX: 0,
|
|
371
|
+
sizeMidY: 0,
|
|
372
|
+
sizeBottomX: 0,
|
|
373
|
+
sizeBottomY: 0,
|
|
374
|
+
holeDiameter: diameter,
|
|
375
|
+
hasTopSolderMaskOpening: false,
|
|
376
|
+
hasBottomSolderMaskOpening: false
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Builds via detail primitives.
|
|
383
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
384
|
+
* @returns {object[]}
|
|
385
|
+
*/
|
|
386
|
+
static #buildVias(index) {
|
|
387
|
+
return (index.elementsByType.get('pcb_via') || []).map((via) => ({
|
|
388
|
+
x: CircuitJsonUnits.mmToMil(via?.x, 0),
|
|
389
|
+
y: CircuitJsonUnits.mmToMil(via?.y, 0),
|
|
390
|
+
diameter: CircuitJsonUnits.mmToMil(via?.outer_diameter, 0),
|
|
391
|
+
holeDiameter: CircuitJsonUnits.mmToMil(via?.hole_diameter, 0),
|
|
392
|
+
isTentingTop: false,
|
|
393
|
+
isTentingBottom: false
|
|
394
|
+
}))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Builds copper track detail primitives from `pcb_trace` wire routes.
|
|
399
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
400
|
+
* @returns {object[]}
|
|
401
|
+
*/
|
|
402
|
+
static #buildTracks(index) {
|
|
403
|
+
const tracks = []
|
|
404
|
+
;(index.elementsByType.get('pcb_trace') || []).forEach((trace) => {
|
|
405
|
+
const route = Array.isArray(trace?.route) ? trace.route : []
|
|
406
|
+
for (let index = 0; index < route.length - 1; index += 1) {
|
|
407
|
+
const start = route[index]
|
|
408
|
+
const end = route[index + 1]
|
|
409
|
+
if (
|
|
410
|
+
String(start?.route_type || 'wire') !== 'wire' ||
|
|
411
|
+
String(end?.route_type || 'wire') !== 'wire'
|
|
412
|
+
) {
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
const layer = start?.layer || end?.layer
|
|
416
|
+
tracks.push({
|
|
417
|
+
x1: CircuitJsonUnits.mmToMil(start?.x, 0),
|
|
418
|
+
y1: CircuitJsonUnits.mmToMil(start?.y, 0),
|
|
419
|
+
x2: CircuitJsonUnits.mmToMil(end?.x, 0),
|
|
420
|
+
y2: CircuitJsonUnits.mmToMil(end?.y, 0),
|
|
421
|
+
width: CircuitJsonUnits.mmToMil(
|
|
422
|
+
start?.width || end?.width,
|
|
423
|
+
0.1524
|
|
424
|
+
),
|
|
425
|
+
layerId:
|
|
426
|
+
PcbScene3dCircuitJsonAdapter.#layerSide(layer) ===
|
|
427
|
+
'bottom'
|
|
428
|
+
? BOTTOM_LAYER_ID
|
|
429
|
+
: TOP_LAYER_ID,
|
|
430
|
+
solderMaskOpening: true
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
return tracks
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Builds basic silkscreen detail from known CircuitJSON drawing elements.
|
|
439
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
440
|
+
* @returns {{ top: object, bottom: object }}
|
|
441
|
+
*/
|
|
442
|
+
static #buildSilkscreen(index) {
|
|
443
|
+
const top = { tracks: [], arcs: [], fills: [], texts: [] }
|
|
444
|
+
const bottom = { tracks: [], arcs: [], fills: [], texts: [] }
|
|
445
|
+
;(index.elementsByType.get('pcb_silkscreen_line') || []).forEach(
|
|
446
|
+
(line) => {
|
|
447
|
+
const target =
|
|
448
|
+
PcbScene3dCircuitJsonAdapter.#layerSide(line?.layer) ===
|
|
449
|
+
'bottom'
|
|
450
|
+
? bottom
|
|
451
|
+
: top
|
|
452
|
+
target.tracks.push({
|
|
453
|
+
x1: CircuitJsonUnits.mmToMil(line?.x1, 0),
|
|
454
|
+
y1: CircuitJsonUnits.mmToMil(line?.y1, 0),
|
|
455
|
+
x2: CircuitJsonUnits.mmToMil(line?.x2, 0),
|
|
456
|
+
y2: CircuitJsonUnits.mmToMil(line?.y2, 0),
|
|
457
|
+
width: CircuitJsonUnits.mmToMil(line?.stroke_width, 0.12)
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
;(index.elementsByType.get('pcb_silkscreen_text') || []).forEach(
|
|
462
|
+
(text) => {
|
|
463
|
+
const target =
|
|
464
|
+
PcbScene3dCircuitJsonAdapter.#layerSide(text?.layer) ===
|
|
465
|
+
'bottom'
|
|
466
|
+
? bottom
|
|
467
|
+
: top
|
|
468
|
+
target.texts.push({
|
|
469
|
+
value: String(text?.text || text?.value || ''),
|
|
470
|
+
x: CircuitJsonUnits.mmToMil(text?.x, 0),
|
|
471
|
+
y: CircuitJsonUnits.mmToMil(text?.y, 0),
|
|
472
|
+
rotation: Number(text?.ccw_rotation || 0),
|
|
473
|
+
sizeX: CircuitJsonUnits.mmToMil(text?.font_size, 1),
|
|
474
|
+
sizeY: CircuitJsonUnits.mmToMil(text?.font_size, 1),
|
|
475
|
+
width: CircuitJsonUnits.mmToMil(text?.stroke_width, 0.12)
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
return { top, bottom }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Resolves pad copper size from CircuitJSON pad fields.
|
|
484
|
+
* @param {object} pad Pad element.
|
|
485
|
+
* @returns {{ width: number, height: number }}
|
|
486
|
+
*/
|
|
487
|
+
static #padSize(pad) {
|
|
488
|
+
if (String(pad?.shape || '') === 'circle') {
|
|
489
|
+
const diameter = CircuitJsonUnits.mmToMil(
|
|
490
|
+
Number(pad?.radius || 0) * 2 || pad?.diameter,
|
|
491
|
+
1
|
|
492
|
+
)
|
|
493
|
+
return { width: diameter, height: diameter }
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
width: CircuitJsonUnits.mmToMil(pad?.width, 1),
|
|
497
|
+
height: CircuitJsonUnits.mmToMil(pad?.height, 1)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Resolves plated-hole copper size from CircuitJSON fields.
|
|
503
|
+
* @param {object} hole Plated-hole element.
|
|
504
|
+
* @returns {{ width: number, height: number }}
|
|
505
|
+
*/
|
|
506
|
+
static #platedHoleOuterSize(hole) {
|
|
507
|
+
if (Number(hole?.outer_diameter || 0) > 0) {
|
|
508
|
+
const diameter = CircuitJsonUnits.mmToMil(hole.outer_diameter, 1)
|
|
509
|
+
return { width: diameter, height: diameter }
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
width: CircuitJsonUnits.mmToMil(
|
|
513
|
+
hole?.outer_width || hole?.rect_pad_width,
|
|
514
|
+
1
|
|
515
|
+
),
|
|
516
|
+
height: CircuitJsonUnits.mmToMil(
|
|
517
|
+
hole?.outer_height || hole?.rect_pad_height,
|
|
518
|
+
1
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Resolves the viewer pad shape code.
|
|
525
|
+
* @param {object} pad CircuitJSON pad element.
|
|
526
|
+
* @returns {number}
|
|
527
|
+
*/
|
|
528
|
+
static #padShape(pad) {
|
|
529
|
+
return String(pad?.shape || '').includes('rect')
|
|
530
|
+
? RECTANGULAR_PAD_SHAPE
|
|
531
|
+
: CIRCULAR_PAD_SHAPE
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Resolves a board side from a CircuitJSON layer name.
|
|
536
|
+
* @param {unknown} layer Layer value.
|
|
537
|
+
* @returns {'top' | 'bottom'}
|
|
538
|
+
*/
|
|
539
|
+
static #layerSide(layer) {
|
|
540
|
+
const value = String(layer || 'top').toLowerCase()
|
|
541
|
+
return value.includes('bottom') || value === 'b.cu' || value === 'back'
|
|
542
|
+
? 'bottom'
|
|
543
|
+
: 'top'
|
|
544
|
+
}
|
|
545
|
+
}
|