circuitjson-toolkit 1.0.3 → 1.0.16
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 +5 -3
- package/README.md +21 -2
- package/docs/api.md +50 -4
- package/docs/model-format.md +21 -3
- package/package.json +3 -2
- package/spec/library-scope.md +4 -1
- package/src/core/CircuitJsonBomBuilder.mjs +143 -0
- package/src/core/CircuitJsonDocument.mjs +46 -13
- package/src/core/CircuitJsonElementValidator.mjs +990 -0
- package/src/core/CircuitJsonIndexer.mjs +773 -4
- package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
- package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
- package/src/core/CircuitJsonParser.mjs +22 -6
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
- package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
- package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
- package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
- package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
- package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
- package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
- package/src/core/CircuitJsonUnits.mjs +133 -8
- package/src/core/PcbBoundsSelectionModel.mjs +250 -0
- package/src/core/PcbCandidateSelectionModel.mjs +77 -0
- package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
- package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
- package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
- package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
- package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
- package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
- package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
- package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
- package/src/core/spice/SpiceSimulationService.mjs +92 -0
- package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
- package/src/index.mjs +8 -0
- package/src/renderers.mjs +29 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
|
|
2
|
+
import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds manufacturing-oriented metadata from element arrays.
|
|
6
|
+
*/
|
|
7
|
+
export class CircuitJsonManufacturingBuilder {
|
|
8
|
+
/**
|
|
9
|
+
* Builds pick-and-place rows and routing exchange text.
|
|
10
|
+
* @param {object[]} circuitJson Parsed element array.
|
|
11
|
+
* @param {{ elementsByType?: Map<string, object[]>, sourceComponentById?: Map<string, object> }} [index] Optional index.
|
|
12
|
+
* @returns {{ pickAndPlaceRows: object[], routingDsn: string, routingGuides: object[], fabricationNotes: object[] }}
|
|
13
|
+
*/
|
|
14
|
+
static build(circuitJson, index = CircuitJsonIndexer.index(circuitJson)) {
|
|
15
|
+
return {
|
|
16
|
+
pickAndPlaceRows:
|
|
17
|
+
CircuitJsonManufacturingBuilder.#pickAndPlaceRows(index),
|
|
18
|
+
routingDsn: CircuitJsonManufacturingBuilder.#routingDsn(index),
|
|
19
|
+
routingGuides:
|
|
20
|
+
CircuitJsonManufacturingBuilder.#routingGuides(index),
|
|
21
|
+
fabricationNotes:
|
|
22
|
+
CircuitJsonManufacturingBuilder.#fabricationNotes(index)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Builds assembly placement rows.
|
|
28
|
+
* @param {{ elementsByType?: Map<string, object[]>, sourceComponentById?: Map<string, object> }} index Element index.
|
|
29
|
+
* @returns {object[]}
|
|
30
|
+
*/
|
|
31
|
+
static #pickAndPlaceRows(index) {
|
|
32
|
+
return CircuitJsonManufacturingBuilder.#all(index, 'pcb_component').map(
|
|
33
|
+
(component) =>
|
|
34
|
+
CircuitJsonManufacturingBuilder.#pickAndPlaceRow(
|
|
35
|
+
component,
|
|
36
|
+
index.sourceComponentById || new Map()
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Builds one placement row.
|
|
43
|
+
* @param {object} component PCB component element.
|
|
44
|
+
* @param {Map<string, object>} sourceComponentById Source lookup.
|
|
45
|
+
* @returns {object}
|
|
46
|
+
*/
|
|
47
|
+
static #pickAndPlaceRow(component, sourceComponentById) {
|
|
48
|
+
const sourceId = String(component.source_component_id || '').trim()
|
|
49
|
+
const source = sourceComponentById.get(sourceId) || {}
|
|
50
|
+
const center = CircuitJsonUnits.optionalPoint(
|
|
51
|
+
component.center || component
|
|
52
|
+
) || {
|
|
53
|
+
x: 0,
|
|
54
|
+
y: 0
|
|
55
|
+
}
|
|
56
|
+
const layer = CircuitJsonManufacturingBuilder.#layer(component.layer)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
designator: CircuitJsonManufacturingBuilder.#designator(
|
|
60
|
+
component,
|
|
61
|
+
source
|
|
62
|
+
),
|
|
63
|
+
componentId: String(component.pcb_component_id || ''),
|
|
64
|
+
sourceComponentId: sourceId,
|
|
65
|
+
x: CircuitJsonManufacturingBuilder.#round(center.x),
|
|
66
|
+
y: CircuitJsonManufacturingBuilder.#round(center.y),
|
|
67
|
+
rotation: CircuitJsonUnits.angle(
|
|
68
|
+
component.rotation ?? component.ccw_rotation,
|
|
69
|
+
0
|
|
70
|
+
),
|
|
71
|
+
layer,
|
|
72
|
+
side: CircuitJsonManufacturingBuilder.#side(layer),
|
|
73
|
+
value: CircuitJsonManufacturingBuilder.#value(source),
|
|
74
|
+
package: String(
|
|
75
|
+
source.ftype || source.package || source.footprint || ''
|
|
76
|
+
),
|
|
77
|
+
manufacturerPartNumber: String(
|
|
78
|
+
source.manufacturer_part_number ||
|
|
79
|
+
source.manufacturerPartNumber ||
|
|
80
|
+
''
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Builds a compact routing exchange text payload.
|
|
87
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Element index.
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
static #routingDsn(index) {
|
|
91
|
+
const lines = ['(pcb assembly)', ' (unit mm)']
|
|
92
|
+
lines.push(...CircuitJsonManufacturingBuilder.#boardLines(index))
|
|
93
|
+
lines.push(...CircuitJsonManufacturingBuilder.#placementLines(index))
|
|
94
|
+
lines.push(...CircuitJsonManufacturingBuilder.#networkLines(index))
|
|
95
|
+
lines.push(')')
|
|
96
|
+
return lines.join('\n')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Builds board structure lines.
|
|
101
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Element index.
|
|
102
|
+
* @returns {string[]}
|
|
103
|
+
*/
|
|
104
|
+
static #boardLines(index) {
|
|
105
|
+
const board = CircuitJsonManufacturingBuilder.#all(
|
|
106
|
+
index,
|
|
107
|
+
'pcb_board'
|
|
108
|
+
)[0]
|
|
109
|
+
const center = CircuitJsonUnits.optionalPoint(
|
|
110
|
+
board?.center || board
|
|
111
|
+
) || {
|
|
112
|
+
x: 0,
|
|
113
|
+
y: 0
|
|
114
|
+
}
|
|
115
|
+
const width = CircuitJsonUnits.length(board?.width, 0)
|
|
116
|
+
const height = CircuitJsonUnits.length(board?.height, 0)
|
|
117
|
+
const minX = CircuitJsonManufacturingBuilder.#round(
|
|
118
|
+
center.x - width / 2
|
|
119
|
+
)
|
|
120
|
+
const minY = CircuitJsonManufacturingBuilder.#round(
|
|
121
|
+
center.y - height / 2
|
|
122
|
+
)
|
|
123
|
+
const maxX = CircuitJsonManufacturingBuilder.#round(
|
|
124
|
+
center.x + width / 2
|
|
125
|
+
)
|
|
126
|
+
const maxY = CircuitJsonManufacturingBuilder.#round(
|
|
127
|
+
center.y + height / 2
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return [
|
|
131
|
+
' (structure',
|
|
132
|
+
' (boundary (rect ' + [minX, minY, maxX, maxY].join(' ') + '))',
|
|
133
|
+
...CircuitJsonManufacturingBuilder.#layers(board).map(
|
|
134
|
+
(layer) => ' (layer ' + layer + ' signal)'
|
|
135
|
+
),
|
|
136
|
+
' )'
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Builds component placement lines.
|
|
142
|
+
* @param {{ elementsByType?: Map<string, object[]>, sourceComponentById?: Map<string, object> }} index Element index.
|
|
143
|
+
* @returns {string[]}
|
|
144
|
+
*/
|
|
145
|
+
static #placementLines(index) {
|
|
146
|
+
return [
|
|
147
|
+
' (placement',
|
|
148
|
+
...CircuitJsonManufacturingBuilder.#pickAndPlaceRows(index).map(
|
|
149
|
+
(row) =>
|
|
150
|
+
' (component ' +
|
|
151
|
+
CircuitJsonManufacturingBuilder.#token(row.designator) +
|
|
152
|
+
' (place ' +
|
|
153
|
+
[
|
|
154
|
+
row.x,
|
|
155
|
+
row.y,
|
|
156
|
+
row.side || row.layer || 'top',
|
|
157
|
+
row.rotation
|
|
158
|
+
].join(' ') +
|
|
159
|
+
'))'
|
|
160
|
+
),
|
|
161
|
+
' )'
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Builds network lines for nets, pads, vias, and wires.
|
|
167
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Element index.
|
|
168
|
+
* @returns {string[]}
|
|
169
|
+
*/
|
|
170
|
+
static #networkLines(index) {
|
|
171
|
+
const sourceNetNames =
|
|
172
|
+
CircuitJsonManufacturingBuilder.#sourceNetNames(index)
|
|
173
|
+
return [
|
|
174
|
+
' (network',
|
|
175
|
+
...CircuitJsonManufacturingBuilder.#netNames(index).flatMap(
|
|
176
|
+
(netName) => [
|
|
177
|
+
' (net ' +
|
|
178
|
+
CircuitJsonManufacturingBuilder.#token(netName),
|
|
179
|
+
...CircuitJsonManufacturingBuilder.#pinLines(
|
|
180
|
+
index,
|
|
181
|
+
netName,
|
|
182
|
+
sourceNetNames
|
|
183
|
+
),
|
|
184
|
+
...CircuitJsonManufacturingBuilder.#drillLines(
|
|
185
|
+
index,
|
|
186
|
+
netName,
|
|
187
|
+
sourceNetNames
|
|
188
|
+
),
|
|
189
|
+
...CircuitJsonManufacturingBuilder.#wireLines(
|
|
190
|
+
index,
|
|
191
|
+
netName,
|
|
192
|
+
sourceNetNames
|
|
193
|
+
),
|
|
194
|
+
' )'
|
|
195
|
+
]
|
|
196
|
+
),
|
|
197
|
+
' )'
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Builds pad pin lines for one net.
|
|
203
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Element index.
|
|
204
|
+
* @param {string} netName Net name.
|
|
205
|
+
* @param {Map<string, string>} sourceNetNames Source net lookup.
|
|
206
|
+
* @returns {string[]}
|
|
207
|
+
*/
|
|
208
|
+
static #pinLines(index, netName, sourceNetNames) {
|
|
209
|
+
return CircuitJsonManufacturingBuilder.#all(index, 'pcb_smtpad')
|
|
210
|
+
.filter(
|
|
211
|
+
(pad) =>
|
|
212
|
+
CircuitJsonManufacturingBuilder.#netName(
|
|
213
|
+
pad,
|
|
214
|
+
sourceNetNames
|
|
215
|
+
) === netName
|
|
216
|
+
)
|
|
217
|
+
.map((pad) => {
|
|
218
|
+
const point = CircuitJsonUnits.optionalPoint(
|
|
219
|
+
pad.center || pad
|
|
220
|
+
) || {
|
|
221
|
+
x: 0,
|
|
222
|
+
y: 0
|
|
223
|
+
}
|
|
224
|
+
return (
|
|
225
|
+
' (pin ' +
|
|
226
|
+
CircuitJsonManufacturingBuilder.#token(
|
|
227
|
+
pad.pcb_smtpad_id || ''
|
|
228
|
+
) +
|
|
229
|
+
' ' +
|
|
230
|
+
[
|
|
231
|
+
CircuitJsonManufacturingBuilder.#round(point.x),
|
|
232
|
+
CircuitJsonManufacturingBuilder.#round(point.y)
|
|
233
|
+
].join(' ') +
|
|
234
|
+
')'
|
|
235
|
+
)
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Builds drill feature lines for one net.
|
|
241
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Element index.
|
|
242
|
+
* @param {string} netName Net name.
|
|
243
|
+
* @param {Map<string, string>} sourceNetNames Source net lookup.
|
|
244
|
+
* @returns {string[]}
|
|
245
|
+
*/
|
|
246
|
+
static #drillLines(index, netName, sourceNetNames) {
|
|
247
|
+
return [
|
|
248
|
+
...CircuitJsonManufacturingBuilder.#all(index, 'pcb_via')
|
|
249
|
+
.filter(
|
|
250
|
+
(via) =>
|
|
251
|
+
CircuitJsonManufacturingBuilder.#netName(
|
|
252
|
+
via,
|
|
253
|
+
sourceNetNames
|
|
254
|
+
) === netName
|
|
255
|
+
)
|
|
256
|
+
.map((via) =>
|
|
257
|
+
CircuitJsonManufacturingBuilder.#drillLine(via, 'via')
|
|
258
|
+
),
|
|
259
|
+
...CircuitJsonManufacturingBuilder.#all(index, 'pcb_plated_hole')
|
|
260
|
+
.filter(
|
|
261
|
+
(hole) =>
|
|
262
|
+
CircuitJsonManufacturingBuilder.#netName(
|
|
263
|
+
hole,
|
|
264
|
+
sourceNetNames
|
|
265
|
+
) === netName
|
|
266
|
+
)
|
|
267
|
+
.map((hole) =>
|
|
268
|
+
CircuitJsonManufacturingBuilder.#drillLine(
|
|
269
|
+
hole,
|
|
270
|
+
'plated_hole'
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
].filter(Boolean)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Builds one drill feature line.
|
|
278
|
+
* @param {object} element Drill-bearing element.
|
|
279
|
+
* @param {'via' | 'plated_hole'} kind Drill line kind.
|
|
280
|
+
* @returns {string}
|
|
281
|
+
*/
|
|
282
|
+
static #drillLine(element, kind) {
|
|
283
|
+
const point = CircuitJsonUnits.optionalPoint(element.center || element)
|
|
284
|
+
const layers = CircuitJsonManufacturingBuilder.#elementLayers(element)
|
|
285
|
+
return (
|
|
286
|
+
' (' +
|
|
287
|
+
kind +
|
|
288
|
+
' ' +
|
|
289
|
+
[
|
|
290
|
+
CircuitJsonManufacturingBuilder.#token(
|
|
291
|
+
element.pcb_via_id ||
|
|
292
|
+
element.pcb_plated_hole_id ||
|
|
293
|
+
element.pcb_hole_id ||
|
|
294
|
+
''
|
|
295
|
+
),
|
|
296
|
+
CircuitJsonManufacturingBuilder.#round(point?.x),
|
|
297
|
+
CircuitJsonManufacturingBuilder.#round(point?.y),
|
|
298
|
+
CircuitJsonManufacturingBuilder.#round(
|
|
299
|
+
CircuitJsonManufacturingBuilder.#holeDiameter(element)
|
|
300
|
+
),
|
|
301
|
+
CircuitJsonManufacturingBuilder.#round(
|
|
302
|
+
CircuitJsonManufacturingBuilder.#outerDiameter(element)
|
|
303
|
+
),
|
|
304
|
+
...layers
|
|
305
|
+
].join(' ') +
|
|
306
|
+
')'
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Builds routed wire lines for one net.
|
|
312
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Element index.
|
|
313
|
+
* @param {string} netName Net name.
|
|
314
|
+
* @param {Map<string, string>} sourceNetNames Source net lookup.
|
|
315
|
+
* @returns {string[]}
|
|
316
|
+
*/
|
|
317
|
+
static #wireLines(index, netName, sourceNetNames) {
|
|
318
|
+
return CircuitJsonManufacturingBuilder.#all(index, 'pcb_trace')
|
|
319
|
+
.filter(
|
|
320
|
+
(trace) =>
|
|
321
|
+
CircuitJsonManufacturingBuilder.#netName(
|
|
322
|
+
trace,
|
|
323
|
+
sourceNetNames
|
|
324
|
+
) === netName
|
|
325
|
+
)
|
|
326
|
+
.flatMap((trace) =>
|
|
327
|
+
CircuitJsonManufacturingBuilder.#traceWireLines(trace)
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Builds routed wire lines for one trace.
|
|
333
|
+
* @param {object} trace Trace element.
|
|
334
|
+
* @returns {string[]}
|
|
335
|
+
*/
|
|
336
|
+
static #traceWireLines(trace) {
|
|
337
|
+
const route = Array.isArray(trace.route) ? trace.route : []
|
|
338
|
+
const lines = []
|
|
339
|
+
let previous = null
|
|
340
|
+
for (const entry of route) {
|
|
341
|
+
const current = CircuitJsonUnits.optionalPoint(
|
|
342
|
+
entry.center || entry
|
|
343
|
+
)
|
|
344
|
+
if (!current) continue
|
|
345
|
+
if (previous) {
|
|
346
|
+
const layer = CircuitJsonManufacturingBuilder.#layer(
|
|
347
|
+
entry.layer || previous.layer || trace.layer
|
|
348
|
+
)
|
|
349
|
+
const width = CircuitJsonUnits.length(
|
|
350
|
+
entry.width ?? previous.width ?? trace.width,
|
|
351
|
+
0
|
|
352
|
+
)
|
|
353
|
+
lines.push(
|
|
354
|
+
' (wire ' +
|
|
355
|
+
[
|
|
356
|
+
layer || 'top',
|
|
357
|
+
CircuitJsonManufacturingBuilder.#round(previous.x),
|
|
358
|
+
CircuitJsonManufacturingBuilder.#round(previous.y),
|
|
359
|
+
CircuitJsonManufacturingBuilder.#round(current.x),
|
|
360
|
+
CircuitJsonManufacturingBuilder.#round(current.y),
|
|
361
|
+
CircuitJsonManufacturingBuilder.#round(width)
|
|
362
|
+
].join(' ') +
|
|
363
|
+
')'
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
previous = { ...entry, x: current.x, y: current.y }
|
|
367
|
+
}
|
|
368
|
+
return lines
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Builds sorted net names.
|
|
373
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Element index.
|
|
374
|
+
* @returns {string[]}
|
|
375
|
+
*/
|
|
376
|
+
static #netNames(index) {
|
|
377
|
+
const names = new Set()
|
|
378
|
+
const sourceNetNames =
|
|
379
|
+
CircuitJsonManufacturingBuilder.#sourceNetNames(index)
|
|
380
|
+
for (const name of sourceNetNames.values()) {
|
|
381
|
+
if (name) names.add(name)
|
|
382
|
+
}
|
|
383
|
+
for (const type of [
|
|
384
|
+
'pcb_smtpad',
|
|
385
|
+
'pcb_trace',
|
|
386
|
+
'pcb_via',
|
|
387
|
+
'pcb_plated_hole',
|
|
388
|
+
'pcb_trace_hint',
|
|
389
|
+
'pcb_breakout_point'
|
|
390
|
+
]) {
|
|
391
|
+
for (const element of CircuitJsonManufacturingBuilder.#all(
|
|
392
|
+
index,
|
|
393
|
+
type
|
|
394
|
+
)) {
|
|
395
|
+
const name = CircuitJsonManufacturingBuilder.#netName(
|
|
396
|
+
element,
|
|
397
|
+
sourceNetNames
|
|
398
|
+
)
|
|
399
|
+
if (name) names.add(name)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return [...names].sort((left, right) => left.localeCompare(right))
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Builds route guide metadata from hints and breakout points.
|
|
407
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Element index.
|
|
408
|
+
* @returns {object[]}
|
|
409
|
+
*/
|
|
410
|
+
static #routingGuides(index) {
|
|
411
|
+
const sourceNetNames =
|
|
412
|
+
CircuitJsonManufacturingBuilder.#sourceNetNames(index)
|
|
413
|
+
return [
|
|
414
|
+
...CircuitJsonManufacturingBuilder.#all(index, 'pcb_trace_hint')
|
|
415
|
+
.map((hint) =>
|
|
416
|
+
CircuitJsonManufacturingBuilder.#traceHintGuide(
|
|
417
|
+
hint,
|
|
418
|
+
sourceNetNames
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
.filter(Boolean),
|
|
422
|
+
...CircuitJsonManufacturingBuilder.#all(index, 'pcb_breakout_point')
|
|
423
|
+
.map((point) =>
|
|
424
|
+
CircuitJsonManufacturingBuilder.#breakoutGuide(
|
|
425
|
+
point,
|
|
426
|
+
sourceNetNames
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
.filter(Boolean)
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Builds one trace hint guide.
|
|
435
|
+
* @param {object} hint Trace hint element.
|
|
436
|
+
* @param {Map<string, string>} sourceNetNames Source net lookup.
|
|
437
|
+
* @returns {object | null}
|
|
438
|
+
*/
|
|
439
|
+
static #traceHintGuide(hint, sourceNetNames) {
|
|
440
|
+
const route = (Array.isArray(hint.route) ? hint.route : [])
|
|
441
|
+
.map((point) =>
|
|
442
|
+
CircuitJsonManufacturingBuilder.#routeGuidePoint(point)
|
|
443
|
+
)
|
|
444
|
+
.filter(Boolean)
|
|
445
|
+
if (!route.length) return null
|
|
446
|
+
return {
|
|
447
|
+
type: 'trace_hint',
|
|
448
|
+
id: String(hint.pcb_trace_hint_id || ''),
|
|
449
|
+
pcbComponentId: String(hint.pcb_component_id || ''),
|
|
450
|
+
pcbPortId: String(hint.pcb_port_id || ''),
|
|
451
|
+
sourceNetId: String(hint.source_net_id || ''),
|
|
452
|
+
netName: CircuitJsonManufacturingBuilder.#netName(
|
|
453
|
+
hint,
|
|
454
|
+
sourceNetNames
|
|
455
|
+
),
|
|
456
|
+
subcircuitId: String(hint.subcircuit_id || ''),
|
|
457
|
+
route
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Builds one breakout guide.
|
|
463
|
+
* @param {object} breakout Breakout point element.
|
|
464
|
+
* @param {Map<string, string>} sourceNetNames Source net lookup.
|
|
465
|
+
* @returns {object | null}
|
|
466
|
+
*/
|
|
467
|
+
static #breakoutGuide(breakout, sourceNetNames) {
|
|
468
|
+
const point = CircuitJsonUnits.optionalPoint(
|
|
469
|
+
breakout.center || breakout
|
|
470
|
+
)
|
|
471
|
+
if (!point) return null
|
|
472
|
+
return {
|
|
473
|
+
type: 'breakout_point',
|
|
474
|
+
id: String(breakout.pcb_breakout_point_id || ''),
|
|
475
|
+
pcbGroupId: String(breakout.pcb_group_id || ''),
|
|
476
|
+
sourceTraceId: String(breakout.source_trace_id || ''),
|
|
477
|
+
sourcePortId: String(breakout.source_port_id || ''),
|
|
478
|
+
sourceNetId: String(breakout.source_net_id || ''),
|
|
479
|
+
netName: CircuitJsonManufacturingBuilder.#netName(
|
|
480
|
+
breakout,
|
|
481
|
+
sourceNetNames
|
|
482
|
+
),
|
|
483
|
+
subcircuitId: String(breakout.subcircuit_id || ''),
|
|
484
|
+
point: {
|
|
485
|
+
x: CircuitJsonManufacturingBuilder.#round(point.x),
|
|
486
|
+
y: CircuitJsonManufacturingBuilder.#round(point.y)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Builds a normalized route guide point.
|
|
493
|
+
* @param {object} value Point row.
|
|
494
|
+
* @returns {{ x: number, y: number, layer: string } | null}
|
|
495
|
+
*/
|
|
496
|
+
static #routeGuidePoint(value) {
|
|
497
|
+
const point = CircuitJsonUnits.optionalPoint(value?.center || value)
|
|
498
|
+
if (!point) return null
|
|
499
|
+
return {
|
|
500
|
+
x: CircuitJsonManufacturingBuilder.#round(point.x),
|
|
501
|
+
y: CircuitJsonManufacturingBuilder.#round(point.y),
|
|
502
|
+
layer: CircuitJsonManufacturingBuilder.#layer(value?.layer)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Builds fabrication note metadata.
|
|
508
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Element index.
|
|
509
|
+
* @returns {object[]}
|
|
510
|
+
*/
|
|
511
|
+
static #fabricationNotes(index) {
|
|
512
|
+
return [
|
|
513
|
+
...CircuitJsonManufacturingBuilder.#all(
|
|
514
|
+
index,
|
|
515
|
+
'pcb_fabrication_note_text'
|
|
516
|
+
).map((note) =>
|
|
517
|
+
CircuitJsonManufacturingBuilder.#fabricationTextNote(note)
|
|
518
|
+
),
|
|
519
|
+
...CircuitJsonManufacturingBuilder.#all(
|
|
520
|
+
index,
|
|
521
|
+
'pcb_fabrication_note_path'
|
|
522
|
+
).map((note) =>
|
|
523
|
+
CircuitJsonManufacturingBuilder.#fabricationPathNote(note)
|
|
524
|
+
),
|
|
525
|
+
...CircuitJsonManufacturingBuilder.#all(
|
|
526
|
+
index,
|
|
527
|
+
'pcb_fabrication_note_rect'
|
|
528
|
+
).map((note) =>
|
|
529
|
+
CircuitJsonManufacturingBuilder.#fabricationRectNote(note)
|
|
530
|
+
),
|
|
531
|
+
...CircuitJsonManufacturingBuilder.#all(
|
|
532
|
+
index,
|
|
533
|
+
'pcb_fabrication_note_dimension'
|
|
534
|
+
).map((note) =>
|
|
535
|
+
CircuitJsonManufacturingBuilder.#fabricationDimensionNote(note)
|
|
536
|
+
)
|
|
537
|
+
].filter(Boolean)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Builds one fabrication text note row.
|
|
542
|
+
* @param {object} note Fabrication text element.
|
|
543
|
+
* @returns {object | null}
|
|
544
|
+
*/
|
|
545
|
+
static #fabricationTextNote(note) {
|
|
546
|
+
const anchor = CircuitJsonUnits.optionalPoint(
|
|
547
|
+
note.anchor_position || note.center || note
|
|
548
|
+
) || { x: 0, y: 0 }
|
|
549
|
+
return {
|
|
550
|
+
...CircuitJsonManufacturingBuilder.#fabricationBaseNote(
|
|
551
|
+
note,
|
|
552
|
+
'text'
|
|
553
|
+
),
|
|
554
|
+
text: String(note.text || ''),
|
|
555
|
+
anchor: {
|
|
556
|
+
x: CircuitJsonManufacturingBuilder.#round(anchor.x),
|
|
557
|
+
y: CircuitJsonManufacturingBuilder.#round(anchor.y)
|
|
558
|
+
},
|
|
559
|
+
rotation: CircuitJsonUnits.angle(note.ccw_rotation, 0),
|
|
560
|
+
fontSize: CircuitJsonUnits.length(note.font_size, 1),
|
|
561
|
+
color: String(note.color || '')
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Builds one fabrication path note row.
|
|
567
|
+
* @param {object} note Fabrication path element.
|
|
568
|
+
* @returns {object | null}
|
|
569
|
+
*/
|
|
570
|
+
static #fabricationPathNote(note) {
|
|
571
|
+
const route = (Array.isArray(note.route) ? note.route : [])
|
|
572
|
+
.map((point) => CircuitJsonUnits.optionalPoint(point))
|
|
573
|
+
.filter(Boolean)
|
|
574
|
+
.map((point) => ({
|
|
575
|
+
x: CircuitJsonManufacturingBuilder.#round(point.x),
|
|
576
|
+
y: CircuitJsonManufacturingBuilder.#round(point.y)
|
|
577
|
+
}))
|
|
578
|
+
if (!route.length) return null
|
|
579
|
+
return {
|
|
580
|
+
...CircuitJsonManufacturingBuilder.#fabricationBaseNote(
|
|
581
|
+
note,
|
|
582
|
+
'path'
|
|
583
|
+
),
|
|
584
|
+
route,
|
|
585
|
+
strokeWidth: CircuitJsonUnits.length(note.stroke_width, 0),
|
|
586
|
+
color: String(note.color || '')
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Builds one fabrication rectangle note row.
|
|
592
|
+
* @param {object} note Fabrication rectangle element.
|
|
593
|
+
* @returns {object | null}
|
|
594
|
+
*/
|
|
595
|
+
static #fabricationRectNote(note) {
|
|
596
|
+
const center = CircuitJsonUnits.optionalPoint(note.center || note)
|
|
597
|
+
if (!center) return null
|
|
598
|
+
return {
|
|
599
|
+
...CircuitJsonManufacturingBuilder.#fabricationBaseNote(
|
|
600
|
+
note,
|
|
601
|
+
'rect'
|
|
602
|
+
),
|
|
603
|
+
center: {
|
|
604
|
+
x: CircuitJsonManufacturingBuilder.#round(center.x),
|
|
605
|
+
y: CircuitJsonManufacturingBuilder.#round(center.y)
|
|
606
|
+
},
|
|
607
|
+
width: CircuitJsonUnits.length(note.width, 0),
|
|
608
|
+
height: CircuitJsonUnits.length(note.height, 0),
|
|
609
|
+
strokeWidth: CircuitJsonUnits.length(note.stroke_width, 0.1),
|
|
610
|
+
cornerRadius: CircuitJsonUnits.length(note.corner_radius, 0),
|
|
611
|
+
isFilled: note.is_filled === true,
|
|
612
|
+
hasStroke: note.has_stroke !== false,
|
|
613
|
+
isStrokeDashed: note.is_stroke_dashed === true,
|
|
614
|
+
color: String(note.color || '')
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Builds one fabrication dimension note row.
|
|
620
|
+
* @param {object} note Fabrication dimension element.
|
|
621
|
+
* @returns {object | null}
|
|
622
|
+
*/
|
|
623
|
+
static #fabricationDimensionNote(note) {
|
|
624
|
+
const from = CircuitJsonUnits.optionalPoint(note.from)
|
|
625
|
+
const to = CircuitJsonUnits.optionalPoint(note.to)
|
|
626
|
+
if (!from || !to) return null
|
|
627
|
+
return {
|
|
628
|
+
...CircuitJsonManufacturingBuilder.#fabricationBaseNote(
|
|
629
|
+
note,
|
|
630
|
+
'dimension'
|
|
631
|
+
),
|
|
632
|
+
from: {
|
|
633
|
+
x: CircuitJsonManufacturingBuilder.#round(from.x),
|
|
634
|
+
y: CircuitJsonManufacturingBuilder.#round(from.y)
|
|
635
|
+
},
|
|
636
|
+
to: {
|
|
637
|
+
x: CircuitJsonManufacturingBuilder.#round(to.x),
|
|
638
|
+
y: CircuitJsonManufacturingBuilder.#round(to.y)
|
|
639
|
+
},
|
|
640
|
+
text: String(note.text || ''),
|
|
641
|
+
offset: CircuitJsonUnits.length(note.offset, 0),
|
|
642
|
+
offsetDistance: CircuitJsonUnits.length(note.offset_distance, 0),
|
|
643
|
+
offsetDirection: CircuitJsonManufacturingBuilder.#offsetDirection(
|
|
644
|
+
note.offset_direction
|
|
645
|
+
),
|
|
646
|
+
rotation: CircuitJsonUnits.angle(
|
|
647
|
+
note.text_ccw_rotation ?? note.ccw_rotation,
|
|
648
|
+
0
|
|
649
|
+
),
|
|
650
|
+
fontSize: CircuitJsonUnits.length(note.font_size, 1),
|
|
651
|
+
arrowSize: CircuitJsonUnits.length(note.arrow_size, 1),
|
|
652
|
+
color: String(note.color || '')
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Builds common fabrication note metadata.
|
|
658
|
+
* @param {object} note Fabrication note element.
|
|
659
|
+
* @param {string} type Note type.
|
|
660
|
+
* @returns {object}
|
|
661
|
+
*/
|
|
662
|
+
static #fabricationBaseNote(note, type) {
|
|
663
|
+
return {
|
|
664
|
+
type,
|
|
665
|
+
elementType: String(note.type || ''),
|
|
666
|
+
id: CircuitJsonIndexer.getElementId(note),
|
|
667
|
+
pcbComponentId: String(note.pcb_component_id || ''),
|
|
668
|
+
pcbGroupId: String(note.pcb_group_id || ''),
|
|
669
|
+
subcircuitId: String(note.subcircuit_id || ''),
|
|
670
|
+
layer: CircuitJsonManufacturingBuilder.#layer(note.layer)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Normalizes an offset direction row.
|
|
676
|
+
* @param {object | null | undefined} direction Direction row.
|
|
677
|
+
* @returns {{ x: number, y: number } | null}
|
|
678
|
+
*/
|
|
679
|
+
static #offsetDirection(direction) {
|
|
680
|
+
const x = Number(direction?.x)
|
|
681
|
+
const y = Number(direction?.y)
|
|
682
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
|
|
683
|
+
return {
|
|
684
|
+
x: CircuitJsonManufacturingBuilder.#round(x),
|
|
685
|
+
y: CircuitJsonManufacturingBuilder.#round(y)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Builds source net id to display name lookup.
|
|
691
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Element index.
|
|
692
|
+
* @returns {Map<string, string>}
|
|
693
|
+
*/
|
|
694
|
+
static #sourceNetNames(index) {
|
|
695
|
+
return new Map(
|
|
696
|
+
CircuitJsonManufacturingBuilder.#all(index, 'source_net').map(
|
|
697
|
+
(net) => {
|
|
698
|
+
const id = String(net.source_net_id || '').trim()
|
|
699
|
+
const name = String(net.name || id).trim()
|
|
700
|
+
return [id, name]
|
|
701
|
+
}
|
|
702
|
+
)
|
|
703
|
+
)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Resolves layer names from a drill-bearing element.
|
|
708
|
+
* @param {object} element Element row.
|
|
709
|
+
* @returns {string[]}
|
|
710
|
+
*/
|
|
711
|
+
static #elementLayers(element) {
|
|
712
|
+
const values = Array.isArray(element.layers)
|
|
713
|
+
? element.layers
|
|
714
|
+
: [element.from_layer, element.to_layer, element.layer]
|
|
715
|
+
const layers = values
|
|
716
|
+
.map((value) => CircuitJsonManufacturingBuilder.#layer(value))
|
|
717
|
+
.filter(Boolean)
|
|
718
|
+
return CircuitJsonManufacturingBuilder.#uniqueStrings(
|
|
719
|
+
layers.length ? layers : ['top', 'bottom']
|
|
720
|
+
)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Resolves drill hole diameter.
|
|
725
|
+
* @param {object} element Element row.
|
|
726
|
+
* @returns {number}
|
|
727
|
+
*/
|
|
728
|
+
static #holeDiameter(element) {
|
|
729
|
+
return CircuitJsonManufacturingBuilder.#maxLength([
|
|
730
|
+
element.hole_diameter,
|
|
731
|
+
element.hole_width,
|
|
732
|
+
element.hole_height
|
|
733
|
+
])
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Resolves drill outer diameter.
|
|
738
|
+
* @param {object} element Element row.
|
|
739
|
+
* @returns {number}
|
|
740
|
+
*/
|
|
741
|
+
static #outerDiameter(element) {
|
|
742
|
+
return CircuitJsonManufacturingBuilder.#maxLength([
|
|
743
|
+
element.outer_diameter,
|
|
744
|
+
element.outer_width,
|
|
745
|
+
element.outer_height,
|
|
746
|
+
element.rect_pad_width,
|
|
747
|
+
element.rect_pad_height
|
|
748
|
+
])
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Returns the largest valid length in a list.
|
|
753
|
+
* @param {unknown[]} values Candidate values.
|
|
754
|
+
* @returns {number}
|
|
755
|
+
*/
|
|
756
|
+
static #maxLength(values) {
|
|
757
|
+
const lengths = values
|
|
758
|
+
.map((value) => CircuitJsonUnits.optionalLength(value))
|
|
759
|
+
.filter((value) => value !== null)
|
|
760
|
+
return lengths.length ? Math.max(...lengths) : 0
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Returns unique strings in input order.
|
|
765
|
+
* @param {string[]} values Candidate strings.
|
|
766
|
+
* @returns {string[]}
|
|
767
|
+
*/
|
|
768
|
+
static #uniqueStrings(values) {
|
|
769
|
+
return [
|
|
770
|
+
...new Set(values.map((value) => String(value || '').trim()))
|
|
771
|
+
].filter(Boolean)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Resolves layer names from board metadata.
|
|
776
|
+
* @param {object | undefined} board Board element.
|
|
777
|
+
* @returns {string[]}
|
|
778
|
+
*/
|
|
779
|
+
static #layers(board) {
|
|
780
|
+
const count = Math.max(1, Math.round(Number(board?.num_layers || 2)))
|
|
781
|
+
if (count === 1) return ['top']
|
|
782
|
+
return [
|
|
783
|
+
'top',
|
|
784
|
+
...Array.from(
|
|
785
|
+
{ length: Math.max(count - 2, 0) },
|
|
786
|
+
(_entry, index) => 'inner' + (index + 1)
|
|
787
|
+
),
|
|
788
|
+
'bottom'
|
|
789
|
+
]
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Resolves rows by type.
|
|
794
|
+
* @param {{ elementsByType?: Map<string, object[]> }} index Element index.
|
|
795
|
+
* @param {string} type Element type.
|
|
796
|
+
* @returns {object[]}
|
|
797
|
+
*/
|
|
798
|
+
static #all(index, type) {
|
|
799
|
+
return index.elementsByType?.get(type) || []
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Resolves a component designator.
|
|
804
|
+
* @param {object} component PCB component.
|
|
805
|
+
* @param {object} source Source component.
|
|
806
|
+
* @returns {string}
|
|
807
|
+
*/
|
|
808
|
+
static #designator(component, source) {
|
|
809
|
+
return String(
|
|
810
|
+
source.name ||
|
|
811
|
+
source.reference ||
|
|
812
|
+
source.designator ||
|
|
813
|
+
component.name ||
|
|
814
|
+
component.pcb_component_id ||
|
|
815
|
+
''
|
|
816
|
+
).trim()
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Resolves a source value field.
|
|
821
|
+
* @param {object} source Source component.
|
|
822
|
+
* @returns {string}
|
|
823
|
+
*/
|
|
824
|
+
static #value(source) {
|
|
825
|
+
return String(
|
|
826
|
+
source.value ||
|
|
827
|
+
source.resistance ||
|
|
828
|
+
source.capacitance ||
|
|
829
|
+
source.inductance ||
|
|
830
|
+
''
|
|
831
|
+
)
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Resolves a normalized net name.
|
|
836
|
+
* @param {object} element Element row.
|
|
837
|
+
* @param {Map<string, string>} [sourceNetNames] Source net lookup.
|
|
838
|
+
* @returns {string}
|
|
839
|
+
*/
|
|
840
|
+
static #netName(element, sourceNetNames = new Map()) {
|
|
841
|
+
const sourceNetId = String(element?.source_net_id || '').trim()
|
|
842
|
+
return String(
|
|
843
|
+
element?.netName ??
|
|
844
|
+
element?.net ??
|
|
845
|
+
element?.net_name ??
|
|
846
|
+
element?.source_net_name ??
|
|
847
|
+
sourceNetNames.get(sourceNetId) ??
|
|
848
|
+
''
|
|
849
|
+
).trim()
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Resolves a layer string.
|
|
854
|
+
* @param {unknown} value Layer candidate.
|
|
855
|
+
* @returns {string}
|
|
856
|
+
*/
|
|
857
|
+
static #layer(value) {
|
|
858
|
+
const raw =
|
|
859
|
+
typeof value === 'object' && value !== null ? value.name : value
|
|
860
|
+
const text = String(raw ?? '').trim()
|
|
861
|
+
const lowered = text.toLowerCase()
|
|
862
|
+
if (['front', 'f.cu', '1'].includes(lowered)) return 'top'
|
|
863
|
+
if (['back', 'b.cu', '32'].includes(lowered)) return 'bottom'
|
|
864
|
+
return text || 'top'
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Resolves an assembly side.
|
|
869
|
+
* @param {string} layer Layer name.
|
|
870
|
+
* @returns {'top' | 'bottom' | ''}
|
|
871
|
+
*/
|
|
872
|
+
static #side(layer) {
|
|
873
|
+
const text = String(layer || '').toLowerCase()
|
|
874
|
+
if (/\b(bottom|back)\b|\bb[._-]/u.test(text)) return 'bottom'
|
|
875
|
+
if (/\b(top|front)\b|\bf[._-]/u.test(text)) return 'top'
|
|
876
|
+
return ''
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Builds a DSN-safe token.
|
|
881
|
+
* @param {unknown} value Raw value.
|
|
882
|
+
* @returns {string}
|
|
883
|
+
*/
|
|
884
|
+
static #token(value) {
|
|
885
|
+
return String(value || 'unnamed').replace(/[^A-Za-z0-9_.:-]+/gu, '_')
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Rounds a numeric value for deterministic output.
|
|
890
|
+
* @param {number} value Number.
|
|
891
|
+
* @returns {number}
|
|
892
|
+
*/
|
|
893
|
+
static #round(value) {
|
|
894
|
+
const number = Number(value)
|
|
895
|
+
if (!Number.isFinite(number)) return 0
|
|
896
|
+
return Number(number.toFixed(6))
|
|
897
|
+
}
|
|
898
|
+
}
|