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.
@@ -5,6 +5,7 @@
5
5
  import { PcbReviewDrillMetadataBuilder } from './PcbReviewDrillMetadataBuilder.mjs'
6
6
  import { PcbReviewPolygonRealizationBuilder } from './PcbReviewPolygonRealizationBuilder.mjs'
7
7
  import { PcbReviewRouteHighlightProfileBuilder } from './PcbReviewRouteHighlightProfileBuilder.mjs'
8
+ import { NaturalStringComparator } from './NaturalStringComparator.mjs'
8
9
 
9
10
  /**
10
11
  * Builds PCB review metadata for routed-class and board-assembly workflows.
@@ -18,11 +19,15 @@ export class PcbReviewMetadataBuilder {
18
19
  * @returns {object}
19
20
  */
20
21
  static build(pcb = {}) {
22
+ const routeAnalysis = pcb.routeAnalysis || {}
23
+ const routeRowsByName =
24
+ PcbReviewMetadataBuilder.#routeRowsByName(routeAnalysis)
21
25
  const routeGroups = PcbReviewMetadataBuilder.#routeGroups(
22
- pcb.routeAnalysis || {}
26
+ routeAnalysis,
27
+ routeRowsByName
23
28
  )
24
29
  const routeHighlightProfiles =
25
- PcbReviewRouteHighlightProfileBuilder.build(pcb.routeAnalysis || {})
30
+ PcbReviewRouteHighlightProfileBuilder.build(routeAnalysis)
26
31
  const polygonRealizations =
27
32
  PcbReviewPolygonRealizationBuilder.build(pcb)
28
33
  const drillReview = PcbReviewDrillMetadataBuilder.build(pcb)
@@ -51,7 +56,7 @@ export class PcbReviewMetadataBuilder {
51
56
  polygonRealizations,
52
57
  drillReview,
53
58
  boardAssemblyViews,
54
- pcb.routeAnalysis || {}
59
+ routeAnalysis
55
60
  )
56
61
  }
57
62
  }
@@ -59,21 +64,29 @@ export class PcbReviewMetadataBuilder {
59
64
  /**
60
65
  * Builds route highlight groups from route analysis.
61
66
  * @param {object} routeAnalysis Route analysis model.
67
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
62
68
  * @returns {object[]}
63
69
  */
64
- static #routeGroups(routeAnalysis) {
70
+ static #routeGroups(routeAnalysis, routeRowsByName) {
65
71
  return [
66
- ...PcbReviewMetadataBuilder.#classGroups(routeAnalysis),
67
- ...PcbReviewMetadataBuilder.#differentialPairGroups(routeAnalysis)
72
+ ...PcbReviewMetadataBuilder.#classGroups(
73
+ routeAnalysis,
74
+ routeRowsByName
75
+ ),
76
+ ...PcbReviewMetadataBuilder.#differentialPairGroups(
77
+ routeAnalysis,
78
+ routeRowsByName
79
+ )
68
80
  ]
69
81
  }
70
82
 
71
83
  /**
72
84
  * Builds net-class route groups.
73
85
  * @param {object} routeAnalysis Route analysis model.
86
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
74
87
  * @returns {object[]}
75
88
  */
76
- static #classGroups(routeAnalysis) {
89
+ static #classGroups(routeAnalysis, routeRowsByName) {
77
90
  return (routeAnalysis.classes || []).map((classRow) =>
78
91
  PcbReviewMetadataBuilder.#stripEmpty({
79
92
  key:
@@ -83,11 +96,11 @@ export class PcbReviewMetadataBuilder {
83
96
  name: classRow.name,
84
97
  netNames: classRow.netNames || [],
85
98
  layerKeys: PcbReviewMetadataBuilder.#layerKeysForNets(
86
- routeAnalysis,
99
+ routeRowsByName,
87
100
  classRow.netNames || []
88
101
  ),
89
102
  primitiveKeys: PcbReviewMetadataBuilder.#primitiveKeysForNets(
90
- routeAnalysis,
103
+ routeRowsByName,
91
104
  classRow.netNames || []
92
105
  ),
93
106
  totalLengthMil: classRow.totalLengthMil
@@ -98,9 +111,10 @@ export class PcbReviewMetadataBuilder {
98
111
  /**
99
112
  * Builds differential-pair route groups.
100
113
  * @param {object} routeAnalysis Route analysis model.
114
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
101
115
  * @returns {object[]}
102
116
  */
103
- static #differentialPairGroups(routeAnalysis) {
117
+ static #differentialPairGroups(routeAnalysis, routeRowsByName) {
104
118
  return (routeAnalysis.differentialPairs || []).map((pair) => {
105
119
  const netNames = [
106
120
  pair.positiveNetName,
@@ -115,11 +129,11 @@ export class PcbReviewMetadataBuilder {
115
129
  name: pair.name,
116
130
  netNames,
117
131
  layerKeys: PcbReviewMetadataBuilder.#layerKeysForNets(
118
- routeAnalysis,
132
+ routeRowsByName,
119
133
  netNames
120
134
  ),
121
135
  primitiveKeys: PcbReviewMetadataBuilder.#primitiveKeysForNets(
122
- routeAnalysis,
136
+ routeRowsByName,
123
137
  netNames
124
138
  ),
125
139
  totalLengthMil: PcbReviewMetadataBuilder.#round(
@@ -226,20 +240,20 @@ export class PcbReviewMetadataBuilder {
226
240
  }
227
241
  return Object.fromEntries(
228
242
  Object.entries(entries).sort(([left], [right]) =>
229
- left.localeCompare(right, undefined, { numeric: true })
243
+ NaturalStringComparator.compare(left, right)
230
244
  )
231
245
  )
232
246
  }
233
247
 
234
248
  /**
235
249
  * Returns layer keys participating in a list of nets.
236
- * @param {object} routeAnalysis Route analysis model.
250
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
237
251
  * @param {string[]} netNames Net names.
238
252
  * @returns {string[]}
239
253
  */
240
- static #layerKeysForNets(routeAnalysis, netNames) {
254
+ static #layerKeysForNets(routeRowsByName, netNames) {
241
255
  const nets = PcbReviewMetadataBuilder.#netsByName(
242
- routeAnalysis,
256
+ routeRowsByName,
243
257
  netNames
244
258
  )
245
259
  return PcbReviewMetadataBuilder.#sortedStrings(
@@ -249,13 +263,13 @@ export class PcbReviewMetadataBuilder {
249
263
 
250
264
  /**
251
265
  * Returns primitive keys participating in a list of nets.
252
- * @param {object} routeAnalysis Route analysis model.
266
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
253
267
  * @param {string[]} netNames Net names.
254
268
  * @returns {string[]}
255
269
  */
256
- static #primitiveKeysForNets(routeAnalysis, netNames) {
270
+ static #primitiveKeysForNets(routeRowsByName, netNames) {
257
271
  const nets = PcbReviewMetadataBuilder.#netsByName(
258
- routeAnalysis,
272
+ routeRowsByName,
259
273
  netNames
260
274
  )
261
275
  return PcbReviewMetadataBuilder.#sortedStrings(
@@ -264,15 +278,32 @@ export class PcbReviewMetadataBuilder {
264
278
  }
265
279
 
266
280
  /**
267
- * Resolves net route rows by name.
281
+ * Builds route rows indexed by net name.
268
282
  * @param {object} routeAnalysis Route analysis model.
283
+ * @returns {Map<string, object[]>}
284
+ */
285
+ static #routeRowsByName(routeAnalysis) {
286
+ const rowsByName = new Map()
287
+ for (const net of routeAnalysis.byNet || []) {
288
+ const netName = String(net?.netName || '')
289
+ if (!netName) continue
290
+ if (!rowsByName.has(netName)) {
291
+ rowsByName.set(netName, [])
292
+ }
293
+ rowsByName.get(netName).push(net)
294
+ }
295
+ return rowsByName
296
+ }
297
+
298
+ /**
299
+ * Resolves net route rows by name.
300
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
269
301
  * @param {string[]} netNames Net names.
270
302
  * @returns {object[]}
271
303
  */
272
- static #netsByName(routeAnalysis, netNames) {
273
- const wanted = new Set(netNames || [])
274
- return (routeAnalysis.byNet || []).filter((net) =>
275
- wanted.has(net.netName)
304
+ static #netsByName(routeRowsByName, netNames) {
305
+ return (netNames || []).flatMap(
306
+ (netName) => routeRowsByName.get(netName) || []
276
307
  )
277
308
  }
278
309
 
@@ -323,10 +354,10 @@ export class PcbReviewMetadataBuilder {
323
354
  * @returns {string[]}
324
355
  */
325
356
  static #sortedStrings(values) {
326
- return [...new Set((values || []).filter(Boolean))].sort(
327
- (left, right) =>
328
- left.localeCompare(right, undefined, { numeric: true })
329
- )
357
+ const sortedValues = [...new Set((values || []).filter(Boolean))]
358
+ return sortedValues.length < 2
359
+ ? sortedValues
360
+ : sortedValues.sort(NaturalStringComparator.compare)
330
361
  }
331
362
 
332
363
  /**
@@ -2,6 +2,8 @@
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
+ import { NaturalStringComparator } from './NaturalStringComparator.mjs'
6
+
5
7
  /**
6
8
  * Builds route-highlight profile rows for PCB review metadata.
7
9
  */
@@ -12,33 +14,45 @@ export class PcbReviewRouteHighlightProfileBuilder {
12
14
  * @returns {object[]}
13
15
  */
14
16
  static build(routeAnalysis = {}) {
17
+ const routeRowsByName =
18
+ PcbReviewRouteHighlightProfileBuilder.#routeRowsByName(
19
+ routeAnalysis
20
+ )
15
21
  return [
16
22
  ...PcbReviewRouteHighlightProfileBuilder.#differentialPairProfiles(
17
- routeAnalysis
23
+ routeAnalysis,
24
+ routeRowsByName
18
25
  ),
19
26
  ...PcbReviewRouteHighlightProfileBuilder.#differentialPairClassProfiles(
20
- routeAnalysis
27
+ routeAnalysis,
28
+ routeRowsByName
21
29
  ),
22
30
  ...PcbReviewRouteHighlightProfileBuilder.#netClassProfiles(
23
- routeAnalysis
31
+ routeAnalysis,
32
+ routeRowsByName
24
33
  ),
25
- ...PcbReviewRouteHighlightProfileBuilder.#netProfiles(routeAnalysis)
34
+ ...PcbReviewRouteHighlightProfileBuilder.#netProfiles(
35
+ routeAnalysis,
36
+ routeRowsByName
37
+ )
26
38
  ]
27
39
  }
28
40
 
29
41
  /**
30
42
  * Builds net-class highlight profiles.
31
43
  * @param {object} routeAnalysis Route analysis model.
44
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
32
45
  * @returns {object[]}
33
46
  */
34
- static #netClassProfiles(routeAnalysis) {
47
+ static #netClassProfiles(routeAnalysis, routeRowsByName) {
35
48
  return (routeAnalysis.classes || []).map((classRow) =>
36
49
  PcbReviewRouteHighlightProfileBuilder.#highlightProfile({
37
50
  selectorKind: 'net-class',
38
51
  keyPrefix: 'highlight-net-class-',
39
52
  name: classRow.name,
40
53
  netNames: classRow.netNames || [],
41
- routeAnalysis
54
+ routeAnalysis,
55
+ routeRowsByName
42
56
  })
43
57
  )
44
58
  }
@@ -46,9 +60,10 @@ export class PcbReviewRouteHighlightProfileBuilder {
46
60
  /**
47
61
  * Builds differential-pair highlight profiles.
48
62
  * @param {object} routeAnalysis Route analysis model.
63
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
49
64
  * @returns {object[]}
50
65
  */
51
- static #differentialPairProfiles(routeAnalysis) {
66
+ static #differentialPairProfiles(routeAnalysis, routeRowsByName) {
52
67
  return (routeAnalysis.differentialPairs || []).map((pair) =>
53
68
  PcbReviewRouteHighlightProfileBuilder.#highlightProfile({
54
69
  selectorKind: 'differential-pair',
@@ -57,7 +72,8 @@ export class PcbReviewRouteHighlightProfileBuilder {
57
72
  netNames: [pair.positiveNetName, pair.negativeNetName].filter(
58
73
  Boolean
59
74
  ),
60
- routeAnalysis
75
+ routeAnalysis,
76
+ routeRowsByName
61
77
  })
62
78
  )
63
79
  }
@@ -65,9 +81,10 @@ export class PcbReviewRouteHighlightProfileBuilder {
65
81
  /**
66
82
  * Builds differential-pair class highlight profiles.
67
83
  * @param {object} routeAnalysis Route analysis model.
84
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
68
85
  * @returns {object[]}
69
86
  */
70
- static #differentialPairClassProfiles(routeAnalysis) {
87
+ static #differentialPairClassProfiles(routeAnalysis, routeRowsByName) {
71
88
  const classNames = new Map()
72
89
  for (const pair of routeAnalysis.differentialPairs || []) {
73
90
  for (const className of pair.classes || []) {
@@ -88,7 +105,7 @@ export class PcbReviewRouteHighlightProfileBuilder {
88
105
 
89
106
  return [...classNames.entries()]
90
107
  .sort(([left], [right]) =>
91
- left.localeCompare(right, undefined, { numeric: true })
108
+ NaturalStringComparator.compare(left, right)
92
109
  )
93
110
  .map(([className, netNames]) =>
94
111
  PcbReviewRouteHighlightProfileBuilder.#highlightProfile({
@@ -96,7 +113,8 @@ export class PcbReviewRouteHighlightProfileBuilder {
96
113
  keyPrefix: 'highlight-diff-pair-class-',
97
114
  name: className,
98
115
  netNames: [...netNames],
99
- routeAnalysis
116
+ routeAnalysis,
117
+ routeRowsByName
100
118
  })
101
119
  )
102
120
  }
@@ -104,23 +122,25 @@ export class PcbReviewRouteHighlightProfileBuilder {
104
122
  /**
105
123
  * Builds scalar net highlight profiles.
106
124
  * @param {object} routeAnalysis Route analysis model.
125
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
107
126
  * @returns {object[]}
108
127
  */
109
- static #netProfiles(routeAnalysis) {
128
+ static #netProfiles(routeAnalysis, routeRowsByName) {
110
129
  return (routeAnalysis.byNet || []).map((net) =>
111
130
  PcbReviewRouteHighlightProfileBuilder.#highlightProfile({
112
131
  selectorKind: 'net',
113
132
  keyPrefix: 'highlight-net-',
114
133
  name: net.netName,
115
134
  netNames: [net.netName],
116
- routeAnalysis
135
+ routeAnalysis,
136
+ routeRowsByName
117
137
  })
118
138
  )
119
139
  }
120
140
 
121
141
  /**
122
142
  * Builds one route-highlight profile.
123
- * @param {{ selectorKind: string, keyPrefix: string, name: string, netNames: string[], routeAnalysis: object }} options Profile options.
143
+ * @param {{ selectorKind: string, keyPrefix: string, name: string, netNames: string[], routeAnalysis: object, routeRowsByName: Map<string, object[]> }} options Profile options.
124
144
  * @returns {object}
125
145
  */
126
146
  static #highlightProfile(options) {
@@ -129,7 +149,8 @@ export class PcbReviewRouteHighlightProfileBuilder {
129
149
  )
130
150
  const layerGroups = PcbReviewRouteHighlightProfileBuilder.#layerGroups(
131
151
  options.routeAnalysis,
132
- netNames
152
+ netNames,
153
+ options.routeRowsByName
133
154
  )
134
155
 
135
156
  return PcbReviewRouteHighlightProfileBuilder.#stripEmpty({
@@ -158,12 +179,13 @@ export class PcbReviewRouteHighlightProfileBuilder {
158
179
  * Builds per-layer route-highlight groups.
159
180
  * @param {object} routeAnalysis Route analysis model.
160
181
  * @param {string[]} netNames Net names.
182
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
161
183
  * @returns {object[]}
162
184
  */
163
- static #layerGroups(routeAnalysis, netNames) {
185
+ static #layerGroups(routeAnalysis, netNames, routeRowsByName) {
164
186
  const groupsByLayer = new Map()
165
187
  for (const net of PcbReviewRouteHighlightProfileBuilder.#netsByName(
166
- routeAnalysis,
188
+ routeRowsByName,
167
189
  netNames
168
190
  )) {
169
191
  for (const participation of net.layerParticipation || []) {
@@ -205,22 +227,37 @@ export class PcbReviewRouteHighlightProfileBuilder {
205
227
  )
206
228
  }))
207
229
  .sort((left, right) =>
208
- left.layerKey.localeCompare(right.layerKey, undefined, {
209
- numeric: true
210
- })
230
+ NaturalStringComparator.compare(left.layerKey, right.layerKey)
211
231
  )
212
232
  }
213
233
 
214
234
  /**
215
- * Resolves net route rows by name.
235
+ * Builds route rows indexed by net name.
216
236
  * @param {object} routeAnalysis Route analysis model.
237
+ * @returns {Map<string, object[]>}
238
+ */
239
+ static #routeRowsByName(routeAnalysis) {
240
+ const rowsByName = new Map()
241
+ for (const net of routeAnalysis.byNet || []) {
242
+ const netName = String(net?.netName || '')
243
+ if (!netName) continue
244
+ if (!rowsByName.has(netName)) {
245
+ rowsByName.set(netName, [])
246
+ }
247
+ rowsByName.get(netName).push(net)
248
+ }
249
+ return rowsByName
250
+ }
251
+
252
+ /**
253
+ * Resolves net route rows by name.
254
+ * @param {Map<string, object[]>} routeRowsByName Route rows by net name.
217
255
  * @param {string[]} netNames Net names.
218
256
  * @returns {object[]}
219
257
  */
220
- static #netsByName(routeAnalysis, netNames) {
221
- const wanted = new Set(netNames || [])
222
- return (routeAnalysis.byNet || []).filter((net) =>
223
- wanted.has(net.netName)
258
+ static #netsByName(routeRowsByName, netNames) {
259
+ return (netNames || []).flatMap(
260
+ (netName) => routeRowsByName.get(netName) || []
224
261
  )
225
262
  }
226
263
 
@@ -248,10 +285,10 @@ export class PcbReviewRouteHighlightProfileBuilder {
248
285
  * @returns {string[]}
249
286
  */
250
287
  static #sortedStrings(values) {
251
- return [...new Set((values || []).filter(Boolean))].sort(
252
- (left, right) =>
253
- left.localeCompare(right, undefined, { numeric: true })
254
- )
288
+ const sortedValues = [...new Set((values || []).filter(Boolean))]
289
+ return sortedValues.length < 2
290
+ ? sortedValues
291
+ : sortedValues.sort(NaturalStringComparator.compare)
255
292
  }
256
293
 
257
294
  /**
@@ -183,23 +183,30 @@ export class PcbRouteAnalysisBuilder {
183
183
  * @returns {object[]}
184
184
  */
185
185
  static #netRows(pcb, routePrimitives, viaRows) {
186
+ const primitivesByNet =
187
+ PcbRouteAnalysisBuilder.#rowsByNet(routePrimitives)
188
+ const viasByNet = PcbRouteAnalysisBuilder.#rowsByNet(viaRows)
186
189
  const netNames = new Set([
187
190
  ...(pcb?.nets || []).map((net) => net.name).filter(Boolean),
188
- ...routePrimitives.map((primitive) => primitive.netName),
189
- ...viaRows.map((via) => via.netName)
191
+ ...primitivesByNet.keys(),
192
+ ...viasByNet.keys()
190
193
  ])
191
194
 
192
195
  return [...netNames]
193
- .map((netName) =>
194
- PcbRouteAnalysisBuilder.#netRow(
196
+ .map((netName) => {
197
+ const primitives = primitivesByNet.get(netName) || []
198
+ const vias = viasByNet.get(netName) || []
199
+ if (!primitives.length && !vias.length) {
200
+ return null
201
+ }
202
+
203
+ return PcbRouteAnalysisBuilder.#netRow(
195
204
  netName,
196
- routePrimitives.filter(
197
- (primitive) => primitive.netName === netName
198
- ),
199
- viaRows.filter((via) => via.netName === netName)
205
+ primitives,
206
+ vias
200
207
  )
201
- )
202
- .filter((net) => net.totalLengthMil > 0 || net.viaCount > 0)
208
+ })
209
+ .filter(Boolean)
203
210
  .sort((left, right) =>
204
211
  left.netName.localeCompare(right.netName, undefined, {
205
212
  numeric: true
@@ -215,12 +222,16 @@ export class PcbRouteAnalysisBuilder {
215
222
  * @returns {object}
216
223
  */
217
224
  static #netRow(netName, primitives, vias) {
218
- const trackRows = primitives.filter(
219
- (primitive) => primitive.kind === 'track'
220
- )
221
- const arcRows = primitives.filter(
222
- (primitive) => primitive.kind === 'arc'
223
- )
225
+ const trackRows = []
226
+ const arcRows = []
227
+
228
+ for (const primitive of primitives || []) {
229
+ if (primitive.kind === 'track') {
230
+ trackRows.push(primitive)
231
+ } else if (primitive.kind === 'arc') {
232
+ arcRows.push(primitive)
233
+ }
234
+ }
224
235
 
225
236
  return {
226
237
  netName,
@@ -240,6 +251,29 @@ export class PcbRouteAnalysisBuilder {
240
251
  }
241
252
  }
242
253
 
254
+ /**
255
+ * Groups route rows by net name.
256
+ * @param {object[]} rows Route or via rows.
257
+ * @returns {Map<string, object[]>}
258
+ */
259
+ static #rowsByNet(rows) {
260
+ const rowsByNet = new Map()
261
+
262
+ for (const row of rows || []) {
263
+ const netName = String(row?.netName || '').trim()
264
+ if (!netName) {
265
+ continue
266
+ }
267
+
268
+ if (!rowsByNet.has(netName)) {
269
+ rowsByNet.set(netName, [])
270
+ }
271
+ rowsByNet.get(netName).push(row)
272
+ }
273
+
274
+ return rowsByNet
275
+ }
276
+
243
277
  /**
244
278
  * Builds per-net-class length summaries.
245
279
  * @param {object} pcb Normalized PCB model.
@@ -18,12 +18,14 @@ export class SchematicDisplayModeCatalogParser {
18
18
  * @returns {object | null}
19
19
  */
20
20
  static parse(records) {
21
+ const childrenByOwner =
22
+ SchematicDisplayModeCatalogParser.#ownerChildrenByOwner(records)
21
23
  const components = (records || [])
22
24
  .filter((record) => getField(record.fields, 'RECORD') === '1')
23
25
  .map((record) =>
24
26
  SchematicDisplayModeCatalogParser.#componentCatalog(
25
27
  record,
26
- records
28
+ childrenByOwner
27
29
  )
28
30
  )
29
31
  .filter(Boolean)
@@ -41,17 +43,17 @@ export class SchematicDisplayModeCatalogParser {
41
43
  /**
42
44
  * Builds one component display-mode catalog row.
43
45
  * @param {object} componentRecord Component record.
44
- * @param {object[]} records All schematic records.
46
+ * @param {Map<string, object[]>} childrenByOwner Child records by owner index.
45
47
  * @returns {object}
46
48
  */
47
- static #componentCatalog(componentRecord, records) {
49
+ static #componentCatalog(componentRecord, childrenByOwner) {
48
50
  const indexInSheet = parseNumericField(
49
51
  componentRecord.fields,
50
52
  'IndexInSheet'
51
53
  )
52
54
  const ownerIndex = String(indexInSheet ?? componentRecord.recordIndex)
53
55
  const children = SchematicDisplayModeCatalogParser.#ownerChildren(
54
- records,
56
+ childrenByOwner,
55
57
  ownerIndex
56
58
  )
57
59
  const declaredPartCount =
@@ -95,16 +97,39 @@ export class SchematicDisplayModeCatalogParser {
95
97
 
96
98
  /**
97
99
  * Returns child records with owner part/display metadata.
98
- * @param {object[]} records All schematic records.
100
+ * @param {Map<string, object[]>} childrenByOwner Child records by owner index.
99
101
  * @param {string} ownerIndex Component owner index.
100
102
  * @returns {object[]}
101
103
  */
102
- static #ownerChildren(records, ownerIndex) {
103
- return (records || []).filter(
104
- (record) =>
105
- getField(record.fields, 'OwnerIndex') === ownerIndex &&
106
- parseNumericField(record.fields, 'OwnerPartID') !== null
107
- )
104
+ static #ownerChildren(childrenByOwner, ownerIndex) {
105
+ return childrenByOwner.get(ownerIndex) || []
106
+ }
107
+
108
+ /**
109
+ * Indexes schematic owner child records once for display-mode cataloguing.
110
+ * @param {object[]} records Schematic records.
111
+ * @returns {Map<string, object[]>}
112
+ */
113
+ static #ownerChildrenByOwner(records) {
114
+ const childrenByOwner = new Map()
115
+
116
+ for (const record of records || []) {
117
+ if (parseNumericField(record.fields, 'OwnerPartID') === null) {
118
+ continue
119
+ }
120
+
121
+ const ownerIndex = getField(record.fields, 'OwnerIndex')
122
+ if (!ownerIndex) {
123
+ continue
124
+ }
125
+
126
+ if (!childrenByOwner.has(ownerIndex)) {
127
+ childrenByOwner.set(ownerIndex, [])
128
+ }
129
+ childrenByOwner.get(ownerIndex).push(record)
130
+ }
131
+
132
+ return childrenByOwner
108
133
  }
109
134
 
110
135
  /**