altium-toolkit 1.1.3 → 1.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/api.md +37 -0
- package/docs/model-format.md +18 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
- package/docs/testing.md +5 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +5 -2
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AsciiRecordParser.mjs +43 -11
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicPinParser.mjs +87 -20
- package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
- package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +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 +37 -11
- package/src/ui/SchematicSvgRenderer.mjs +944 -51
|
@@ -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
|
+
}
|
|
@@ -146,17 +146,11 @@ export class AsciiRecordParser {
|
|
|
146
146
|
const rawKey = AsciiRecordParser.#trimAscii(
|
|
147
147
|
segment.slice(0, separatorIndex)
|
|
148
148
|
)
|
|
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
149
|
const isUtf8Field = rawKey.startsWith('%UTF8%')
|
|
150
|
+
const value = AsciiRecordParser.#decodeFieldValue(
|
|
151
|
+
AsciiRecordParser.#trimAscii(segment.slice(separatorIndex + 1)),
|
|
152
|
+
isUtf8Field ? 'utf-8' : ''
|
|
153
|
+
)
|
|
160
154
|
const key = rawKey.replace(/^%UTF8%/, '')
|
|
161
155
|
if (!key) continue
|
|
162
156
|
|
|
@@ -228,13 +222,51 @@ export class AsciiRecordParser {
|
|
|
228
222
|
return normalizedKeyIndex
|
|
229
223
|
}
|
|
230
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Decodes one pipe-delimited field value from the byte-preserving run
|
|
227
|
+
* string. Plain ASCII is already decoded by construction and can avoid
|
|
228
|
+
* byte-array allocation plus TextDecoder fallback probing.
|
|
229
|
+
* @param {string} value Byte-preserving field value.
|
|
230
|
+
* @param {string} preferredEncoding Optional preferred decoder encoding.
|
|
231
|
+
* @returns {string}
|
|
232
|
+
*/
|
|
233
|
+
static #decodeFieldValue(value, preferredEncoding) {
|
|
234
|
+
if (!AsciiRecordParser.#hasExtendedByte(value)) {
|
|
235
|
+
return value
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return PrintableTextDecoder.decodeBytes(
|
|
239
|
+
AsciiRecordParser.#binaryStringToBytes(value),
|
|
240
|
+
{ encoding: preferredEncoding || undefined }
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
231
244
|
/**
|
|
232
245
|
* Converts one binary string into bytes without altering byte values.
|
|
233
246
|
* @param {string} value
|
|
234
247
|
* @returns {Uint8Array}
|
|
235
248
|
*/
|
|
236
249
|
static #binaryStringToBytes(value) {
|
|
237
|
-
|
|
250
|
+
const bytes = new Uint8Array(value.length)
|
|
251
|
+
|
|
252
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
253
|
+
bytes[index] = value.charCodeAt(index) & 0xff
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return bytes
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Returns true when the byte-preserving string contains non-ASCII bytes.
|
|
261
|
+
* @param {string} value Field value.
|
|
262
|
+
* @returns {boolean}
|
|
263
|
+
*/
|
|
264
|
+
static #hasExtendedByte(value) {
|
|
265
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
266
|
+
if (value.charCodeAt(index) > 0x7f) return true
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return false
|
|
238
270
|
}
|
|
239
271
|
|
|
240
272
|
/**
|
|
@@ -12,49 +12,49 @@ const KIND_BY_VALUE = {
|
|
|
12
12
|
displayName: 'Standard',
|
|
13
13
|
includeInBom: true,
|
|
14
14
|
includeInNetlist: true,
|
|
15
|
-
|
|
15
|
+
includeInPlacement: true
|
|
16
16
|
},
|
|
17
17
|
1: {
|
|
18
18
|
name: 'mechanical',
|
|
19
19
|
displayName: 'Mechanical',
|
|
20
20
|
includeInBom: true,
|
|
21
21
|
includeInNetlist: true,
|
|
22
|
-
|
|
22
|
+
includeInPlacement: true
|
|
23
23
|
},
|
|
24
24
|
2: {
|
|
25
25
|
name: 'graphical',
|
|
26
26
|
displayName: 'Graphical',
|
|
27
27
|
includeInBom: false,
|
|
28
28
|
includeInNetlist: false,
|
|
29
|
-
|
|
29
|
+
includeInPlacement: false
|
|
30
30
|
},
|
|
31
31
|
3: {
|
|
32
32
|
name: 'net-tie-bom',
|
|
33
33
|
displayName: 'Net Tie BOM',
|
|
34
34
|
includeInBom: true,
|
|
35
35
|
includeInNetlist: true,
|
|
36
|
-
|
|
36
|
+
includeInPlacement: true
|
|
37
37
|
},
|
|
38
38
|
4: {
|
|
39
39
|
name: 'net-tie-no-bom',
|
|
40
40
|
displayName: 'Net Tie No BOM',
|
|
41
41
|
includeInBom: false,
|
|
42
42
|
includeInNetlist: true,
|
|
43
|
-
|
|
43
|
+
includeInPlacement: true
|
|
44
44
|
},
|
|
45
45
|
5: {
|
|
46
46
|
name: 'standard-no-bom',
|
|
47
47
|
displayName: 'Standard No BOM',
|
|
48
48
|
includeInBom: false,
|
|
49
49
|
includeInNetlist: true,
|
|
50
|
-
|
|
50
|
+
includeInPlacement: true
|
|
51
51
|
},
|
|
52
52
|
6: {
|
|
53
53
|
name: 'jumper',
|
|
54
54
|
displayName: 'Jumper',
|
|
55
55
|
includeInBom: true,
|
|
56
56
|
includeInNetlist: true,
|
|
57
|
-
|
|
57
|
+
includeInPlacement: true
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -65,7 +65,7 @@ export class PcbComponentKindPolicy {
|
|
|
65
65
|
/**
|
|
66
66
|
* Parses native versioned component-kind fields.
|
|
67
67
|
* @param {Record<string, string | string[]>} fields Native component row.
|
|
68
|
-
* @returns {{ value: number, name: string, displayName: string, includeInBom: boolean, includeInNetlist: boolean,
|
|
68
|
+
* @returns {{ value: number, name: string, displayName: string, includeInBom: boolean, includeInNetlist: boolean, includeInPlacement: boolean } | undefined}
|
|
69
69
|
*/
|
|
70
70
|
static parse(fields) {
|
|
71
71
|
if (!PcbComponentKindPolicy.#hasKindField(fields)) return undefined
|
|
@@ -76,7 +76,7 @@ export class PcbComponentKindPolicy {
|
|
|
76
76
|
displayName: 'Unknown',
|
|
77
77
|
includeInBom: true,
|
|
78
78
|
includeInNetlist: true,
|
|
79
|
-
|
|
79
|
+
includeInPlacement: true
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
return {
|
|
@@ -20,14 +20,16 @@ export class PcbEmbeddedModelExtractor {
|
|
|
20
20
|
* @returns {{ models: { id: string, checksum: number, name: string, format: string, payloadText: string, sourceStream: string, transform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number } }[], componentBodies: { sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null }[] }}
|
|
21
21
|
*/
|
|
22
22
|
static extractFromStreams(streams) {
|
|
23
|
+
const modelStreamPrefix =
|
|
24
|
+
PcbEmbeddedModelExtractor.#resolveModelStreamPrefix(streams)
|
|
23
25
|
const modelMetadataRecords =
|
|
24
26
|
PcbEmbeddedModelExtractor.#parseModelMetadataStream(
|
|
25
|
-
streams.get('
|
|
27
|
+
streams.get(modelStreamPrefix + '/Data')
|
|
26
28
|
)
|
|
27
29
|
const modelMetadataRows = modelMetadataRecords.map((fields, index) => ({
|
|
28
30
|
fields,
|
|
29
31
|
index,
|
|
30
|
-
sourceStream: '
|
|
32
|
+
sourceStream: modelStreamPrefix + '/' + index,
|
|
31
33
|
id: PcbEmbeddedModelExtractor.#getField(fields, 'ID'),
|
|
32
34
|
name: PcbEmbeddedModelExtractor.#getField(fields, 'NAME'),
|
|
33
35
|
checksum: PcbEmbeddedModelExtractor.#normalizeChecksum(
|
|
@@ -70,7 +72,24 @@ export class PcbEmbeddedModelExtractor {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
73
|
-
*
|
|
75
|
+
* Resolves the embedded-model stream folder used by the compound document.
|
|
76
|
+
* @param {Map<string, Uint8Array>} streams
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
static #resolveModelStreamPrefix(streams) {
|
|
80
|
+
if (streams.has('Models/Data')) {
|
|
81
|
+
return 'Models'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (streams.has('Library/Models/Data')) {
|
|
85
|
+
return 'Library/Models'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return 'Models'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parses the length-prefixed model metadata stream.
|
|
74
93
|
* @param {Uint8Array | undefined} bytes
|
|
75
94
|
* @returns {Record<string, string | string[]>[]}
|
|
76
95
|
*/
|
|
@@ -24,6 +24,8 @@ export class PcbOutlineRecovery {
|
|
|
24
24
|
|
|
25
25
|
static #MAX_DIRECT_RENDER_ARC_SWEEP_DEGREES = 120
|
|
26
26
|
|
|
27
|
+
static #MAX_MECHANICAL_FRAME_TO_AUTHORED_AREA_RATIO = 16
|
|
28
|
+
|
|
27
29
|
/**
|
|
28
30
|
* Selects a recoverable board outline from mechanical track layers.
|
|
29
31
|
* @param {{ fallbackOutline: { minX: number, minY: number, widthMil: number, heightMil: number, segments: Array<Record<string, number | string>> }, components: { x: number, y: number }[], tracks: { x1: number, y1: number, x2: number, y2: number, width: number, layerId?: number }[] }} options
|
|
@@ -76,6 +78,20 @@ export class PcbOutlineRecovery {
|
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
if (
|
|
82
|
+
PcbOutlineRecovery.#shouldKeepAuthoredOutline(
|
|
83
|
+
fallbackOutline,
|
|
84
|
+
boundaryLayer.bounds,
|
|
85
|
+
componentBounds
|
|
86
|
+
)
|
|
87
|
+
) {
|
|
88
|
+
return {
|
|
89
|
+
source: 'fallback',
|
|
90
|
+
layerId: null,
|
|
91
|
+
outline: fallbackOutline
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
const recoveredOutline =
|
|
80
96
|
PcbOutlineRecovery.#traceTrackOutline(
|
|
81
97
|
boundaryLayer.tracks,
|
|
@@ -166,6 +182,84 @@ export class PcbOutlineRecovery {
|
|
|
166
182
|
return candidates[0] || null
|
|
167
183
|
}
|
|
168
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Returns true when the mechanical candidate is likely a drawing frame
|
|
187
|
+
* around an otherwise plausible authored board route.
|
|
188
|
+
* @param {{ minX: number, minY: number, widthMil: number, heightMil: number, segments?: Array<Record<string, number | string>> }} authoredOutline Authored board-route outline.
|
|
189
|
+
* @param {{ widthMil: number, heightMil: number }} mechanicalBounds Mechanical track-layer bounds.
|
|
190
|
+
* @param {{ centerX: number, centerY: number }} componentBounds Component placement envelope.
|
|
191
|
+
* @returns {boolean}
|
|
192
|
+
*/
|
|
193
|
+
static #shouldKeepAuthoredOutline(
|
|
194
|
+
authoredOutline,
|
|
195
|
+
mechanicalBounds,
|
|
196
|
+
componentBounds
|
|
197
|
+
) {
|
|
198
|
+
if (
|
|
199
|
+
!PcbOutlineRecovery.#isClosedOutlinePath(
|
|
200
|
+
authoredOutline?.segments || []
|
|
201
|
+
)
|
|
202
|
+
) {
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const authoredArea =
|
|
207
|
+
Number(authoredOutline?.widthMil || 0) *
|
|
208
|
+
Number(authoredOutline?.heightMil || 0)
|
|
209
|
+
const mechanicalArea =
|
|
210
|
+
Number(mechanicalBounds?.widthMil || 0) *
|
|
211
|
+
Number(mechanicalBounds?.heightMil || 0)
|
|
212
|
+
|
|
213
|
+
if (!authoredArea || !mechanicalArea) {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
mechanicalArea / authoredArea <=
|
|
219
|
+
PcbOutlineRecovery.#MAX_MECHANICAL_FRAME_TO_AUTHORED_AREA_RATIO
|
|
220
|
+
) {
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return PcbOutlineRecovery.#outlineEnvelopeContainsPoint(
|
|
225
|
+
authoredOutline,
|
|
226
|
+
componentBounds?.centerX,
|
|
227
|
+
componentBounds?.centerY
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Returns true when one point falls within an outline's bounding envelope.
|
|
233
|
+
* @param {{ minX?: number, minY?: number, widthMil?: number, heightMil?: number }} outline Outline bounds.
|
|
234
|
+
* @param {number} x Point X coordinate.
|
|
235
|
+
* @param {number} y Point Y coordinate.
|
|
236
|
+
* @returns {boolean}
|
|
237
|
+
*/
|
|
238
|
+
static #outlineEnvelopeContainsPoint(outline, x, y) {
|
|
239
|
+
const minX = Number(outline?.minX)
|
|
240
|
+
const minY = Number(outline?.minY)
|
|
241
|
+
const widthMil = Number(outline?.widthMil)
|
|
242
|
+
const heightMil = Number(outline?.heightMil)
|
|
243
|
+
|
|
244
|
+
if (
|
|
245
|
+
!Number.isFinite(minX) ||
|
|
246
|
+
!Number.isFinite(minY) ||
|
|
247
|
+
!Number.isFinite(widthMil) ||
|
|
248
|
+
!Number.isFinite(heightMil) ||
|
|
249
|
+
!Number.isFinite(x) ||
|
|
250
|
+
!Number.isFinite(y)
|
|
251
|
+
) {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
x >= minX &&
|
|
257
|
+
x <= minX + widthMil &&
|
|
258
|
+
y >= minY &&
|
|
259
|
+
y <= minY + heightMil
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
169
263
|
/**
|
|
170
264
|
* Builds one track-bounds envelope.
|
|
171
265
|
* @param {{ x1: number, y1: number, x2: number, y2: number }[]} tracks
|
|
@@ -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 { SchematicNoErcSymbolResolver } from './SchematicNoErcSymbolResolver.mjs'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Helpers for normalized schematic directive primitives.
|
|
@@ -144,6 +145,7 @@ export class SchematicDirectiveParser {
|
|
|
144
145
|
const x = ParserUtils.parseNumericField(record.fields, 'Location.X')
|
|
145
146
|
const y = ParserUtils.parseNumericField(record.fields, 'Location.Y')
|
|
146
147
|
if (x === null || y === null) return null
|
|
148
|
+
const rawSymbol = ParserUtils.getField(record.fields, 'Symbol')
|
|
147
149
|
const symbol = ParserUtils.parseNumericField(record.fields, 'Symbol')
|
|
148
150
|
|
|
149
151
|
return {
|
|
@@ -160,7 +162,9 @@ export class SchematicDirectiveParser {
|
|
|
160
162
|
ParserUtils.parseNumericField(record.fields, 'Orientation') ||
|
|
161
163
|
0,
|
|
162
164
|
symbol,
|
|
163
|
-
symbolName:
|
|
165
|
+
symbolName: SchematicNoErcSymbolResolver.resolveSymbolName(
|
|
166
|
+
rawSymbol || symbol
|
|
167
|
+
)
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
170
|
|
|
@@ -332,22 +336,6 @@ export class SchematicDirectiveParser {
|
|
|
332
336
|
return 'record-' + String(record.recordIndex ?? 0)
|
|
333
337
|
}
|
|
334
338
|
|
|
335
|
-
/**
|
|
336
|
-
* Converts common No ERC symbol ids into public labels.
|
|
337
|
-
* @param {number | null} symbol Symbol id.
|
|
338
|
-
* @returns {string}
|
|
339
|
-
*/
|
|
340
|
-
static #noErcSymbolName(symbol) {
|
|
341
|
-
return (
|
|
342
|
-
{
|
|
343
|
-
0: 'generic',
|
|
344
|
-
1: 'box',
|
|
345
|
-
2: 'cross',
|
|
346
|
-
3: 'triangle'
|
|
347
|
-
}[Number(symbol)] || 'unknown'
|
|
348
|
-
)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
339
|
/**
|
|
352
340
|
* Parses one boolean-ish directive parameter value.
|
|
353
341
|
* @param {unknown} value Raw parameter value.
|
|
@@ -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
|
+
}
|