altium-toolkit 1.1.3 → 1.1.23
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 +34 -5
- package/docs/api.md +171 -23
- package/docs/model-format.md +192 -20
- package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
- package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
- package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
- package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
- package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
- package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
- package/docs/testing.md +7 -0
- package/examples/README.md +21 -0
- package/examples/cli-utils.mjs +148 -0
- package/examples/corpus-smoke.mjs +523 -0
- package/examples/extract-bom.mjs +47 -0
- package/examples/generate-pnp.mjs +59 -0
- package/examples/inspect-board.mjs +70 -0
- package/examples/inspect-schematic.mjs +406 -0
- package/examples/library-catalog.mjs +115 -0
- package/examples/net-report.mjs +61 -0
- package/examples/validate-library.mjs +59 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +357 -16
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +52 -11
- package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
- package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
- package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
- package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
- package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
- package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
- package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
- package/src/core/altium/NormalizedModelSchema.mjs +3 -31
- package/src/core/altium/ParameterCollection.mjs +431 -0
- package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
- package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
- package/src/core/altium/ParserErrors.mjs +90 -0
- package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
- package/src/core/altium/ParserUtils.mjs +24 -0
- package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
- package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
- package/src/core/altium/PcbLayerGroups.mjs +308 -0
- package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
- package/src/core/altium/PcbLibModelParser.mjs +20 -4
- package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
- package/src/core/altium/PcbModelParser.mjs +223 -4
- package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PcbStreamExtractor.mjs +130 -6
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
- package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
- package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
- package/src/core/altium/SchLibModelParser.mjs +840 -0
- package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
- package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
- package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
- package/src/core/altium/SchematicHarnessParser.mjs +9 -3
- package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +99 -65
- package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
- package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
- package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
- package/src/core/altium/SchematicSheetParser.mjs +8 -2
- package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
- package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
- package/src/core/altium/SchematicTextParser.mjs +28 -12
- package/src/core/altium/SchematicTextRunParser.mjs +81 -0
- package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
- 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/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +43 -1
- package/src/renderers.mjs +1 -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 +185 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNoteRenderer.mjs +5 -1
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
- package/src/ui/SchematicShapeRenderer.mjs +86 -17
- package/src/ui/SchematicSvgRenderer.mjs +980 -58
- package/src/ui/SchematicTypography.mjs +4 -3
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
export class SchematicNetlistBuilder {
|
|
10
10
|
/**
|
|
11
11
|
* Builds normalized nets and connectivity diagnostics.
|
|
12
|
-
* @param {{ lines: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], texts: { x: number, y: number, text: string, recordType?: string }[], pins?: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string }[], ports?: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], crossSheetConnectors?: { key: string, x: number, y: number, name: string, style?: string }[], junctions?: { x: number, y: number, color: string }[], busEntries?: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries?: { x: number, y: number, name: string }[] }} schematic
|
|
13
|
-
* @returns {{ nets: { name: string, segments: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], labels: { x: number, y: number, text: string, recordType?: string }[], powerPorts: { x: number, y: number, text: string, recordType?: string }[], crossSheetConnectors: { key: string, name: string, x: number, y: number, style?: string }[], pins: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string }[], ports: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], junctions: { x: number, y: number, color: string }[], busEntries: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries: { x: number, y: number, name: string }[] }[], diagnostics: { severity: 'warning', message: string }[] }}
|
|
12
|
+
* @param {{ lines: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], texts: { x: number, y: number, text: string, recordType?: string }[], pins?: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string, ownerIndex?: string, componentDesignator?: string }[], ports?: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], crossSheetConnectors?: { key: string, x: number, y: number, name: string, style?: string }[], junctions?: { x: number, y: number, color: string }[], busEntries?: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries?: { x: number, y: number, name: string }[], componentDesignatorsByOwnerIndex?: Map<string, string> | Record<string, string> }} schematic
|
|
13
|
+
* @returns {{ nets: { name: string, autoName?: string, autoNameSource?: string, aliasCandidates?: string[], segments: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], labels: { x: number, y: number, text: string, recordType?: string }[], powerPorts: { x: number, y: number, text: string, recordType?: string }[], crossSheetConnectors: { key: string, name: string, x: number, y: number, style?: string }[], pins: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string, ownerIndex?: string, componentDesignator?: string }[], ports: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], junctions: { x: number, y: number, color: string }[], busEntries: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries: { x: number, y: number, name: string }[] }[], diagnostics: { severity: 'warning', message: string }[] }}
|
|
14
14
|
*/
|
|
15
15
|
static build(schematic) {
|
|
16
16
|
const diagnostics = []
|
|
@@ -43,12 +43,19 @@ export class SchematicNetlistBuilder {
|
|
|
43
43
|
).filter((connector) =>
|
|
44
44
|
SchematicNetlistBuilder.#groupContainsPoint(group, connector)
|
|
45
45
|
)
|
|
46
|
-
const pins = (schematic.pins || [])
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const pins = (schematic.pins || [])
|
|
47
|
+
.filter((pin) =>
|
|
48
|
+
SchematicNetlistBuilder.#groupContainsPoint(
|
|
49
|
+
group,
|
|
50
|
+
SchematicNetlistBuilder.#resolvePinConnectionPoint(pin)
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
.map((pin) =>
|
|
54
|
+
SchematicNetlistBuilder.#annotatePinComponentDesignator(
|
|
55
|
+
pin,
|
|
56
|
+
schematic.componentDesignatorsByOwnerIndex
|
|
57
|
+
)
|
|
50
58
|
)
|
|
51
|
-
)
|
|
52
59
|
const ports = (schematic.ports || []).filter((port) =>
|
|
53
60
|
SchematicNetlistBuilder.#groupContainsPoint(
|
|
54
61
|
group,
|
|
@@ -89,6 +96,11 @@ export class SchematicNetlistBuilder {
|
|
|
89
96
|
]
|
|
90
97
|
const name =
|
|
91
98
|
explicitNames[0] || 'UnknownNet' + String(unknownNetIndex++)
|
|
99
|
+
const implicitNaming =
|
|
100
|
+
SchematicNetlistBuilder.#deriveImplicitNetNaming({
|
|
101
|
+
explicitNames,
|
|
102
|
+
pins
|
|
103
|
+
})
|
|
92
104
|
|
|
93
105
|
if (explicitNames.length > 1) {
|
|
94
106
|
diagnostics.push({
|
|
@@ -102,6 +114,7 @@ export class SchematicNetlistBuilder {
|
|
|
102
114
|
|
|
103
115
|
return {
|
|
104
116
|
name,
|
|
117
|
+
...implicitNaming,
|
|
105
118
|
segments: group,
|
|
106
119
|
labels,
|
|
107
120
|
powerPorts,
|
|
@@ -120,7 +133,257 @@ export class SchematicNetlistBuilder {
|
|
|
120
133
|
}
|
|
121
134
|
})
|
|
122
135
|
|
|
123
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
nets: SchematicNetlistBuilder.#mergeMatchingPowerPortNets(nets),
|
|
138
|
+
diagnostics
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Adds a component designator to a net pin when owner metadata resolves it.
|
|
144
|
+
* @param {object} pin Net pin.
|
|
145
|
+
* @param {Map<string, string> | Record<string, string> | undefined} componentDesignatorsByOwnerIndex Component designators by native owner id.
|
|
146
|
+
* @returns {object}
|
|
147
|
+
*/
|
|
148
|
+
static #annotatePinComponentDesignator(
|
|
149
|
+
pin,
|
|
150
|
+
componentDesignatorsByOwnerIndex
|
|
151
|
+
) {
|
|
152
|
+
const componentDesignator =
|
|
153
|
+
pin?.componentDesignator ||
|
|
154
|
+
SchematicNetlistBuilder.#lookupComponentDesignator(
|
|
155
|
+
componentDesignatorsByOwnerIndex,
|
|
156
|
+
pin?.ownerIndex
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if (!componentDesignator) {
|
|
160
|
+
return pin
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...pin,
|
|
165
|
+
componentDesignator
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Derives non-breaking names for unnamed nets from attached pins.
|
|
171
|
+
* @param {{ explicitNames: string[], pins: object[] }} options Naming inputs.
|
|
172
|
+
* @returns {{ autoName?: string, autoNameSource?: string, aliasCandidates?: string[] }}
|
|
173
|
+
*/
|
|
174
|
+
static #deriveImplicitNetNaming(options) {
|
|
175
|
+
if ((options.explicitNames || []).length) {
|
|
176
|
+
return {}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const candidates = SchematicNetlistBuilder.#pinAliasCandidates(
|
|
180
|
+
options.pins || []
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if (!candidates.length) {
|
|
184
|
+
return {}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
autoName: candidates[0].name,
|
|
189
|
+
autoNameSource: candidates[0].source,
|
|
190
|
+
aliasCandidates: candidates.map((candidate) => candidate.name)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Builds deterministic alias candidates from connected pins.
|
|
196
|
+
* @param {object[]} pins Connected pins.
|
|
197
|
+
* @returns {{ name: string, source: string }[]}
|
|
198
|
+
*/
|
|
199
|
+
static #pinAliasCandidates(pins) {
|
|
200
|
+
const candidates = []
|
|
201
|
+
|
|
202
|
+
for (const pin of pins || []) {
|
|
203
|
+
const componentDesignator = String(
|
|
204
|
+
pin?.componentDesignator || ''
|
|
205
|
+
).trim()
|
|
206
|
+
const ownerIndex = String(pin?.ownerIndex || '').trim()
|
|
207
|
+
const pinDesignator = String(pin?.designator || '').trim()
|
|
208
|
+
const pinName = String(pin?.name || '').trim()
|
|
209
|
+
|
|
210
|
+
if (componentDesignator) {
|
|
211
|
+
SchematicNetlistBuilder.#pushPinAliasCandidate(
|
|
212
|
+
candidates,
|
|
213
|
+
componentDesignator,
|
|
214
|
+
pinDesignator,
|
|
215
|
+
'component-pin'
|
|
216
|
+
)
|
|
217
|
+
SchematicNetlistBuilder.#pushPinAliasCandidate(
|
|
218
|
+
candidates,
|
|
219
|
+
componentDesignator,
|
|
220
|
+
pinName,
|
|
221
|
+
'component-pin-name'
|
|
222
|
+
)
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (ownerIndex) {
|
|
227
|
+
SchematicNetlistBuilder.#pushPinAliasCandidate(
|
|
228
|
+
candidates,
|
|
229
|
+
'owner-' + ownerIndex,
|
|
230
|
+
pinDesignator,
|
|
231
|
+
'owner-pin'
|
|
232
|
+
)
|
|
233
|
+
SchematicNetlistBuilder.#pushPinAliasCandidate(
|
|
234
|
+
candidates,
|
|
235
|
+
'owner-' + ownerIndex,
|
|
236
|
+
pinName,
|
|
237
|
+
'owner-pin-name'
|
|
238
|
+
)
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (pinName) {
|
|
243
|
+
candidates.push({
|
|
244
|
+
name: 'pin.' + pinName,
|
|
245
|
+
source: 'pin-name'
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return SchematicNetlistBuilder.#dedupeAliasCandidates(candidates)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Adds one scoped pin alias candidate.
|
|
255
|
+
* @param {{ name: string, source: string }[]} candidates Alias candidates.
|
|
256
|
+
* @param {string} scope Alias scope.
|
|
257
|
+
* @param {string} pinLabel Pin designator or name.
|
|
258
|
+
* @param {string} source Alias source label.
|
|
259
|
+
* @returns {void}
|
|
260
|
+
*/
|
|
261
|
+
static #pushPinAliasCandidate(candidates, scope, pinLabel, source) {
|
|
262
|
+
const trimmedScope = String(scope || '').trim()
|
|
263
|
+
const trimmedPinLabel = String(pinLabel || '').trim()
|
|
264
|
+
|
|
265
|
+
if (!trimmedScope || !trimmedPinLabel) {
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
candidates.push({
|
|
270
|
+
name: trimmedScope + '.' + trimmedPinLabel,
|
|
271
|
+
source
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Deduplicates alias candidates by visible alias name.
|
|
277
|
+
* @param {{ name: string, source: string }[]} candidates Alias candidates.
|
|
278
|
+
* @returns {{ name: string, source: string }[]}
|
|
279
|
+
*/
|
|
280
|
+
static #dedupeAliasCandidates(candidates) {
|
|
281
|
+
const seen = new Set()
|
|
282
|
+
const uniqueCandidates = []
|
|
283
|
+
|
|
284
|
+
for (const candidate of candidates || []) {
|
|
285
|
+
if (!candidate.name || seen.has(candidate.name)) {
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
seen.add(candidate.name)
|
|
290
|
+
uniqueCandidates.push(candidate)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return uniqueCandidates
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Looks up a component designator by native owner id.
|
|
298
|
+
* @param {Map<string, string> | Record<string, string> | undefined} componentDesignatorsByOwnerIndex Component designators by native owner id.
|
|
299
|
+
* @param {string | undefined} ownerIndex Native owner id.
|
|
300
|
+
* @returns {string}
|
|
301
|
+
*/
|
|
302
|
+
static #lookupComponentDesignator(
|
|
303
|
+
componentDesignatorsByOwnerIndex,
|
|
304
|
+
ownerIndex
|
|
305
|
+
) {
|
|
306
|
+
const key = String(ownerIndex || '').trim()
|
|
307
|
+
if (!key || !componentDesignatorsByOwnerIndex) {
|
|
308
|
+
return ''
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (componentDesignatorsByOwnerIndex instanceof Map) {
|
|
312
|
+
return String(componentDesignatorsByOwnerIndex.get(key) || '')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return String(componentDesignatorsByOwnerIndex[key] || '')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Merges physically separate net groups that share one power-port label.
|
|
320
|
+
* @param {{ name: string, powerPorts: { text?: string }[] }[]} nets Source nets.
|
|
321
|
+
* @returns {object[]}
|
|
322
|
+
*/
|
|
323
|
+
static #mergeMatchingPowerPortNets(nets) {
|
|
324
|
+
const mergedNets = []
|
|
325
|
+
const netsByPowerName = new Map()
|
|
326
|
+
|
|
327
|
+
for (const net of nets) {
|
|
328
|
+
const powerName =
|
|
329
|
+
SchematicNetlistBuilder.#singlePowerPortNetName(net)
|
|
330
|
+
|
|
331
|
+
if (!powerName) {
|
|
332
|
+
mergedNets.push(net)
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const existingNet = netsByPowerName.get(powerName)
|
|
337
|
+
if (!existingNet) {
|
|
338
|
+
netsByPowerName.set(powerName, net)
|
|
339
|
+
mergedNets.push(net)
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
SchematicNetlistBuilder.#appendNet(existingNet, net)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return mergedNets
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Returns one normalized power-port name when every port agrees.
|
|
351
|
+
* @param {{ powerPorts?: { text?: string }[] }} net Source net.
|
|
352
|
+
* @returns {string}
|
|
353
|
+
*/
|
|
354
|
+
static #singlePowerPortNetName(net) {
|
|
355
|
+
const names = [
|
|
356
|
+
...new Set(
|
|
357
|
+
(net.powerPorts || [])
|
|
358
|
+
.map((powerPort) => String(powerPort.text || '').trim())
|
|
359
|
+
.filter(Boolean)
|
|
360
|
+
.map((name) => name.toUpperCase())
|
|
361
|
+
)
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
return names.length === 1 ? names[0] : ''
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Appends one net's observable members to an existing logical net.
|
|
369
|
+
* @param {object} target Existing logical net.
|
|
370
|
+
* @param {object} source Source net to append.
|
|
371
|
+
* @returns {void}
|
|
372
|
+
*/
|
|
373
|
+
static #appendNet(target, source) {
|
|
374
|
+
for (const key of [
|
|
375
|
+
'segments',
|
|
376
|
+
'labels',
|
|
377
|
+
'powerPorts',
|
|
378
|
+
'crossSheetConnectors',
|
|
379
|
+
'pins',
|
|
380
|
+
'ports',
|
|
381
|
+
'junctions',
|
|
382
|
+
'busEntries',
|
|
383
|
+
'sheetEntries'
|
|
384
|
+
]) {
|
|
385
|
+
target[key].push(...(source[key] || []))
|
|
386
|
+
}
|
|
124
387
|
}
|
|
125
388
|
|
|
126
389
|
/**
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves Altium No ERC marker symbol identifiers into stable names.
|
|
7
|
+
*/
|
|
8
|
+
export class SchematicNoErcSymbolResolver {
|
|
9
|
+
/**
|
|
10
|
+
* Converts common No ERC symbol ids and names into public labels.
|
|
11
|
+
* @param {number | string | null | undefined} symbol Symbol id or source name.
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
static resolveSymbolName(symbol) {
|
|
15
|
+
const text = String(symbol ?? '')
|
|
16
|
+
.trim()
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
|
|
19
|
+
if (text) {
|
|
20
|
+
if (/check\s*box/.test(text)) return 'checkbox'
|
|
21
|
+
if (/cross/.test(text)) return 'cross'
|
|
22
|
+
if (/triangle/.test(text)) return 'triangle'
|
|
23
|
+
if (/box/.test(text)) return 'box'
|
|
24
|
+
if (/generic/.test(text)) return 'generic'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
{
|
|
29
|
+
0: 'generic',
|
|
30
|
+
1: 'box',
|
|
31
|
+
2: 'cross',
|
|
32
|
+
3: 'triangle'
|
|
33
|
+
}[Number(symbol)] || 'unknown'
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -15,7 +15,7 @@ export class SchematicOwnershipGraphParser {
|
|
|
15
15
|
/**
|
|
16
16
|
* Parses record parent/child links from raw schematic records.
|
|
17
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> }}
|
|
18
|
+
* @returns {{ schema: string, records: object[], hierarchy: object[], recordsByRecordIndex: Record<string, object>, recordsByIndexInSheet: Record<string, object>, childrenByParentKey: Record<string, object[]>, parentsByChildKey: Record<string, object> }}
|
|
19
19
|
*/
|
|
20
20
|
static parse(records) {
|
|
21
21
|
const entries = (records || []).map((record, fallbackIndex) =>
|
|
@@ -54,6 +54,12 @@ export class SchematicOwnershipGraphParser {
|
|
|
54
54
|
records: entries.map((entry) =>
|
|
55
55
|
SchematicOwnershipGraphParser.#publicRecord(entry)
|
|
56
56
|
),
|
|
57
|
+
hierarchy: SchematicOwnershipGraphParser.#hierarchy(
|
|
58
|
+
entries,
|
|
59
|
+
parentsByChildKey
|
|
60
|
+
),
|
|
61
|
+
recordsByRecordIndex:
|
|
62
|
+
SchematicOwnershipGraphParser.#recordsByRecordIndex(entries),
|
|
57
63
|
recordsByIndexInSheet:
|
|
58
64
|
SchematicOwnershipGraphParser.#recordsByIndexInSheet(entries),
|
|
59
65
|
childrenByParentKey,
|
|
@@ -61,6 +67,63 @@ export class SchematicOwnershipGraphParser {
|
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Builds a nested public hierarchy from resolved parent links.
|
|
72
|
+
* @param {object[]} entries Record sidecar rows.
|
|
73
|
+
* @param {Record<string, { parentKey: string }>} parentsByChildKey Resolved parent links.
|
|
74
|
+
* @returns {object[]}
|
|
75
|
+
*/
|
|
76
|
+
static #hierarchy(entries, parentsByChildKey) {
|
|
77
|
+
const entriesByKey = new Map(entries.map((entry) => [entry.key, entry]))
|
|
78
|
+
const childrenByParentKey = new Map()
|
|
79
|
+
const childKeys = new Set()
|
|
80
|
+
|
|
81
|
+
for (const [childKey, parentLink] of Object.entries(
|
|
82
|
+
parentsByChildKey
|
|
83
|
+
)) {
|
|
84
|
+
const child = entriesByKey.get(childKey)
|
|
85
|
+
const parent = entriesByKey.get(parentLink.parentKey)
|
|
86
|
+
|
|
87
|
+
if (!child || !parent) {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
childKeys.add(childKey)
|
|
92
|
+
if (!childrenByParentKey.has(parent.key)) {
|
|
93
|
+
childrenByParentKey.set(parent.key, [])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
childrenByParentKey.get(parent.key).push(child)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return entries
|
|
100
|
+
.filter((entry) => !childKeys.has(entry.key))
|
|
101
|
+
.map((entry) =>
|
|
102
|
+
SchematicOwnershipGraphParser.#hierarchyNode(
|
|
103
|
+
entry,
|
|
104
|
+
childrenByParentKey
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Builds one nested hierarchy node.
|
|
111
|
+
* @param {object} entry Record sidecar row.
|
|
112
|
+
* @param {Map<string, object[]>} childrenByParentKey Child rows by parent key.
|
|
113
|
+
* @returns {object}
|
|
114
|
+
*/
|
|
115
|
+
static #hierarchyNode(entry, childrenByParentKey) {
|
|
116
|
+
return {
|
|
117
|
+
...SchematicOwnershipGraphParser.#publicRecord(entry),
|
|
118
|
+
children: (childrenByParentKey.get(entry.key) || []).map((child) =>
|
|
119
|
+
SchematicOwnershipGraphParser.#hierarchyNode(
|
|
120
|
+
child,
|
|
121
|
+
childrenByParentKey
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
64
127
|
/**
|
|
65
128
|
* Normalizes one raw schematic record into a sidecar row.
|
|
66
129
|
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} record Source record.
|
|
@@ -83,7 +146,8 @@ export class SchematicOwnershipGraphParser {
|
|
|
83
146
|
getField(record?.fields, 'UniqueID') ||
|
|
84
147
|
getField(record?.fields, 'UniqueId'),
|
|
85
148
|
name: getField(record?.fields, 'Name'),
|
|
86
|
-
text: getField(record?.fields, 'Text')
|
|
149
|
+
text: getField(record?.fields, 'Text'),
|
|
150
|
+
fields: SchematicOwnershipGraphParser.#publicFields(record?.fields)
|
|
87
151
|
}
|
|
88
152
|
}
|
|
89
153
|
|
|
@@ -139,7 +203,8 @@ export class SchematicOwnershipGraphParser {
|
|
|
139
203
|
ownerIndex: entry.ownerIndex,
|
|
140
204
|
uniqueId: entry.uniqueId,
|
|
141
205
|
name: entry.name,
|
|
142
|
-
text: entry.text
|
|
206
|
+
text: entry.text,
|
|
207
|
+
fields: entry.fields
|
|
143
208
|
})
|
|
144
209
|
}
|
|
145
210
|
|
|
@@ -159,6 +224,22 @@ export class SchematicOwnershipGraphParser {
|
|
|
159
224
|
})
|
|
160
225
|
}
|
|
161
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Indexes records by parser record index.
|
|
229
|
+
* @param {object[]} entries Record sidecar rows.
|
|
230
|
+
* @returns {Record<string, object>}
|
|
231
|
+
*/
|
|
232
|
+
static #recordsByRecordIndex(entries) {
|
|
233
|
+
const index = {}
|
|
234
|
+
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
index[String(entry.recordIndex)] =
|
|
237
|
+
SchematicOwnershipGraphParser.#publicRecord(entry)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return index
|
|
241
|
+
}
|
|
242
|
+
|
|
162
243
|
/**
|
|
163
244
|
* Indexes records by native IndexInSheet values.
|
|
164
245
|
* @param {object[]} entries Record sidecar rows.
|
|
@@ -177,6 +258,24 @@ export class SchematicOwnershipGraphParser {
|
|
|
177
258
|
return index
|
|
178
259
|
}
|
|
179
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Copies source fields into a stable plain object for read-only consumers.
|
|
263
|
+
* @param {Record<string, string | string[]> | undefined} fields Source fields.
|
|
264
|
+
* @returns {Record<string, string | string[]>}
|
|
265
|
+
*/
|
|
266
|
+
static #publicFields(fields) {
|
|
267
|
+
if (!fields || typeof fields !== 'object') {
|
|
268
|
+
return {}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return Object.fromEntries(
|
|
272
|
+
Object.entries(fields).map(([key, value]) => [
|
|
273
|
+
key,
|
|
274
|
+
Array.isArray(value) ? [...value] : value
|
|
275
|
+
])
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
180
279
|
/**
|
|
181
280
|
* Drops empty object fields while preserving numeric zero.
|
|
182
281
|
* @param {Record<string, unknown>} value Candidate object.
|