altium-toolkit 1.1.24 → 1.1.26
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 +5 -3
- package/docs/api.md +43 -17
- package/docs/model-format.md +51 -11
- package/docs/schemas/altium_toolkit/library_compatibility_a1.schema.json +116 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +58 -1
- package/package.json +1 -1
- package/spec/library-compatibility.md +65 -0
- package/src/core/altium/AltiumParser.mjs +8 -1
- package/src/core/altium/LibraryCompatibilityGeometry.mjs +618 -0
- package/src/core/altium/LibraryCompatibilityModelHintBuilder.mjs +296 -0
- package/src/core/altium/LibraryCompatibilityReportBuilder.mjs +930 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +29 -1
- package/src/core/altium/PcbDimensionParser.mjs +1 -0
- package/src/core/altium/PcbDimensionReportBuilder.mjs +190 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +140 -10
- package/src/core/altium/PcbFabricationReadinessReportBuilder.mjs +607 -0
- package/src/core/altium/PcbInspectionReportBuilder.mjs +56 -3
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +138 -3
- package/src/core/altium/PcbLibModelParser.mjs +24 -2
- package/src/core/altium/PcbLibStorageNameResolver.mjs +171 -0
- package/src/core/altium/PcbLibStreamExtractor.mjs +15 -93
- package/src/core/altium/PcbPrimitiveFieldOffsetReportBuilder.mjs +352 -0
- package/src/core/altium/PcbRuleImpactReportBuilder.mjs +313 -0
- package/src/core/altium/PcbShapeBasedBodyGeometryParser.mjs +514 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +442 -2
- package/src/core/altium/SchematicImageDiagnosticsBuilder.mjs +214 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +523 -3
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +36 -0
- package/src/ui/PcbDimensionPrimitiveRenderer.mjs +422 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +6 -0
- package/src/ui/PcbScene3dBuilder.mjs +10 -2
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +225 -0
- package/src/ui/PcbSvgRenderer.mjs +78 -2
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parses shape-based 3D component-body geometry from native PCB streams.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbShapeBasedBodyGeometryParser {
|
|
11
|
+
/**
|
|
12
|
+
* Parses binary shape-based body records from a native stream.
|
|
13
|
+
* @param {Uint8Array | undefined} bytes Stream bytes.
|
|
14
|
+
* @returns {{ fields: Record<string, string | string[]>, staticGeometry?: object }[]}
|
|
15
|
+
*/
|
|
16
|
+
static parse(bytes) {
|
|
17
|
+
if (!(bytes instanceof Uint8Array) || !bytes.byteLength) {
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const records = []
|
|
22
|
+
let offset = 0
|
|
23
|
+
|
|
24
|
+
while (offset + 22 <= bytes.byteLength) {
|
|
25
|
+
const parsed = PcbShapeBasedBodyGeometryParser.#parseRecordAt(
|
|
26
|
+
bytes,
|
|
27
|
+
offset
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if (parsed) {
|
|
31
|
+
records.push({
|
|
32
|
+
fields: parsed.fields,
|
|
33
|
+
staticGeometry: parsed.staticGeometry
|
|
34
|
+
})
|
|
35
|
+
offset = Math.max(parsed.nextOffset, offset + 1)
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
offset += 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return records
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds a static geometry description from printable body fields.
|
|
47
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
48
|
+
* @param {{ x: number, y: number }[]} [verticesMil] Extracted vertices.
|
|
49
|
+
* @returns {object | undefined}
|
|
50
|
+
*/
|
|
51
|
+
static buildStaticGeometry(fields, verticesMil = []) {
|
|
52
|
+
const modelType = PcbShapeBasedBodyGeometryParser.#parseIntegerField(
|
|
53
|
+
fields,
|
|
54
|
+
'MODEL.MODELTYPE'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if (modelType === null) {
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const standoffHeightMil =
|
|
62
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
63
|
+
fields,
|
|
64
|
+
'STANDOFFHEIGHT'
|
|
65
|
+
) || 0
|
|
66
|
+
|
|
67
|
+
switch (modelType) {
|
|
68
|
+
case 0:
|
|
69
|
+
return PcbShapeBasedBodyGeometryParser.#extrudedPolygonGeometry(
|
|
70
|
+
fields,
|
|
71
|
+
verticesMil,
|
|
72
|
+
standoffHeightMil
|
|
73
|
+
)
|
|
74
|
+
case 1:
|
|
75
|
+
return PcbShapeBasedBodyGeometryParser.#roundBodyGeometry(
|
|
76
|
+
fields,
|
|
77
|
+
'cone',
|
|
78
|
+
standoffHeightMil
|
|
79
|
+
)
|
|
80
|
+
case 2:
|
|
81
|
+
return PcbShapeBasedBodyGeometryParser.#roundBodyGeometry(
|
|
82
|
+
fields,
|
|
83
|
+
'cylinder',
|
|
84
|
+
standoffHeightMil
|
|
85
|
+
)
|
|
86
|
+
case 3:
|
|
87
|
+
return PcbShapeBasedBodyGeometryParser.#sphereGeometry(
|
|
88
|
+
fields,
|
|
89
|
+
standoffHeightMil
|
|
90
|
+
)
|
|
91
|
+
default:
|
|
92
|
+
return {
|
|
93
|
+
kind: 'unknown-' + modelType,
|
|
94
|
+
status: 'incomplete',
|
|
95
|
+
units: 'mil',
|
|
96
|
+
standoffHeightMil
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parses one possible shape-based body record at a byte offset.
|
|
103
|
+
* @param {Uint8Array} bytes Stream bytes.
|
|
104
|
+
* @param {number} offset Candidate offset.
|
|
105
|
+
* @returns {{ fields: Record<string, string | string[]>, staticGeometry?: object, nextOffset: number } | null}
|
|
106
|
+
*/
|
|
107
|
+
static #parseRecordAt(bytes, offset) {
|
|
108
|
+
const view = new DataView(
|
|
109
|
+
bytes.buffer,
|
|
110
|
+
bytes.byteOffset,
|
|
111
|
+
bytes.byteLength
|
|
112
|
+
)
|
|
113
|
+
const textLength = view.getInt32(offset + 18, true)
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
textLength <= 0 ||
|
|
117
|
+
textLength > 65536 ||
|
|
118
|
+
offset + 22 + textLength > bytes.byteLength
|
|
119
|
+
) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const textBytes = bytes.subarray(offset + 22, offset + 22 + textLength)
|
|
124
|
+
const text = PrintableTextDecoder.decodeBytes(textBytes).replaceAll(
|
|
125
|
+
'\u0000',
|
|
126
|
+
''
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
!text.includes('MODEL.') ||
|
|
131
|
+
(!text.includes('MODELID=') && !text.includes('MODEL.NAME='))
|
|
132
|
+
) {
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fields =
|
|
137
|
+
PcbShapeBasedBodyGeometryParser.#parseFieldRecordBytes(textBytes)
|
|
138
|
+
const vertexBlock = PcbShapeBasedBodyGeometryParser.#parseVertexBlock(
|
|
139
|
+
bytes,
|
|
140
|
+
offset + 22 + textLength
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
fields,
|
|
145
|
+
staticGeometry: PcbShapeBasedBodyGeometryParser.buildStaticGeometry(
|
|
146
|
+
fields,
|
|
147
|
+
vertexBlock.verticesMil
|
|
148
|
+
),
|
|
149
|
+
nextOffset:
|
|
150
|
+
vertexBlock.nextOffset > offset
|
|
151
|
+
? vertexBlock.nextOffset
|
|
152
|
+
: offset + 22 + textLength
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parses the optional coordinate block after a shape-body field record.
|
|
158
|
+
* @param {Uint8Array} bytes Stream bytes.
|
|
159
|
+
* @param {number} offset Coordinate-count offset.
|
|
160
|
+
* @returns {{ verticesMil: { x: number, y: number }[], nextOffset: number }}
|
|
161
|
+
*/
|
|
162
|
+
static #parseVertexBlock(bytes, offset) {
|
|
163
|
+
if (offset + 4 > bytes.byteLength) {
|
|
164
|
+
return {
|
|
165
|
+
verticesMil: [],
|
|
166
|
+
nextOffset: offset
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const view = new DataView(
|
|
171
|
+
bytes.buffer,
|
|
172
|
+
bytes.byteOffset,
|
|
173
|
+
bytes.byteLength
|
|
174
|
+
)
|
|
175
|
+
const vertexCount = view.getInt32(offset, true)
|
|
176
|
+
const dataOffset = offset + 4
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
vertexCount < 0 ||
|
|
180
|
+
vertexCount > 10000 ||
|
|
181
|
+
dataOffset + vertexCount * 37 > bytes.byteLength
|
|
182
|
+
) {
|
|
183
|
+
return {
|
|
184
|
+
verticesMil: [],
|
|
185
|
+
nextOffset: offset
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const verticesMil = []
|
|
190
|
+
|
|
191
|
+
for (let index = 0; index < vertexCount; index += 1) {
|
|
192
|
+
const vertexOffset = dataOffset + index * 37
|
|
193
|
+
verticesMil.push({
|
|
194
|
+
x: PcbShapeBasedBodyGeometryParser.#roundMil(
|
|
195
|
+
view.getInt32(vertexOffset + 1, true) / 10000
|
|
196
|
+
),
|
|
197
|
+
y: PcbShapeBasedBodyGeometryParser.#roundMil(
|
|
198
|
+
view.getInt32(vertexOffset + 5, true) / 10000
|
|
199
|
+
)
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
verticesMil,
|
|
205
|
+
nextOffset: dataOffset + vertexCount * 37
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Parses one printable field record.
|
|
211
|
+
* @param {Uint8Array} bytes Record bytes.
|
|
212
|
+
* @returns {Record<string, string | string[]>}
|
|
213
|
+
*/
|
|
214
|
+
static #parseFieldRecordBytes(bytes) {
|
|
215
|
+
const fields = {}
|
|
216
|
+
const text = PrintableTextDecoder.decodeBytes(bytes)
|
|
217
|
+
.replaceAll('\u0000', '')
|
|
218
|
+
.trim()
|
|
219
|
+
|
|
220
|
+
for (const segment of text.split('|')) {
|
|
221
|
+
const trimmedSegment = segment.trim()
|
|
222
|
+
if (!trimmedSegment) {
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const separatorIndex = trimmedSegment.indexOf('=')
|
|
227
|
+
if (separatorIndex === -1) {
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const key = trimmedSegment.slice(0, separatorIndex).trim()
|
|
232
|
+
const value = trimmedSegment.slice(separatorIndex + 1).trim()
|
|
233
|
+
|
|
234
|
+
if (!key) {
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
PcbShapeBasedBodyGeometryParser.#appendFieldValue(
|
|
239
|
+
fields,
|
|
240
|
+
key,
|
|
241
|
+
value
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return fields
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Builds extruded-polygon static geometry.
|
|
250
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
251
|
+
* @param {{ x: number, y: number }[]} verticesMil Extracted vertices.
|
|
252
|
+
* @param {number} standoffHeightMil Standoff height.
|
|
253
|
+
* @returns {object}
|
|
254
|
+
*/
|
|
255
|
+
static #extrudedPolygonGeometry(fields, verticesMil, standoffHeightMil) {
|
|
256
|
+
const minZMil =
|
|
257
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
258
|
+
fields,
|
|
259
|
+
'MODEL.EXTRUDED.MINZ'
|
|
260
|
+
) || 0
|
|
261
|
+
const maxZMil =
|
|
262
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
263
|
+
fields,
|
|
264
|
+
'MODEL.EXTRUDED.MAXZ'
|
|
265
|
+
) ||
|
|
266
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
267
|
+
fields,
|
|
268
|
+
'OVERALLHEIGHT'
|
|
269
|
+
) ||
|
|
270
|
+
0
|
|
271
|
+
const heightMil = PcbShapeBasedBodyGeometryParser.#positiveNumber(
|
|
272
|
+
maxZMil - minZMil
|
|
273
|
+
)
|
|
274
|
+
const hasCompleteGeometry =
|
|
275
|
+
Array.isArray(verticesMil) && verticesMil.length >= 3 && heightMil
|
|
276
|
+
|
|
277
|
+
return PcbShapeBasedBodyGeometryParser.#stripUndefined({
|
|
278
|
+
kind: 'extruded-polygon',
|
|
279
|
+
status: hasCompleteGeometry ? 'complete' : 'incomplete',
|
|
280
|
+
units: 'mil',
|
|
281
|
+
minZMil: PcbShapeBasedBodyGeometryParser.#roundMil(minZMil),
|
|
282
|
+
maxZMil: PcbShapeBasedBodyGeometryParser.#roundMil(maxZMil),
|
|
283
|
+
heightMil,
|
|
284
|
+
standoffHeightMil,
|
|
285
|
+
verticesMil: verticesMil?.length ? verticesMil : undefined
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Builds cone or cylinder static geometry.
|
|
291
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
292
|
+
* @param {'cone' | 'cylinder'} kind Body kind.
|
|
293
|
+
* @param {number} standoffHeightMil Standoff height.
|
|
294
|
+
* @returns {object}
|
|
295
|
+
*/
|
|
296
|
+
static #roundBodyGeometry(fields, kind, standoffHeightMil) {
|
|
297
|
+
const upperKind = kind.toUpperCase()
|
|
298
|
+
const radiusMil =
|
|
299
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
300
|
+
fields,
|
|
301
|
+
'MODEL.' + upperKind + '.RADIUS'
|
|
302
|
+
) ||
|
|
303
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
304
|
+
fields,
|
|
305
|
+
'MODEL.CYLINDER.RADIUS'
|
|
306
|
+
)
|
|
307
|
+
const heightMil =
|
|
308
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
309
|
+
fields,
|
|
310
|
+
'MODEL.' + upperKind + '.HEIGHT'
|
|
311
|
+
) ||
|
|
312
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
313
|
+
fields,
|
|
314
|
+
'MODEL.CYLINDER.HEIGHT'
|
|
315
|
+
) ||
|
|
316
|
+
PcbShapeBasedBodyGeometryParser.#heightFromOverall(fields)
|
|
317
|
+
const complete =
|
|
318
|
+
PcbShapeBasedBodyGeometryParser.#positiveNumber(radiusMil) &&
|
|
319
|
+
PcbShapeBasedBodyGeometryParser.#positiveNumber(heightMil)
|
|
320
|
+
|
|
321
|
+
return PcbShapeBasedBodyGeometryParser.#stripUndefined({
|
|
322
|
+
kind,
|
|
323
|
+
status: complete ? 'complete' : 'incomplete',
|
|
324
|
+
units: 'mil',
|
|
325
|
+
radiusMil:
|
|
326
|
+
PcbShapeBasedBodyGeometryParser.#positiveNumber(radiusMil),
|
|
327
|
+
heightMil:
|
|
328
|
+
PcbShapeBasedBodyGeometryParser.#positiveNumber(heightMil),
|
|
329
|
+
standoffHeightMil
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Builds sphere static geometry.
|
|
335
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
336
|
+
* @param {number} standoffHeightMil Standoff height.
|
|
337
|
+
* @returns {object}
|
|
338
|
+
*/
|
|
339
|
+
static #sphereGeometry(fields, standoffHeightMil) {
|
|
340
|
+
const heightFromOverall =
|
|
341
|
+
PcbShapeBasedBodyGeometryParser.#heightFromOverall(fields)
|
|
342
|
+
const radiusMil =
|
|
343
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
344
|
+
fields,
|
|
345
|
+
'MODEL.SPHERE.RADIUS'
|
|
346
|
+
) ||
|
|
347
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
348
|
+
fields,
|
|
349
|
+
'MODEL.CYLINDER.RADIUS'
|
|
350
|
+
) ||
|
|
351
|
+
(heightFromOverall ? heightFromOverall / 2 : undefined)
|
|
352
|
+
const complete =
|
|
353
|
+
PcbShapeBasedBodyGeometryParser.#positiveNumber(radiusMil)
|
|
354
|
+
|
|
355
|
+
return PcbShapeBasedBodyGeometryParser.#stripUndefined({
|
|
356
|
+
kind: 'sphere',
|
|
357
|
+
status: complete ? 'complete' : 'incomplete',
|
|
358
|
+
units: 'mil',
|
|
359
|
+
radiusMil:
|
|
360
|
+
PcbShapeBasedBodyGeometryParser.#positiveNumber(radiusMil),
|
|
361
|
+
standoffHeightMil
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Derives body height from overall and standoff fields.
|
|
367
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
368
|
+
* @returns {number | undefined}
|
|
369
|
+
*/
|
|
370
|
+
static #heightFromOverall(fields) {
|
|
371
|
+
const overallHeightMil =
|
|
372
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
373
|
+
fields,
|
|
374
|
+
'OVERALLHEIGHT'
|
|
375
|
+
) || 0
|
|
376
|
+
const standoffHeightMil =
|
|
377
|
+
PcbShapeBasedBodyGeometryParser.#parseMilLikeField(
|
|
378
|
+
fields,
|
|
379
|
+
'STANDOFFHEIGHT'
|
|
380
|
+
) || 0
|
|
381
|
+
|
|
382
|
+
return PcbShapeBasedBodyGeometryParser.#positiveNumber(
|
|
383
|
+
overallHeightMil - standoffHeightMil
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Returns the latest meaningful field value from one parsed field map.
|
|
389
|
+
* @param {Record<string, string | string[]>} fields Field map.
|
|
390
|
+
* @param {string} key Field key.
|
|
391
|
+
* @returns {string}
|
|
392
|
+
*/
|
|
393
|
+
static #getField(fields, key) {
|
|
394
|
+
const raw = fields[key]
|
|
395
|
+
const values = Array.isArray(raw) ? raw : [raw]
|
|
396
|
+
|
|
397
|
+
return (
|
|
398
|
+
values
|
|
399
|
+
.map((value) => String(value || '').trim())
|
|
400
|
+
.findLast((value) => value.length > 0) || ''
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Appends one field value while preserving duplicate keys.
|
|
406
|
+
* @param {Record<string, string | string[]>} fields Field map.
|
|
407
|
+
* @param {string} key Field key.
|
|
408
|
+
* @param {string} value Field value.
|
|
409
|
+
* @returns {void}
|
|
410
|
+
*/
|
|
411
|
+
static #appendFieldValue(fields, key, value) {
|
|
412
|
+
if (!(key in fields)) {
|
|
413
|
+
fields[key] = value
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const previous = fields[key]
|
|
418
|
+
if (Array.isArray(previous)) {
|
|
419
|
+
previous.push(value)
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
fields[key] = [previous, value]
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Parses one floating-point field.
|
|
428
|
+
* @param {Record<string, string | string[]>} fields Field map.
|
|
429
|
+
* @param {string} key Field key.
|
|
430
|
+
* @returns {number | null}
|
|
431
|
+
*/
|
|
432
|
+
static #parseNumberField(fields, key) {
|
|
433
|
+
const raw = PcbShapeBasedBodyGeometryParser.#getField(fields, key)
|
|
434
|
+
const match = raw.match(/-?\d+(?:\.\d+)?(?:E[+-]?\d+)?/i)
|
|
435
|
+
|
|
436
|
+
if (!match) {
|
|
437
|
+
return null
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const parsed = Number(match[0])
|
|
441
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Parses one integer-like field.
|
|
446
|
+
* @param {Record<string, string | string[]>} fields Field map.
|
|
447
|
+
* @param {string} key Field key.
|
|
448
|
+
* @returns {number | null}
|
|
449
|
+
*/
|
|
450
|
+
static #parseIntegerField(fields, key) {
|
|
451
|
+
const parsed = PcbShapeBasedBodyGeometryParser.#parseNumberField(
|
|
452
|
+
fields,
|
|
453
|
+
key
|
|
454
|
+
)
|
|
455
|
+
if (!Number.isFinite(parsed)) {
|
|
456
|
+
return null
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return Math.trunc(parsed)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Parses one mil-like field from text or raw fixed-point storage.
|
|
464
|
+
* @param {Record<string, string | string[]>} fields Field map.
|
|
465
|
+
* @param {string} key Field key.
|
|
466
|
+
* @returns {number | null}
|
|
467
|
+
*/
|
|
468
|
+
static #parseMilLikeField(fields, key) {
|
|
469
|
+
const raw = PcbShapeBasedBodyGeometryParser.#getField(fields, key)
|
|
470
|
+
const parsed = PcbShapeBasedBodyGeometryParser.#parseNumberField(
|
|
471
|
+
fields,
|
|
472
|
+
key
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if (!Number.isFinite(parsed)) {
|
|
476
|
+
return null
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return /mil/i.test(raw) ? parsed : parsed / 10000
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Returns a rounded positive number or undefined.
|
|
484
|
+
* @param {unknown} value Candidate value.
|
|
485
|
+
* @returns {number | undefined}
|
|
486
|
+
*/
|
|
487
|
+
static #positiveNumber(value) {
|
|
488
|
+
const number = Number(value)
|
|
489
|
+
return Number.isFinite(number) && number > 0
|
|
490
|
+
? PcbShapeBasedBodyGeometryParser.#roundMil(number)
|
|
491
|
+
: undefined
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Rounds one mil value for stable JSON output.
|
|
496
|
+
* @param {number} value Candidate value.
|
|
497
|
+
* @returns {number}
|
|
498
|
+
*/
|
|
499
|
+
static #roundMil(value) {
|
|
500
|
+
const rounded = Math.round(Number(value) * 10000) / 10000
|
|
501
|
+
return Object.is(rounded, -0) ? 0 : rounded
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Removes undefined values from a row.
|
|
506
|
+
* @param {object} row Row to clean.
|
|
507
|
+
* @returns {object}
|
|
508
|
+
*/
|
|
509
|
+
static #stripUndefined(row) {
|
|
510
|
+
return Object.fromEntries(
|
|
511
|
+
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
}
|