altium-toolkit 1.1.2 → 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/PcbEmbeddedFontExtractor.mjs +186 -43
- 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/SchematicImageParser.mjs +291 -6
- 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/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- 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,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
+
import { unzlibSync } from 'fflate'
|
|
5
6
|
import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
|
|
6
7
|
import { ParserUtils } from './ParserUtils.mjs'
|
|
7
8
|
|
|
@@ -53,11 +54,15 @@ export class SchematicImageParser {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
const packedStorageImages = oleDocument
|
|
58
|
+
? SchematicImageParser.#indexPackedStorageImages(oleDocument)
|
|
59
|
+
: SchematicImageParser.#createPackedStorageImageIndex()
|
|
56
60
|
const images = imageRecords
|
|
57
61
|
.map((record) =>
|
|
58
62
|
SchematicImageParser.#parseSchematicImageRecord(
|
|
59
63
|
record,
|
|
60
64
|
oleDocument,
|
|
65
|
+
packedStorageImages,
|
|
61
66
|
diagnostics
|
|
62
67
|
)
|
|
63
68
|
)
|
|
@@ -79,10 +84,16 @@ export class SchematicImageParser {
|
|
|
79
84
|
* Normalizes one image placement record.
|
|
80
85
|
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record
|
|
81
86
|
* @param {OleCompoundDocument | null} oleDocument
|
|
87
|
+
* @param {{ byPath: Map<string, Uint8Array>, byBaseName: Map<string, Uint8Array | null> }} packedStorageImages
|
|
82
88
|
* @param {{ severity: 'info' | 'warning', message: string }[]} diagnostics
|
|
83
|
-
* @returns {{ x: number, y: number, cornerX: number, cornerY: number, fileName: string, embedded: boolean, keepAspect: boolean, mimeType: string, dataBase64: string, renderOrder: number, diagnosticState: string } | null}
|
|
89
|
+
* @returns {{ x: number, y: number, cornerX: number, cornerY: number, fileName: string, embedded: boolean, keepAspect: boolean, mimeType: string, dataBase64: string, renderOrder: number, diagnosticState: string, ownerIndex?: string } | null}
|
|
84
90
|
*/
|
|
85
|
-
static #parseSchematicImageRecord(
|
|
91
|
+
static #parseSchematicImageRecord(
|
|
92
|
+
record,
|
|
93
|
+
oleDocument,
|
|
94
|
+
packedStorageImages,
|
|
95
|
+
diagnostics
|
|
96
|
+
) {
|
|
86
97
|
const x = parseNumericField(record.fields, 'Location.X')
|
|
87
98
|
const y = parseNumericField(record.fields, 'Location.Y')
|
|
88
99
|
const cornerX = parseNumericField(record.fields, 'Corner.X')
|
|
@@ -110,8 +121,13 @@ export class SchematicImageParser {
|
|
|
110
121
|
let diagnosticState = embedded ? 'missing-embedded-payload' : 'external'
|
|
111
122
|
|
|
112
123
|
if (embedded && fileName && oleDocument) {
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
const streamBytes = SchematicImageParser.#resolveEmbeddedImageBytes(
|
|
125
|
+
fileName,
|
|
126
|
+
oleDocument,
|
|
127
|
+
packedStorageImages
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if (streamBytes) {
|
|
115
131
|
const decoded =
|
|
116
132
|
SchematicImageParser.#decodeEmbeddedImagePayload(
|
|
117
133
|
streamBytes,
|
|
@@ -123,7 +139,7 @@ export class SchematicImageParser {
|
|
|
123
139
|
hasAlpha = decoded.hasAlpha
|
|
124
140
|
dataBase64 = SchematicImageParser.#encodeBase64(decoded.bytes)
|
|
125
141
|
diagnosticState = 'embedded'
|
|
126
|
-
}
|
|
142
|
+
} else {
|
|
127
143
|
diagnostics.push({
|
|
128
144
|
severity: 'warning',
|
|
129
145
|
message:
|
|
@@ -153,7 +169,10 @@ export class SchematicImageParser {
|
|
|
153
169
|
mimeType,
|
|
154
170
|
dataBase64,
|
|
155
171
|
renderOrder,
|
|
156
|
-
diagnosticState
|
|
172
|
+
diagnosticState,
|
|
173
|
+
...(getField(record.fields, 'OwnerIndex')
|
|
174
|
+
? { ownerIndex: getField(record.fields, 'OwnerIndex') }
|
|
175
|
+
: {})
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
if (sourceMimeType && sourceMimeType !== mimeType) {
|
|
@@ -169,6 +188,272 @@ export class SchematicImageParser {
|
|
|
169
188
|
return image
|
|
170
189
|
}
|
|
171
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Resolves an embedded image payload from direct OLE streams or packed
|
|
193
|
+
* Altium icon storage.
|
|
194
|
+
* @param {string} fileName Image file name from the schematic record.
|
|
195
|
+
* @param {OleCompoundDocument} oleDocument Parsed OLE container.
|
|
196
|
+
* @param {{ byPath: Map<string, Uint8Array>, byBaseName: Map<string, Uint8Array | null> }} packedStorageImages
|
|
197
|
+
* @returns {Uint8Array | null}
|
|
198
|
+
*/
|
|
199
|
+
static #resolveEmbeddedImageBytes(
|
|
200
|
+
fileName,
|
|
201
|
+
oleDocument,
|
|
202
|
+
packedStorageImages
|
|
203
|
+
) {
|
|
204
|
+
try {
|
|
205
|
+
return oleDocument.getStream(fileName)
|
|
206
|
+
} catch {
|
|
207
|
+
return SchematicImageParser.#findPackedStorageImage(
|
|
208
|
+
fileName,
|
|
209
|
+
packedStorageImages
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Creates an empty packed-storage image index.
|
|
216
|
+
* @returns {{ byPath: Map<string, Uint8Array>, byBaseName: Map<string, Uint8Array | null> }}
|
|
217
|
+
*/
|
|
218
|
+
static #createPackedStorageImageIndex() {
|
|
219
|
+
return {
|
|
220
|
+
byPath: new Map(),
|
|
221
|
+
byBaseName: new Map()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Reads Altium's packed schematic image storage stream.
|
|
227
|
+
* @param {OleCompoundDocument} oleDocument Parsed OLE container.
|
|
228
|
+
* @returns {{ byPath: Map<string, Uint8Array>, byBaseName: Map<string, Uint8Array | null> }}
|
|
229
|
+
*/
|
|
230
|
+
static #indexPackedStorageImages(oleDocument) {
|
|
231
|
+
try {
|
|
232
|
+
return SchematicImageParser.#parsePackedStorageImages(
|
|
233
|
+
oleDocument.getStream('Storage')
|
|
234
|
+
)
|
|
235
|
+
} catch {
|
|
236
|
+
return SchematicImageParser.#createPackedStorageImageIndex()
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Parses Altium's compact icon-storage records into decoded image bytes.
|
|
242
|
+
* @param {Uint8Array} bytes Packed storage stream bytes.
|
|
243
|
+
* @returns {{ byPath: Map<string, Uint8Array>, byBaseName: Map<string, Uint8Array | null> }}
|
|
244
|
+
*/
|
|
245
|
+
static #parsePackedStorageImages(bytes) {
|
|
246
|
+
const index = SchematicImageParser.#createPackedStorageImageIndex()
|
|
247
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength < 16) {
|
|
248
|
+
return index
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const view = new DataView(
|
|
252
|
+
bytes.buffer,
|
|
253
|
+
bytes.byteOffset,
|
|
254
|
+
bytes.byteLength
|
|
255
|
+
)
|
|
256
|
+
const headerLength = view.getUint32(0, true)
|
|
257
|
+
if (headerLength <= 0 || 4 + headerLength > bytes.byteLength) {
|
|
258
|
+
return index
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const headerText = new TextDecoder('windows-1252').decode(
|
|
262
|
+
bytes.subarray(4, 4 + headerLength)
|
|
263
|
+
)
|
|
264
|
+
if (!headerText.includes('Icon storage')) {
|
|
265
|
+
return index
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let offset = 4 + headerLength
|
|
269
|
+
if (bytes[offset] === 0) {
|
|
270
|
+
offset += 1
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
while (offset + 10 <= bytes.byteLength) {
|
|
274
|
+
while (offset < bytes.byteLength && bytes[offset] === 0) {
|
|
275
|
+
offset += 1
|
|
276
|
+
}
|
|
277
|
+
if (offset + 10 > bytes.byteLength) {
|
|
278
|
+
break
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const entryLength =
|
|
282
|
+
bytes[offset] |
|
|
283
|
+
(bytes[offset + 1] << 8) |
|
|
284
|
+
(bytes[offset + 2] << 16)
|
|
285
|
+
const parsedEntry =
|
|
286
|
+
SchematicImageParser.#parsePackedStorageImageEntry(
|
|
287
|
+
bytes,
|
|
288
|
+
view,
|
|
289
|
+
offset
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if (parsedEntry) {
|
|
293
|
+
SchematicImageParser.#addPackedStorageImage(
|
|
294
|
+
index,
|
|
295
|
+
parsedEntry.fileName,
|
|
296
|
+
parsedEntry.bytes
|
|
297
|
+
)
|
|
298
|
+
offset = parsedEntry.nextOffset
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
offset += Math.max(entryLength + 3, 1)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return index
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Parses one packed icon-storage entry.
|
|
310
|
+
* @param {Uint8Array} bytes Storage stream bytes.
|
|
311
|
+
* @param {DataView} view Storage stream view.
|
|
312
|
+
* @param {number} offset Entry offset.
|
|
313
|
+
* @returns {{ fileName: string, bytes: Uint8Array, nextOffset: number } | null}
|
|
314
|
+
*/
|
|
315
|
+
static #parsePackedStorageImageEntry(bytes, view, offset) {
|
|
316
|
+
const entryStart = offset + 3
|
|
317
|
+
if (entryStart + 3 > bytes.byteLength) {
|
|
318
|
+
return null
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const pathLength = bytes[entryStart + 2]
|
|
322
|
+
const pathOffset = entryStart + 3
|
|
323
|
+
const compressedLengthOffset = pathOffset + pathLength
|
|
324
|
+
const compressedOffset = compressedLengthOffset + 4
|
|
325
|
+
|
|
326
|
+
if (pathLength <= 0 || compressedLengthOffset + 4 > bytes.byteLength) {
|
|
327
|
+
return null
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const compressedLength = view.getUint32(compressedLengthOffset, true)
|
|
331
|
+
const compressedEnd = compressedOffset + compressedLength
|
|
332
|
+
if (
|
|
333
|
+
compressedLength <= 0 ||
|
|
334
|
+
compressedEnd > bytes.byteLength ||
|
|
335
|
+
!SchematicImageParser.#looksLikeZlibStream(bytes, compressedOffset)
|
|
336
|
+
) {
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
return {
|
|
342
|
+
fileName: SchematicImageParser.#decodePackedImagePath(
|
|
343
|
+
bytes.subarray(pathOffset, compressedLengthOffset)
|
|
344
|
+
),
|
|
345
|
+
bytes: unzlibSync(
|
|
346
|
+
bytes.subarray(compressedOffset, compressedEnd)
|
|
347
|
+
),
|
|
348
|
+
nextOffset: compressedEnd
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
return null
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Adds one decoded image to path and unique-basename lookup indexes.
|
|
357
|
+
* @param {{ byPath: Map<string, Uint8Array>, byBaseName: Map<string, Uint8Array | null> }} index Packed-storage index.
|
|
358
|
+
* @param {string} fileName Image path.
|
|
359
|
+
* @param {Uint8Array} bytes Image bytes.
|
|
360
|
+
*/
|
|
361
|
+
static #addPackedStorageImage(index, fileName, bytes) {
|
|
362
|
+
const normalizedPath =
|
|
363
|
+
SchematicImageParser.#normalizeImageLookupPath(fileName)
|
|
364
|
+
if (!normalizedPath) {
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
index.byPath.set(normalizedPath, bytes)
|
|
369
|
+
|
|
370
|
+
const baseName =
|
|
371
|
+
SchematicImageParser.#imageLookupBaseName(normalizedPath)
|
|
372
|
+
if (!baseName) {
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (
|
|
377
|
+
index.byBaseName.has(baseName) &&
|
|
378
|
+
index.byBaseName.get(baseName) !== bytes
|
|
379
|
+
) {
|
|
380
|
+
index.byBaseName.set(baseName, null)
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
index.byBaseName.set(baseName, bytes)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Finds one packed-storage image by full path or unique basename.
|
|
389
|
+
* @param {string} fileName Requested image path.
|
|
390
|
+
* @param {{ byPath: Map<string, Uint8Array>, byBaseName: Map<string, Uint8Array | null> }} index Packed-storage image index.
|
|
391
|
+
* @returns {Uint8Array | null}
|
|
392
|
+
*/
|
|
393
|
+
static #findPackedStorageImage(fileName, index) {
|
|
394
|
+
const normalizedPath =
|
|
395
|
+
SchematicImageParser.#normalizeImageLookupPath(fileName)
|
|
396
|
+
if (!normalizedPath) {
|
|
397
|
+
return null
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const directMatch = index.byPath.get(normalizedPath)
|
|
401
|
+
if (directMatch) {
|
|
402
|
+
return directMatch
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const baseName =
|
|
406
|
+
SchematicImageParser.#imageLookupBaseName(normalizedPath)
|
|
407
|
+
return index.byBaseName.get(baseName) || null
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Decodes a packed-storage path string.
|
|
412
|
+
* @param {Uint8Array} bytes Path bytes.
|
|
413
|
+
* @returns {string}
|
|
414
|
+
*/
|
|
415
|
+
static #decodePackedImagePath(bytes) {
|
|
416
|
+
return new TextDecoder('windows-1252')
|
|
417
|
+
.decode(bytes)
|
|
418
|
+
.replace(/\0+$/u, '')
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Checks whether bytes at an offset resemble a zlib stream.
|
|
423
|
+
* @param {Uint8Array} bytes Source bytes.
|
|
424
|
+
* @param {number} offset Candidate offset.
|
|
425
|
+
* @returns {boolean}
|
|
426
|
+
*/
|
|
427
|
+
static #looksLikeZlibStream(bytes, offset) {
|
|
428
|
+
if (offset + 2 > bytes.byteLength || bytes[offset] !== 0x78) {
|
|
429
|
+
return false
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return ((bytes[offset] << 8) + bytes[offset + 1]) % 31 === 0
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Normalizes image paths for cross-platform lookup.
|
|
437
|
+
* @param {string} fileName Image path.
|
|
438
|
+
* @returns {string}
|
|
439
|
+
*/
|
|
440
|
+
static #normalizeImageLookupPath(fileName) {
|
|
441
|
+
return String(fileName || '')
|
|
442
|
+
.replace(/\\+/gu, '/')
|
|
443
|
+
.replace(/\/+/gu, '/')
|
|
444
|
+
.toLowerCase()
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Returns the final path segment from a normalized image lookup path.
|
|
449
|
+
* @param {string} normalizedPath Normalized path.
|
|
450
|
+
* @returns {string}
|
|
451
|
+
*/
|
|
452
|
+
static #imageLookupBaseName(normalizedPath) {
|
|
453
|
+
const parts = normalizedPath.split('/')
|
|
454
|
+
return parts[parts.length - 1] || ''
|
|
455
|
+
}
|
|
456
|
+
|
|
172
457
|
/**
|
|
173
458
|
* Chooses the browser-facing image payload from one embedded stream.
|
|
174
459
|
* @param {Uint8Array} bytes Embedded image stream bytes.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes visible designator labels for repeated multipart schematic units.
|
|
7
|
+
*/
|
|
8
|
+
export class SchematicMultipartDesignatorNormalizer {
|
|
9
|
+
/**
|
|
10
|
+
* Rewrites multipart designator labels from the active Altium part id.
|
|
11
|
+
* @param {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]} texts
|
|
12
|
+
* @param {Map<string, string>} activeMultipartOwnerParts
|
|
13
|
+
* @returns {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]}
|
|
14
|
+
*/
|
|
15
|
+
static normalize(texts, activeMultipartOwnerParts) {
|
|
16
|
+
const repeatedStems =
|
|
17
|
+
SchematicMultipartDesignatorNormalizer.#collectRepeatedStems(
|
|
18
|
+
texts,
|
|
19
|
+
activeMultipartOwnerParts
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return texts.map((text) =>
|
|
23
|
+
SchematicMultipartDesignatorNormalizer.#normalizeText(
|
|
24
|
+
text,
|
|
25
|
+
activeMultipartOwnerParts,
|
|
26
|
+
repeatedStems
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Collects designator stems used by multiple active multipart owners.
|
|
33
|
+
* @param {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]} texts
|
|
34
|
+
* @param {Map<string, string>} activeMultipartOwnerParts
|
|
35
|
+
* @returns {Set<string>}
|
|
36
|
+
*/
|
|
37
|
+
static #collectRepeatedStems(texts, activeMultipartOwnerParts) {
|
|
38
|
+
const counts = new Map()
|
|
39
|
+
|
|
40
|
+
for (const text of texts) {
|
|
41
|
+
if (
|
|
42
|
+
!SchematicMultipartDesignatorNormalizer.#isActiveDesignator(
|
|
43
|
+
text,
|
|
44
|
+
activeMultipartOwnerParts
|
|
45
|
+
)
|
|
46
|
+
) {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parsed =
|
|
51
|
+
SchematicMultipartDesignatorNormalizer.#parseDesignatorStem(
|
|
52
|
+
text.text
|
|
53
|
+
)
|
|
54
|
+
if (!parsed) {
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
counts.set(parsed.stem, (counts.get(parsed.stem) || 0) + 1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return new Set(
|
|
62
|
+
[...counts.entries()]
|
|
63
|
+
.filter(([, count]) => count > 1)
|
|
64
|
+
.map(([stem]) => stem)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Normalizes one designator text row.
|
|
70
|
+
* @param {{ text: string, name?: string, ownerIndex?: string, recordType?: string }} text
|
|
71
|
+
* @param {Map<string, string>} activeMultipartOwnerParts
|
|
72
|
+
* @param {Set<string>} repeatedStems
|
|
73
|
+
* @returns {{ text: string, name?: string, ownerIndex?: string, recordType?: string }}
|
|
74
|
+
*/
|
|
75
|
+
static #normalizeText(text, activeMultipartOwnerParts, repeatedStems) {
|
|
76
|
+
if (
|
|
77
|
+
!SchematicMultipartDesignatorNormalizer.#isActiveDesignator(
|
|
78
|
+
text,
|
|
79
|
+
activeMultipartOwnerParts
|
|
80
|
+
)
|
|
81
|
+
) {
|
|
82
|
+
return text
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const parsed =
|
|
86
|
+
SchematicMultipartDesignatorNormalizer.#parseDesignatorStem(
|
|
87
|
+
text.text
|
|
88
|
+
)
|
|
89
|
+
if (!parsed || !repeatedStems.has(parsed.stem)) {
|
|
90
|
+
return text
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const suffix = SchematicMultipartDesignatorNormalizer.#formatPartSuffix(
|
|
94
|
+
activeMultipartOwnerParts.get(String(text.ownerIndex || ''))
|
|
95
|
+
)
|
|
96
|
+
const normalizedText = parsed.stem + suffix
|
|
97
|
+
|
|
98
|
+
return normalizedText === text.text
|
|
99
|
+
? text
|
|
100
|
+
: {
|
|
101
|
+
...text,
|
|
102
|
+
text: normalizedText
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns true when a text row is an active multipart designator.
|
|
108
|
+
* @param {{ name?: string, ownerIndex?: string, recordType?: string }} text
|
|
109
|
+
* @param {Map<string, string>} activeMultipartOwnerParts
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
static #isActiveDesignator(text, activeMultipartOwnerParts) {
|
|
113
|
+
const ownerIndex = String(text?.ownerIndex || '')
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
activeMultipartOwnerParts.has(ownerIndex) &&
|
|
117
|
+
text?.recordType === '34' &&
|
|
118
|
+
String(text?.name || '')
|
|
119
|
+
.trim()
|
|
120
|
+
.toLowerCase() === 'designator'
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parses a component stem before an optional multipart suffix.
|
|
126
|
+
* @param {string} text Designator text.
|
|
127
|
+
* @returns {{ stem: string, suffix: string } | null}
|
|
128
|
+
*/
|
|
129
|
+
static #parseDesignatorStem(text) {
|
|
130
|
+
const match = /^(?<stem>.*\d)(?<suffix>[A-Z]+)?$/u.exec(
|
|
131
|
+
String(text || '').trim()
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return match?.groups
|
|
135
|
+
? {
|
|
136
|
+
stem: match.groups.stem,
|
|
137
|
+
suffix: match.groups.suffix || ''
|
|
138
|
+
}
|
|
139
|
+
: null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Converts one numeric multipart part id into an alphabetic suffix.
|
|
144
|
+
* @param {string | undefined} partId
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
static #formatPartSuffix(partId) {
|
|
148
|
+
const numericPartId = Number.parseInt(String(partId || ''), 10)
|
|
149
|
+
if (!Number.isInteger(numericPartId) || numericPartId <= 0) {
|
|
150
|
+
return ''
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let suffix = ''
|
|
154
|
+
let remaining = numericPartId
|
|
155
|
+
|
|
156
|
+
while (remaining > 0) {
|
|
157
|
+
remaining -= 1
|
|
158
|
+
suffix = String.fromCharCode(65 + (remaining % 26)) + suffix
|
|
159
|
+
remaining = Math.floor(remaining / 26)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return suffix
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -542,10 +542,12 @@ export class SchematicMultipartOwnerMatcher {
|
|
|
542
542
|
static #inferSchematicPinOrientation(conglomerate) {
|
|
543
543
|
switch (conglomerate) {
|
|
544
544
|
case 34:
|
|
545
|
+
case 42:
|
|
545
546
|
case 50:
|
|
546
547
|
case 58:
|
|
547
548
|
return 'left'
|
|
548
549
|
case 32:
|
|
550
|
+
case 40:
|
|
549
551
|
case 48:
|
|
550
552
|
case 56:
|
|
551
553
|
return 'right'
|