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
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
import { SchematicNoErcSymbolResolver } from './SchematicNoErcSymbolResolver.mjs'
|
|
6
7
|
import { SchematicPinDesignatorInferer } from './SchematicPinDesignatorInferer.mjs'
|
|
8
|
+
import { SchematicTextRunParser } from './SchematicTextRunParser.mjs'
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Helpers for normalized schematic pins, ports, and crosses.
|
|
@@ -66,11 +68,9 @@ export class SchematicPinParser {
|
|
|
66
68
|
),
|
|
67
69
|
designator: ParserUtils.getField(record.fields, 'Designator'),
|
|
68
70
|
orientation,
|
|
69
|
-
electrical:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
'Electrical'
|
|
73
|
-
) || undefined,
|
|
71
|
+
electrical: SchematicPinParser.#parseSchematicPinElectrical(
|
|
72
|
+
record.fields
|
|
73
|
+
),
|
|
74
74
|
symbolOuter:
|
|
75
75
|
ParserUtils.parseNumericField(
|
|
76
76
|
record.fields,
|
|
@@ -94,6 +94,30 @@ export class SchematicPinParser {
|
|
|
94
94
|
)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Parses Altium's pin electrical type, including its omitted-field default.
|
|
99
|
+
* Formal schematic pin records omit Electrical for input pins and serialize
|
|
100
|
+
* passive pins explicitly as Electrical=4.
|
|
101
|
+
* @param {Record<string, string | string[]>} fields Pin record fields.
|
|
102
|
+
* @returns {number | undefined}
|
|
103
|
+
*/
|
|
104
|
+
static #parseSchematicPinElectrical(fields) {
|
|
105
|
+
const explicitElectrical = ParserUtils.parseNumericField(
|
|
106
|
+
fields,
|
|
107
|
+
'Electrical'
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if (explicitElectrical !== null) {
|
|
111
|
+
return explicitElectrical
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (ParserUtils.parseNumericField(fields, 'FormalType') !== null) {
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
/**
|
|
98
122
|
* Normalizes schematic port records into drawable port boxes.
|
|
99
123
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
@@ -386,24 +410,36 @@ export class SchematicPinParser {
|
|
|
386
410
|
/**
|
|
387
411
|
* Normalizes no-connect crosses from schematic records.
|
|
388
412
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
389
|
-
* @returns {{ x: number, y: number, size: number, color: string }[]}
|
|
413
|
+
* @returns {{ x: number, y: number, size: number, color: string, symbol: number | null, symbolName: string }[]}
|
|
390
414
|
*/
|
|
391
415
|
static parseSchematicCrosses(records) {
|
|
392
416
|
return records
|
|
393
|
-
.map((record) =>
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
417
|
+
.map((record) => {
|
|
418
|
+
const rawSymbol = ParserUtils.getField(record.fields, 'Symbol')
|
|
419
|
+
const symbol = ParserUtils.parseNumericField(
|
|
420
|
+
record.fields,
|
|
421
|
+
'Symbol'
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
x:
|
|
426
|
+
ParserUtils.parseNumericField(
|
|
427
|
+
record.fields,
|
|
428
|
+
'Location.X'
|
|
429
|
+
) || 0,
|
|
430
|
+
y:
|
|
431
|
+
ParserUtils.parseNumericField(
|
|
432
|
+
record.fields,
|
|
433
|
+
'Location.Y'
|
|
434
|
+
) || 0,
|
|
435
|
+
size: 6,
|
|
436
|
+
color: ParserUtils.toColor(record.fields.Color, '#ff0000'),
|
|
437
|
+
symbol,
|
|
438
|
+
symbolName: SchematicNoErcSymbolResolver.resolveSymbolName(
|
|
439
|
+
rawSymbol || symbol
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
})
|
|
407
443
|
.filter((cross) => cross.x || cross.y)
|
|
408
444
|
}
|
|
409
445
|
|
|
@@ -445,7 +481,7 @@ export class SchematicPinParser {
|
|
|
445
481
|
options.recordType
|
|
446
482
|
)
|
|
447
483
|
),
|
|
448
|
-
width: ParserUtils.
|
|
484
|
+
width: ParserUtils.parseSchematicLineWidth(fields),
|
|
449
485
|
lineStyle,
|
|
450
486
|
isBus: options.isBus === true ? true : undefined,
|
|
451
487
|
recordType: options.recordType || undefined,
|
|
@@ -489,7 +525,7 @@ export class SchematicPinParser {
|
|
|
489
525
|
fields.Color,
|
|
490
526
|
SchematicPinParser.#resolveDefaultPolylineColor(fields, '7')
|
|
491
527
|
),
|
|
492
|
-
width: ParserUtils.
|
|
528
|
+
width: ParserUtils.parseSchematicLineWidth(fields),
|
|
493
529
|
lineStyle
|
|
494
530
|
})
|
|
495
531
|
}
|
|
@@ -506,7 +542,7 @@ export class SchematicPinParser {
|
|
|
506
542
|
fields.Color,
|
|
507
543
|
SchematicPinParser.#resolveDefaultPolylineColor(fields, '7')
|
|
508
544
|
),
|
|
509
|
-
width: ParserUtils.
|
|
545
|
+
width: ParserUtils.parseSchematicLineWidth(fields),
|
|
510
546
|
lineStyle
|
|
511
547
|
})
|
|
512
548
|
|
|
@@ -609,8 +645,12 @@ export class SchematicPinParser {
|
|
|
609
645
|
case 2:
|
|
610
646
|
return 'filled-arrow'
|
|
611
647
|
case 3:
|
|
612
|
-
return '
|
|
648
|
+
return 'tail'
|
|
613
649
|
case 4:
|
|
650
|
+
return 'filled-tail'
|
|
651
|
+
case 5:
|
|
652
|
+
return 'circle'
|
|
653
|
+
case 6:
|
|
614
654
|
return 'square'
|
|
615
655
|
default:
|
|
616
656
|
return 'marker-' + shape
|
|
@@ -816,6 +856,13 @@ export class SchematicPinParser {
|
|
|
816
856
|
SchematicPinParser.#isTwoPinNumericEndpointGroup(normalizedPins)
|
|
817
857
|
) {
|
|
818
858
|
labelMode = 'number-only'
|
|
859
|
+
} else if (
|
|
860
|
+
SchematicPinParser.#isCompactSinglePinMarkerGroup(
|
|
861
|
+
normalizedPins,
|
|
862
|
+
names
|
|
863
|
+
)
|
|
864
|
+
) {
|
|
865
|
+
labelMode = 'hidden'
|
|
819
866
|
} else if (allPassive && normalizedPins.length <= 2) {
|
|
820
867
|
labelMode = SchematicPinParser.#isCanonicalPassiveTwoPinGroup(
|
|
821
868
|
normalizedPins
|
|
@@ -875,6 +922,31 @@ export class SchematicPinParser {
|
|
|
875
922
|
}))
|
|
876
923
|
}
|
|
877
924
|
|
|
925
|
+
/**
|
|
926
|
+
* Returns true when a single owner pin belongs to compact marker artwork
|
|
927
|
+
* rather than to a visibly numbered electrical contact.
|
|
928
|
+
* @param {{ designator: string, name: string, length: number, electrical?: number }[]} pins
|
|
929
|
+
* @param {string[]} names
|
|
930
|
+
* @returns {boolean}
|
|
931
|
+
*/
|
|
932
|
+
static #isCompactSinglePinMarkerGroup(pins, names) {
|
|
933
|
+
if (pins.length !== 1 || names.length !== 0) {
|
|
934
|
+
return false
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const pin = pins[0]
|
|
938
|
+
const designator = String(pin.designator || '').trim()
|
|
939
|
+
const length = Math.abs(Number(pin.length || 0))
|
|
940
|
+
const electrical = Number(pin.electrical)
|
|
941
|
+
|
|
942
|
+
return (
|
|
943
|
+
/^\d+$/.test(designator) &&
|
|
944
|
+
length > 0 &&
|
|
945
|
+
length <= 15 &&
|
|
946
|
+
(!Number.isFinite(electrical) || electrical === 4)
|
|
947
|
+
)
|
|
948
|
+
}
|
|
949
|
+
|
|
878
950
|
/**
|
|
879
951
|
* Returns true when a compact owner-drawn FET body uses semantic terminal
|
|
880
952
|
* names internally but still exposes external numeric contact labels.
|
|
@@ -1142,49 +1214,11 @@ export class SchematicPinParser {
|
|
|
1142
1214
|
* @returns {{ name: string, nameSegments?: { text: string, overline: boolean }[] }}
|
|
1143
1215
|
*/
|
|
1144
1216
|
static #parseSchematicPinName(name) {
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
for (const character of String(name || '').trim()) {
|
|
1148
|
-
if (character === '\\') {
|
|
1149
|
-
const previousCharacter = characters.at(-1)
|
|
1150
|
-
if (previousCharacter) {
|
|
1151
|
-
previousCharacter.overline = true
|
|
1152
|
-
}
|
|
1153
|
-
continue
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
characters.push({
|
|
1157
|
-
text: character,
|
|
1158
|
-
overline: false
|
|
1159
|
-
})
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
const normalizedName = characters
|
|
1163
|
-
.map((character) => character.text)
|
|
1164
|
-
.join('')
|
|
1165
|
-
const nameSegments = []
|
|
1166
|
-
|
|
1167
|
-
for (const character of characters) {
|
|
1168
|
-
const previousSegment = nameSegments.at(-1)
|
|
1169
|
-
if (
|
|
1170
|
-
previousSegment &&
|
|
1171
|
-
previousSegment.overline === character.overline
|
|
1172
|
-
) {
|
|
1173
|
-
previousSegment.text += character.text
|
|
1174
|
-
continue
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
nameSegments.push({
|
|
1178
|
-
text: character.text,
|
|
1179
|
-
overline: character.overline
|
|
1180
|
-
})
|
|
1181
|
-
}
|
|
1217
|
+
const parsed = SchematicTextRunParser.parseOptionalOverlineRuns(name)
|
|
1182
1218
|
|
|
1183
1219
|
return {
|
|
1184
|
-
name:
|
|
1185
|
-
nameSegments:
|
|
1186
|
-
? nameSegments
|
|
1187
|
-
: undefined
|
|
1220
|
+
name: parsed.text,
|
|
1221
|
+
nameSegments: parsed.segments
|
|
1188
1222
|
}
|
|
1189
1223
|
}
|
|
1190
1224
|
|
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
parseBoolean,
|
|
10
10
|
parseNumericField,
|
|
11
11
|
parseNumericFieldWithFraction,
|
|
12
|
+
parseSchematicLineWidth,
|
|
12
13
|
toColor
|
|
13
14
|
} = ParserUtils
|
|
14
15
|
|
|
@@ -98,6 +99,18 @@ export class SchematicPrimitiveParser {
|
|
|
98
99
|
)
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Returns true when one point-listed record carries only an axis-aligned
|
|
104
|
+
* rectangular point list without authored `Location` and `Corner` fields.
|
|
105
|
+
* @param {Record<string, string | string[]>} fields
|
|
106
|
+
* @returns {boolean}
|
|
107
|
+
*/
|
|
108
|
+
static isPointListedRectangleRecord(fields) {
|
|
109
|
+
return Boolean(
|
|
110
|
+
SchematicPrimitiveParser.#pointListedRectangleBounds(fields)
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
/**
|
|
102
115
|
* Normalizes record-7 polygon primitives into fill-capable polygons.
|
|
103
116
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
@@ -122,8 +135,7 @@ export class SchematicPrimitiveParser {
|
|
|
122
135
|
fill: toColor(record.fields.AreaColor, color),
|
|
123
136
|
isSolid: parseBoolean(record.fields.IsSolid),
|
|
124
137
|
transparent: parseBoolean(record.fields.Transparent),
|
|
125
|
-
lineWidth:
|
|
126
|
-
parseNumericField(record.fields, 'LineWidth') || 1,
|
|
138
|
+
lineWidth: parseSchematicLineWidth(record.fields),
|
|
127
139
|
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
128
140
|
record.fields,
|
|
129
141
|
index
|
|
@@ -157,7 +169,7 @@ export class SchematicPrimitiveParser {
|
|
|
157
169
|
points,
|
|
158
170
|
segments,
|
|
159
171
|
color: toColor(record.fields.Color, '#000000'),
|
|
160
|
-
width:
|
|
172
|
+
width: parseSchematicLineWidth(record.fields),
|
|
161
173
|
lineStyle:
|
|
162
174
|
parseNumericField(record.fields, 'LineStyle') || 0,
|
|
163
175
|
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
@@ -221,8 +233,7 @@ export class SchematicPrimitiveParser {
|
|
|
221
233
|
fill: toColor(record.fields.AreaColor, '#ffffff'),
|
|
222
234
|
isSolid: parseBoolean(record.fields.IsSolid),
|
|
223
235
|
transparent: parseBoolean(record.fields.Transparent),
|
|
224
|
-
lineWidth:
|
|
225
|
-
parseNumericField(record.fields, 'LineWidth') || 1,
|
|
236
|
+
lineWidth: parseSchematicLineWidth(record.fields),
|
|
226
237
|
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
227
238
|
record.fields,
|
|
228
239
|
index
|
|
@@ -268,8 +279,7 @@ export class SchematicPrimitiveParser {
|
|
|
268
279
|
fill: toColor(record.fields.AreaColor, '#ffffff'),
|
|
269
280
|
isSolid: parseBoolean(record.fields.IsSolid),
|
|
270
281
|
transparent: parseBoolean(record.fields.Transparent),
|
|
271
|
-
lineWidth:
|
|
272
|
-
parseNumericField(record.fields, 'LineWidth') || 1,
|
|
282
|
+
lineWidth: parseSchematicLineWidth(record.fields),
|
|
273
283
|
lineStyle:
|
|
274
284
|
parseNumericField(record.fields, 'LineStyle') || 0,
|
|
275
285
|
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
@@ -327,8 +337,7 @@ export class SchematicPrimitiveParser {
|
|
|
327
337
|
1
|
|
328
338
|
),
|
|
329
339
|
color: toColor(record.fields.Color, '#000000'),
|
|
330
|
-
lineWidth:
|
|
331
|
-
parseNumericField(record.fields, 'LineWidth') || 1,
|
|
340
|
+
lineWidth: parseSchematicLineWidth(record.fields),
|
|
332
341
|
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
333
342
|
record.fields,
|
|
334
343
|
index
|
|
@@ -352,16 +361,32 @@ export class SchematicPrimitiveParser {
|
|
|
352
361
|
static parseSchematicRectangles(records) {
|
|
353
362
|
return records
|
|
354
363
|
.map((record, index) => {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
364
|
+
const pointListedBounds =
|
|
365
|
+
SchematicPrimitiveParser.#pointListedRectangleBounds(
|
|
366
|
+
record.fields
|
|
367
|
+
)
|
|
368
|
+
const x1 =
|
|
369
|
+
parseNumericField(record.fields, 'Location.X') ??
|
|
370
|
+
pointListedBounds?.minX ??
|
|
371
|
+
null
|
|
372
|
+
const y1 =
|
|
373
|
+
parseNumericField(record.fields, 'Location.Y') ??
|
|
374
|
+
pointListedBounds?.minY ??
|
|
375
|
+
null
|
|
376
|
+
const x2 =
|
|
377
|
+
parseNumericField(record.fields, 'Corner.X') ??
|
|
378
|
+
pointListedBounds?.maxX ??
|
|
379
|
+
null
|
|
380
|
+
const y2 =
|
|
381
|
+
parseNumericField(record.fields, 'Corner.Y') ??
|
|
382
|
+
pointListedBounds?.maxY ??
|
|
383
|
+
null
|
|
359
384
|
const isRectangleRecord =
|
|
360
385
|
SchematicPrimitiveParser.isRectangleRecord(record.fields)
|
|
361
386
|
const isListedRectangle =
|
|
362
387
|
SchematicPrimitiveParser.isListedRectangleRecord(
|
|
363
388
|
record.fields
|
|
364
|
-
)
|
|
389
|
+
) || Boolean(pointListedBounds)
|
|
365
390
|
const usesFrameFallback =
|
|
366
391
|
SchematicPrimitiveParser.#shouldUseFrameFallback(
|
|
367
392
|
record.fields,
|
|
@@ -400,11 +425,16 @@ export class SchematicPrimitiveParser {
|
|
|
400
425
|
transparent: usesFrameFallback
|
|
401
426
|
? false
|
|
402
427
|
: parseBoolean(record.fields.Transparent),
|
|
403
|
-
lineWidth:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
428
|
+
lineWidth: parseSchematicLineWidth(record.fields),
|
|
429
|
+
lineStyle:
|
|
430
|
+
usesFrameFallback ||
|
|
431
|
+
SchematicPrimitiveParser.#shouldDefaultBlanketDash(
|
|
432
|
+
record.fields,
|
|
433
|
+
pointListedBounds
|
|
434
|
+
)
|
|
435
|
+
? 1
|
|
436
|
+
: parseNumericField(record.fields, 'LineStyle') ||
|
|
437
|
+
0,
|
|
408
438
|
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
409
439
|
record.fields,
|
|
410
440
|
index
|
|
@@ -570,7 +600,7 @@ export class SchematicPrimitiveParser {
|
|
|
570
600
|
startAngle: startAngle === null ? 0 : startAngle,
|
|
571
601
|
endAngle: endAngle === null ? 360 : endAngle,
|
|
572
602
|
color: toColor(record.fields.Color, '#a44a1b'),
|
|
573
|
-
width:
|
|
603
|
+
width: parseSchematicLineWidth(record.fields),
|
|
574
604
|
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
575
605
|
record.fields,
|
|
576
606
|
index
|
|
@@ -627,8 +657,7 @@ export class SchematicPrimitiveParser {
|
|
|
627
657
|
fill: toColor(record.fields.AreaColor, '#ffffff'),
|
|
628
658
|
isSolid: parseBoolean(record.fields.IsSolid),
|
|
629
659
|
transparent: parseBoolean(record.fields.Transparent),
|
|
630
|
-
lineWidth:
|
|
631
|
-
parseNumericField(record.fields, 'LineWidth') || 1,
|
|
660
|
+
lineWidth: parseSchematicLineWidth(record.fields),
|
|
632
661
|
renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
|
|
633
662
|
record.fields,
|
|
634
663
|
index
|
|
@@ -989,6 +1018,80 @@ export class SchematicPrimitiveParser {
|
|
|
989
1018
|
)
|
|
990
1019
|
}
|
|
991
1020
|
|
|
1021
|
+
/**
|
|
1022
|
+
* Returns true when a point-listed blanket should use Altium's dashed frame
|
|
1023
|
+
* style even when the printable record omits `LineStyle`.
|
|
1024
|
+
* @param {Record<string, string | string[]>} fields Record fields.
|
|
1025
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number } | null} pointListedBounds Point-listed bounds.
|
|
1026
|
+
* @returns {boolean}
|
|
1027
|
+
*/
|
|
1028
|
+
static #shouldDefaultBlanketDash(fields, pointListedBounds) {
|
|
1029
|
+
return (
|
|
1030
|
+
Boolean(pointListedBounds) &&
|
|
1031
|
+
getField(fields, 'RECORD') === '225' &&
|
|
1032
|
+
getField(fields, 'LineStyle') === ''
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Resolves rectangle bounds from a point-listed axis-aligned frame.
|
|
1038
|
+
* @param {Record<string, string | string[]>} fields Record fields.
|
|
1039
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
1040
|
+
*/
|
|
1041
|
+
static #pointListedRectangleBounds(fields) {
|
|
1042
|
+
const points = SchematicPrimitiveParser.#normalizeClosedPointList(
|
|
1043
|
+
SchematicPrimitiveParser.#collectPolygonPoints(fields)
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
if (points.length !== 4) {
|
|
1047
|
+
return null
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const xs = [...new Set(points.map((point) => point.x))]
|
|
1051
|
+
const ys = [...new Set(points.map((point) => point.y))]
|
|
1052
|
+
|
|
1053
|
+
if (xs.length !== 2 || ys.length !== 2) {
|
|
1054
|
+
return null
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const minX = Math.min(...xs)
|
|
1058
|
+
const maxX = Math.max(...xs)
|
|
1059
|
+
const minY = Math.min(...ys)
|
|
1060
|
+
const maxY = Math.max(...ys)
|
|
1061
|
+
const corners = new Set([
|
|
1062
|
+
minX + ':' + minY,
|
|
1063
|
+
minX + ':' + maxY,
|
|
1064
|
+
maxX + ':' + minY,
|
|
1065
|
+
maxX + ':' + maxY
|
|
1066
|
+
])
|
|
1067
|
+
|
|
1068
|
+
if (!points.every((point) => corners.has(point.x + ':' + point.y))) {
|
|
1069
|
+
return null
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
return { minX, minY, maxX, maxY }
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Removes an optional repeated closing point from one source point list.
|
|
1077
|
+
* @param {{ x: number, y: number }[]} points Source points.
|
|
1078
|
+
* @returns {{ x: number, y: number }[]}
|
|
1079
|
+
*/
|
|
1080
|
+
static #normalizeClosedPointList(points) {
|
|
1081
|
+
if (points.length < 2) {
|
|
1082
|
+
return points
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const first = points[0]
|
|
1086
|
+
const last = points.at(-1)
|
|
1087
|
+
|
|
1088
|
+
if (first.x === last.x && first.y === last.y) {
|
|
1089
|
+
return points.slice(0, -1)
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return points
|
|
1093
|
+
}
|
|
1094
|
+
|
|
992
1095
|
/**
|
|
993
1096
|
* Collects one record-7 polygon point list in source order.
|
|
994
1097
|
* @param {Record<string, string | string[]>} fields
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
import { SchematicFieldCoverageReportBuilder } from './SchematicFieldCoverageReportBuilder.mjs'
|
|
6
7
|
|
|
7
8
|
const { parseBoolean, parseNumericField, toColor } = ParserUtils
|
|
8
9
|
|
|
@@ -55,6 +56,7 @@ export class SchematicQaReportBuilder {
|
|
|
55
56
|
geometryFallbacks: SchematicQaReportBuilder.#geometryFallbacks(
|
|
56
57
|
input?.texts
|
|
57
58
|
),
|
|
59
|
+
fieldCoverage: SchematicFieldCoverageReportBuilder.build(records),
|
|
58
60
|
findings
|
|
59
61
|
}
|
|
60
62
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { AsciiRecordParser } from './AsciiRecordParser.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parses schematic stream records from native framed streams or printable runs.
|
|
9
|
+
*/
|
|
10
|
+
export class SchematicRecordStreamParser {
|
|
11
|
+
/**
|
|
12
|
+
* Parses one schematic stream into record objects.
|
|
13
|
+
* @param {ArrayBuffer} arrayBuffer Stream bytes.
|
|
14
|
+
* @returns {{ raw: string, fields: Record<string, string | string[]> }[]}
|
|
15
|
+
*/
|
|
16
|
+
static parse(arrayBuffer) {
|
|
17
|
+
return SchematicRecordStreamParser.parseWithOpaqueRecords(arrayBuffer)
|
|
18
|
+
.records
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parses one schematic stream and preserves unmodelled native frames.
|
|
23
|
+
* @param {ArrayBuffer} arrayBuffer Stream bytes.
|
|
24
|
+
* @param {{ source?: string, sourceStorage?: string, sourceStream?: string }} [options]
|
|
25
|
+
* @returns {{ records: { raw: string, fields: Record<string, string | string[]> }[], opaqueRecords: object[] }}
|
|
26
|
+
*/
|
|
27
|
+
static parseWithOpaqueRecords(arrayBuffer, options = {}) {
|
|
28
|
+
const framedRecords = SchematicRecordStreamParser.#parseFramedRecords(
|
|
29
|
+
arrayBuffer,
|
|
30
|
+
options
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if (framedRecords) {
|
|
34
|
+
return framedRecords
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
records: AsciiRecordParser.parse(arrayBuffer),
|
|
39
|
+
opaqueRecords: []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parses length-prefixed native records when the full stream validates as
|
|
45
|
+
* framed schematic data.
|
|
46
|
+
* @param {ArrayBuffer} arrayBuffer Stream bytes.
|
|
47
|
+
* @param {{ source?: string, sourceStorage?: string, sourceStream?: string }} options Parser metadata.
|
|
48
|
+
* @returns {{ records: { raw: string, fields: Record<string, string | string[]> }[], opaqueRecords: object[] } | null}
|
|
49
|
+
*/
|
|
50
|
+
static #parseFramedRecords(arrayBuffer, options) {
|
|
51
|
+
const bytes = new Uint8Array(arrayBuffer)
|
|
52
|
+
const records = []
|
|
53
|
+
const opaqueRecords = []
|
|
54
|
+
let offset = 0
|
|
55
|
+
let recordIndex = 0
|
|
56
|
+
|
|
57
|
+
while (offset < bytes.byteLength) {
|
|
58
|
+
if (offset + 4 > bytes.byteLength) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const payloadLength = bytes[offset] | (bytes[offset + 1] << 8)
|
|
63
|
+
const separatorByte = bytes[offset + 2]
|
|
64
|
+
const recordType = bytes[offset + 3]
|
|
65
|
+
const payloadStart = offset + 4
|
|
66
|
+
const payloadEnd = payloadStart + payloadLength
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
payloadLength <= 0 ||
|
|
70
|
+
separatorByte !== 0 ||
|
|
71
|
+
payloadEnd > bytes.byteLength
|
|
72
|
+
) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (recordType === 0) {
|
|
77
|
+
if (bytes[payloadEnd - 1] !== 0) {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const raw = SchematicRecordStreamParser.#bytesToBinaryString(
|
|
82
|
+
bytes.subarray(payloadStart, payloadEnd - 1)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if (raw.trim()) {
|
|
86
|
+
records.push(AsciiRecordParser.parseRecord(raw))
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
opaqueRecords.push(
|
|
90
|
+
SchematicRecordStreamParser.#opaqueRecord(
|
|
91
|
+
bytes.subarray(payloadStart, payloadEnd),
|
|
92
|
+
recordType,
|
|
93
|
+
recordIndex,
|
|
94
|
+
options
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
offset = payloadEnd
|
|
100
|
+
recordIndex += 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return records.length || opaqueRecords.length
|
|
104
|
+
? { records, opaqueRecords }
|
|
105
|
+
: null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Builds one opaque framed-record metadata row.
|
|
110
|
+
* @param {Uint8Array} bytes Raw payload bytes.
|
|
111
|
+
* @param {number} frameType Native frame type byte.
|
|
112
|
+
* @param {number} recordIndex Zero-based frame index.
|
|
113
|
+
* @param {{ source?: string, sourceStorage?: string, sourceStream?: string }} options Parser metadata.
|
|
114
|
+
* @returns {object}
|
|
115
|
+
*/
|
|
116
|
+
static #opaqueRecord(bytes, frameType, recordIndex, options) {
|
|
117
|
+
return SchematicRecordStreamParser.#stripUndefined({
|
|
118
|
+
source: options.source || 'schdoc',
|
|
119
|
+
sourceStorage: options.sourceStorage,
|
|
120
|
+
sourceStream: options.sourceStream,
|
|
121
|
+
frameType,
|
|
122
|
+
recordIndex,
|
|
123
|
+
byteLength: bytes.byteLength,
|
|
124
|
+
rawBase64: SchematicRecordStreamParser.#bytesToBase64(bytes)
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Converts bytes into a byte-preserving string.
|
|
130
|
+
* @param {Uint8Array} bytes Source bytes.
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
static #bytesToBinaryString(bytes) {
|
|
134
|
+
let value = ''
|
|
135
|
+
|
|
136
|
+
for (const byte of bytes) {
|
|
137
|
+
value += String.fromCharCode(byte)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return value
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Encodes bytes as base64 without depending on Node globals.
|
|
145
|
+
* @param {Uint8Array} bytes Source bytes.
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
static #bytesToBase64(bytes) {
|
|
149
|
+
const alphabet =
|
|
150
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
151
|
+
let output = ''
|
|
152
|
+
|
|
153
|
+
for (let index = 0; index < bytes.byteLength; index += 3) {
|
|
154
|
+
const first = bytes[index]
|
|
155
|
+
const second = bytes[index + 1]
|
|
156
|
+
const third = bytes[index + 2]
|
|
157
|
+
const hasSecond = index + 1 < bytes.byteLength
|
|
158
|
+
const hasThird = index + 2 < bytes.byteLength
|
|
159
|
+
const value =
|
|
160
|
+
(first << 16) |
|
|
161
|
+
((hasSecond ? second : 0) << 8) |
|
|
162
|
+
(hasThird ? third : 0)
|
|
163
|
+
|
|
164
|
+
output += alphabet[(value >> 18) & 0x3f]
|
|
165
|
+
output += alphabet[(value >> 12) & 0x3f]
|
|
166
|
+
output += hasSecond ? alphabet[(value >> 6) & 0x3f] : '='
|
|
167
|
+
output += hasThird ? alphabet[value & 0x3f] : '='
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return output
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Removes undefined values from an object.
|
|
175
|
+
* @param {object} row Row to strip.
|
|
176
|
+
* @returns {object}
|
|
177
|
+
*/
|
|
178
|
+
static #stripUndefined(row) {
|
|
179
|
+
return Object.fromEntries(
|
|
180
|
+
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|