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,13 +2,16 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Unzlib } from 'fflate'
|
|
6
6
|
import { PcbFontMetricsParser } from './PcbFontMetricsParser.mjs'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Extracts zlib-compressed embedded font payloads from PCB compound streams.
|
|
10
10
|
*/
|
|
11
11
|
export class PcbEmbeddedFontExtractor {
|
|
12
|
+
static #BASE64_ALPHABET =
|
|
13
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
14
|
+
|
|
12
15
|
static #CANDIDATE_STREAM_NAMES = [
|
|
13
16
|
'EmbeddedFonts6/Data',
|
|
14
17
|
'EmbeddedFonts/Data',
|
|
@@ -114,16 +117,19 @@ export class PcbEmbeddedFontExtractor {
|
|
|
114
117
|
return null
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
const
|
|
120
|
+
const payload = PcbEmbeddedFontExtractor.#inflateZlibPayloadAt(
|
|
118
121
|
bytes,
|
|
119
122
|
zlibOffset
|
|
120
123
|
)
|
|
121
|
-
if (
|
|
124
|
+
if (!payload) {
|
|
122
125
|
return null
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
const compressedBytes = bytes.subarray(
|
|
126
|
-
|
|
128
|
+
const compressedBytes = bytes.subarray(
|
|
129
|
+
zlibOffset,
|
|
130
|
+
payload.compressedEnd
|
|
131
|
+
)
|
|
132
|
+
const payloadBytes = payload.bytes
|
|
127
133
|
const metadata = PcbEmbeddedFontExtractor.#normalizeFontMetadata(
|
|
128
134
|
familyField.text,
|
|
129
135
|
alternateField.text,
|
|
@@ -151,7 +157,7 @@ export class PcbEmbeddedFontExtractor {
|
|
|
151
157
|
PcbEmbeddedFontExtractor.#bytesToBase64(payloadBytes),
|
|
152
158
|
metrics
|
|
153
159
|
},
|
|
154
|
-
nextOffset: compressedEnd
|
|
160
|
+
nextOffset: payload.compressedEnd
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
163
|
|
|
@@ -224,48 +230,146 @@ export class PcbEmbeddedFontExtractor {
|
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
/**
|
|
227
|
-
*
|
|
233
|
+
* Inflates one zlib payload and returns its exact stream boundary.
|
|
228
234
|
* @param {Uint8Array} bytes
|
|
229
235
|
* @param {number} zlibOffset
|
|
236
|
+
* @returns {{ bytes: Uint8Array, compressedEnd: number } | null}
|
|
237
|
+
*/
|
|
238
|
+
static #inflateZlibPayloadAt(bytes, zlibOffset) {
|
|
239
|
+
const input = bytes.subarray(zlibOffset)
|
|
240
|
+
const chunks = []
|
|
241
|
+
let inflater
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
inflater = new Unzlib((chunk) => {
|
|
245
|
+
chunks.push(chunk)
|
|
246
|
+
})
|
|
247
|
+
inflater.push(input, false)
|
|
248
|
+
} catch {
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!Number(inflater?.s?.f || 0)) {
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const payloadBytes = PcbEmbeddedFontExtractor.#concatBytes(chunks)
|
|
257
|
+
const compressedByteCount =
|
|
258
|
+
PcbEmbeddedFontExtractor.#resolveCompressedByteCount(
|
|
259
|
+
input,
|
|
260
|
+
inflater,
|
|
261
|
+
payloadBytes
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if (compressedByteCount <= 2) {
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
bytes: payloadBytes,
|
|
270
|
+
compressedEnd: zlibOffset + compressedByteCount
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Resolves the zlib stream length from fflate's remaining input buffer.
|
|
276
|
+
* @param {Uint8Array} input
|
|
277
|
+
* @param {Unzlib} inflater
|
|
278
|
+
* @param {Uint8Array} payloadBytes
|
|
230
279
|
* @returns {number}
|
|
231
280
|
*/
|
|
232
|
-
static #
|
|
233
|
-
|
|
234
|
-
|
|
281
|
+
static #resolveCompressedByteCount(input, inflater, payloadBytes) {
|
|
282
|
+
const remainingByteCount = Number(inflater?.p?.byteLength || 0)
|
|
283
|
+
if (remainingByteCount < 4) {
|
|
284
|
+
return -1
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const baseByteCount = input.byteLength - remainingByteCount + 4
|
|
288
|
+
const checksum = PcbEmbeddedFontExtractor.#adler32(payloadBytes)
|
|
235
289
|
|
|
236
|
-
|
|
237
|
-
|
|
290
|
+
// fflate can leave the final consumed deflate byte in `p` when the
|
|
291
|
+
// stream ends mid-byte, so validate both adjacent boundary candidates.
|
|
292
|
+
for (const compressedByteCount of [baseByteCount, baseByteCount + 1]) {
|
|
238
293
|
if (
|
|
239
|
-
PcbEmbeddedFontExtractor.#
|
|
240
|
-
|
|
294
|
+
PcbEmbeddedFontExtractor.#hasZlibChecksumAt(
|
|
295
|
+
input,
|
|
296
|
+
compressedByteCount,
|
|
297
|
+
checksum
|
|
241
298
|
)
|
|
242
299
|
) {
|
|
243
|
-
|
|
244
|
-
} else {
|
|
245
|
-
low = midpoint + 1
|
|
300
|
+
return compressedByteCount
|
|
246
301
|
}
|
|
247
302
|
}
|
|
248
303
|
|
|
249
|
-
return
|
|
250
|
-
bytes.subarray(zlibOffset, low)
|
|
251
|
-
)
|
|
252
|
-
? low
|
|
253
|
-
: -1
|
|
304
|
+
return -1
|
|
254
305
|
}
|
|
255
306
|
|
|
256
307
|
/**
|
|
257
|
-
* Returns true when
|
|
258
|
-
*
|
|
259
|
-
* @param {
|
|
308
|
+
* Returns true when a candidate zlib boundary ends with the checksum.
|
|
309
|
+
* @param {Uint8Array} input
|
|
310
|
+
* @param {number} compressedByteCount
|
|
311
|
+
* @param {number} checksum
|
|
260
312
|
* @returns {boolean}
|
|
261
313
|
*/
|
|
262
|
-
static #
|
|
263
|
-
|
|
264
|
-
unzlibSync(bytes)
|
|
265
|
-
return true
|
|
266
|
-
} catch {
|
|
314
|
+
static #hasZlibChecksumAt(input, compressedByteCount, checksum) {
|
|
315
|
+
if (compressedByteCount < 6 || compressedByteCount > input.byteLength) {
|
|
267
316
|
return false
|
|
268
317
|
}
|
|
318
|
+
|
|
319
|
+
const checksumOffset = compressedByteCount - 4
|
|
320
|
+
const actualChecksum = new DataView(
|
|
321
|
+
input.buffer,
|
|
322
|
+
input.byteOffset + checksumOffset,
|
|
323
|
+
4
|
|
324
|
+
).getUint32(0, false)
|
|
325
|
+
|
|
326
|
+
return actualChecksum === checksum
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Computes the Adler-32 checksum used by zlib trailers.
|
|
331
|
+
* @param {Uint8Array} bytes
|
|
332
|
+
* @returns {number}
|
|
333
|
+
*/
|
|
334
|
+
static #adler32(bytes) {
|
|
335
|
+
const modulo = 65521
|
|
336
|
+
let low = 1
|
|
337
|
+
let high = 0
|
|
338
|
+
|
|
339
|
+
for (let offset = 0; offset < bytes.byteLength; offset += 5552) {
|
|
340
|
+
const end = Math.min(offset + 5552, bytes.byteLength)
|
|
341
|
+
for (let index = offset; index < end; index += 1) {
|
|
342
|
+
low += bytes[index]
|
|
343
|
+
high += low
|
|
344
|
+
}
|
|
345
|
+
low %= modulo
|
|
346
|
+
high %= modulo
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return ((high << 16) | low) >>> 0
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Concatenates inflated output chunks.
|
|
354
|
+
* @param {Uint8Array[]} chunks
|
|
355
|
+
* @returns {Uint8Array}
|
|
356
|
+
*/
|
|
357
|
+
static #concatBytes(chunks) {
|
|
358
|
+
if (chunks.length === 1) {
|
|
359
|
+
return chunks[0]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const bytes = new Uint8Array(
|
|
363
|
+
chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0)
|
|
364
|
+
)
|
|
365
|
+
let offset = 0
|
|
366
|
+
|
|
367
|
+
for (const chunk of chunks) {
|
|
368
|
+
bytes.set(chunk, offset)
|
|
369
|
+
offset += chunk.byteLength
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return bytes
|
|
269
373
|
}
|
|
270
374
|
|
|
271
375
|
/**
|
|
@@ -451,22 +555,61 @@ export class PcbEmbeddedFontExtractor {
|
|
|
451
555
|
* @returns {string}
|
|
452
556
|
*/
|
|
453
557
|
static #bytesToBase64(bytes) {
|
|
454
|
-
if (typeof
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
558
|
+
if (typeof Buffer === 'function' && typeof Buffer.from === 'function') {
|
|
559
|
+
return Buffer.from(bytes).toString('base64')
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return PcbEmbeddedFontExtractor.#bytesToBase64Portable(bytes)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Encodes bytes as base64 without relying on Node APIs.
|
|
567
|
+
* @param {Uint8Array} bytes
|
|
568
|
+
* @returns {string}
|
|
569
|
+
*/
|
|
570
|
+
static #bytesToBase64Portable(bytes) {
|
|
571
|
+
const alphabet = PcbEmbeddedFontExtractor.#BASE64_ALPHABET
|
|
572
|
+
const groupBuffer = new Array(4096)
|
|
573
|
+
const outputChunks = []
|
|
574
|
+
let groupIndex = 0
|
|
575
|
+
let byteIndex = 0
|
|
576
|
+
|
|
577
|
+
for (; byteIndex + 2 < bytes.byteLength; byteIndex += 3) {
|
|
578
|
+
const value =
|
|
579
|
+
(bytes[byteIndex] << 16) |
|
|
580
|
+
(bytes[byteIndex + 1] << 8) |
|
|
581
|
+
bytes[byteIndex + 2]
|
|
582
|
+
groupBuffer[groupIndex] =
|
|
583
|
+
alphabet[(value >> 18) & 63] +
|
|
584
|
+
alphabet[(value >> 12) & 63] +
|
|
585
|
+
alphabet[(value >> 6) & 63] +
|
|
586
|
+
alphabet[value & 63]
|
|
587
|
+
groupIndex += 1
|
|
588
|
+
|
|
589
|
+
if (groupIndex === groupBuffer.length) {
|
|
590
|
+
outputChunks.push(groupBuffer.join(''))
|
|
591
|
+
groupIndex = 0
|
|
465
592
|
}
|
|
466
|
-
return btoa(binary)
|
|
467
593
|
}
|
|
468
594
|
|
|
469
|
-
|
|
595
|
+
if (byteIndex < bytes.byteLength) {
|
|
596
|
+
const hasSecondByte = byteIndex + 1 < bytes.byteLength
|
|
597
|
+
const value =
|
|
598
|
+
(bytes[byteIndex] << 16) |
|
|
599
|
+
((hasSecondByte ? bytes[byteIndex + 1] : 0) << 8)
|
|
600
|
+
groupBuffer[groupIndex] =
|
|
601
|
+
alphabet[(value >> 18) & 63] +
|
|
602
|
+
alphabet[(value >> 12) & 63] +
|
|
603
|
+
(hasSecondByte ? alphabet[(value >> 6) & 63] : '=') +
|
|
604
|
+
'='
|
|
605
|
+
groupIndex += 1
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (groupIndex > 0) {
|
|
609
|
+
outputChunks.push(groupBuffer.slice(0, groupIndex).join(''))
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return outputChunks.join('')
|
|
470
613
|
}
|
|
471
614
|
|
|
472
615
|
/**
|
|
@@ -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
|