altium-toolkit 1.1.0 → 1.1.2
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/package.json +1 -1
- package/src/core/altium/NaturalStringComparator.mjs +175 -0
- package/src/core/altium/ParserUtils.mjs +83 -14
- package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +104 -33
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +26 -4
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +28 -5
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +1 -3
- package/src/core/altium/PcbReviewMetadataBuilder.mjs +59 -28
- package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +66 -29
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +50 -16
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +36 -11
- package/src/core/altium/SchematicImplementationParser.mjs +75 -36
- package/src/core/altium/SchematicQaReportBuilder.mjs +115 -38
- package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +155 -5
- package/src/ui/PcbInteractionIndex.mjs +16 -1
package/package.json
CHANGED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compares generated ECAD keys in natural ASCII order without locale collation.
|
|
7
|
+
*/
|
|
8
|
+
export class NaturalStringComparator {
|
|
9
|
+
/**
|
|
10
|
+
* Compares two strings with numeric runs ordered by numeric value.
|
|
11
|
+
* @param {string} left Left value.
|
|
12
|
+
* @param {string} right Right value.
|
|
13
|
+
* @returns {number}
|
|
14
|
+
*/
|
|
15
|
+
static compare(left, right) {
|
|
16
|
+
const leftValue = String(left ?? '')
|
|
17
|
+
const rightValue = String(right ?? '')
|
|
18
|
+
let leftIndex = 0
|
|
19
|
+
let rightIndex = 0
|
|
20
|
+
|
|
21
|
+
while (leftIndex < leftValue.length && rightIndex < rightValue.length) {
|
|
22
|
+
const leftCode = leftValue.charCodeAt(leftIndex)
|
|
23
|
+
const rightCode = rightValue.charCodeAt(rightIndex)
|
|
24
|
+
|
|
25
|
+
if (leftCode > 0x7f || rightCode > 0x7f) {
|
|
26
|
+
return leftValue.localeCompare(rightValue, undefined, {
|
|
27
|
+
numeric: true
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
NaturalStringComparator.#isDigit(leftCode) &&
|
|
33
|
+
NaturalStringComparator.#isDigit(rightCode)
|
|
34
|
+
) {
|
|
35
|
+
const digitComparison =
|
|
36
|
+
NaturalStringComparator.#compareDigitRuns(
|
|
37
|
+
leftValue,
|
|
38
|
+
rightValue,
|
|
39
|
+
leftIndex,
|
|
40
|
+
rightIndex
|
|
41
|
+
)
|
|
42
|
+
if (digitComparison.comparison !== 0) {
|
|
43
|
+
return digitComparison.comparison
|
|
44
|
+
}
|
|
45
|
+
leftIndex = digitComparison.leftEnd
|
|
46
|
+
rightIndex = digitComparison.rightEnd
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const normalizedLeft =
|
|
51
|
+
NaturalStringComparator.#toLowerAsciiCode(leftCode)
|
|
52
|
+
const normalizedRight =
|
|
53
|
+
NaturalStringComparator.#toLowerAsciiCode(rightCode)
|
|
54
|
+
if (normalizedLeft !== normalizedRight) {
|
|
55
|
+
return normalizedLeft - normalizedRight
|
|
56
|
+
}
|
|
57
|
+
if (leftCode !== rightCode) {
|
|
58
|
+
return leftCode - rightCode
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
leftIndex += 1
|
|
62
|
+
rightIndex += 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return leftValue.length - rightValue.length
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compares numeric runs starting at the provided offsets.
|
|
70
|
+
* @param {string} leftValue Left value.
|
|
71
|
+
* @param {string} rightValue Right value.
|
|
72
|
+
* @param {number} leftStart Left digit offset.
|
|
73
|
+
* @param {number} rightStart Right digit offset.
|
|
74
|
+
* @returns {{ comparison: number, leftEnd: number, rightEnd: number }}
|
|
75
|
+
*/
|
|
76
|
+
static #compareDigitRuns(leftValue, rightValue, leftStart, rightStart) {
|
|
77
|
+
const leftEnd = NaturalStringComparator.#digitRunEnd(
|
|
78
|
+
leftValue,
|
|
79
|
+
leftStart
|
|
80
|
+
)
|
|
81
|
+
const rightEnd = NaturalStringComparator.#digitRunEnd(
|
|
82
|
+
rightValue,
|
|
83
|
+
rightStart
|
|
84
|
+
)
|
|
85
|
+
const leftSignificant = NaturalStringComparator.#skipLeadingZeros(
|
|
86
|
+
leftValue,
|
|
87
|
+
leftStart,
|
|
88
|
+
leftEnd
|
|
89
|
+
)
|
|
90
|
+
const rightSignificant = NaturalStringComparator.#skipLeadingZeros(
|
|
91
|
+
rightValue,
|
|
92
|
+
rightStart,
|
|
93
|
+
rightEnd
|
|
94
|
+
)
|
|
95
|
+
const leftLength = leftEnd - leftSignificant
|
|
96
|
+
const rightLength = rightEnd - rightSignificant
|
|
97
|
+
|
|
98
|
+
if (leftLength !== rightLength) {
|
|
99
|
+
return {
|
|
100
|
+
comparison: leftLength - rightLength,
|
|
101
|
+
leftEnd,
|
|
102
|
+
rightEnd
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (
|
|
107
|
+
let offset = 0;
|
|
108
|
+
offset < leftLength && offset < rightLength;
|
|
109
|
+
offset += 1
|
|
110
|
+
) {
|
|
111
|
+
const comparison =
|
|
112
|
+
leftValue.charCodeAt(leftSignificant + offset) -
|
|
113
|
+
rightValue.charCodeAt(rightSignificant + offset)
|
|
114
|
+
if (comparison !== 0) {
|
|
115
|
+
return { comparison, leftEnd, rightEnd }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
comparison: leftEnd - leftStart - (rightEnd - rightStart),
|
|
121
|
+
leftEnd,
|
|
122
|
+
rightEnd
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Returns the offset after one ASCII digit run.
|
|
128
|
+
* @param {string} value Source value.
|
|
129
|
+
* @param {number} start Start offset.
|
|
130
|
+
* @returns {number}
|
|
131
|
+
*/
|
|
132
|
+
static #digitRunEnd(value, start) {
|
|
133
|
+
let end = start
|
|
134
|
+
while (
|
|
135
|
+
end < value.length &&
|
|
136
|
+
NaturalStringComparator.#isDigit(value.charCodeAt(end))
|
|
137
|
+
) {
|
|
138
|
+
end += 1
|
|
139
|
+
}
|
|
140
|
+
return end
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Skips leading zeroes while leaving one digit for all-zero runs.
|
|
145
|
+
* @param {string} value Source value.
|
|
146
|
+
* @param {number} start Start offset.
|
|
147
|
+
* @param {number} end End offset.
|
|
148
|
+
* @returns {number}
|
|
149
|
+
*/
|
|
150
|
+
static #skipLeadingZeros(value, start, end) {
|
|
151
|
+
let index = start
|
|
152
|
+
while (index < end - 1 && value.charCodeAt(index) === 48) {
|
|
153
|
+
index += 1
|
|
154
|
+
}
|
|
155
|
+
return index
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns true for ASCII digit code points.
|
|
160
|
+
* @param {number} code Character code.
|
|
161
|
+
* @returns {boolean}
|
|
162
|
+
*/
|
|
163
|
+
static #isDigit(code) {
|
|
164
|
+
return code >= 48 && code <= 57
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Converts uppercase ASCII code points to lowercase.
|
|
169
|
+
* @param {number} code Character code.
|
|
170
|
+
* @returns {number}
|
|
171
|
+
*/
|
|
172
|
+
static #toLowerAsciiCode(code) {
|
|
173
|
+
return code >= 65 && code <= 90 ? code + 32 : code
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* Shared parsing helpers for normalized Altium records.
|
|
7
7
|
*/
|
|
8
8
|
export class ParserUtils {
|
|
9
|
+
/** @type {WeakMap<object, Map<string, { raw: unknown, rawUtf8: unknown, value: string }>>} */
|
|
10
|
+
static #fieldValueCache = new WeakMap()
|
|
11
|
+
|
|
9
12
|
/**
|
|
10
13
|
* Removes duplicate PCB placements by designator.
|
|
11
14
|
* @param {{ designator: string }[]} components
|
|
@@ -141,16 +144,69 @@ export class ParserUtils {
|
|
|
141
144
|
* @returns {string}
|
|
142
145
|
*/
|
|
143
146
|
static #getPreferredFieldValue(fields, key, skipAsterisk) {
|
|
144
|
-
if (!fields) return ''
|
|
147
|
+
if (!fields || typeof fields !== 'object') return ''
|
|
145
148
|
|
|
146
149
|
const utf8Key = 'UTF8:' + key
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
const rawUtf8 = fields[utf8Key]
|
|
151
|
+
const raw = fields[key]
|
|
152
|
+
|
|
153
|
+
if (!Array.isArray(rawUtf8) && !Array.isArray(raw)) {
|
|
154
|
+
const utf8Value = ParserUtils.#pickFieldValue(rawUtf8, skipAsterisk)
|
|
155
|
+
return utf8Value || ParserUtils.#pickFieldValue(raw, skipAsterisk)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const cacheKey = key + ':' + (skipAsterisk ? 'text' : 'field')
|
|
159
|
+
const cached = ParserUtils.#cachedFieldValue(
|
|
160
|
+
fields,
|
|
161
|
+
cacheKey,
|
|
162
|
+
raw,
|
|
163
|
+
rawUtf8
|
|
150
164
|
)
|
|
151
|
-
if (
|
|
165
|
+
if (cached !== null) return cached
|
|
166
|
+
|
|
167
|
+
const utf8Value = ParserUtils.#pickFieldValue(rawUtf8, skipAsterisk)
|
|
168
|
+
const value =
|
|
169
|
+
utf8Value || ParserUtils.#pickFieldValue(raw, skipAsterisk)
|
|
170
|
+
|
|
171
|
+
ParserUtils.#cacheFieldValue(fields, cacheKey, raw, rawUtf8, value)
|
|
172
|
+
return value
|
|
173
|
+
}
|
|
152
174
|
|
|
153
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Returns a cached normalized value when the raw field references match.
|
|
177
|
+
* @param {object} fields Field object.
|
|
178
|
+
* @param {string} cacheKey Cache key.
|
|
179
|
+
* @param {unknown} raw Raw field payload.
|
|
180
|
+
* @param {unknown} rawUtf8 Raw UTF-8 field payload.
|
|
181
|
+
* @returns {string | null}
|
|
182
|
+
*/
|
|
183
|
+
static #cachedFieldValue(fields, cacheKey, raw, rawUtf8) {
|
|
184
|
+
const fieldCache = ParserUtils.#fieldValueCache.get(fields)
|
|
185
|
+
const cached = fieldCache?.get(cacheKey)
|
|
186
|
+
if (!cached || cached.raw !== raw || cached.rawUtf8 !== rawUtf8) {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return cached.value
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Stores one normalized field value.
|
|
195
|
+
* @param {object} fields Field object.
|
|
196
|
+
* @param {string} cacheKey Cache key.
|
|
197
|
+
* @param {unknown} raw Raw field payload.
|
|
198
|
+
* @param {unknown} rawUtf8 Raw UTF-8 field payload.
|
|
199
|
+
* @param {string} value Normalized value.
|
|
200
|
+
* @returns {void}
|
|
201
|
+
*/
|
|
202
|
+
static #cacheFieldValue(fields, cacheKey, raw, rawUtf8, value) {
|
|
203
|
+
let fieldCache = ParserUtils.#fieldValueCache.get(fields)
|
|
204
|
+
if (!fieldCache) {
|
|
205
|
+
fieldCache = new Map()
|
|
206
|
+
ParserUtils.#fieldValueCache.set(fields, fieldCache)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fieldCache.set(cacheKey, { raw, rawUtf8, value })
|
|
154
210
|
}
|
|
155
211
|
|
|
156
212
|
/**
|
|
@@ -160,14 +216,27 @@ export class ParserUtils {
|
|
|
160
216
|
* @returns {string}
|
|
161
217
|
*/
|
|
162
218
|
static #pickFieldValue(raw, skipAsterisk) {
|
|
163
|
-
|
|
219
|
+
if (!Array.isArray(raw)) {
|
|
220
|
+
const value = ParserUtils.#normalizeFieldValue(raw)
|
|
221
|
+
return value && (!skipAsterisk || value !== '*') ? value : ''
|
|
222
|
+
}
|
|
164
223
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
224
|
+
for (let index = raw.length - 1; index >= 0; index -= 1) {
|
|
225
|
+
const value = ParserUtils.#normalizeFieldValue(raw[index])
|
|
226
|
+
if (value && (!skipAsterisk || value !== '*')) {
|
|
227
|
+
return value
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return ''
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Normalizes one field payload value.
|
|
236
|
+
* @param {string | undefined} value
|
|
237
|
+
* @returns {string}
|
|
238
|
+
*/
|
|
239
|
+
static #normalizeFieldValue(value) {
|
|
240
|
+
return String(value || '').trim()
|
|
172
241
|
}
|
|
173
242
|
}
|
|
@@ -14,47 +14,54 @@ export class PcbComponentPrimitiveIndexer {
|
|
|
14
14
|
* @returns {{ componentIndex: number, designator: string, pads: object[], tracks: object[], arcs: object[], fills: object[], vias: object[], regions: object[], shapeBasedRegions: object[], texts: object[], componentBodies: object[] }[]}
|
|
15
15
|
*/
|
|
16
16
|
static buildGroups(components, pcb, componentBodies) {
|
|
17
|
+
const primitiveGroups =
|
|
18
|
+
PcbComponentPrimitiveIndexer.#primitiveGroupsByComponent(
|
|
19
|
+
pcb,
|
|
20
|
+
componentBodies
|
|
21
|
+
)
|
|
22
|
+
|
|
17
23
|
return (components || []).map((component) => {
|
|
18
24
|
const componentIndex = Number(component.componentIndex)
|
|
19
25
|
|
|
20
26
|
return {
|
|
21
27
|
componentIndex,
|
|
22
28
|
designator: component.designator,
|
|
23
|
-
pads: PcbComponentPrimitiveIndexer.#
|
|
24
|
-
|
|
29
|
+
pads: PcbComponentPrimitiveIndexer.#groupForComponent(
|
|
30
|
+
primitiveGroups.pads,
|
|
25
31
|
componentIndex
|
|
26
32
|
),
|
|
27
|
-
tracks: PcbComponentPrimitiveIndexer.#
|
|
28
|
-
|
|
33
|
+
tracks: PcbComponentPrimitiveIndexer.#groupForComponent(
|
|
34
|
+
primitiveGroups.tracks,
|
|
29
35
|
componentIndex
|
|
30
36
|
),
|
|
31
|
-
arcs: PcbComponentPrimitiveIndexer.#
|
|
32
|
-
|
|
37
|
+
arcs: PcbComponentPrimitiveIndexer.#groupForComponent(
|
|
38
|
+
primitiveGroups.arcs,
|
|
33
39
|
componentIndex
|
|
34
40
|
),
|
|
35
|
-
fills: PcbComponentPrimitiveIndexer.#
|
|
36
|
-
|
|
41
|
+
fills: PcbComponentPrimitiveIndexer.#groupForComponent(
|
|
42
|
+
primitiveGroups.fills,
|
|
37
43
|
componentIndex
|
|
38
44
|
),
|
|
39
|
-
vias: PcbComponentPrimitiveIndexer.#
|
|
40
|
-
|
|
45
|
+
vias: PcbComponentPrimitiveIndexer.#groupForComponent(
|
|
46
|
+
primitiveGroups.vias,
|
|
41
47
|
componentIndex
|
|
42
48
|
),
|
|
43
|
-
regions: PcbComponentPrimitiveIndexer.#
|
|
44
|
-
|
|
49
|
+
regions: PcbComponentPrimitiveIndexer.#groupForComponent(
|
|
50
|
+
primitiveGroups.regions,
|
|
45
51
|
componentIndex
|
|
46
52
|
),
|
|
47
53
|
shapeBasedRegions:
|
|
48
|
-
PcbComponentPrimitiveIndexer.#
|
|
49
|
-
|
|
54
|
+
PcbComponentPrimitiveIndexer.#groupForComponent(
|
|
55
|
+
primitiveGroups.shapeBasedRegions,
|
|
50
56
|
componentIndex
|
|
51
57
|
),
|
|
52
|
-
texts: (
|
|
53
|
-
|
|
58
|
+
texts: PcbComponentPrimitiveIndexer.#groupForComponent(
|
|
59
|
+
primitiveGroups.texts,
|
|
60
|
+
componentIndex
|
|
54
61
|
),
|
|
55
62
|
componentBodies:
|
|
56
|
-
PcbComponentPrimitiveIndexer.#
|
|
57
|
-
componentBodies,
|
|
63
|
+
PcbComponentPrimitiveIndexer.#groupForComponent(
|
|
64
|
+
primitiveGroups.componentBodies,
|
|
58
65
|
componentIndex
|
|
59
66
|
)
|
|
60
67
|
}
|
|
@@ -87,23 +94,87 @@ export class PcbComponentPrimitiveIndexer {
|
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
/**
|
|
90
|
-
*
|
|
91
|
-
* @param {{
|
|
92
|
-
* @param {number}
|
|
93
|
-
* @returns {object[]}
|
|
97
|
+
* Builds primitive maps by native component ownership index.
|
|
98
|
+
* @param {{ fills?: object[], tracks?: object[], arcs?: object[], vias?: object[], pads?: object[], regions?: object[], shapeBasedRegions?: object[], texts?: object[] }} pcb
|
|
99
|
+
* @param {{ componentIndex?: number | null }[]} componentBodies
|
|
100
|
+
* @returns {{ pads: Map<number, object[]>, tracks: Map<number, object[]>, arcs: Map<number, object[]>, fills: Map<number, object[]>, vias: Map<number, object[]>, regions: Map<number, object[]>, shapeBasedRegions: Map<number, object[]>, texts: Map<number, object[]>, componentBodies: Map<number, object[]> }}
|
|
101
|
+
*/
|
|
102
|
+
static #primitiveGroupsByComponent(pcb, componentBodies) {
|
|
103
|
+
return {
|
|
104
|
+
pads: PcbComponentPrimitiveIndexer.#groupPrimitivesByIndex(
|
|
105
|
+
pcb?.pads
|
|
106
|
+
),
|
|
107
|
+
tracks: PcbComponentPrimitiveIndexer.#groupPrimitivesByIndex(
|
|
108
|
+
pcb?.tracks
|
|
109
|
+
),
|
|
110
|
+
arcs: PcbComponentPrimitiveIndexer.#groupPrimitivesByIndex(
|
|
111
|
+
pcb?.arcs
|
|
112
|
+
),
|
|
113
|
+
fills: PcbComponentPrimitiveIndexer.#groupPrimitivesByIndex(
|
|
114
|
+
pcb?.fills
|
|
115
|
+
),
|
|
116
|
+
vias: PcbComponentPrimitiveIndexer.#groupPrimitivesByIndex(
|
|
117
|
+
pcb?.vias
|
|
118
|
+
),
|
|
119
|
+
regions: PcbComponentPrimitiveIndexer.#groupPrimitivesByIndex(
|
|
120
|
+
pcb?.regions
|
|
121
|
+
),
|
|
122
|
+
shapeBasedRegions:
|
|
123
|
+
PcbComponentPrimitiveIndexer.#groupPrimitivesByIndex(
|
|
124
|
+
pcb?.shapeBasedRegions
|
|
125
|
+
),
|
|
126
|
+
texts: PcbComponentPrimitiveIndexer.#groupPrimitivesByIndex(
|
|
127
|
+
pcb?.texts,
|
|
128
|
+
'ownerIndex'
|
|
129
|
+
),
|
|
130
|
+
componentBodies:
|
|
131
|
+
PcbComponentPrimitiveIndexer.#groupPrimitivesByIndex(
|
|
132
|
+
componentBodies
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Groups primitives by one numeric owner-index field.
|
|
139
|
+
* @param {object[] | undefined} primitives Source primitives.
|
|
140
|
+
* @param {string} key Owner-index field name.
|
|
141
|
+
* @returns {Map<number, object[]>}
|
|
94
142
|
*/
|
|
95
|
-
static #
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
|
|
143
|
+
static #groupPrimitivesByIndex(primitives, key = 'componentIndex') {
|
|
144
|
+
const groupedPrimitives = new Map()
|
|
145
|
+
|
|
146
|
+
for (const primitive of primitives || []) {
|
|
147
|
+
const componentIndex = PcbComponentPrimitiveIndexer.#optionalIndex(
|
|
148
|
+
primitive?.[key]
|
|
149
|
+
)
|
|
150
|
+
if (componentIndex === null) continue
|
|
151
|
+
if (!groupedPrimitives.has(componentIndex)) {
|
|
152
|
+
groupedPrimitives.set(componentIndex, [])
|
|
104
153
|
}
|
|
154
|
+
groupedPrimitives.get(componentIndex).push(primitive)
|
|
155
|
+
}
|
|
105
156
|
|
|
106
|
-
|
|
107
|
-
|
|
157
|
+
return groupedPrimitives
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Returns primitives linked to one component index.
|
|
162
|
+
* @param {Map<number, object[]>} groupedPrimitives Grouped primitives.
|
|
163
|
+
* @param {number} componentIndex Component index.
|
|
164
|
+
* @returns {object[]}
|
|
165
|
+
*/
|
|
166
|
+
static #groupForComponent(groupedPrimitives, componentIndex) {
|
|
167
|
+
return groupedPrimitives.get(componentIndex) || []
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parses one optional component owner index.
|
|
172
|
+
* @param {unknown} value Candidate index value.
|
|
173
|
+
* @returns {number | null}
|
|
174
|
+
*/
|
|
175
|
+
static #optionalIndex(value) {
|
|
176
|
+
if (value === null || value === undefined || value === '') return null
|
|
177
|
+
const index = Number(value)
|
|
178
|
+
return Number.isFinite(index) ? index : null
|
|
108
179
|
}
|
|
109
180
|
}
|
|
@@ -14,6 +14,8 @@ const { parseNumericField } = ParserUtils
|
|
|
14
14
|
export class PcbLayerStackReadModelBuilder {
|
|
15
15
|
static SCHEMA_ID = 'altium-toolkit.pcb.layer-stack.a1'
|
|
16
16
|
|
|
17
|
+
static #fieldIndexes = new WeakMap()
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
20
|
* Builds the layer-stack sidecar.
|
|
19
21
|
* @param {{ fileName: string, boardRecords: { fields: Record<string, string | string[]>, sourceStream?: string }[], streamNames?: string[], layers: object[], primitiveLayers: object[], layerSubstacks: object[], boardRegions: object[] }} input Source model context.
|
|
@@ -799,17 +801,37 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
799
801
|
* @returns {string}
|
|
800
802
|
*/
|
|
801
803
|
static #field(fields, key) {
|
|
802
|
-
if (Object.hasOwn(fields, key)) {
|
|
804
|
+
if (Object.hasOwn(fields, key) || key in fields) {
|
|
803
805
|
return ParserUtils.getField(fields, key)
|
|
804
806
|
}
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
(fieldKey) => fieldKey.toUpperCase() === upperKey
|
|
807
|
+
const realKey = PcbLayerStackReadModelBuilder.#fieldIndex(fields).get(
|
|
808
|
+
key.toUpperCase()
|
|
808
809
|
)
|
|
809
810
|
|
|
810
811
|
return realKey ? ParserUtils.getField(fields, realKey) : ''
|
|
811
812
|
}
|
|
812
813
|
|
|
814
|
+
/**
|
|
815
|
+
* Builds or returns a cached case-insensitive field-key index.
|
|
816
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
817
|
+
* @returns {Map<string, string>}
|
|
818
|
+
*/
|
|
819
|
+
static #fieldIndex(fields) {
|
|
820
|
+
const cached = PcbLayerStackReadModelBuilder.#fieldIndexes.get(fields)
|
|
821
|
+
if (cached) return cached
|
|
822
|
+
|
|
823
|
+
const fieldIndex = new Map()
|
|
824
|
+
for (const fieldKey of Object.keys(fields)) {
|
|
825
|
+
const upperKey = fieldKey.toUpperCase()
|
|
826
|
+
if (!fieldIndex.has(upperKey)) {
|
|
827
|
+
fieldIndex.set(upperKey, fieldKey)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
PcbLayerStackReadModelBuilder.#fieldIndexes.set(fields, fieldIndex)
|
|
832
|
+
return fieldIndex
|
|
833
|
+
}
|
|
834
|
+
|
|
813
835
|
/**
|
|
814
836
|
* Parses layer-id lists.
|
|
815
837
|
* @param {string} value Raw list value.
|
|
@@ -8,6 +8,8 @@ import { ParserUtils } from './ParserUtils.mjs'
|
|
|
8
8
|
* Parses source-only layer-stack metadata that is not part of core geometry.
|
|
9
9
|
*/
|
|
10
10
|
export class PcbLayerStackSourceMetadataParser {
|
|
11
|
+
static #fieldIndexes = new WeakMap()
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Parses source-aware extras for one layer-stack row.
|
|
13
15
|
* @param {Record<string, string | string[]>} fields Source fields.
|
|
@@ -401,17 +403,38 @@ export class PcbLayerStackSourceMetadataParser {
|
|
|
401
403
|
* @returns {string}
|
|
402
404
|
*/
|
|
403
405
|
static #field(fields, key) {
|
|
404
|
-
if (Object.hasOwn(fields, key)) {
|
|
406
|
+
if (Object.hasOwn(fields, key) || key in fields) {
|
|
405
407
|
return ParserUtils.getField(fields, key)
|
|
406
408
|
}
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
)
|
|
409
|
+
const realKey = PcbLayerStackSourceMetadataParser.#fieldIndex(
|
|
410
|
+
fields
|
|
411
|
+
).get(key.toUpperCase())
|
|
411
412
|
|
|
412
413
|
return realKey ? ParserUtils.getField(fields, realKey) : ''
|
|
413
414
|
}
|
|
414
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Builds or returns a cached case-insensitive field-key index.
|
|
418
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
419
|
+
* @returns {Map<string, string>}
|
|
420
|
+
*/
|
|
421
|
+
static #fieldIndex(fields) {
|
|
422
|
+
const cached =
|
|
423
|
+
PcbLayerStackSourceMetadataParser.#fieldIndexes.get(fields)
|
|
424
|
+
if (cached) return cached
|
|
425
|
+
|
|
426
|
+
const fieldIndex = new Map()
|
|
427
|
+
for (const fieldKey of Object.keys(fields)) {
|
|
428
|
+
const upperKey = fieldKey.toUpperCase()
|
|
429
|
+
if (!fieldIndex.has(upperKey)) {
|
|
430
|
+
fieldIndex.set(upperKey, fieldKey)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
PcbLayerStackSourceMetadataParser.#fieldIndexes.set(fields, fieldIndex)
|
|
435
|
+
return fieldIndex
|
|
436
|
+
}
|
|
437
|
+
|
|
415
438
|
/**
|
|
416
439
|
* Splits a native list field.
|
|
417
440
|
* @param {string} value Raw list value.
|
|
@@ -228,9 +228,7 @@ export class PcbOwnershipGraphBuilder {
|
|
|
228
228
|
if (!groups[key]) {
|
|
229
229
|
groups[key] = fallbackGroup
|
|
230
230
|
}
|
|
231
|
-
|
|
232
|
-
groups[key].primitiveKeys.push(primitiveKey)
|
|
233
|
-
}
|
|
231
|
+
groups[key].primitiveKeys.push(primitiveKey)
|
|
234
232
|
}
|
|
235
233
|
|
|
236
234
|
/**
|