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,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
|
-
|
|
103
|
-
|
|
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
|
|
269
|
-
text?.
|
|
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
|
|
287
|
-
sizeY
|
|
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(
|
|
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
|
|
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
|
})
|