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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { OleCompoundDocumentWriter } from '../ole/OleCompoundDocumentWriter.mjs'
|
|
6
|
+
import { AltiumLibraryRecordBuilder } from './AltiumLibraryRecordBuilder.mjs'
|
|
7
|
+
import { SourceComponentBundleNormalizer } from './SourceComponentBundleNormalizer.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Exports normalized bundles into a compact OLE-backed footprint library.
|
|
11
|
+
*/
|
|
12
|
+
export class AltiumPcbLibExporter {
|
|
13
|
+
/**
|
|
14
|
+
* Exports one or more component bundles as `.PcbLib` bytes.
|
|
15
|
+
* @param {object[] | object} bundles Component bundles.
|
|
16
|
+
* @returns {Uint8Array}
|
|
17
|
+
*/
|
|
18
|
+
static export(bundles) {
|
|
19
|
+
const normalizedBundles =
|
|
20
|
+
AltiumPcbLibExporter.#normalizeBundles(bundles)
|
|
21
|
+
const streams = new Map()
|
|
22
|
+
const modelRows = AltiumPcbLibExporter.#collectModels(normalizedBundles)
|
|
23
|
+
|
|
24
|
+
streams.set(
|
|
25
|
+
'Library/Data',
|
|
26
|
+
AltiumLibraryRecordBuilder.buildPcbLibraryData(normalizedBundles)
|
|
27
|
+
)
|
|
28
|
+
streams.set(
|
|
29
|
+
'Library/ComponentParamsTOC/Data',
|
|
30
|
+
AltiumLibraryRecordBuilder.buildComponentParamsToc(
|
|
31
|
+
normalizedBundles
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
streams.set(
|
|
35
|
+
'SectionKeys',
|
|
36
|
+
AltiumLibraryRecordBuilder.buildSectionKeys(normalizedBundles)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
for (const bundle of normalizedBundles) {
|
|
40
|
+
const storageName = AltiumLibraryRecordBuilder.sanitizeStorageName(
|
|
41
|
+
bundle.footprint.name
|
|
42
|
+
)
|
|
43
|
+
streams.set(
|
|
44
|
+
storageName + '/Header',
|
|
45
|
+
AltiumLibraryRecordBuilder.createCountHeader(0)
|
|
46
|
+
)
|
|
47
|
+
streams.set(
|
|
48
|
+
storageName + '/Parameters',
|
|
49
|
+
AltiumLibraryRecordBuilder.buildFootprintParameters(bundle)
|
|
50
|
+
)
|
|
51
|
+
streams.set(
|
|
52
|
+
storageName + '/Data',
|
|
53
|
+
AltiumLibraryRecordBuilder.buildFootprintData(bundle)
|
|
54
|
+
)
|
|
55
|
+
streams.set(
|
|
56
|
+
storageName + '/SourceRecord',
|
|
57
|
+
new TextEncoder().encode(
|
|
58
|
+
AltiumLibraryRecordBuilder.buildPcbFootprintRecord(bundle)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (modelRows.length) {
|
|
64
|
+
streams.set(
|
|
65
|
+
'Models/Data',
|
|
66
|
+
AltiumLibraryRecordBuilder.buildModelsData(modelRows)
|
|
67
|
+
)
|
|
68
|
+
modelRows.forEach((row, index) => {
|
|
69
|
+
streams.set('Models/' + index, row.model.bytes)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return OleCompoundDocumentWriter.write({ streams })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Collects model rows with deterministic generated ids.
|
|
78
|
+
* @param {object[]} bundles Normalized bundles.
|
|
79
|
+
* @returns {{ model: object, id: string, checksum: number }[]}
|
|
80
|
+
*/
|
|
81
|
+
static #collectModels(bundles) {
|
|
82
|
+
return bundles.flatMap((bundle, bundleIndex) =>
|
|
83
|
+
bundle.models.map((model, modelIndex) => ({
|
|
84
|
+
model,
|
|
85
|
+
id: 'model-' + bundleIndex + '-' + modelIndex,
|
|
86
|
+
checksum: AltiumLibraryRecordBuilder.checksumBytes(model.bytes)
|
|
87
|
+
}))
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Normalizes one or more bundles.
|
|
93
|
+
* @param {object[] | object} bundles Bundle input.
|
|
94
|
+
* @returns {object[]}
|
|
95
|
+
*/
|
|
96
|
+
static #normalizeBundles(bundles) {
|
|
97
|
+
return (Array.isArray(bundles) ? bundles : [bundles]).map((bundle) =>
|
|
98
|
+
SourceComponentBundleNormalizer.normalize(bundle)
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { OleCompoundDocumentWriter } from '../ole/OleCompoundDocumentWriter.mjs'
|
|
6
|
+
import { AltiumLibraryRecordBuilder } from './AltiumLibraryRecordBuilder.mjs'
|
|
7
|
+
import { SourceComponentBundleNormalizer } from './SourceComponentBundleNormalizer.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Exports normalized bundles into a compact OLE-backed schematic library.
|
|
11
|
+
*/
|
|
12
|
+
export class AltiumSchLibExporter {
|
|
13
|
+
/**
|
|
14
|
+
* Exports one or more component bundles as `.SchLib` bytes.
|
|
15
|
+
* @param {object[] | object} bundles Component bundles.
|
|
16
|
+
* @returns {Uint8Array}
|
|
17
|
+
*/
|
|
18
|
+
static export(bundles) {
|
|
19
|
+
const normalizedBundles =
|
|
20
|
+
AltiumSchLibExporter.#normalizeBundles(bundles)
|
|
21
|
+
const streams = new Map()
|
|
22
|
+
const libraryRecord = normalizedBundles
|
|
23
|
+
.map((bundle) =>
|
|
24
|
+
AltiumLibraryRecordBuilder.buildSchematicComponentRecord(bundle)
|
|
25
|
+
)
|
|
26
|
+
.join('\n')
|
|
27
|
+
|
|
28
|
+
streams.set('Library/Data', new TextEncoder().encode(libraryRecord))
|
|
29
|
+
for (const bundle of normalizedBundles) {
|
|
30
|
+
streams.set(
|
|
31
|
+
'Components/' +
|
|
32
|
+
AltiumLibraryRecordBuilder.sanitizeStorageName(
|
|
33
|
+
bundle.symbol.name
|
|
34
|
+
) +
|
|
35
|
+
'/Data',
|
|
36
|
+
new TextEncoder().encode(
|
|
37
|
+
AltiumLibraryRecordBuilder.buildSchematicComponentRecord(
|
|
38
|
+
bundle
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return OleCompoundDocumentWriter.write({ streams })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes one or more bundles.
|
|
49
|
+
* @param {object[] | object} bundles Bundle input.
|
|
50
|
+
* @returns {object[]}
|
|
51
|
+
*/
|
|
52
|
+
static #normalizeBundles(bundles) {
|
|
53
|
+
return (Array.isArray(bundles) ? bundles : [bundles]).map((bundle) =>
|
|
54
|
+
SourceComponentBundleNormalizer.normalize(bundle)
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Provides deterministic conversions for common Altium length units.
|
|
7
|
+
*/
|
|
8
|
+
export class AltiumUnits {
|
|
9
|
+
static RAW_UNITS_PER_MIL = 10000
|
|
10
|
+
|
|
11
|
+
static MM_PER_MIL = 0.0254
|
|
12
|
+
|
|
13
|
+
static MIL_PER_INCH = 1000
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts millimeters to mils.
|
|
17
|
+
* @param {number} value Length in millimeters.
|
|
18
|
+
* @returns {number}
|
|
19
|
+
*/
|
|
20
|
+
static mmToMil(value) {
|
|
21
|
+
return AltiumUnits.#round(Number(value) / AltiumUnits.MM_PER_MIL)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Converts mils to millimeters.
|
|
26
|
+
* @param {number} value Length in mils.
|
|
27
|
+
* @returns {number}
|
|
28
|
+
*/
|
|
29
|
+
static milToMm(value) {
|
|
30
|
+
return AltiumUnits.#round(Number(value) * AltiumUnits.MM_PER_MIL)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Converts inches to mils.
|
|
35
|
+
* @param {number} value Length in inches.
|
|
36
|
+
* @returns {number}
|
|
37
|
+
*/
|
|
38
|
+
static inchToMil(value) {
|
|
39
|
+
return AltiumUnits.#round(Number(value) * AltiumUnits.MIL_PER_INCH)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Converts mils to inches.
|
|
44
|
+
* @param {number} value Length in mils.
|
|
45
|
+
* @returns {number}
|
|
46
|
+
*/
|
|
47
|
+
static milToInch(value) {
|
|
48
|
+
return AltiumUnits.#round(Number(value) / AltiumUnits.MIL_PER_INCH)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Converts raw Altium coordinate units to mils.
|
|
53
|
+
* @param {number} value Raw coordinate units.
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
static rawToMil(value) {
|
|
57
|
+
return AltiumUnits.#round(Number(value) / AltiumUnits.RAW_UNITS_PER_MIL)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converts mils to raw Altium coordinate units.
|
|
62
|
+
* @param {number} value Length in mils.
|
|
63
|
+
* @returns {number}
|
|
64
|
+
*/
|
|
65
|
+
static milToRaw(value) {
|
|
66
|
+
return AltiumUnits.#round(Number(value) * AltiumUnits.RAW_UNITS_PER_MIL)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parses one numeric or textual length into a deterministic unit bundle.
|
|
71
|
+
* @param {unknown} value Length value.
|
|
72
|
+
* @param {{ defaultUnit?: 'mil' | 'mm' | 'in' | 'raw' }} [options]
|
|
73
|
+
* @returns {{ value: number, unit: string, mil: number, mm: number, inch: number, raw: number } | null}
|
|
74
|
+
*/
|
|
75
|
+
static parseLength(value, options = {}) {
|
|
76
|
+
const parsed = AltiumUnits.#parseNumericAndUnit(value, options)
|
|
77
|
+
if (!parsed) return null
|
|
78
|
+
|
|
79
|
+
const mil = AltiumUnits.#toMil(parsed.value, parsed.unit)
|
|
80
|
+
if (!Number.isFinite(mil)) return null
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
value: AltiumUnits.#round(parsed.value),
|
|
84
|
+
unit: parsed.unit,
|
|
85
|
+
mil: AltiumUnits.#round(mil),
|
|
86
|
+
mm: AltiumUnits.milToMm(mil),
|
|
87
|
+
inch: AltiumUnits.milToInch(mil),
|
|
88
|
+
raw: AltiumUnits.milToRaw(mil)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Formats a mil value in the requested target unit.
|
|
94
|
+
* @param {number} value Length in mils.
|
|
95
|
+
* @param {'mil' | 'mm' | 'in' | 'raw'} [unit]
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
static formatMil(value, unit = 'mil') {
|
|
99
|
+
const mil = Number(value)
|
|
100
|
+
if (!Number.isFinite(mil)) return ''
|
|
101
|
+
|
|
102
|
+
const normalizedUnit = AltiumUnits.#normalizeUnit(unit)
|
|
103
|
+
if (normalizedUnit === 'mm') {
|
|
104
|
+
return AltiumUnits.#formatNumber(AltiumUnits.milToMm(mil)) + 'mm'
|
|
105
|
+
}
|
|
106
|
+
if (normalizedUnit === 'in') {
|
|
107
|
+
return AltiumUnits.#formatNumber(AltiumUnits.milToInch(mil)) + 'in'
|
|
108
|
+
}
|
|
109
|
+
if (normalizedUnit === 'raw') {
|
|
110
|
+
return AltiumUnits.#formatNumber(AltiumUnits.milToRaw(mil)) + 'raw'
|
|
111
|
+
}
|
|
112
|
+
return AltiumUnits.#formatNumber(mil) + 'mil'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parses an input value into a number and normalized unit token.
|
|
117
|
+
* @param {unknown} value Length input.
|
|
118
|
+
* @param {{ defaultUnit?: string }} options Parse options.
|
|
119
|
+
* @returns {{ value: number, unit: 'mil' | 'mm' | 'in' | 'raw' } | null}
|
|
120
|
+
*/
|
|
121
|
+
static #parseNumericAndUnit(value, options) {
|
|
122
|
+
const defaultUnit = AltiumUnits.#normalizeUnit(
|
|
123
|
+
options?.defaultUnit || 'mil'
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if (typeof value === 'number') {
|
|
127
|
+
return Number.isFinite(value) ? { value, unit: defaultUnit } : null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const text = String(value ?? '').trim()
|
|
131
|
+
const match = text.match(
|
|
132
|
+
/^([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]?\d+)?)\s*([A-Za-z]*)$/u
|
|
133
|
+
)
|
|
134
|
+
if (!match) return null
|
|
135
|
+
|
|
136
|
+
const numericValue = Number(match[1])
|
|
137
|
+
if (!Number.isFinite(numericValue)) return null
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
value: numericValue,
|
|
141
|
+
unit: AltiumUnits.#normalizeUnit(match[2] || defaultUnit)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Converts one normalized length value to mils.
|
|
147
|
+
* @param {number} value Numeric length.
|
|
148
|
+
* @param {'mil' | 'mm' | 'in' | 'raw'} unit Length unit.
|
|
149
|
+
* @returns {number}
|
|
150
|
+
*/
|
|
151
|
+
static #toMil(value, unit) {
|
|
152
|
+
if (unit === 'mm') return AltiumUnits.mmToMil(value)
|
|
153
|
+
if (unit === 'in') return AltiumUnits.inchToMil(value)
|
|
154
|
+
if (unit === 'raw') return AltiumUnits.rawToMil(value)
|
|
155
|
+
return Number(value)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Normalizes one unit label.
|
|
160
|
+
* @param {unknown} unit Unit label.
|
|
161
|
+
* @returns {'mil' | 'mm' | 'in' | 'raw'}
|
|
162
|
+
*/
|
|
163
|
+
static #normalizeUnit(unit) {
|
|
164
|
+
const normalized = String(unit || '')
|
|
165
|
+
.trim()
|
|
166
|
+
.toLowerCase()
|
|
167
|
+
if (
|
|
168
|
+
normalized === 'mm' ||
|
|
169
|
+
normalized === 'millimeter' ||
|
|
170
|
+
normalized === 'millimeters'
|
|
171
|
+
) {
|
|
172
|
+
return 'mm'
|
|
173
|
+
}
|
|
174
|
+
if (
|
|
175
|
+
normalized === 'in' ||
|
|
176
|
+
normalized === 'inch' ||
|
|
177
|
+
normalized === 'inches'
|
|
178
|
+
) {
|
|
179
|
+
return 'in'
|
|
180
|
+
}
|
|
181
|
+
if (normalized === 'raw') {
|
|
182
|
+
return 'raw'
|
|
183
|
+
}
|
|
184
|
+
return 'mil'
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Formats a number without unstable floating-point tails.
|
|
189
|
+
* @param {number} value Numeric value.
|
|
190
|
+
* @returns {string}
|
|
191
|
+
*/
|
|
192
|
+
static #formatNumber(value) {
|
|
193
|
+
return String(AltiumUnits.#round(value))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Rounds a numeric value to stable JSON/report precision.
|
|
198
|
+
* @param {number} value Numeric value.
|
|
199
|
+
* @returns {number}
|
|
200
|
+
*/
|
|
201
|
+
static #round(value) {
|
|
202
|
+
const numeric = Number(value)
|
|
203
|
+
return Number.isFinite(numeric) ? Number(numeric.toFixed(9)) : numeric
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -63,6 +63,15 @@ export class AsciiRecordParser {
|
|
|
63
63
|
return records
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Parses one pipe-delimited record string into a field object.
|
|
68
|
+
* @param {string} raw Raw record text.
|
|
69
|
+
* @returns {{ raw: string, fields: Record<string, string | string[]> }}
|
|
70
|
+
*/
|
|
71
|
+
static parseRecord(raw) {
|
|
72
|
+
return AsciiRecordParser.#parseRecord(raw)
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
/**
|
|
67
76
|
* Returns true when a printable run looks like an Altium record block.
|
|
68
77
|
* @param {string} candidate
|
|
@@ -146,17 +155,11 @@ export class AsciiRecordParser {
|
|
|
146
155
|
const rawKey = AsciiRecordParser.#trimAscii(
|
|
147
156
|
segment.slice(0, separatorIndex)
|
|
148
157
|
)
|
|
149
|
-
const value = PrintableTextDecoder.decodeBytes(
|
|
150
|
-
AsciiRecordParser.#binaryStringToBytes(
|
|
151
|
-
AsciiRecordParser.#trimAscii(
|
|
152
|
-
segment.slice(separatorIndex + 1)
|
|
153
|
-
)
|
|
154
|
-
),
|
|
155
|
-
{
|
|
156
|
-
encoding: rawKey.startsWith('%UTF8%') ? 'utf-8' : undefined
|
|
157
|
-
}
|
|
158
|
-
)
|
|
159
158
|
const isUtf8Field = rawKey.startsWith('%UTF8%')
|
|
159
|
+
const value = AsciiRecordParser.#decodeFieldValue(
|
|
160
|
+
AsciiRecordParser.#trimAscii(segment.slice(separatorIndex + 1)),
|
|
161
|
+
isUtf8Field ? 'utf-8' : ''
|
|
162
|
+
)
|
|
160
163
|
const key = rawKey.replace(/^%UTF8%/, '')
|
|
161
164
|
if (!key) continue
|
|
162
165
|
|
|
@@ -228,13 +231,51 @@ export class AsciiRecordParser {
|
|
|
228
231
|
return normalizedKeyIndex
|
|
229
232
|
}
|
|
230
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Decodes one pipe-delimited field value from the byte-preserving run
|
|
236
|
+
* string. Plain ASCII is already decoded by construction and can avoid
|
|
237
|
+
* byte-array allocation plus TextDecoder fallback probing.
|
|
238
|
+
* @param {string} value Byte-preserving field value.
|
|
239
|
+
* @param {string} preferredEncoding Optional preferred decoder encoding.
|
|
240
|
+
* @returns {string}
|
|
241
|
+
*/
|
|
242
|
+
static #decodeFieldValue(value, preferredEncoding) {
|
|
243
|
+
if (!AsciiRecordParser.#hasExtendedByte(value)) {
|
|
244
|
+
return value
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return PrintableTextDecoder.decodeBytes(
|
|
248
|
+
AsciiRecordParser.#binaryStringToBytes(value),
|
|
249
|
+
{ encoding: preferredEncoding || undefined }
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
231
253
|
/**
|
|
232
254
|
* Converts one binary string into bytes without altering byte values.
|
|
233
255
|
* @param {string} value
|
|
234
256
|
* @returns {Uint8Array}
|
|
235
257
|
*/
|
|
236
258
|
static #binaryStringToBytes(value) {
|
|
237
|
-
|
|
259
|
+
const bytes = new Uint8Array(value.length)
|
|
260
|
+
|
|
261
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
262
|
+
bytes[index] = value.charCodeAt(index) & 0xff
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return bytes
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Returns true when the byte-preserving string contains non-ASCII bytes.
|
|
270
|
+
* @param {string} value Field value.
|
|
271
|
+
* @returns {boolean}
|
|
272
|
+
*/
|
|
273
|
+
static #hasExtendedByte(value) {
|
|
274
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
275
|
+
if (value.charCodeAt(index) > 0x7f) return true
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return false
|
|
238
279
|
}
|
|
239
280
|
|
|
240
281
|
/**
|