altium-toolkit 1.1.1 → 1.1.3
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/AltiumLayoutParser.mjs +275 -13
- package/src/core/altium/AltiumParser.mjs +240 -8
- 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/PcbEmbeddedFontExtractor.mjs +186 -43
- 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/PrintableTextDecoder.mjs +133 -13
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +36 -11
- package/src/core/altium/SchematicImageParser.mjs +291 -6
- package/src/core/altium/SchematicImplementationParser.mjs +75 -36
- package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
- package/src/core/altium/SchematicPinParser.mjs +175 -4
- package/src/core/altium/SchematicQaReportBuilder.mjs +115 -38
- package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- package/src/ui/PcbInteractionIndex.mjs +16 -1
- package/src/ui/SchematicColorResolver.mjs +78 -0
- package/src/ui/SchematicContentLayout.mjs +58 -1
- package/src/ui/SchematicImageRenderer.mjs +125 -10
- package/src/ui/SchematicJunctionRenderer.mjs +1 -1
- package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
- package/src/ui/SchematicNoteRenderer.mjs +82 -6
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +292 -3
- package/src/ui/SchematicPinSvgRenderer.mjs +197 -15
- package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
- package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
- package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +82 -23
- package/src/ui/SchematicSvgRenderer.mjs +293 -47
|
@@ -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
|
}
|