altium-toolkit 1.0.7 → 1.0.9
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/README.md +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +191 -45
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +466 -28
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +58 -0
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +257 -5
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +29 -0
- package/src/renderers.mjs +3 -0
- package/src/styles/altium-renderers.css +25 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionGeometry.mjs +350 -0
- package/src/ui/PcbInteractionIndex.mjs +593 -0
- package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
- package/src/ui/PcbInteractionLayerModel.mjs +99 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
- package/src/ui/PcbScene3dBuilder.mjs +169 -7
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1187 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
|
|
7
|
+
const { getField, parseNumericField } = ParserUtils
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds a read-only ownership graph from schematic record owner indexes.
|
|
11
|
+
*/
|
|
12
|
+
export class SchematicOwnershipGraphParser {
|
|
13
|
+
static SCHEMA_ID = 'altium-toolkit.schematic.ownership.a1'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses record parent/child links from raw schematic records.
|
|
17
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Schematic records.
|
|
18
|
+
* @returns {{ schema: string, records: object[], recordsByIndexInSheet: Record<string, object>, childrenByParentKey: Record<string, object[]>, parentsByChildKey: Record<string, object> }}
|
|
19
|
+
*/
|
|
20
|
+
static parse(records) {
|
|
21
|
+
const entries = (records || []).map((record, fallbackIndex) =>
|
|
22
|
+
SchematicOwnershipGraphParser.#recordEntry(record, fallbackIndex)
|
|
23
|
+
)
|
|
24
|
+
const ownerLookup =
|
|
25
|
+
SchematicOwnershipGraphParser.#buildOwnerLookup(entries)
|
|
26
|
+
const childrenByParentKey = {}
|
|
27
|
+
const parentsByChildKey = {}
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (!entry.ownerIndex) {
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parent = ownerLookup.get(entry.ownerIndex)
|
|
35
|
+
if (!parent || parent.key === entry.key) {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!childrenByParentKey[parent.key]) {
|
|
40
|
+
childrenByParentKey[parent.key] = []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
childrenByParentKey[parent.key].push(
|
|
44
|
+
SchematicOwnershipGraphParser.#childDescriptor(entry)
|
|
45
|
+
)
|
|
46
|
+
parentsByChildKey[entry.key] = {
|
|
47
|
+
parentKey: parent.key,
|
|
48
|
+
ownerIndex: entry.ownerIndex
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
schema: SchematicOwnershipGraphParser.SCHEMA_ID,
|
|
54
|
+
records: entries.map((entry) =>
|
|
55
|
+
SchematicOwnershipGraphParser.#publicRecord(entry)
|
|
56
|
+
),
|
|
57
|
+
recordsByIndexInSheet:
|
|
58
|
+
SchematicOwnershipGraphParser.#recordsByIndexInSheet(entries),
|
|
59
|
+
childrenByParentKey,
|
|
60
|
+
parentsByChildKey
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Normalizes one raw schematic record into a sidecar row.
|
|
66
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} record Source record.
|
|
67
|
+
* @param {number} fallbackIndex Fallback record index.
|
|
68
|
+
* @returns {object}
|
|
69
|
+
*/
|
|
70
|
+
static #recordEntry(record, fallbackIndex) {
|
|
71
|
+
const recordIndex = Number.isInteger(record?.recordIndex)
|
|
72
|
+
? record.recordIndex
|
|
73
|
+
: fallbackIndex
|
|
74
|
+
const indexInSheet = parseNumericField(record?.fields, 'IndexInSheet')
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
key: 'schematic-record-' + recordIndex,
|
|
78
|
+
recordIndex,
|
|
79
|
+
recordType: getField(record?.fields, 'RECORD'),
|
|
80
|
+
indexInSheet,
|
|
81
|
+
ownerIndex: getField(record?.fields, 'OwnerIndex'),
|
|
82
|
+
uniqueId:
|
|
83
|
+
getField(record?.fields, 'UniqueID') ||
|
|
84
|
+
getField(record?.fields, 'UniqueId'),
|
|
85
|
+
name: getField(record?.fields, 'Name'),
|
|
86
|
+
text: getField(record?.fields, 'Text')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Builds tolerant owner-index lookup keys for record parents.
|
|
92
|
+
* @param {object[]} entries Record sidecar rows.
|
|
93
|
+
* @returns {Map<string, object>}
|
|
94
|
+
*/
|
|
95
|
+
static #buildOwnerLookup(entries) {
|
|
96
|
+
const lookup = new Map()
|
|
97
|
+
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
for (const key of SchematicOwnershipGraphParser.#ownerKeys(entry)) {
|
|
100
|
+
if (!lookup.has(key)) {
|
|
101
|
+
lookup.set(key, entry)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return lookup
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns candidate owner-index keys for one record.
|
|
111
|
+
* @param {object} entry Record sidecar row.
|
|
112
|
+
* @returns {string[]}
|
|
113
|
+
*/
|
|
114
|
+
static #ownerKeys(entry) {
|
|
115
|
+
const keys = new Set([
|
|
116
|
+
String(entry.recordIndex),
|
|
117
|
+
String(entry.recordIndex + 1)
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
if (Number.isInteger(entry.indexInSheet)) {
|
|
121
|
+
keys.add(String(entry.indexInSheet))
|
|
122
|
+
keys.add(String(entry.indexInSheet + 1))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [...keys]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Builds a compact public record row.
|
|
130
|
+
* @param {object} entry Internal record row.
|
|
131
|
+
* @returns {object}
|
|
132
|
+
*/
|
|
133
|
+
static #publicRecord(entry) {
|
|
134
|
+
return SchematicOwnershipGraphParser.#stripEmpty({
|
|
135
|
+
key: entry.key,
|
|
136
|
+
recordIndex: entry.recordIndex,
|
|
137
|
+
recordType: entry.recordType,
|
|
138
|
+
indexInSheet: entry.indexInSheet,
|
|
139
|
+
ownerIndex: entry.ownerIndex,
|
|
140
|
+
uniqueId: entry.uniqueId,
|
|
141
|
+
name: entry.name,
|
|
142
|
+
text: entry.text
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Builds a compact child descriptor for grouped owner lists.
|
|
148
|
+
* @param {object} entry Internal record row.
|
|
149
|
+
* @returns {object}
|
|
150
|
+
*/
|
|
151
|
+
static #childDescriptor(entry) {
|
|
152
|
+
return SchematicOwnershipGraphParser.#stripEmpty({
|
|
153
|
+
key: entry.key,
|
|
154
|
+
recordIndex: entry.recordIndex,
|
|
155
|
+
recordType: entry.recordType,
|
|
156
|
+
ownerIndex: entry.ownerIndex,
|
|
157
|
+
indexInSheet: entry.indexInSheet,
|
|
158
|
+
name: entry.name
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Indexes records by native IndexInSheet values.
|
|
164
|
+
* @param {object[]} entries Record sidecar rows.
|
|
165
|
+
* @returns {Record<string, object>}
|
|
166
|
+
*/
|
|
167
|
+
static #recordsByIndexInSheet(entries) {
|
|
168
|
+
const index = {}
|
|
169
|
+
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
if (Number.isInteger(entry.indexInSheet)) {
|
|
172
|
+
index[String(entry.indexInSheet)] =
|
|
173
|
+
SchematicOwnershipGraphParser.#publicRecord(entry)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return index
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Drops empty object fields while preserving numeric zero.
|
|
182
|
+
* @param {Record<string, unknown>} value Candidate object.
|
|
183
|
+
* @returns {Record<string, unknown>}
|
|
184
|
+
*/
|
|
185
|
+
static #stripEmpty(value) {
|
|
186
|
+
return Object.fromEntries(
|
|
187
|
+
Object.entries(value || {}).filter(
|
|
188
|
+
([, entryValue]) =>
|
|
189
|
+
entryValue !== null &&
|
|
190
|
+
entryValue !== undefined &&
|
|
191
|
+
entryValue !== ''
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -409,6 +409,14 @@ export class SchematicPinParser {
|
|
|
409
409
|
|
|
410
410
|
const segments = []
|
|
411
411
|
const lineStyle = SchematicPinParser.#resolveSchematicLineStyle(fields)
|
|
412
|
+
const startMarker = SchematicPinParser.#resolvePolylineMarker(
|
|
413
|
+
fields,
|
|
414
|
+
'Start'
|
|
415
|
+
)
|
|
416
|
+
const endMarker = SchematicPinParser.#resolvePolylineMarker(
|
|
417
|
+
fields,
|
|
418
|
+
'End'
|
|
419
|
+
)
|
|
412
420
|
|
|
413
421
|
for (let index = 1; index < points.length; index += 1) {
|
|
414
422
|
const previous = points[index - 1]
|
|
@@ -433,7 +441,11 @@ export class SchematicPinParser {
|
|
|
433
441
|
isBus: options.isBus === true ? true : undefined,
|
|
434
442
|
recordType: options.recordType || undefined,
|
|
435
443
|
omittedEndpointAxis: omittedEndpointAxis || undefined,
|
|
436
|
-
sourceLocationCount: points.length
|
|
444
|
+
sourceLocationCount: points.length,
|
|
445
|
+
...(index === 1 && startMarker ? { startMarker } : {}),
|
|
446
|
+
...(index === points.length - 1 && endMarker
|
|
447
|
+
? { endMarker }
|
|
448
|
+
: {})
|
|
437
449
|
})
|
|
438
450
|
}
|
|
439
451
|
|
|
@@ -525,6 +537,77 @@ export class SchematicPinParser {
|
|
|
525
537
|
: '#a44a1b'
|
|
526
538
|
}
|
|
527
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Resolves one authored polyline endpoint marker.
|
|
542
|
+
* @param {Record<string, string | string[]>} fields Source record fields.
|
|
543
|
+
* @param {'Start' | 'End'} edge Endpoint prefix.
|
|
544
|
+
* @returns {{ shape: number, shapeName: string, size: number } | null}
|
|
545
|
+
*/
|
|
546
|
+
static #resolvePolylineMarker(fields, edge) {
|
|
547
|
+
const shape = SchematicPinParser.#parseFirstNumericField(fields, [
|
|
548
|
+
edge + 'LineShape',
|
|
549
|
+
edge + 'LineMarker',
|
|
550
|
+
edge + 'MarkerShape',
|
|
551
|
+
edge + 'ArrowKind'
|
|
552
|
+
])
|
|
553
|
+
|
|
554
|
+
if (shape === null || shape <= 0) {
|
|
555
|
+
return null
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const size =
|
|
559
|
+
SchematicPinParser.#parseFirstNumericField(fields, [
|
|
560
|
+
edge + 'LineShapeSize',
|
|
561
|
+
edge + 'LineMarkerSize',
|
|
562
|
+
edge + 'MarkerSize',
|
|
563
|
+
edge + 'ArrowSize'
|
|
564
|
+
]) || 6
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
shape,
|
|
568
|
+
shapeName:
|
|
569
|
+
SchematicPinParser.#resolvePolylineMarkerShapeName(shape),
|
|
570
|
+
size
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Parses the first present numeric field from a candidate list.
|
|
576
|
+
* @param {Record<string, string | string[]>} fields Source record fields.
|
|
577
|
+
* @param {string[]} names Candidate field names.
|
|
578
|
+
* @returns {number | null}
|
|
579
|
+
*/
|
|
580
|
+
static #parseFirstNumericField(fields, names) {
|
|
581
|
+
for (const name of names) {
|
|
582
|
+
const value = ParserUtils.parseNumericField(fields, name)
|
|
583
|
+
if (value !== null) {
|
|
584
|
+
return value
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return null
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Maps native polyline marker codes onto stable renderer labels.
|
|
593
|
+
* @param {number} shape Numeric marker code.
|
|
594
|
+
* @returns {string}
|
|
595
|
+
*/
|
|
596
|
+
static #resolvePolylineMarkerShapeName(shape) {
|
|
597
|
+
switch (shape) {
|
|
598
|
+
case 1:
|
|
599
|
+
return 'arrow'
|
|
600
|
+
case 2:
|
|
601
|
+
return 'filled-arrow'
|
|
602
|
+
case 3:
|
|
603
|
+
return 'circle'
|
|
604
|
+
case 4:
|
|
605
|
+
return 'square'
|
|
606
|
+
default:
|
|
607
|
+
return 'marker-' + shape
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
528
611
|
/**
|
|
529
612
|
* Collects a schematic point list, carrying forward a missing coordinate
|
|
530
613
|
* axis from the preceding point when Altium omitted an unchanged value.
|
|
@@ -32,6 +32,24 @@ export class SchematicPrimitiveParser {
|
|
|
32
32
|
)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Returns true when one record belongs to the rounded-rectangle primitive.
|
|
37
|
+
* @param {Record<string, string | string[]>} fields
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
static isRoundedRectangleRecord(fields) {
|
|
41
|
+
return getField(fields, 'RECORD') === '10'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns true when one record belongs to the schematic IEEE-symbol family.
|
|
46
|
+
* @param {Record<string, string | string[]>} fields
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
static isIeeeSymbolRecord(fields) {
|
|
50
|
+
return getField(fields, 'RECORD') === '3'
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
/**
|
|
36
54
|
* Returns true when one point-listed primitive describes an axis-aligned
|
|
37
55
|
* rectangle instead of an arbitrary polyline.
|
|
@@ -117,6 +135,215 @@ export class SchematicPrimitiveParser {
|
|
|
117
135
|
.filter(Boolean)
|
|
118
136
|
}
|
|
119
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Normalizes record-5 cubic Bezier primitives.
|
|
140
|
+
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
141
|
+
* @returns {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, control1: { x: number, y: number }, control2: { x: number, y: number }, end: { x: number, y: number } }[], color: string, width: number, lineStyle: number, renderOrder: number, ownerIndex?: string }[]}
|
|
142
|
+
*/
|
|
143
|
+
static parseSchematicBeziers(records) {
|
|
144
|
+
return records
|
|
145
|
+
.map((record, index) => {
|
|
146
|
+
const points = SchematicPrimitiveParser.#collectPolygonPoints(
|
|
147
|
+
record.fields
|
|
148
|
+
)
|
|
149
|
+
const segments =
|
|
150
|
+
SchematicPrimitiveParser.#buildBezierSegments(points)
|
|
151
|
+
|
|
152
|
+
if (!segments.length) {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return SchematicPrimitiveParser.#stripUndefined({
|
|
157
|
+
points,
|
|
158
|
+
segments,
|
|
159
|
+
color: toColor(record.fields.Color, '#000000'),
|
|
160
|
+
width: parseNumericField(record.fields, 'LineWidth') || 1,
|
|
161
|
+
lineStyle:
|
|
162
|
+
parseNumericField(record.fields, 'LineStyle') || 0,
|
|
163
|
+
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
164
|
+
record.fields,
|
|
165
|
+
index
|
|
166
|
+
),
|
|
167
|
+
ownerIndex:
|
|
168
|
+
getField(record.fields, 'OwnerIndex') || undefined
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Normalizes record-9 pie/wedge primitives.
|
|
176
|
+
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
177
|
+
* @returns {{ x: number, y: number, radius: number, radiusY: number, startAngle: number, endAngle: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, renderOrder: number, ownerIndex?: string }[]}
|
|
178
|
+
*/
|
|
179
|
+
static parseSchematicPies(records) {
|
|
180
|
+
return records
|
|
181
|
+
.map((record, index) => {
|
|
182
|
+
const x = parseNumericFieldWithFraction(
|
|
183
|
+
record.fields,
|
|
184
|
+
'Location.X'
|
|
185
|
+
)
|
|
186
|
+
const y = parseNumericFieldWithFraction(
|
|
187
|
+
record.fields,
|
|
188
|
+
'Location.Y'
|
|
189
|
+
)
|
|
190
|
+
const radius = parseNumericFieldWithFraction(
|
|
191
|
+
record.fields,
|
|
192
|
+
'Radius'
|
|
193
|
+
)
|
|
194
|
+
const radiusY =
|
|
195
|
+
parseNumericFieldWithFraction(
|
|
196
|
+
record.fields,
|
|
197
|
+
'SecondaryRadius'
|
|
198
|
+
) ?? radius
|
|
199
|
+
|
|
200
|
+
if (
|
|
201
|
+
x === null ||
|
|
202
|
+
y === null ||
|
|
203
|
+
radius === null ||
|
|
204
|
+
radius <= 0 ||
|
|
205
|
+
radiusY === null ||
|
|
206
|
+
radiusY <= 0
|
|
207
|
+
) {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return SchematicPrimitiveParser.#stripUndefined({
|
|
212
|
+
x,
|
|
213
|
+
y,
|
|
214
|
+
radius,
|
|
215
|
+
radiusY,
|
|
216
|
+
startAngle:
|
|
217
|
+
parseNumericField(record.fields, 'StartAngle') ?? 0,
|
|
218
|
+
endAngle:
|
|
219
|
+
parseNumericField(record.fields, 'EndAngle') ?? 360,
|
|
220
|
+
color: toColor(record.fields.Color, '#000000'),
|
|
221
|
+
fill: toColor(record.fields.AreaColor, '#ffffff'),
|
|
222
|
+
isSolid: parseBoolean(record.fields.IsSolid),
|
|
223
|
+
transparent: parseBoolean(record.fields.Transparent),
|
|
224
|
+
lineWidth:
|
|
225
|
+
parseNumericField(record.fields, 'LineWidth') || 1,
|
|
226
|
+
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
227
|
+
record.fields,
|
|
228
|
+
index
|
|
229
|
+
),
|
|
230
|
+
ownerIndex:
|
|
231
|
+
getField(record.fields, 'OwnerIndex') || undefined
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
.filter(Boolean)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Normalizes record-10 rounded rectangle primitives.
|
|
239
|
+
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
240
|
+
* @returns {{ x: number, y: number, width: number, height: number, radius: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, lineStyle: number, renderOrder: number, ownerIndex?: string, recordId?: string }[]}
|
|
241
|
+
*/
|
|
242
|
+
static parseSchematicRoundedRectangles(records) {
|
|
243
|
+
return records
|
|
244
|
+
.map((record, index) => {
|
|
245
|
+
const x1 = parseNumericField(record.fields, 'Location.X')
|
|
246
|
+
const y1 = parseNumericField(record.fields, 'Location.Y')
|
|
247
|
+
const x2 = parseNumericField(record.fields, 'Corner.X')
|
|
248
|
+
const y2 = parseNumericField(record.fields, 'Corner.Y')
|
|
249
|
+
|
|
250
|
+
if (x1 === null || y1 === null || x2 === null || y2 === null) {
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const width = Math.abs(x2 - x1)
|
|
255
|
+
const height = Math.abs(y2 - y1)
|
|
256
|
+
|
|
257
|
+
return SchematicPrimitiveParser.#stripUndefined({
|
|
258
|
+
x: Math.min(x1, x2),
|
|
259
|
+
y: Math.min(y1, y2),
|
|
260
|
+
width,
|
|
261
|
+
height,
|
|
262
|
+
radius: SchematicPrimitiveParser.#resolveCornerRadius(
|
|
263
|
+
record.fields,
|
|
264
|
+
width,
|
|
265
|
+
height
|
|
266
|
+
),
|
|
267
|
+
color: toColor(record.fields.Color, '#a44a1b'),
|
|
268
|
+
fill: toColor(record.fields.AreaColor, '#ffffff'),
|
|
269
|
+
isSolid: parseBoolean(record.fields.IsSolid),
|
|
270
|
+
transparent: parseBoolean(record.fields.Transparent),
|
|
271
|
+
lineWidth:
|
|
272
|
+
parseNumericField(record.fields, 'LineWidth') || 1,
|
|
273
|
+
lineStyle:
|
|
274
|
+
parseNumericField(record.fields, 'LineStyle') || 0,
|
|
275
|
+
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
276
|
+
record.fields,
|
|
277
|
+
index
|
|
278
|
+
),
|
|
279
|
+
ownerIndex:
|
|
280
|
+
getField(record.fields, 'OwnerIndex') || undefined,
|
|
281
|
+
recordId:
|
|
282
|
+
getField(record.fields, 'UniqueID') ||
|
|
283
|
+
getField(record.fields, 'UniqueId') ||
|
|
284
|
+
undefined
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
.filter(Boolean)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Normalizes record-3 IEEE-style schematic symbol primitives.
|
|
292
|
+
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
293
|
+
* @returns {{ x: number, y: number, symbol: number, symbolName: string, size: number, color: string, lineWidth: number, renderOrder: number, ownerIndex?: string, recordId?: string }[]}
|
|
294
|
+
*/
|
|
295
|
+
static parseSchematicIeeeSymbols(records) {
|
|
296
|
+
return records
|
|
297
|
+
.map((record, index) => {
|
|
298
|
+
const x = parseNumericFieldWithFraction(
|
|
299
|
+
record.fields,
|
|
300
|
+
'Location.X'
|
|
301
|
+
)
|
|
302
|
+
const y = parseNumericFieldWithFraction(
|
|
303
|
+
record.fields,
|
|
304
|
+
'Location.Y'
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
if (x === null || y === null) {
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const symbol =
|
|
312
|
+
parseNumericField(record.fields, 'Symbol') ??
|
|
313
|
+
parseNumericField(record.fields, 'SymbolKind') ??
|
|
314
|
+
parseNumericField(record.fields, 'IeeeSymbol') ??
|
|
315
|
+
0
|
|
316
|
+
|
|
317
|
+
return SchematicPrimitiveParser.#stripUndefined({
|
|
318
|
+
x,
|
|
319
|
+
y,
|
|
320
|
+
symbol,
|
|
321
|
+
symbolName:
|
|
322
|
+
SchematicPrimitiveParser.#ieeeSymbolName(symbol),
|
|
323
|
+
size: Math.max(
|
|
324
|
+
parseNumericField(record.fields, 'Size') ||
|
|
325
|
+
parseNumericField(record.fields, 'Radius') ||
|
|
326
|
+
12,
|
|
327
|
+
1
|
|
328
|
+
),
|
|
329
|
+
color: toColor(record.fields.Color, '#000000'),
|
|
330
|
+
lineWidth:
|
|
331
|
+
parseNumericField(record.fields, 'LineWidth') || 1,
|
|
332
|
+
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
333
|
+
record.fields,
|
|
334
|
+
index
|
|
335
|
+
),
|
|
336
|
+
ownerIndex:
|
|
337
|
+
getField(record.fields, 'OwnerIndex') || undefined,
|
|
338
|
+
recordId:
|
|
339
|
+
getField(record.fields, 'UniqueID') ||
|
|
340
|
+
getField(record.fields, 'UniqueId') ||
|
|
341
|
+
undefined
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
.filter(Boolean)
|
|
345
|
+
}
|
|
346
|
+
|
|
120
347
|
/**
|
|
121
348
|
* Normalizes record-14 body primitives into drawable rectangles.
|
|
122
349
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
@@ -429,6 +656,80 @@ export class SchematicPrimitiveParser {
|
|
|
429
656
|
return fallbackOrder
|
|
430
657
|
}
|
|
431
658
|
|
|
659
|
+
/**
|
|
660
|
+
* Builds cubic Bezier spans from Altium's `4 + 3n` point sequence.
|
|
661
|
+
* @param {{ x: number, y: number }[]} points Source control points.
|
|
662
|
+
* @returns {{ start: { x: number, y: number }, control1: { x: number, y: number }, control2: { x: number, y: number }, end: { x: number, y: number } }[]}
|
|
663
|
+
*/
|
|
664
|
+
static #buildBezierSegments(points) {
|
|
665
|
+
if (!Array.isArray(points) || points.length < 4) {
|
|
666
|
+
return []
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const segments = []
|
|
670
|
+
for (let index = 0; index + 3 < points.length; index += 3) {
|
|
671
|
+
segments.push({
|
|
672
|
+
start: points[index],
|
|
673
|
+
control1: points[index + 1],
|
|
674
|
+
control2: points[index + 2],
|
|
675
|
+
end: points[index + 3]
|
|
676
|
+
})
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return segments
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Removes undefined fields from a primitive object.
|
|
684
|
+
* @param {object} value Source object.
|
|
685
|
+
* @returns {object}
|
|
686
|
+
*/
|
|
687
|
+
static #stripUndefined(value) {
|
|
688
|
+
return Object.fromEntries(
|
|
689
|
+
Object.entries(value || {}).filter(
|
|
690
|
+
([, entryValue]) => entryValue !== undefined
|
|
691
|
+
)
|
|
692
|
+
)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Resolves a stable rounded-rectangle corner radius.
|
|
697
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
698
|
+
* @param {number} width Width in schematic units.
|
|
699
|
+
* @param {number} height Height in schematic units.
|
|
700
|
+
* @returns {number}
|
|
701
|
+
*/
|
|
702
|
+
static #resolveCornerRadius(fields, width, height) {
|
|
703
|
+
const radius =
|
|
704
|
+
parseNumericField(fields, 'CornerRadius') ??
|
|
705
|
+
parseNumericField(fields, 'Radius') ??
|
|
706
|
+
parseNumericField(fields, 'CornerRadius.X') ??
|
|
707
|
+
Math.min(width, height) / 5
|
|
708
|
+
|
|
709
|
+
return Math.max(Math.min(radius, width / 2, height / 2), 0)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Maps native IEEE symbol codes to stable display names.
|
|
714
|
+
* @param {number} symbol Native symbol code.
|
|
715
|
+
* @returns {string}
|
|
716
|
+
*/
|
|
717
|
+
static #ieeeSymbolName(symbol) {
|
|
718
|
+
return (
|
|
719
|
+
{
|
|
720
|
+
1: 'and',
|
|
721
|
+
2: 'or',
|
|
722
|
+
3: 'xor',
|
|
723
|
+
4: 'inverter',
|
|
724
|
+
5: 'buffer',
|
|
725
|
+
6: 'clock',
|
|
726
|
+
7: 'schmitt',
|
|
727
|
+
8: 'open-collector',
|
|
728
|
+
9: 'open-emitter'
|
|
729
|
+
}[Number(symbol)] || 'unknown'
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
|
|
432
733
|
/**
|
|
433
734
|
* Builds one stable geometry-key queue for rectangle source metadata.
|
|
434
735
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|