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
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
209
|
-
numeric: true
|
|
210
|
-
})
|
|
230
|
+
NaturalStringComparator.compare(left.layerKey, right.layerKey)
|
|
211
231
|
)
|
|
212
232
|
}
|
|
213
233
|
|
|
214
234
|
/**
|
|
215
|
-
*
|
|
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(
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
...
|
|
189
|
-
...
|
|
191
|
+
...primitivesByNet.keys(),
|
|
192
|
+
...viasByNet.keys()
|
|
190
193
|
])
|
|
191
194
|
|
|
192
195
|
return [...netNames]
|
|
193
|
-
.map((netName) =>
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
),
|
|
199
|
-
viaRows.filter((via) => via.netName === netName)
|
|
205
|
+
primitives,
|
|
206
|
+
vias
|
|
200
207
|
)
|
|
201
|
-
)
|
|
202
|
-
.filter(
|
|
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 =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
(primitive
|
|
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.
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* Extracts long printable runs from binary Altium documents.
|
|
7
7
|
*/
|
|
8
8
|
export class PrintableTextDecoder {
|
|
9
|
+
static #decoderCache = new Map()
|
|
10
|
+
static #decoderConstructor = null
|
|
11
|
+
|
|
9
12
|
static #WINDOWS_1252_PRINTABLE_CONTROL_BYTES = new Set([
|
|
10
13
|
0x80, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c,
|
|
11
14
|
0x8e, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b,
|
|
@@ -115,7 +118,7 @@ export class PrintableTextDecoder {
|
|
|
115
118
|
if (preferredEncoding === 'utf-8') {
|
|
116
119
|
return (
|
|
117
120
|
PrintableTextDecoder.#tryDecode(bytes, 'utf-8') ||
|
|
118
|
-
|
|
121
|
+
PrintableTextDecoder.#decode(bytes, 'utf-8')
|
|
119
122
|
)
|
|
120
123
|
}
|
|
121
124
|
if (
|
|
@@ -124,7 +127,7 @@ export class PrintableTextDecoder {
|
|
|
124
127
|
) {
|
|
125
128
|
return (
|
|
126
129
|
PrintableTextDecoder.#tryDecodeWindows1252(bytes) ||
|
|
127
|
-
|
|
130
|
+
PrintableTextDecoder.#decode(bytes, 'utf-8')
|
|
128
131
|
)
|
|
129
132
|
}
|
|
130
133
|
|
|
@@ -144,7 +147,7 @@ export class PrintableTextDecoder {
|
|
|
144
147
|
return (
|
|
145
148
|
PrintableTextDecoder.#tryDecode(bytes, 'gb18030') ||
|
|
146
149
|
PrintableTextDecoder.#tryDecodeWindows1252(bytes) ||
|
|
147
|
-
|
|
150
|
+
PrintableTextDecoder.#decode(bytes, 'utf-8')
|
|
148
151
|
)
|
|
149
152
|
}
|
|
150
153
|
|
|
@@ -157,18 +160,25 @@ export class PrintableTextDecoder {
|
|
|
157
160
|
* @param {number} minLength
|
|
158
161
|
*/
|
|
159
162
|
static #pushRunBytes(runs, bytes, start, end, minLength) {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const normalized = PrintableTextDecoder.#normalizeRun(
|
|
165
|
-
PrintableTextDecoder.decodeBytes(slice)
|
|
163
|
+
const bounds = PrintableTextDecoder.#trimAsciiByteRange(
|
|
164
|
+
bytes,
|
|
165
|
+
start,
|
|
166
|
+
end
|
|
166
167
|
)
|
|
168
|
+
const length = bounds.end - bounds.start
|
|
169
|
+
if (length < minLength) return
|
|
167
170
|
|
|
168
|
-
if (
|
|
169
|
-
|
|
171
|
+
if (
|
|
172
|
+
!PrintableTextDecoder.#containsRecordDelimiterBytes(
|
|
173
|
+
bytes,
|
|
174
|
+
bounds.start,
|
|
175
|
+
bounds.end
|
|
176
|
+
)
|
|
177
|
+
) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
170
180
|
|
|
171
|
-
runs.push(slice)
|
|
181
|
+
runs.push(bytes.slice(start, end))
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
/**
|
|
@@ -218,6 +228,72 @@ export class PrintableTextDecoder {
|
|
|
218
228
|
return false
|
|
219
229
|
}
|
|
220
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Trims ASCII whitespace from one byte range.
|
|
233
|
+
* @param {Uint8Array} bytes
|
|
234
|
+
* @param {number} start
|
|
235
|
+
* @param {number} end
|
|
236
|
+
* @returns {{ start: number, end: number }}
|
|
237
|
+
*/
|
|
238
|
+
static #trimAsciiByteRange(bytes, start, end) {
|
|
239
|
+
let trimmedStart = start
|
|
240
|
+
let trimmedEnd = end
|
|
241
|
+
|
|
242
|
+
while (
|
|
243
|
+
trimmedStart < trimmedEnd &&
|
|
244
|
+
PrintableTextDecoder.#isAsciiWhitespaceByte(bytes[trimmedStart])
|
|
245
|
+
) {
|
|
246
|
+
trimmedStart += 1
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
while (
|
|
250
|
+
trimmedEnd > trimmedStart &&
|
|
251
|
+
PrintableTextDecoder.#isAsciiWhitespaceByte(bytes[trimmedEnd - 1])
|
|
252
|
+
) {
|
|
253
|
+
trimmedEnd -= 1
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
start: trimmedStart,
|
|
258
|
+
end: trimmedEnd
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Returns true when one byte range contains Altium record delimiters.
|
|
264
|
+
* @param {Uint8Array} bytes
|
|
265
|
+
* @param {number} start
|
|
266
|
+
* @param {number} end
|
|
267
|
+
* @returns {boolean}
|
|
268
|
+
*/
|
|
269
|
+
static #containsRecordDelimiterBytes(bytes, start, end) {
|
|
270
|
+
let hasPipe = false
|
|
271
|
+
let hasEquals = false
|
|
272
|
+
|
|
273
|
+
for (let index = start; index < end; index += 1) {
|
|
274
|
+
if (bytes[index] === 0x7c) {
|
|
275
|
+
hasPipe = true
|
|
276
|
+
} else if (bytes[index] === 0x3d) {
|
|
277
|
+
hasEquals = true
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (hasPipe && hasEquals) {
|
|
281
|
+
return true
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Returns true when a byte is ASCII whitespace normalized around runs.
|
|
290
|
+
* @param {number} byte
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
static #isAsciiWhitespaceByte(byte) {
|
|
294
|
+
return byte === 9 || byte === 10 || byte === 13 || byte === 32
|
|
295
|
+
}
|
|
296
|
+
|
|
221
297
|
/**
|
|
222
298
|
* Tries one strict decode and returns null when bytes are invalid for it.
|
|
223
299
|
* @param {Uint8Array} bytes
|
|
@@ -226,12 +302,56 @@ export class PrintableTextDecoder {
|
|
|
226
302
|
*/
|
|
227
303
|
static #tryDecode(bytes, encoding) {
|
|
228
304
|
try {
|
|
229
|
-
return
|
|
305
|
+
return PrintableTextDecoder.#decode(bytes, encoding, {
|
|
306
|
+
fatal: true
|
|
307
|
+
})
|
|
230
308
|
} catch {
|
|
231
309
|
return null
|
|
232
310
|
}
|
|
233
311
|
}
|
|
234
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Decodes one byte slice with a cached runtime decoder.
|
|
315
|
+
* @param {Uint8Array} bytes
|
|
316
|
+
* @param {string} encoding
|
|
317
|
+
* @param {{ fatal?: boolean }} [options]
|
|
318
|
+
* @returns {string}
|
|
319
|
+
*/
|
|
320
|
+
static #decode(bytes, encoding, options = {}) {
|
|
321
|
+
return PrintableTextDecoder.#getTextDecoder(encoding, options).decode(
|
|
322
|
+
bytes
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Resolves a cached TextDecoder for one encoding and fatal mode.
|
|
328
|
+
* @param {string} encoding
|
|
329
|
+
* @param {{ fatal?: boolean }} options
|
|
330
|
+
* @returns {TextDecoder}
|
|
331
|
+
*/
|
|
332
|
+
static #getTextDecoder(encoding, options) {
|
|
333
|
+
const Decoder = globalThis.TextDecoder
|
|
334
|
+
if (PrintableTextDecoder.#decoderConstructor !== Decoder) {
|
|
335
|
+
PrintableTextDecoder.#decoderCache = new Map()
|
|
336
|
+
PrintableTextDecoder.#decoderConstructor = Decoder
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const normalizedEncoding = String(encoding || 'utf-8').toLowerCase()
|
|
340
|
+
const fatal = Boolean(options?.fatal)
|
|
341
|
+
const cacheKey = `${normalizedEncoding}:${fatal ? 'fatal' : 'replace'}`
|
|
342
|
+
const cached = PrintableTextDecoder.#decoderCache.get(cacheKey)
|
|
343
|
+
if (cached) {
|
|
344
|
+
return cached
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const decoder = new Decoder(
|
|
348
|
+
normalizedEncoding,
|
|
349
|
+
fatal ? { fatal: true } : {}
|
|
350
|
+
)
|
|
351
|
+
PrintableTextDecoder.#decoderCache.set(cacheKey, decoder)
|
|
352
|
+
return decoder
|
|
353
|
+
}
|
|
354
|
+
|
|
235
355
|
/**
|
|
236
356
|
* Tries a Windows-1252 decode and normalizes runtimes that expose C1 bytes
|
|
237
357
|
* as control characters instead of punctuation.
|
|
@@ -66,6 +66,19 @@ export class SchematicComponentOwnerTextResolver {
|
|
|
66
66
|
)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Resolves candidate owner indexes for one schematic component record.
|
|
71
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} componentRecord Component placement record.
|
|
72
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Indexed schematic records.
|
|
73
|
+
* @returns {string[]}
|
|
74
|
+
*/
|
|
75
|
+
static resolveOwnerIndexes(componentRecord, records) {
|
|
76
|
+
return SchematicComponentOwnerTextResolver.#resolveOwnerIndexes(
|
|
77
|
+
componentRecord,
|
|
78
|
+
records
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
/**
|
|
70
83
|
* Resolves candidate owner indexes for one schematic component record.
|
|
71
84
|
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} componentRecord Component placement record.
|
|
@@ -56,7 +56,7 @@ export class SchematicComponentTextResolver {
|
|
|
56
56
|
*/
|
|
57
57
|
static resolveValue(ownerTexts, texts, component) {
|
|
58
58
|
const ownerValue =
|
|
59
|
-
SchematicComponentTextResolver.#
|
|
59
|
+
SchematicComponentTextResolver.#findFirstUsableRelatedTextRecord(
|
|
60
60
|
ownerTexts,
|
|
61
61
|
['Comment', 'VALUE']
|
|
62
62
|
)
|
|
@@ -128,6 +128,45 @@ export class SchematicComponentTextResolver {
|
|
|
128
128
|
return { found: false, text: '' }
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Finds the first owner text that is resolved or explicitly empty, while
|
|
133
|
+
* allowing unresolved templates to fall through to later owner parameters.
|
|
134
|
+
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
135
|
+
* @param {string[]} logicalNames Logical text names.
|
|
136
|
+
* @returns {{ found: boolean, text: string }}
|
|
137
|
+
*/
|
|
138
|
+
static #findFirstUsableRelatedTextRecord(records, logicalNames) {
|
|
139
|
+
let unresolvedMatch = { found: false, text: '' }
|
|
140
|
+
|
|
141
|
+
for (const logicalName of logicalNames) {
|
|
142
|
+
const match = SchematicComponentTextResolver.#findRelatedTextRecord(
|
|
143
|
+
records,
|
|
144
|
+
logicalName
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if (!match.found) {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
SchematicComponentTextResolver.#isExplicitEmptyText(
|
|
153
|
+
match.text
|
|
154
|
+
) ||
|
|
155
|
+
SchematicComponentTextResolver.#isResolvedComponentText(
|
|
156
|
+
match.text
|
|
157
|
+
)
|
|
158
|
+
) {
|
|
159
|
+
return match
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!unresolvedMatch.found) {
|
|
163
|
+
unresolvedMatch = match
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return unresolvedMatch
|
|
168
|
+
}
|
|
169
|
+
|
|
131
170
|
/**
|
|
132
171
|
* Finds the closest nearby designator text for one component.
|
|
133
172
|
* @param {{ x: number, y: number, text: string, name: string }[]} texts
|
|
@@ -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
|
-
|
|
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
|
|
46
|
+
* @param {Map<string, object[]>} childrenByOwner Child records by owner index.
|
|
45
47
|
* @returns {object}
|
|
46
48
|
*/
|
|
47
|
-
static #componentCatalog(componentRecord,
|
|
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
|
-
|
|
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
|
|
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(
|
|
103
|
-
return (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
/**
|