altium-toolkit 1.1.2 → 1.1.22
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/docs/api.md +37 -0
- package/docs/model-format.md +18 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
- package/docs/testing.md +5 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/altium/AltiumLayoutParser.mjs +275 -13
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +245 -10
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AsciiRecordParser.mjs +43 -11
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- 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/SchematicDirectiveParser.mjs +5 -17
- 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/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicPinParser.mjs +262 -24
- package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
- package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +263 -0
- package/src/ui/SchematicContentLayout.mjs +58 -1
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicImageRenderer.mjs +125 -10
- package/src/ui/SchematicJunctionRenderer.mjs +1 -1
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
- package/src/ui/SchematicNoteRenderer.mjs +87 -7
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
- 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 +109 -24
- package/src/ui/SchematicSvgRenderer.mjs +1210 -71
|
@@ -12,6 +12,8 @@ import { OleConstants } from '../ole/OleConstants.mjs'
|
|
|
12
12
|
* containers.
|
|
13
13
|
*/
|
|
14
14
|
export class SchematicStreamExtractor {
|
|
15
|
+
static #AUXILIARY_PRINTABLE_STREAMS = new Set(['Additional'])
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Returns true when one buffer starts with the OLE compound-document
|
|
17
19
|
* signature.
|
|
@@ -38,9 +40,10 @@ export class SchematicStreamExtractor {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
|
-
* Extracts schematic records from the logical `FileHeader` stream
|
|
43
|
+
* Extracts schematic records from the logical `FileHeader` stream and
|
|
44
|
+
* Altium's auxiliary printable schematic streams.
|
|
42
45
|
* @param {ArrayBuffer} arrayBuffer
|
|
43
|
-
* @returns {{ records: Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>, streamNames: string[] } | null}
|
|
46
|
+
* @returns {{ records: Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>, streamNames: string[], embeddedFiles?: object } | null}
|
|
44
47
|
*/
|
|
45
48
|
static extractFromArrayBuffer(arrayBuffer) {
|
|
46
49
|
if (!SchematicStreamExtractor.isCompoundDocument(arrayBuffer)) {
|
|
@@ -55,20 +58,28 @@ export class SchematicStreamExtractor {
|
|
|
55
58
|
return null
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
let
|
|
61
|
+
let fileHeaderRecords
|
|
59
62
|
|
|
60
63
|
try {
|
|
61
|
-
|
|
64
|
+
fileHeaderRecords = SchematicStreamExtractor.#parseStreamRecords(
|
|
65
|
+
compoundDocument,
|
|
66
|
+
'FileHeader'
|
|
67
|
+
)
|
|
62
68
|
} catch {
|
|
63
69
|
return null
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
const streamNames = compoundDocument.listStreams()
|
|
73
|
+
const auxiliaryStreamNames = streamNames.filter((streamName) =>
|
|
74
|
+
SchematicStreamExtractor.#isAuxiliaryPrintableStream(streamName)
|
|
75
|
+
)
|
|
76
|
+
const auxiliaryRecords = auxiliaryStreamNames.flatMap((streamName) =>
|
|
77
|
+
SchematicStreamExtractor.#parseStreamRecords(
|
|
78
|
+
compoundDocument,
|
|
79
|
+
streamName
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
const records = [...fileHeaderRecords, ...auxiliaryRecords]
|
|
72
83
|
|
|
73
84
|
if (!records.length) {
|
|
74
85
|
return null
|
|
@@ -76,18 +87,54 @@ export class SchematicStreamExtractor {
|
|
|
76
87
|
|
|
77
88
|
return {
|
|
78
89
|
records,
|
|
79
|
-
streamNames
|
|
90
|
+
streamNames,
|
|
80
91
|
embeddedFiles: EmbeddedFileInventoryBuilder.buildFromStreams(
|
|
81
92
|
new Map(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
streamNames.map((name) => [
|
|
94
|
+
name,
|
|
95
|
+
compoundDocument.getStream(name)
|
|
96
|
+
])
|
|
85
97
|
),
|
|
86
|
-
{
|
|
98
|
+
{
|
|
99
|
+
skipStreamNames: ['FileHeader', ...auxiliaryStreamNames]
|
|
100
|
+
}
|
|
87
101
|
)
|
|
88
102
|
}
|
|
89
103
|
}
|
|
90
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Parses printable records from one compound-document stream.
|
|
107
|
+
* @param {OleCompoundDocument} compoundDocument
|
|
108
|
+
* @param {string} streamName
|
|
109
|
+
* @returns {Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>}
|
|
110
|
+
*/
|
|
111
|
+
static #parseStreamRecords(compoundDocument, streamName) {
|
|
112
|
+
return AsciiRecordParser.parse(
|
|
113
|
+
SchematicStreamExtractor.#toArrayBuffer(
|
|
114
|
+
compoundDocument.getStream(streamName)
|
|
115
|
+
)
|
|
116
|
+
).map((record) => ({
|
|
117
|
+
...record,
|
|
118
|
+
sourceStream: streamName
|
|
119
|
+
}))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns true for known non-embedded schematic streams with printable
|
|
124
|
+
* primitive records.
|
|
125
|
+
* @param {string} streamName
|
|
126
|
+
* @returns {boolean}
|
|
127
|
+
*/
|
|
128
|
+
static #isAuxiliaryPrintableStream(streamName) {
|
|
129
|
+
const leafName = String(streamName || '')
|
|
130
|
+
.split('/')
|
|
131
|
+
.at(-1)
|
|
132
|
+
|
|
133
|
+
return SchematicStreamExtractor.#AUXILIARY_PRINTABLE_STREAMS.has(
|
|
134
|
+
leafName
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
91
138
|
/**
|
|
92
139
|
* Returns an ArrayBuffer view over one byte slice.
|
|
93
140
|
* @param {Uint8Array} bytes
|
|
@@ -17,6 +17,10 @@ export class SchematicTextParser {
|
|
|
17
17
|
const metadata = {}
|
|
18
18
|
|
|
19
19
|
for (const record of records) {
|
|
20
|
+
if (ParserUtils.getField(record.fields, 'OwnerIndex')) {
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
const name = ParserUtils.getField(record.fields, 'Name').trim()
|
|
21
25
|
const value = ParserUtils.getDisplayText(record.fields)
|
|
22
26
|
|
|
@@ -30,6 +34,39 @@ export class SchematicTextParser {
|
|
|
30
34
|
return metadata
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Extracts owner-local parameter values used by component text templates.
|
|
39
|
+
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
40
|
+
* @returns {Map<string, Record<string, string>>}
|
|
41
|
+
*/
|
|
42
|
+
static extractSchematicOwnerMetadata(records) {
|
|
43
|
+
const ownerMetadata = new Map()
|
|
44
|
+
|
|
45
|
+
for (const record of records) {
|
|
46
|
+
const ownerIndex = ParserUtils.getField(record.fields, 'OwnerIndex')
|
|
47
|
+
const name = ParserUtils.getField(record.fields, 'Name').trim()
|
|
48
|
+
const value = ParserUtils.getDisplayText(record.fields)
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
!ownerIndex ||
|
|
52
|
+
!name ||
|
|
53
|
+
!value ||
|
|
54
|
+
value === '*' ||
|
|
55
|
+
String(value).trim().startsWith('=')
|
|
56
|
+
) {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!ownerMetadata.has(ownerIndex)) {
|
|
61
|
+
ownerMetadata.set(ownerIndex, {})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ownerMetadata.get(ownerIndex)[name.toLowerCase()] = value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return ownerMetadata
|
|
68
|
+
}
|
|
69
|
+
|
|
33
70
|
/**
|
|
34
71
|
* Builds a font table from the sheet header.
|
|
35
72
|
* @param {Record<string, string | string[]> | undefined} fields
|
|
@@ -96,18 +133,30 @@ export class SchematicTextParser {
|
|
|
96
133
|
* @param {Record<string, string>} metadata
|
|
97
134
|
* @param {{ width: number, marginWidth: number, titleBlockOn?: boolean }} sheet
|
|
98
135
|
* @param {Record<string, { size: number, family: string, bold: boolean, italic?: boolean, rotation: number }>} fonts
|
|
136
|
+
* @param {Map<string, Record<string, string>>} [ownerMetadata]
|
|
99
137
|
* @returns {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] } | null}
|
|
100
138
|
*/
|
|
101
|
-
static normalizeSchematicTextRecord(
|
|
139
|
+
static normalizeSchematicTextRecord(
|
|
140
|
+
fields,
|
|
141
|
+
metadata,
|
|
142
|
+
sheet,
|
|
143
|
+
fonts,
|
|
144
|
+
ownerMetadata = new Map()
|
|
145
|
+
) {
|
|
102
146
|
const x = ParserUtils.parseNumericField(fields, 'Location.X')
|
|
103
147
|
const y = ParserUtils.parseNumericField(fields, 'Location.Y')
|
|
104
148
|
const hidden = ParserUtils.parseBoolean(fields.IsHidden)
|
|
105
149
|
const name = ParserUtils.getField(fields, 'Name')
|
|
106
150
|
const rawText = ParserUtils.getDisplayText(fields)
|
|
107
151
|
const recordType = ParserUtils.getField(fields, 'RECORD')
|
|
152
|
+
const ownerIndex = ParserUtils.getField(fields, 'OwnerIndex')
|
|
108
153
|
const text = SchematicTextParser.#resolveSchematicTemplateText(
|
|
109
154
|
rawText,
|
|
110
|
-
|
|
155
|
+
SchematicTextParser.#resolveSchematicTextMetadata(
|
|
156
|
+
ownerIndex,
|
|
157
|
+
metadata,
|
|
158
|
+
ownerMetadata
|
|
159
|
+
)
|
|
111
160
|
)
|
|
112
161
|
|
|
113
162
|
if (hidden || x === null || y === null || !text) {
|
|
@@ -149,7 +198,7 @@ export class SchematicTextParser {
|
|
|
149
198
|
),
|
|
150
199
|
hidden,
|
|
151
200
|
name,
|
|
152
|
-
ownerIndex:
|
|
201
|
+
ownerIndex: ownerIndex || undefined,
|
|
153
202
|
recordType,
|
|
154
203
|
style: ParserUtils.parseNumericField(fields, 'Style') || 0,
|
|
155
204
|
renderOrder:
|
|
@@ -476,6 +525,21 @@ export class SchematicTextParser {
|
|
|
476
525
|
return replacement ? replacement : normalized
|
|
477
526
|
}
|
|
478
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Chooses the parameter map for one text record.
|
|
530
|
+
* @param {string} ownerIndex
|
|
531
|
+
* @param {Record<string, string>} metadata
|
|
532
|
+
* @param {Map<string, Record<string, string>>} ownerMetadata
|
|
533
|
+
* @returns {Record<string, string>}
|
|
534
|
+
*/
|
|
535
|
+
static #resolveSchematicTextMetadata(ownerIndex, metadata, ownerMetadata) {
|
|
536
|
+
if (!ownerIndex) {
|
|
537
|
+
return metadata
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return ownerMetadata.get(ownerIndex) || {}
|
|
541
|
+
}
|
|
542
|
+
|
|
479
543
|
/**
|
|
480
544
|
* Returns true when a text record is metadata rather than sheet content.
|
|
481
545
|
* @param {Record<string, string | string[]>} fields
|
|
@@ -510,7 +574,6 @@ export class SchematicTextParser {
|
|
|
510
574
|
if (nonDrawableNames.has(normalizedName)) return true
|
|
511
575
|
if (/uniqueid$/i.test(normalizedName)) return true
|
|
512
576
|
if (!normalizedText || normalizedText === '*') return true
|
|
513
|
-
if (/^=/.test(normalizedText)) return true
|
|
514
577
|
if (
|
|
515
578
|
sheet.titleBlockOn &&
|
|
516
579
|
SchematicTextParser.isTitleBlockFooterRecord(fields, sheet.width)
|
|
@@ -537,10 +600,47 @@ export class SchematicTextParser {
|
|
|
537
600
|
|
|
538
601
|
if (recordType === '17') return 'middle'
|
|
539
602
|
if (explicitAnchor) return explicitAnchor
|
|
603
|
+
if (
|
|
604
|
+
SchematicTextParser.#shouldCenterSchematicNoteByDefault(
|
|
605
|
+
fields,
|
|
606
|
+
recordType,
|
|
607
|
+
text
|
|
608
|
+
)
|
|
609
|
+
) {
|
|
610
|
+
return 'middle'
|
|
611
|
+
}
|
|
540
612
|
|
|
541
613
|
return 'start'
|
|
542
614
|
}
|
|
543
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Returns true for Altium text frames that encode centered one-line labels
|
|
618
|
+
* without carrying an explicit justification field.
|
|
619
|
+
* @param {Record<string, string | string[]>} fields
|
|
620
|
+
* @param {string} recordType
|
|
621
|
+
* @param {string} text
|
|
622
|
+
* @returns {boolean}
|
|
623
|
+
*/
|
|
624
|
+
static #shouldCenterSchematicNoteByDefault(fields, recordType, text) {
|
|
625
|
+
if (recordType !== '209' && recordType !== '28') return false
|
|
626
|
+
if (String(text || '').includes('~1')) return false
|
|
627
|
+
if (
|
|
628
|
+
ParserUtils.parseNumericField(fields, 'Corner.X') === null ||
|
|
629
|
+
ParserUtils.parseNumericField(fields, 'Corner.Y') === null
|
|
630
|
+
) {
|
|
631
|
+
return false
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (recordType === '28') {
|
|
635
|
+
return ParserUtils.parseBoolean(fields.WordWrap)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return (
|
|
639
|
+
ParserUtils.parseBoolean(fields.WordWrap) &&
|
|
640
|
+
ParserUtils.parseBoolean(fields.ClipToRect)
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
|
|
544
644
|
/**
|
|
545
645
|
* Decodes Altium's three-column text justification grid into one
|
|
546
646
|
* horizontal SVG text anchor.
|
|
@@ -548,23 +648,37 @@ export class SchematicTextParser {
|
|
|
548
648
|
* @returns {'start' | 'middle' | 'end' | null}
|
|
549
649
|
*/
|
|
550
650
|
static #resolveSchematicTextJustificationAnchor(fields) {
|
|
551
|
-
const justification =
|
|
552
|
-
fields,
|
|
553
|
-
'
|
|
554
|
-
)
|
|
651
|
+
const justification =
|
|
652
|
+
ParserUtils.parseNumericField(fields, 'Justification') ??
|
|
653
|
+
ParserUtils.parseNumericField(fields, 'Alignment')
|
|
555
654
|
|
|
556
655
|
if (justification === null) {
|
|
557
656
|
return null
|
|
558
657
|
}
|
|
559
658
|
|
|
659
|
+
let anchor = 'start'
|
|
560
660
|
switch (((justification % 3) + 3) % 3) {
|
|
561
661
|
case 1:
|
|
562
|
-
|
|
662
|
+
anchor = 'middle'
|
|
663
|
+
break
|
|
563
664
|
case 2:
|
|
564
|
-
|
|
665
|
+
anchor = 'end'
|
|
666
|
+
break
|
|
565
667
|
default:
|
|
566
|
-
|
|
668
|
+
anchor = 'start'
|
|
567
669
|
}
|
|
670
|
+
|
|
671
|
+
if (
|
|
672
|
+
anchor !== 'middle' &&
|
|
673
|
+
ParserUtils.getField(fields, 'RECORD') === '4' &&
|
|
674
|
+
ParserUtils.getField(fields, 'OwnerIndex') &&
|
|
675
|
+
ParserUtils.parseBoolean(fields.IsMirrored) &&
|
|
676
|
+
ParserUtils.parseNumericField(fields, 'Orientation') === 2
|
|
677
|
+
) {
|
|
678
|
+
return anchor === 'start' ? 'end' : 'start'
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return anchor
|
|
568
682
|
}
|
|
569
683
|
|
|
570
684
|
/**
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
+
import { SchematicMultipartDesignatorNormalizer } from './SchematicMultipartDesignatorNormalizer.mjs'
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Applies placement-oriented cleanup passes to normalized schematic text.
|
|
7
9
|
*/
|
|
@@ -47,84 +49,17 @@ export class SchematicTextPostProcessor {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
|
-
*
|
|
51
|
-
* when the active Altium part id is stored separately from the
|
|
52
|
+
* Normalizes multipart section suffixes like A/B/J on visible designator
|
|
53
|
+
* texts when the active Altium part id is stored separately from the
|
|
52
54
|
* designator string.
|
|
53
55
|
* @param {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]} texts
|
|
54
56
|
* @param {Map<string, string>} activeMultipartOwnerParts
|
|
55
57
|
* @returns {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]}
|
|
56
58
|
*/
|
|
57
59
|
static decorateMultipartDesignators(texts, activeMultipartOwnerParts) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
activeMultipartOwnerParts
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
return texts.map((text) => {
|
|
65
|
-
const ownerIndex = String(text.ownerIndex || '')
|
|
66
|
-
const suffix =
|
|
67
|
-
SchematicTextPostProcessor.#formatMultipartPartSuffix(
|
|
68
|
-
activeMultipartOwnerParts.get(ownerIndex)
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
!suffix ||
|
|
73
|
-
text.recordType !== '34' ||
|
|
74
|
-
String(text.name || '')
|
|
75
|
-
.trim()
|
|
76
|
-
.toLowerCase() !== 'designator' ||
|
|
77
|
-
!duplicatedActiveBaseDesignators.has(text.text)
|
|
78
|
-
) {
|
|
79
|
-
return text
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!/\d$/i.test(text.text) || text.text.endsWith(suffix)) {
|
|
83
|
-
return text
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
...text,
|
|
88
|
-
text: text.text + suffix
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Collects base multipart designators that belong to more than one active
|
|
95
|
-
* visible owner on the current sheet.
|
|
96
|
-
* @param {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]} texts
|
|
97
|
-
* @param {Map<string, string>} activeMultipartOwnerParts
|
|
98
|
-
* @returns {Set<string>}
|
|
99
|
-
*/
|
|
100
|
-
static #collectDuplicatedActiveMultipartDesignators(
|
|
101
|
-
texts,
|
|
102
|
-
activeMultipartOwnerParts
|
|
103
|
-
) {
|
|
104
|
-
const counts = new Map()
|
|
105
|
-
|
|
106
|
-
for (const text of texts) {
|
|
107
|
-
const ownerIndex = String(text.ownerIndex || '')
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
!activeMultipartOwnerParts.has(ownerIndex) ||
|
|
111
|
-
text.recordType !== '34' ||
|
|
112
|
-
String(text.name || '')
|
|
113
|
-
.trim()
|
|
114
|
-
.toLowerCase() !== 'designator' ||
|
|
115
|
-
!/\d$/i.test(String(text.text || '').trim())
|
|
116
|
-
) {
|
|
117
|
-
continue
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const baseDesignator = String(text.text || '').trim()
|
|
121
|
-
counts.set(baseDesignator, (counts.get(baseDesignator) || 0) + 1)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return new Set(
|
|
125
|
-
[...counts.entries()]
|
|
126
|
-
.filter(([, count]) => count > 1)
|
|
127
|
-
.map(([baseDesignator]) => baseDesignator)
|
|
60
|
+
return SchematicMultipartDesignatorNormalizer.normalize(
|
|
61
|
+
texts,
|
|
62
|
+
activeMultipartOwnerParts
|
|
128
63
|
)
|
|
129
64
|
}
|
|
130
65
|
|
|
@@ -136,7 +71,7 @@ export class SchematicTextPostProcessor {
|
|
|
136
71
|
* @param {{ x1: number, y1: number, x2: number, y2: number, ownerIndex?: string }[]} lines
|
|
137
72
|
* @param {{ x: number, y: number, ownerIndex: string, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
138
73
|
* @param {{ x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down' }[]} ports
|
|
139
|
-
* @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string }[]}
|
|
74
|
+
* @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string }[] | { rectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses?: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }} bodyPrimitives
|
|
140
75
|
* @returns {{ x: number, y: number, text: string, name?: string, ownerIndex?: string, recordType?: string, rotation?: number, anchor?: 'start' | 'middle' | 'end' }[]}
|
|
141
76
|
*/
|
|
142
77
|
static anchorComponentTextsFromOwnerBounds(
|
|
@@ -144,17 +79,22 @@ export class SchematicTextPostProcessor {
|
|
|
144
79
|
lines,
|
|
145
80
|
pins,
|
|
146
81
|
ports = [],
|
|
147
|
-
|
|
82
|
+
bodyPrimitives = []
|
|
148
83
|
) {
|
|
84
|
+
const normalizedBodyPrimitives =
|
|
85
|
+
SchematicTextPostProcessor.#normalizeOwnerBodyPrimitives(
|
|
86
|
+
bodyPrimitives
|
|
87
|
+
)
|
|
149
88
|
const ownerBounds = SchematicTextPostProcessor.#buildOwnerBounds(
|
|
150
89
|
lines,
|
|
151
90
|
pins
|
|
152
91
|
)
|
|
153
|
-
const ownerBodyBounds =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
92
|
+
const ownerBodyBounds =
|
|
93
|
+
SchematicTextPostProcessor.#buildOwnerBodyBounds(
|
|
94
|
+
lines,
|
|
95
|
+
pins,
|
|
96
|
+
normalizedBodyPrimitives
|
|
97
|
+
)
|
|
158
98
|
const ownerPinCounts =
|
|
159
99
|
SchematicTextPostProcessor.#buildOwnerPinCounts(pins)
|
|
160
100
|
const ownerPinOrientations =
|
|
@@ -236,6 +176,131 @@ export class SchematicTextPostProcessor {
|
|
|
236
176
|
})
|
|
237
177
|
}
|
|
238
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Normalizes owner body primitive arguments while preserving the older
|
|
181
|
+
* rectangle-array call shape.
|
|
182
|
+
* @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string }[] | { rectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses?: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }} bodyPrimitives
|
|
183
|
+
* @returns {{ rectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }}
|
|
184
|
+
*/
|
|
185
|
+
static #normalizeOwnerBodyPrimitives(bodyPrimitives) {
|
|
186
|
+
if (Array.isArray(bodyPrimitives)) {
|
|
187
|
+
return {
|
|
188
|
+
rectangles: bodyPrimitives,
|
|
189
|
+
roundedRectangles: [],
|
|
190
|
+
ellipses: [],
|
|
191
|
+
arcs: [],
|
|
192
|
+
pies: []
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
rectangles: bodyPrimitives?.rectangles || [],
|
|
198
|
+
roundedRectangles: bodyPrimitives?.roundedRectangles || [],
|
|
199
|
+
ellipses: bodyPrimitives?.ellipses || [],
|
|
200
|
+
arcs: bodyPrimitives?.arcs || [],
|
|
201
|
+
pies: bodyPrimitives?.pies || []
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Builds per-owner body bounds from straight and curved owner primitives.
|
|
207
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, ownerIndex?: string }[]} lines
|
|
208
|
+
* @param {{ x: number, y: number, ownerIndex: string }[]} pins
|
|
209
|
+
* @param {{ rectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }} bodyPrimitives
|
|
210
|
+
* @returns {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>}
|
|
211
|
+
*/
|
|
212
|
+
static #buildOwnerBodyBounds(lines, pins, bodyPrimitives) {
|
|
213
|
+
const ownerBounds = SchematicTextPostProcessor.#buildOwnerBounds(
|
|
214
|
+
lines,
|
|
215
|
+
pins,
|
|
216
|
+
[...bodyPrimitives.rectangles, ...bodyPrimitives.roundedRectangles]
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
SchematicTextPostProcessor.#extendCenteredOwnerBounds(
|
|
220
|
+
ownerBounds,
|
|
221
|
+
bodyPrimitives.ellipses
|
|
222
|
+
)
|
|
223
|
+
SchematicTextPostProcessor.#extendCenteredOwnerBounds(
|
|
224
|
+
ownerBounds,
|
|
225
|
+
bodyPrimitives.arcs
|
|
226
|
+
)
|
|
227
|
+
SchematicTextPostProcessor.#extendCenteredOwnerBounds(
|
|
228
|
+
ownerBounds,
|
|
229
|
+
bodyPrimitives.pies
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return ownerBounds
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Extends owner bounds by primitives expressed as a center and radii.
|
|
237
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerBounds
|
|
238
|
+
* @param {{ x: number, y: number, radius?: number, radiusX?: number, radiusY?: number, ownerIndex?: string }[]} primitives
|
|
239
|
+
* @returns {void}
|
|
240
|
+
*/
|
|
241
|
+
static #extendCenteredOwnerBounds(ownerBounds, primitives) {
|
|
242
|
+
for (const primitive of primitives) {
|
|
243
|
+
if (!primitive.ownerIndex) {
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const radiusX =
|
|
248
|
+
SchematicTextPostProcessor.#resolveHorizontalRadius(primitive)
|
|
249
|
+
const radiusY =
|
|
250
|
+
SchematicTextPostProcessor.#resolveVerticalRadius(primitive)
|
|
251
|
+
|
|
252
|
+
if (radiusX <= 0 || radiusY <= 0) {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
SchematicTextPostProcessor.#extendBounds(
|
|
257
|
+
ownerBounds,
|
|
258
|
+
primitive.ownerIndex,
|
|
259
|
+
[
|
|
260
|
+
{ x: primitive.x - radiusX, y: primitive.y - radiusY },
|
|
261
|
+
{ x: primitive.x + radiusX, y: primitive.y + radiusY }
|
|
262
|
+
]
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Resolves the horizontal radius of a center-defined primitive.
|
|
269
|
+
* @param {{ radius?: number, radiusX?: number }} primitive
|
|
270
|
+
* @returns {number}
|
|
271
|
+
*/
|
|
272
|
+
static #resolveHorizontalRadius(primitive) {
|
|
273
|
+
return SchematicTextPostProcessor.#coercePositiveNumber(
|
|
274
|
+
primitive.radiusX ?? primitive.radius
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Resolves the vertical radius of a center-defined primitive.
|
|
280
|
+
* @param {{ radius?: number, radiusY?: number }} primitive
|
|
281
|
+
* @returns {number}
|
|
282
|
+
*/
|
|
283
|
+
static #resolveVerticalRadius(primitive) {
|
|
284
|
+
return SchematicTextPostProcessor.#coercePositiveNumber(
|
|
285
|
+
primitive.radiusY ?? primitive.radius
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Converts finite positive numeric values, preserving zero for invalids.
|
|
291
|
+
* @param {number | string | undefined | null} value
|
|
292
|
+
* @returns {number}
|
|
293
|
+
*/
|
|
294
|
+
static #coercePositiveNumber(value) {
|
|
295
|
+
const numericValue = Number(value)
|
|
296
|
+
|
|
297
|
+
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
|
298
|
+
return 0
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return numericValue
|
|
302
|
+
}
|
|
303
|
+
|
|
239
304
|
/**
|
|
240
305
|
* Right-aligns wire labels that precede a same-row component designator so
|
|
241
306
|
* they stay clear of the symbol body.
|
|
@@ -401,6 +466,8 @@ export class SchematicTextPostProcessor {
|
|
|
401
466
|
case 2:
|
|
402
467
|
case 33:
|
|
403
468
|
return 8
|
|
469
|
+
case 6:
|
|
470
|
+
return 12
|
|
404
471
|
default:
|
|
405
472
|
return 2
|
|
406
473
|
}
|
|
@@ -744,29 +811,6 @@ export class SchematicTextPostProcessor {
|
|
|
744
811
|
)
|
|
745
812
|
}
|
|
746
813
|
|
|
747
|
-
/**
|
|
748
|
-
* Converts one numeric multipart section id into an alphabetic suffix.
|
|
749
|
-
* @param {string | undefined} partId
|
|
750
|
-
* @returns {string}
|
|
751
|
-
*/
|
|
752
|
-
static #formatMultipartPartSuffix(partId) {
|
|
753
|
-
const numericPartId = Number.parseInt(String(partId || ''), 10)
|
|
754
|
-
if (!Number.isInteger(numericPartId) || numericPartId <= 0) {
|
|
755
|
-
return ''
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
let suffix = ''
|
|
759
|
-
let remaining = numericPartId
|
|
760
|
-
|
|
761
|
-
while (remaining > 0) {
|
|
762
|
-
remaining -= 1
|
|
763
|
-
suffix = String.fromCharCode(65 + (remaining % 26)) + suffix
|
|
764
|
-
remaining = Math.floor(remaining / 26)
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
return suffix
|
|
768
|
-
}
|
|
769
|
-
|
|
770
814
|
/**
|
|
771
815
|
* Returns true when the horizontal wire segment under the label starts at a
|
|
772
816
|
* pin endpoint, which means the label should continue reading rightward.
|