altium-toolkit 1.1.23 → 1.1.25
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/docs/api.md +34 -13
- package/docs/model-format.md +37 -8
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +58 -1
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +8 -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/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 +7 -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
|
@@ -25,10 +25,17 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
25
25
|
const fields = PcbLayerStackReadModelBuilder.#mergeFields(
|
|
26
26
|
input.boardRecords || []
|
|
27
27
|
)
|
|
28
|
+
const nativeLayerOrder = (input.layers || []).length
|
|
29
|
+
? null
|
|
30
|
+
: PcbLayerStackReadModelBuilder.#nativeLayerOrder(
|
|
31
|
+
fields,
|
|
32
|
+
input.primitiveLayers || []
|
|
33
|
+
)
|
|
28
34
|
const layers = PcbLayerStackReadModelBuilder.#layers(
|
|
29
35
|
input.layers || [],
|
|
30
36
|
input.primitiveLayers || [],
|
|
31
|
-
fields
|
|
37
|
+
fields,
|
|
38
|
+
nativeLayerOrder
|
|
32
39
|
)
|
|
33
40
|
const layerById = new Map(
|
|
34
41
|
layers
|
|
@@ -85,6 +92,9 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
85
92
|
cavityRegionCount: cavityReport.cavityRegionCount,
|
|
86
93
|
stiffenerLayerCount: cavityReport.stiffenerLayerCount,
|
|
87
94
|
adhesiveLayerCount: cavityReport.adhesiveLayerCount,
|
|
95
|
+
...(nativeLayerOrder
|
|
96
|
+
? { nativeLayerOrderCount: nativeLayerOrder.layerIds.length }
|
|
97
|
+
: {}),
|
|
88
98
|
diagnosticCount: diagnostics.length
|
|
89
99
|
}
|
|
90
100
|
|
|
@@ -101,6 +111,7 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
101
111
|
topLevelBendLines,
|
|
102
112
|
cavityReport
|
|
103
113
|
),
|
|
114
|
+
...(nativeLayerOrder ? { nativeLayerOrder } : {}),
|
|
104
115
|
layers,
|
|
105
116
|
substacks,
|
|
106
117
|
branches,
|
|
@@ -160,9 +171,10 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
160
171
|
* @param {object[]} layers Parsed physical layers.
|
|
161
172
|
* @param {object[]} primitiveLayers Primitive layer map.
|
|
162
173
|
* @param {Record<string, string | string[]>} fields Source fields.
|
|
174
|
+
* @param {{ layerIds: number[] } | null} nativeLayerOrder Native order.
|
|
163
175
|
* @returns {object[]}
|
|
164
176
|
*/
|
|
165
|
-
static #layers(layers, primitiveLayers, fields) {
|
|
177
|
+
static #layers(layers, primitiveLayers, fields, nativeLayerOrder) {
|
|
166
178
|
if (layers.length) {
|
|
167
179
|
return layers.map((layer) =>
|
|
168
180
|
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
@@ -187,7 +199,19 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
187
199
|
)
|
|
188
200
|
}
|
|
189
201
|
|
|
190
|
-
|
|
202
|
+
const nativeOrderByLayerId = new Map(
|
|
203
|
+
(nativeLayerOrder?.layerIds || []).map((layerId, index) => [
|
|
204
|
+
layerId,
|
|
205
|
+
index + 1
|
|
206
|
+
])
|
|
207
|
+
)
|
|
208
|
+
const orderedPrimitiveLayers =
|
|
209
|
+
PcbLayerStackReadModelBuilder.#orderPrimitiveLayers(
|
|
210
|
+
primitiveLayers || [],
|
|
211
|
+
nativeLayerOrder
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return orderedPrimitiveLayers.map((layer, index) =>
|
|
191
215
|
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
192
216
|
index: index + 1,
|
|
193
217
|
layerId: layer.layerId,
|
|
@@ -196,6 +220,7 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
196
220
|
: undefined,
|
|
197
221
|
name: layer.name,
|
|
198
222
|
kind: layer.kind || layer.role,
|
|
223
|
+
nativeOrderIndex: nativeOrderByLayerId.get(layer.layerId),
|
|
199
224
|
...PcbLayerStackSourceMetadataParser.layerSourceFields(
|
|
200
225
|
fields,
|
|
201
226
|
index + 1
|
|
@@ -204,6 +229,116 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
204
229
|
)
|
|
205
230
|
}
|
|
206
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Parses the native layer linked-list order stored in Board6 records.
|
|
234
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
235
|
+
* @param {object[]} primitiveLayers Primitive layer fallbacks.
|
|
236
|
+
* @returns {{ source: string, layerIds: number[], complete: boolean } | null}
|
|
237
|
+
*/
|
|
238
|
+
static #nativeLayerOrder(fields, primitiveLayers) {
|
|
239
|
+
const layerIds = new Set(
|
|
240
|
+
(primitiveLayers || [])
|
|
241
|
+
.map((layer) => Number(layer.layerId))
|
|
242
|
+
.filter(Number.isFinite)
|
|
243
|
+
)
|
|
244
|
+
if (!layerIds.size) return null
|
|
245
|
+
|
|
246
|
+
const links = PcbLayerStackReadModelBuilder.#nativeLayerLinks(fields)
|
|
247
|
+
const firstLayerId = [...links.entries()].find(
|
|
248
|
+
([, link]) => link.prev === 0 && link.next !== 0
|
|
249
|
+
)?.[0]
|
|
250
|
+
if (!firstLayerId) return null
|
|
251
|
+
|
|
252
|
+
const orderedLayerIds = []
|
|
253
|
+
const visited = new Set()
|
|
254
|
+
let currentLayerId = firstLayerId
|
|
255
|
+
|
|
256
|
+
while (
|
|
257
|
+
Number.isFinite(currentLayerId) &&
|
|
258
|
+
currentLayerId !== 0 &&
|
|
259
|
+
!visited.has(currentLayerId)
|
|
260
|
+
) {
|
|
261
|
+
visited.add(currentLayerId)
|
|
262
|
+
|
|
263
|
+
if (layerIds.has(currentLayerId)) {
|
|
264
|
+
orderedLayerIds.push(currentLayerId)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const nextLayerId = links.get(currentLayerId)?.next || 0
|
|
268
|
+
currentLayerId = Number(nextLayerId)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!orderedLayerIds.length) return null
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
source: 'Board6/Data',
|
|
275
|
+
layerIds: orderedLayerIds,
|
|
276
|
+
complete: orderedLayerIds.length === layerIds.size
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Builds native layer link rows from Board6 LAYERnNEXT/PREV fields.
|
|
282
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
283
|
+
* @returns {Map<number, { next: number, prev: number }>}
|
|
284
|
+
*/
|
|
285
|
+
static #nativeLayerLinks(fields) {
|
|
286
|
+
const layerIndexes = PcbLayerStackReadModelBuilder.#indexedRows(
|
|
287
|
+
fields,
|
|
288
|
+
[/^LAYER(\d+)NEXT$/iu, /^LAYER(\d+)PREV$/iu]
|
|
289
|
+
)
|
|
290
|
+
const links = new Map()
|
|
291
|
+
|
|
292
|
+
for (const layerIndex of layerIndexes) {
|
|
293
|
+
const next = Number(
|
|
294
|
+
PcbLayerStackReadModelBuilder.#field(
|
|
295
|
+
fields,
|
|
296
|
+
'LAYER' + layerIndex + 'NEXT'
|
|
297
|
+
) || 0
|
|
298
|
+
)
|
|
299
|
+
const prev = Number(
|
|
300
|
+
PcbLayerStackReadModelBuilder.#field(
|
|
301
|
+
fields,
|
|
302
|
+
'LAYER' + layerIndex + 'PREV'
|
|
303
|
+
) || 0
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if (Number.isFinite(next) || Number.isFinite(prev)) {
|
|
307
|
+
links.set(layerIndex, {
|
|
308
|
+
next: Number.isFinite(next) ? next : 0,
|
|
309
|
+
prev: Number.isFinite(prev) ? prev : 0
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return links
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Orders primitive fallback layers with native linked-list evidence first.
|
|
319
|
+
* @param {object[]} primitiveLayers Primitive layer rows.
|
|
320
|
+
* @param {{ layerIds: number[] } | null} nativeLayerOrder Native order.
|
|
321
|
+
* @returns {object[]}
|
|
322
|
+
*/
|
|
323
|
+
static #orderPrimitiveLayers(primitiveLayers, nativeLayerOrder) {
|
|
324
|
+
if (!nativeLayerOrder?.layerIds?.length) return primitiveLayers
|
|
325
|
+
|
|
326
|
+
const layerById = new Map(
|
|
327
|
+
primitiveLayers
|
|
328
|
+
.filter((layer) => Number.isFinite(Number(layer.layerId)))
|
|
329
|
+
.map((layer) => [Number(layer.layerId), layer])
|
|
330
|
+
)
|
|
331
|
+
const orderedLayers = nativeLayerOrder.layerIds
|
|
332
|
+
.map((layerId) => layerById.get(layerId))
|
|
333
|
+
.filter(Boolean)
|
|
334
|
+
const orderedIds = new Set(nativeLayerOrder.layerIds)
|
|
335
|
+
const remainingLayers = primitiveLayers.filter(
|
|
336
|
+
(layer) => !orderedIds.has(Number(layer.layerId))
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return [...orderedLayers, ...remainingLayers]
|
|
340
|
+
}
|
|
341
|
+
|
|
207
342
|
/**
|
|
208
343
|
* Normalizes substacks and links them to board-region rows.
|
|
209
344
|
* @param {object[]} layerSubstacks Parsed substacks.
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds byte-offset evidence for preserved PCB primitive records.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbPrimitiveFieldOffsetReportBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.pcb.primitive-field-offsets.a1'
|
|
10
|
+
|
|
11
|
+
static #DEFAULT_FIELD_MAPS = Object.freeze([
|
|
12
|
+
PcbPrimitiveFieldOffsetReportBuilder.#fieldMap(
|
|
13
|
+
'Tracks6/Data',
|
|
14
|
+
'payload',
|
|
15
|
+
[
|
|
16
|
+
['layerId', 0, 1, 'uint8'],
|
|
17
|
+
['flags', 1, 1, 'uint8'],
|
|
18
|
+
['keepout', 2, 1, 'uint8'],
|
|
19
|
+
['netIndex', 3, 2, 'int16-le'],
|
|
20
|
+
['polygonIndex', 5, 2, 'int16-le'],
|
|
21
|
+
['componentIndex', 7, 2, 'int16-le'],
|
|
22
|
+
['x1', 13, 4, 'mil-int32-le'],
|
|
23
|
+
['y1', 17, 4, 'mil-int32-le'],
|
|
24
|
+
['x2', 21, 4, 'mil-int32-le'],
|
|
25
|
+
['y2', 25, 4, 'mil-int32-le'],
|
|
26
|
+
['width', 29, 4, 'mil-int32-le'],
|
|
27
|
+
['unionIndex', 36, 1, 'uint8'],
|
|
28
|
+
['lengthTuning', 37, 1, 'uint8'],
|
|
29
|
+
['userRouted', 44, 1, 'uint8']
|
|
30
|
+
]
|
|
31
|
+
),
|
|
32
|
+
PcbPrimitiveFieldOffsetReportBuilder.#fieldMap(
|
|
33
|
+
'Arcs6/Data',
|
|
34
|
+
'payload',
|
|
35
|
+
[
|
|
36
|
+
['layerId', 0, 1, 'uint8'],
|
|
37
|
+
['netIndex', 3, 2, 'int16-le'],
|
|
38
|
+
['polygonIndex', 5, 2, 'int16-le'],
|
|
39
|
+
['componentIndex', 7, 2, 'int16-le'],
|
|
40
|
+
['x', 13, 4, 'mil-int32-le'],
|
|
41
|
+
['y', 17, 4, 'mil-int32-le'],
|
|
42
|
+
['radius', 21, 4, 'mil-int32-le'],
|
|
43
|
+
['startAngle', 25, 8, 'float64-le'],
|
|
44
|
+
['endAngle', 33, 8, 'float64-le'],
|
|
45
|
+
['width', 41, 4, 'mil-int32-le']
|
|
46
|
+
]
|
|
47
|
+
),
|
|
48
|
+
PcbPrimitiveFieldOffsetReportBuilder.#fieldMap(
|
|
49
|
+
'Fills6/Data',
|
|
50
|
+
'record',
|
|
51
|
+
[
|
|
52
|
+
['layerId', 5, 1, 'uint8'],
|
|
53
|
+
['netIndex', 8, 2, 'int16-le'],
|
|
54
|
+
['polygonIndex', 10, 2, 'int16-le'],
|
|
55
|
+
['componentIndex', 12, 2, 'int16-le'],
|
|
56
|
+
['x1', 18, 4, 'mil-int32-le'],
|
|
57
|
+
['y1', 22, 4, 'mil-int32-le'],
|
|
58
|
+
['x2', 26, 4, 'mil-int32-le'],
|
|
59
|
+
['y2', 30, 4, 'mil-int32-le'],
|
|
60
|
+
['layerCode', 46, 2, 'uint16-le']
|
|
61
|
+
]
|
|
62
|
+
),
|
|
63
|
+
PcbPrimitiveFieldOffsetReportBuilder.#fieldMap('Vias6/Data', 'record', [
|
|
64
|
+
['layerId', 5, 1, 'uint8'],
|
|
65
|
+
['netIndex', 8, 2, 'int16-le'],
|
|
66
|
+
['polygonIndex', 10, 2, 'int16-le'],
|
|
67
|
+
['componentIndex', 12, 2, 'int16-le'],
|
|
68
|
+
['x', 18, 4, 'mil-int32-le'],
|
|
69
|
+
['y', 22, 4, 'mil-int32-le'],
|
|
70
|
+
['diameter', 26, 4, 'mil-int32-le'],
|
|
71
|
+
['holeDiameter', 30, 4, 'mil-int32-le'],
|
|
72
|
+
['layerStartId', 34, 1, 'uint8'],
|
|
73
|
+
['layerEndId', 35, 1, 'uint8']
|
|
74
|
+
])
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Builds a field-offset report for preserved primitive records.
|
|
79
|
+
* @param {{ fileName?: string, rawRecords?: object[], records?: object[], fieldMaps?: object[] }} [input]
|
|
80
|
+
* @returns {object}
|
|
81
|
+
*/
|
|
82
|
+
static build(input = {}) {
|
|
83
|
+
const records = PcbPrimitiveFieldOffsetReportBuilder.#records(input)
|
|
84
|
+
const fieldMaps = [
|
|
85
|
+
...PcbPrimitiveFieldOffsetReportBuilder.#DEFAULT_FIELD_MAPS,
|
|
86
|
+
...PcbPrimitiveFieldOffsetReportBuilder.#normalizeFieldMaps(
|
|
87
|
+
input.fieldMaps || []
|
|
88
|
+
)
|
|
89
|
+
]
|
|
90
|
+
const fields = []
|
|
91
|
+
const unmatchedRecords = []
|
|
92
|
+
|
|
93
|
+
for (const record of records) {
|
|
94
|
+
const fieldMap =
|
|
95
|
+
PcbPrimitiveFieldOffsetReportBuilder.#matchingFieldMap(
|
|
96
|
+
record,
|
|
97
|
+
fieldMaps
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if (!fieldMap) {
|
|
101
|
+
unmatchedRecords.push(
|
|
102
|
+
PcbPrimitiveFieldOffsetReportBuilder.#recordKey(record)
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const field of fieldMap.fields) {
|
|
108
|
+
fields.push(
|
|
109
|
+
PcbPrimitiveFieldOffsetReportBuilder.#fieldRow(
|
|
110
|
+
record,
|
|
111
|
+
fieldMap,
|
|
112
|
+
field
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const streams =
|
|
119
|
+
PcbPrimitiveFieldOffsetReportBuilder.#streamSummaries(fields)
|
|
120
|
+
|
|
121
|
+
return PcbPrimitiveFieldOffsetReportBuilder.#stripUndefined({
|
|
122
|
+
schema: PcbPrimitiveFieldOffsetReportBuilder.SCHEMA,
|
|
123
|
+
sourceDocument: input.fileName || undefined,
|
|
124
|
+
summary: {
|
|
125
|
+
recordCount: records.length,
|
|
126
|
+
mappedRecordCount: records.length - unmatchedRecords.length,
|
|
127
|
+
fieldCount: fields.length,
|
|
128
|
+
streamCount: streams.length,
|
|
129
|
+
unmatchedRecordCount: unmatchedRecords.length
|
|
130
|
+
},
|
|
131
|
+
streams,
|
|
132
|
+
fields,
|
|
133
|
+
unmatchedRecords
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Creates one built-in field-map descriptor.
|
|
139
|
+
* @param {string} sourceStream Native stream path.
|
|
140
|
+
* @param {'record' | 'payload'} relativeTo Offset origin.
|
|
141
|
+
* @param {Array<[string, number, number, string]>} fieldRows Field rows.
|
|
142
|
+
* @returns {object}
|
|
143
|
+
*/
|
|
144
|
+
static #fieldMap(sourceStream, relativeTo, fieldRows) {
|
|
145
|
+
return Object.freeze({
|
|
146
|
+
sourceStream,
|
|
147
|
+
relativeTo,
|
|
148
|
+
fields: Object.freeze(
|
|
149
|
+
fieldRows.map(([name, offset, byteLength, encoding]) =>
|
|
150
|
+
Object.freeze({
|
|
151
|
+
name,
|
|
152
|
+
offset,
|
|
153
|
+
byteLength,
|
|
154
|
+
encoding
|
|
155
|
+
})
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Normalizes record input.
|
|
163
|
+
* @param {object} input Report input.
|
|
164
|
+
* @returns {object[]}
|
|
165
|
+
*/
|
|
166
|
+
static #records(input) {
|
|
167
|
+
if (Array.isArray(input.rawRecords)) return input.rawRecords
|
|
168
|
+
if (Array.isArray(input.records)) return input.records
|
|
169
|
+
return []
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Normalizes caller-provided field maps.
|
|
174
|
+
* @param {object[]} fieldMaps Field maps.
|
|
175
|
+
* @returns {object[]}
|
|
176
|
+
*/
|
|
177
|
+
static #normalizeFieldMaps(fieldMaps) {
|
|
178
|
+
return fieldMaps
|
|
179
|
+
.filter((fieldMap) => fieldMap?.sourceStream)
|
|
180
|
+
.map((fieldMap) => ({
|
|
181
|
+
sourceStream: String(fieldMap.sourceStream),
|
|
182
|
+
family: fieldMap.family ? String(fieldMap.family) : undefined,
|
|
183
|
+
type: fieldMap.type ? String(fieldMap.type) : undefined,
|
|
184
|
+
relativeTo:
|
|
185
|
+
fieldMap.relativeTo === 'record' ? 'record' : 'payload',
|
|
186
|
+
fields: (Array.isArray(fieldMap.fields) ? fieldMap.fields : [])
|
|
187
|
+
.filter((field) => field?.name)
|
|
188
|
+
.map((field) =>
|
|
189
|
+
PcbPrimitiveFieldOffsetReportBuilder.#stripUndefined({
|
|
190
|
+
name: String(field.name),
|
|
191
|
+
offset: Number(field.offset || 0),
|
|
192
|
+
byteLength: Number(field.byteLength || 0),
|
|
193
|
+
encoding: field.encoding
|
|
194
|
+
? String(field.encoding)
|
|
195
|
+
: undefined,
|
|
196
|
+
relativeTo:
|
|
197
|
+
field.relativeTo === 'record' ||
|
|
198
|
+
field.relativeTo === 'payload'
|
|
199
|
+
? field.relativeTo
|
|
200
|
+
: undefined
|
|
201
|
+
})
|
|
202
|
+
)
|
|
203
|
+
}))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Finds the field-map matching one raw record.
|
|
208
|
+
* @param {object} record Raw record row.
|
|
209
|
+
* @param {object[]} fieldMaps Field-map descriptors.
|
|
210
|
+
* @returns {object | undefined}
|
|
211
|
+
*/
|
|
212
|
+
static #matchingFieldMap(record, fieldMaps) {
|
|
213
|
+
return fieldMaps.find((fieldMap) => {
|
|
214
|
+
if (fieldMap.sourceStream !== record.sourceStream) return false
|
|
215
|
+
if (fieldMap.family && fieldMap.family !== record.family) {
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
if (fieldMap.type && fieldMap.type !== record.type) return false
|
|
219
|
+
return true
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Builds one field evidence row.
|
|
225
|
+
* @param {object} record Raw record row.
|
|
226
|
+
* @param {object} fieldMap Field-map descriptor.
|
|
227
|
+
* @param {object} field Field descriptor.
|
|
228
|
+
* @returns {object}
|
|
229
|
+
*/
|
|
230
|
+
static #fieldRow(record, fieldMap, field) {
|
|
231
|
+
const relativeTo = field.relativeTo || fieldMap.relativeTo
|
|
232
|
+
const baseOffset =
|
|
233
|
+
Number(record.offset || 0) +
|
|
234
|
+
PcbPrimitiveFieldOffsetReportBuilder.#relativeOffset(
|
|
235
|
+
record,
|
|
236
|
+
relativeTo
|
|
237
|
+
)
|
|
238
|
+
const offset = Number(field.offset || 0)
|
|
239
|
+
const byteLength = Number(field.byteLength || 0)
|
|
240
|
+
const withinRecordOffset = baseOffset - Number(record.offset || 0)
|
|
241
|
+
|
|
242
|
+
return PcbPrimitiveFieldOffsetReportBuilder.#stripUndefined({
|
|
243
|
+
sourceStream: record.sourceStream,
|
|
244
|
+
family: record.family,
|
|
245
|
+
type: record.type,
|
|
246
|
+
typeId: PcbPrimitiveFieldOffsetReportBuilder.#finiteOrUndefined(
|
|
247
|
+
record.typeId
|
|
248
|
+
),
|
|
249
|
+
recordIndex:
|
|
250
|
+
PcbPrimitiveFieldOffsetReportBuilder.#finiteOrUndefined(
|
|
251
|
+
record.recordIndex
|
|
252
|
+
),
|
|
253
|
+
name: field.name,
|
|
254
|
+
relativeTo,
|
|
255
|
+
offset,
|
|
256
|
+
absoluteOffset: baseOffset + offset,
|
|
257
|
+
byteLength,
|
|
258
|
+
endOffset: baseOffset + offset + byteLength,
|
|
259
|
+
encoding: field.encoding,
|
|
260
|
+
available:
|
|
261
|
+
byteLength > 0 &&
|
|
262
|
+
withinRecordOffset + offset + byteLength <=
|
|
263
|
+
Number(record.byteLength || 0)
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Returns the offset of a field origin within a raw record.
|
|
269
|
+
* @param {object} record Raw record row.
|
|
270
|
+
* @param {'record' | 'payload'} relativeTo Offset origin.
|
|
271
|
+
* @returns {number}
|
|
272
|
+
*/
|
|
273
|
+
static #relativeOffset(record, relativeTo) {
|
|
274
|
+
if (relativeTo !== 'payload') return 0
|
|
275
|
+
if (record.encoding === 'length-prefixed') return 5
|
|
276
|
+
return 0
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Summarizes mapped fields by source stream.
|
|
281
|
+
* @param {object[]} fields Field rows.
|
|
282
|
+
* @returns {object[]}
|
|
283
|
+
*/
|
|
284
|
+
static #streamSummaries(fields) {
|
|
285
|
+
const byStream = new Map()
|
|
286
|
+
|
|
287
|
+
for (const field of fields) {
|
|
288
|
+
if (!byStream.has(field.sourceStream)) {
|
|
289
|
+
byStream.set(field.sourceStream, {
|
|
290
|
+
sourceStream: field.sourceStream,
|
|
291
|
+
recordIndexes: new Set(),
|
|
292
|
+
fieldNames: [],
|
|
293
|
+
fieldNameSet: new Set()
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const summary = byStream.get(field.sourceStream)
|
|
298
|
+
summary.recordIndexes.add(field.recordIndex)
|
|
299
|
+
if (!summary.fieldNameSet.has(field.name)) {
|
|
300
|
+
summary.fieldNameSet.add(field.name)
|
|
301
|
+
summary.fieldNames.push(field.name)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return [...byStream.values()].map((summary) => ({
|
|
306
|
+
sourceStream: summary.sourceStream,
|
|
307
|
+
recordCount: summary.recordIndexes.size,
|
|
308
|
+
fieldCount: fields.filter(
|
|
309
|
+
(field) => field.sourceStream === summary.sourceStream
|
|
310
|
+
).length,
|
|
311
|
+
fieldNames: summary.fieldNames
|
|
312
|
+
}))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Builds a compact record key for unmatched records.
|
|
317
|
+
* @param {object} record Raw record row.
|
|
318
|
+
* @returns {object}
|
|
319
|
+
*/
|
|
320
|
+
static #recordKey(record) {
|
|
321
|
+
return PcbPrimitiveFieldOffsetReportBuilder.#stripUndefined({
|
|
322
|
+
sourceStream: record.sourceStream,
|
|
323
|
+
family: record.family,
|
|
324
|
+
type: record.type,
|
|
325
|
+
recordIndex:
|
|
326
|
+
PcbPrimitiveFieldOffsetReportBuilder.#finiteOrUndefined(
|
|
327
|
+
record.recordIndex
|
|
328
|
+
)
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Returns a finite number or undefined.
|
|
334
|
+
* @param {unknown} value Candidate number.
|
|
335
|
+
* @returns {number | undefined}
|
|
336
|
+
*/
|
|
337
|
+
static #finiteOrUndefined(value) {
|
|
338
|
+
const number = Number(value)
|
|
339
|
+
return Number.isFinite(number) ? number : undefined
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Removes undefined values from a report row.
|
|
344
|
+
* @param {object} row Report row.
|
|
345
|
+
* @returns {object}
|
|
346
|
+
*/
|
|
347
|
+
static #stripUndefined(row) {
|
|
348
|
+
return Object.fromEntries(
|
|
349
|
+
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
}
|