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,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
|
/**
|
|
@@ -14,6 +14,8 @@ const { parseNumericField } = ParserUtils
|
|
|
14
14
|
export class PcbLayerStackReadModelBuilder {
|
|
15
15
|
static SCHEMA_ID = 'altium-toolkit.pcb.layer-stack.a1'
|
|
16
16
|
|
|
17
|
+
static #fieldIndexes = new WeakMap()
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
20
|
* Builds the layer-stack sidecar.
|
|
19
21
|
* @param {{ fileName: string, boardRecords: { fields: Record<string, string | string[]>, sourceStream?: string }[], streamNames?: string[], layers: object[], primitiveLayers: object[], layerSubstacks: object[], boardRegions: object[] }} input Source model context.
|
|
@@ -799,17 +801,37 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
799
801
|
* @returns {string}
|
|
800
802
|
*/
|
|
801
803
|
static #field(fields, key) {
|
|
802
|
-
if (Object.hasOwn(fields, key)) {
|
|
804
|
+
if (Object.hasOwn(fields, key) || key in fields) {
|
|
803
805
|
return ParserUtils.getField(fields, key)
|
|
804
806
|
}
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
(fieldKey) => fieldKey.toUpperCase() === upperKey
|
|
807
|
+
const realKey = PcbLayerStackReadModelBuilder.#fieldIndex(fields).get(
|
|
808
|
+
key.toUpperCase()
|
|
808
809
|
)
|
|
809
810
|
|
|
810
811
|
return realKey ? ParserUtils.getField(fields, realKey) : ''
|
|
811
812
|
}
|
|
812
813
|
|
|
814
|
+
/**
|
|
815
|
+
* Builds or returns a cached case-insensitive field-key index.
|
|
816
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
817
|
+
* @returns {Map<string, string>}
|
|
818
|
+
*/
|
|
819
|
+
static #fieldIndex(fields) {
|
|
820
|
+
const cached = PcbLayerStackReadModelBuilder.#fieldIndexes.get(fields)
|
|
821
|
+
if (cached) return cached
|
|
822
|
+
|
|
823
|
+
const fieldIndex = new Map()
|
|
824
|
+
for (const fieldKey of Object.keys(fields)) {
|
|
825
|
+
const upperKey = fieldKey.toUpperCase()
|
|
826
|
+
if (!fieldIndex.has(upperKey)) {
|
|
827
|
+
fieldIndex.set(upperKey, fieldKey)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
PcbLayerStackReadModelBuilder.#fieldIndexes.set(fields, fieldIndex)
|
|
832
|
+
return fieldIndex
|
|
833
|
+
}
|
|
834
|
+
|
|
813
835
|
/**
|
|
814
836
|
* Parses layer-id lists.
|
|
815
837
|
* @param {string} value Raw list value.
|
|
@@ -8,6 +8,8 @@ import { ParserUtils } from './ParserUtils.mjs'
|
|
|
8
8
|
* Parses source-only layer-stack metadata that is not part of core geometry.
|
|
9
9
|
*/
|
|
10
10
|
export class PcbLayerStackSourceMetadataParser {
|
|
11
|
+
static #fieldIndexes = new WeakMap()
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Parses source-aware extras for one layer-stack row.
|
|
13
15
|
* @param {Record<string, string | string[]>} fields Source fields.
|
|
@@ -401,17 +403,38 @@ export class PcbLayerStackSourceMetadataParser {
|
|
|
401
403
|
* @returns {string}
|
|
402
404
|
*/
|
|
403
405
|
static #field(fields, key) {
|
|
404
|
-
if (Object.hasOwn(fields, key)) {
|
|
406
|
+
if (Object.hasOwn(fields, key) || key in fields) {
|
|
405
407
|
return ParserUtils.getField(fields, key)
|
|
406
408
|
}
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
)
|
|
409
|
+
const realKey = PcbLayerStackSourceMetadataParser.#fieldIndex(
|
|
410
|
+
fields
|
|
411
|
+
).get(key.toUpperCase())
|
|
411
412
|
|
|
412
413
|
return realKey ? ParserUtils.getField(fields, realKey) : ''
|
|
413
414
|
}
|
|
414
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Builds or returns a cached case-insensitive field-key index.
|
|
418
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
419
|
+
* @returns {Map<string, string>}
|
|
420
|
+
*/
|
|
421
|
+
static #fieldIndex(fields) {
|
|
422
|
+
const cached =
|
|
423
|
+
PcbLayerStackSourceMetadataParser.#fieldIndexes.get(fields)
|
|
424
|
+
if (cached) return cached
|
|
425
|
+
|
|
426
|
+
const fieldIndex = new Map()
|
|
427
|
+
for (const fieldKey of Object.keys(fields)) {
|
|
428
|
+
const upperKey = fieldKey.toUpperCase()
|
|
429
|
+
if (!fieldIndex.has(upperKey)) {
|
|
430
|
+
fieldIndex.set(upperKey, fieldKey)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
PcbLayerStackSourceMetadataParser.#fieldIndexes.set(fields, fieldIndex)
|
|
435
|
+
return fieldIndex
|
|
436
|
+
}
|
|
437
|
+
|
|
415
438
|
/**
|
|
416
439
|
* Splits a native list field.
|
|
417
440
|
* @param {string} value Raw list value.
|
|
@@ -228,9 +228,7 @@ export class PcbOwnershipGraphBuilder {
|
|
|
228
228
|
if (!groups[key]) {
|
|
229
229
|
groups[key] = fallbackGroup
|
|
230
230
|
}
|
|
231
|
-
|
|
232
|
-
groups[key].primitiveKeys.push(primitiveKey)
|
|
233
|
-
}
|
|
231
|
+
groups[key].primitiveKeys.push(primitiveKey)
|
|
234
232
|
}
|
|
235
233
|
|
|
236
234
|
/**
|
|
@@ -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
|
-
|
|
26
|
+
routeAnalysis,
|
|
27
|
+
routeRowsByName
|
|
23
28
|
)
|
|
24
29
|
const routeHighlightProfiles =
|
|
25
|
-
PcbReviewRouteHighlightProfileBuilder.build(
|
|
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
|
-
|
|
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(
|
|
67
|
-
|
|
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
|
-
|
|
99
|
+
routeRowsByName,
|
|
87
100
|
classRow.netNames || []
|
|
88
101
|
),
|
|
89
102
|
primitiveKeys: PcbReviewMetadataBuilder.#primitiveKeysForNets(
|
|
90
|
-
|
|
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
|
-
|
|
132
|
+
routeRowsByName,
|
|
119
133
|
netNames
|
|
120
134
|
),
|
|
121
135
|
primitiveKeys: PcbReviewMetadataBuilder.#primitiveKeysForNets(
|
|
122
|
-
|
|
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
|
-
|
|
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}
|
|
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(
|
|
254
|
+
static #layerKeysForNets(routeRowsByName, netNames) {
|
|
241
255
|
const nets = PcbReviewMetadataBuilder.#netsByName(
|
|
242
|
-
|
|
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}
|
|
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(
|
|
270
|
+
static #primitiveKeysForNets(routeRowsByName, netNames) {
|
|
257
271
|
const nets = PcbReviewMetadataBuilder.#netsByName(
|
|
258
|
-
|
|
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
|
-
*
|
|
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(
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
/**
|