altium-toolkit 0.1.1 → 0.1.16
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 +24 -6
- package/docs/api.md +42 -4
- package/docs/model-format.md +95 -5
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +553 -0
- package/docs/testing.md +7 -2
- package/package.json +6 -2
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumParser.mjs +22 -325
- package/src/core/altium/NormalizedModelSchema.mjs +28 -0
- package/src/core/altium/PcbArcPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbBinaryPrimitiveParser.mjs +43 -370
- package/src/core/altium/PcbBoardRegionSemanticsParser.mjs +477 -0
- package/src/core/altium/PcbComponentAnnotationNormalizer.mjs +290 -0
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +52 -0
- package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +109 -0
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +484 -0
- package/src/core/altium/PcbFillPrimitiveParser.mjs +84 -0
- package/src/core/altium/PcbFontMetricsParser.mjs +308 -0
- package/src/core/altium/PcbGeometryFlipper.mjs +244 -0
- package/src/core/altium/PcbLayerIdCodec.mjs +136 -0
- package/src/core/altium/PcbLibModelParser.mjs +202 -0
- package/src/core/altium/PcbLibStreamExtractor.mjs +968 -0
- package/src/core/altium/PcbModelParser.mjs +618 -66
- package/src/core/altium/PcbOutlineRecovery.mjs +4 -112
- package/src/core/altium/PcbPadPrimitiveParser.mjs +347 -0
- package/src/core/altium/PcbPadShapeCodec.mjs +158 -0
- package/src/core/altium/PcbPadStackParser.mjs +903 -0
- package/src/core/altium/PcbPrimitiveOwnershipIndexParser.mjs +60 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +212 -0
- package/src/core/altium/PcbPrimitiveRecordSlicer.mjs +243 -0
- package/src/core/altium/PcbRawRecordRegistry.mjs +831 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +317 -0
- package/src/core/altium/PcbRuleParser.mjs +587 -0
- package/src/core/altium/PcbStreamExtractor.mjs +127 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +537 -0
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbViaPrimitiveParser.mjs +88 -0
- package/src/core/altium/PcbViaStackParser.mjs +548 -0
- package/src/core/altium/PcbWideStringTableParser.mjs +108 -0
- package/src/core/altium/PrjPcbModelParser.mjs +797 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +355 -0
- package/src/parser.mjs +13 -0
- package/src/renderers.mjs +5 -0
- package/src/styles/altium-renderers.css +11 -6
- package/src/ui/PcbCopperPrimitiveSplitter.mjs +113 -0
- package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +6 -5
- package/src/ui/PcbEmbeddedFontFaceRenderer.mjs +126 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +27 -6
- package/src/ui/PcbRegionPrimitiveRenderer.mjs +243 -0
- package/src/ui/PcbSideResolvedRenderModel.mjs +336 -0
- package/src/ui/PcbSvgRenderer.mjs +101 -109
- package/src/ui/PcbTextPrimitiveRenderer.mjs +252 -0
- package/src/ui/SchematicSheetChromeRenderer.mjs +2 -93
- package/src/ui/SchematicSheetZoneRenderer.mjs +104 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
|
|
7
|
+
const { getField, parseBoolean, parseNumericField } = ParserUtils
|
|
8
|
+
|
|
9
|
+
const SUBSTACK_FIELD_FAMILIES = [
|
|
10
|
+
{
|
|
11
|
+
fieldFamily: 'v9',
|
|
12
|
+
indexPattern: /^V9_SUBSTACK(\d+)_ID$/i,
|
|
13
|
+
fields: {
|
|
14
|
+
id: 'V9_SUBSTACK{index}_ID',
|
|
15
|
+
name: 'V9_SUBSTACK{index}_NAME',
|
|
16
|
+
isFlex: 'V9_SUBSTACK{index}_ISFLEX',
|
|
17
|
+
showTopDielectric: 'V9_SUBSTACK{index}_SHOWTOPDIELECTRIC',
|
|
18
|
+
showBottomDielectric: 'V9_SUBSTACK{index}_SHOWBOTTOMDIELECTRIC',
|
|
19
|
+
serviceStackup: 'V9_SUBSTACK{index}_SERVICE',
|
|
20
|
+
usedByPrimitives: 'V9_SUBSTACK{index}_USEDBYPRIMS',
|
|
21
|
+
rawStackupType: 'V9_SUBSTACK{index}_TYPE'
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
fieldFamily: 'legacy',
|
|
26
|
+
indexPattern: /^SUBSTACK(\d+)_ID$/i,
|
|
27
|
+
fields: {
|
|
28
|
+
id: 'SUBSTACK{index}_ID',
|
|
29
|
+
name: 'SUBSTACK{index}_NAME',
|
|
30
|
+
isFlex: 'SUBSTACK{index}_ISFLEX',
|
|
31
|
+
showTopDielectric: 'SUBSTACK{index}_SHOWTOPDIELECTRIC',
|
|
32
|
+
showBottomDielectric: 'SUBSTACK{index}_SHOWBOTTOMDIELECTRIC',
|
|
33
|
+
serviceStackup: 'SUBSTACK{index}_SERVICE',
|
|
34
|
+
usedByPrimitives: 'SUBSTACK{index}_USEDBYPRIMS',
|
|
35
|
+
rawStackupType: 'SUBSTACK{index}_TYPE'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
fieldFamily: 'v8',
|
|
40
|
+
indexPattern: /^LAYERSUBSTACK_V8_(\d+)ID$/i,
|
|
41
|
+
fields: {
|
|
42
|
+
id: 'LAYERSUBSTACK_V8_{index}ID',
|
|
43
|
+
name: 'LAYERSUBSTACK_V8_{index}NAME',
|
|
44
|
+
isFlex: 'LAYERSUBSTACK_V8_{index}ISFLEX',
|
|
45
|
+
showTopDielectric: 'LAYERSUBSTACK_V8_{index}SHOWTOPDIELECTRIC',
|
|
46
|
+
showBottomDielectric:
|
|
47
|
+
'LAYERSUBSTACK_V8_{index}SHOWBOTTOMDIELECTRIC',
|
|
48
|
+
serviceStackup: 'LAYERSUBSTACK_V8_{index}SERVICE',
|
|
49
|
+
usedByPrimitives: 'LAYERSUBSTACK_V8_{index}USEDBYPRIMS',
|
|
50
|
+
rawStackupType: 'LAYERSUBSTACK_V8_{index}TYPE'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalizes rigid-flex board-region semantics from decoded PCB records.
|
|
57
|
+
*/
|
|
58
|
+
export class PcbBoardRegionSemanticsParser {
|
|
59
|
+
/**
|
|
60
|
+
* Extracts layer-substack metadata from Board6/Data field sets.
|
|
61
|
+
* @param {Record<string, string | string[]>[]} fieldSets
|
|
62
|
+
* @returns {{ index: number, fieldFamily: string, id: string, name: string, isFlex: boolean | null, showTopDielectric: boolean | null, showBottomDielectric: boolean | null, serviceStackup: boolean | null, usedByPrimitives: boolean | null, rawStackupType: string }[]}
|
|
63
|
+
*/
|
|
64
|
+
static parseLayerSubstacks(fieldSets) {
|
|
65
|
+
const fields = PcbBoardRegionSemanticsParser.#mergeFieldSets(fieldSets)
|
|
66
|
+
const substacks = []
|
|
67
|
+
const seenIds = new Set()
|
|
68
|
+
|
|
69
|
+
for (const family of SUBSTACK_FIELD_FAMILIES) {
|
|
70
|
+
const indexes = PcbBoardRegionSemanticsParser.#findFamilyIndexes(
|
|
71
|
+
fields,
|
|
72
|
+
family.indexPattern
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
for (const index of indexes) {
|
|
76
|
+
const substack =
|
|
77
|
+
PcbBoardRegionSemanticsParser.#parseLayerSubstack(
|
|
78
|
+
fields,
|
|
79
|
+
family,
|
|
80
|
+
index
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if (!substack || seenIds.has(substack.id)) {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
seenIds.add(substack.id)
|
|
88
|
+
substacks.push(substack)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return substacks
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Adds board-planning semantics to decoded BoardRegions/Data records.
|
|
97
|
+
* @param {object[]} boardRegions
|
|
98
|
+
* @param {{ index: number, id: string, name: string, isFlex: boolean | null }[]} layerSubstacks
|
|
99
|
+
* @returns {object[]}
|
|
100
|
+
*/
|
|
101
|
+
static enrichBoardRegions(boardRegions, layerSubstacks) {
|
|
102
|
+
const substacksById = new Map(
|
|
103
|
+
(layerSubstacks || [])
|
|
104
|
+
.filter((substack) => substack.id)
|
|
105
|
+
.map((substack) => [substack.id, substack])
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return (boardRegions || []).map((region, regionIndex) =>
|
|
109
|
+
PcbBoardRegionSemanticsParser.#enrichBoardRegion(
|
|
110
|
+
region,
|
|
111
|
+
regionIndex,
|
|
112
|
+
substacksById
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Builds a compact region-to-substack context list.
|
|
119
|
+
* @param {object[]} boardRegions
|
|
120
|
+
* @returns {{ regionIndex: number, name: string, layerStackId: string, substackIndex: number | null, substackName: string, isFlex: boolean | null, locked3d: boolean, bendingLineCount: number }[]}
|
|
121
|
+
*/
|
|
122
|
+
static buildBoardRegionContexts(boardRegions) {
|
|
123
|
+
return (boardRegions || []).map((region, regionIndex) => ({
|
|
124
|
+
regionIndex,
|
|
125
|
+
name: region.name || '',
|
|
126
|
+
layerStackId: region.layerStackId || '',
|
|
127
|
+
substackIndex:
|
|
128
|
+
region.substackIndex === undefined
|
|
129
|
+
? null
|
|
130
|
+
: region.substackIndex,
|
|
131
|
+
substackName: region.substackName || '',
|
|
132
|
+
isFlex:
|
|
133
|
+
region.isFlexRegion === undefined ? null : region.isFlexRegion,
|
|
134
|
+
locked3d: region.locked3d === true,
|
|
135
|
+
bendingLineCount: Number(region.bendingLineCount || 0)
|
|
136
|
+
}))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Counts board-region semantic records for model summaries.
|
|
141
|
+
* @param {object[]} boardRegions
|
|
142
|
+
* @returns {{ boardRegionCount: number, flexRegionCount: number, bendingLineCount: number }}
|
|
143
|
+
*/
|
|
144
|
+
static summarizeBoardRegions(boardRegions) {
|
|
145
|
+
const regions = boardRegions || []
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
boardRegionCount: regions.length,
|
|
149
|
+
flexRegionCount: regions.filter(
|
|
150
|
+
(region) => region.isFlexRegion === true
|
|
151
|
+
).length,
|
|
152
|
+
bendingLineCount: regions.reduce(
|
|
153
|
+
(total, region) => total + Number(region.bendingLineCount || 0),
|
|
154
|
+
0
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Merges field sets into one lookup while preserving later native records.
|
|
161
|
+
* @param {Record<string, string | string[]>[]} fieldSets
|
|
162
|
+
* @returns {Record<string, string | string[]>}
|
|
163
|
+
*/
|
|
164
|
+
static #mergeFieldSets(fieldSets) {
|
|
165
|
+
return Object.assign({}, ...(fieldSets || []))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Finds all substack indexes for one native field family.
|
|
170
|
+
* @param {Record<string, string | string[]>} fields
|
|
171
|
+
* @param {RegExp} pattern
|
|
172
|
+
* @returns {number[]}
|
|
173
|
+
*/
|
|
174
|
+
static #findFamilyIndexes(fields, pattern) {
|
|
175
|
+
return [
|
|
176
|
+
...new Set(
|
|
177
|
+
Object.keys(fields || {})
|
|
178
|
+
.map((key) => pattern.exec(key)?.[1])
|
|
179
|
+
.filter((index) => index !== undefined)
|
|
180
|
+
.map((index) => Number.parseInt(index, 10))
|
|
181
|
+
.filter((index) => Number.isInteger(index))
|
|
182
|
+
)
|
|
183
|
+
].sort((left, right) => left - right)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Extracts one layer-substack row.
|
|
188
|
+
* @param {Record<string, string | string[]>} fields
|
|
189
|
+
* @param {{ fieldFamily: string, fields: Record<string, string> }} family
|
|
190
|
+
* @param {number} index
|
|
191
|
+
* @returns {object | null}
|
|
192
|
+
*/
|
|
193
|
+
static #parseLayerSubstack(fields, family, index) {
|
|
194
|
+
const names = PcbBoardRegionSemanticsParser.#fieldNames(
|
|
195
|
+
family.fields,
|
|
196
|
+
index
|
|
197
|
+
)
|
|
198
|
+
const id = getField(fields, names.id)
|
|
199
|
+
|
|
200
|
+
if (!id) {
|
|
201
|
+
return null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
index,
|
|
206
|
+
fieldFamily: family.fieldFamily,
|
|
207
|
+
id,
|
|
208
|
+
name:
|
|
209
|
+
getField(fields, names.name) ||
|
|
210
|
+
'Board Layer Stack ' + String(index),
|
|
211
|
+
isFlex: PcbBoardRegionSemanticsParser.#parseOptionalBoolean(
|
|
212
|
+
fields,
|
|
213
|
+
names.isFlex
|
|
214
|
+
),
|
|
215
|
+
showTopDielectric:
|
|
216
|
+
PcbBoardRegionSemanticsParser.#parseOptionalBoolean(
|
|
217
|
+
fields,
|
|
218
|
+
names.showTopDielectric
|
|
219
|
+
),
|
|
220
|
+
showBottomDielectric:
|
|
221
|
+
PcbBoardRegionSemanticsParser.#parseOptionalBoolean(
|
|
222
|
+
fields,
|
|
223
|
+
names.showBottomDielectric
|
|
224
|
+
),
|
|
225
|
+
serviceStackup: PcbBoardRegionSemanticsParser.#parseOptionalBoolean(
|
|
226
|
+
fields,
|
|
227
|
+
names.serviceStackup
|
|
228
|
+
),
|
|
229
|
+
usedByPrimitives:
|
|
230
|
+
PcbBoardRegionSemanticsParser.#parseOptionalBoolean(
|
|
231
|
+
fields,
|
|
232
|
+
names.usedByPrimitives
|
|
233
|
+
),
|
|
234
|
+
rawStackupType: getField(fields, names.rawStackupType)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Replaces field-name placeholders with an index.
|
|
240
|
+
* @param {Record<string, string>} templates
|
|
241
|
+
* @param {number} index
|
|
242
|
+
* @returns {Record<string, string>}
|
|
243
|
+
*/
|
|
244
|
+
static #fieldNames(templates, index) {
|
|
245
|
+
return Object.fromEntries(
|
|
246
|
+
Object.entries(templates).map(([key, template]) => [
|
|
247
|
+
key,
|
|
248
|
+
template.replace('{index}', String(index))
|
|
249
|
+
])
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Adds typed fields to one decoded board region.
|
|
255
|
+
* @param {object} region
|
|
256
|
+
* @param {number} regionIndex
|
|
257
|
+
* @param {Map<string, object>} substacksById
|
|
258
|
+
* @returns {object}
|
|
259
|
+
*/
|
|
260
|
+
static #enrichBoardRegion(region, regionIndex, substacksById) {
|
|
261
|
+
const properties = region?.properties || {}
|
|
262
|
+
const layerStackId = getField(properties, 'LAYERSTACKID')
|
|
263
|
+
const substack = substacksById.get(layerStackId)
|
|
264
|
+
const bendingLines =
|
|
265
|
+
PcbBoardRegionSemanticsParser.#parseBendingLines(properties)
|
|
266
|
+
const bendingLineCount =
|
|
267
|
+
PcbBoardRegionSemanticsParser.#parseIntegerField(
|
|
268
|
+
properties,
|
|
269
|
+
'BENDINGLINECOUNT'
|
|
270
|
+
) ?? bendingLines.length
|
|
271
|
+
const isFlexRegion =
|
|
272
|
+
substack?.isFlex === undefined ? null : substack.isFlex
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
...region,
|
|
276
|
+
boardRegionIndex: regionIndex,
|
|
277
|
+
objectKind:
|
|
278
|
+
getField(properties, 'OBJECTKIND') ||
|
|
279
|
+
(layerStackId ? 'BoardRegion' : ''),
|
|
280
|
+
name: getField(properties, 'NAME'),
|
|
281
|
+
v7Layer: getField(properties, 'V7_LAYER'),
|
|
282
|
+
boardLayerToken: getField(properties, 'LAYER'),
|
|
283
|
+
layerStackId,
|
|
284
|
+
substackIndex: substack?.index ?? null,
|
|
285
|
+
substackName: substack?.name || '',
|
|
286
|
+
isFlexRegion,
|
|
287
|
+
isRigidRegion:
|
|
288
|
+
isFlexRegion === null ? null : isFlexRegion === false,
|
|
289
|
+
locked3d:
|
|
290
|
+
PcbBoardRegionSemanticsParser.#parseOptionalBoolean(
|
|
291
|
+
properties,
|
|
292
|
+
'LOCKED3D'
|
|
293
|
+
) === true,
|
|
294
|
+
cavityHeight: getField(properties, 'CAVITYHEIGHT'),
|
|
295
|
+
arcResolution: getField(properties, 'ARCRESOLUTION'),
|
|
296
|
+
bendingLineCount,
|
|
297
|
+
bendingLines
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Parses indexed BENDINGLINE{n} values in stream order.
|
|
303
|
+
* @param {Record<string, string | string[]>} properties
|
|
304
|
+
* @returns {object[]}
|
|
305
|
+
*/
|
|
306
|
+
static #parseBendingLines(properties) {
|
|
307
|
+
return Object.entries(properties || {})
|
|
308
|
+
.map(([key, value]) => ({
|
|
309
|
+
match: /^BENDINGLINE(\d+)$/i.exec(key),
|
|
310
|
+
value
|
|
311
|
+
}))
|
|
312
|
+
.filter((item) => item.match)
|
|
313
|
+
.map((item) => ({
|
|
314
|
+
index: Number.parseInt(item.match[1], 10),
|
|
315
|
+
raw: PcbBoardRegionSemanticsParser.#stringValue(item.value)
|
|
316
|
+
}))
|
|
317
|
+
.sort((left, right) => left.index - right.index)
|
|
318
|
+
.map((item) =>
|
|
319
|
+
PcbBoardRegionSemanticsParser.#parseBendingLine(
|
|
320
|
+
item.index,
|
|
321
|
+
item.raw
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Parses one semicolon-delimited board-region bending line.
|
|
328
|
+
* @param {number} index
|
|
329
|
+
* @param {string} raw
|
|
330
|
+
* @returns {object}
|
|
331
|
+
*/
|
|
332
|
+
static #parseBendingLine(index, raw) {
|
|
333
|
+
const tokens = String(raw || '')
|
|
334
|
+
.split(';')
|
|
335
|
+
.map((token) => token.trim())
|
|
336
|
+
const angleDeg = PcbBoardRegionSemanticsParser.#parseOptionalNumber(
|
|
337
|
+
tokens[0]
|
|
338
|
+
)
|
|
339
|
+
const radiusRaw = PcbBoardRegionSemanticsParser.#parseOptionalInteger(
|
|
340
|
+
tokens[1]
|
|
341
|
+
)
|
|
342
|
+
const radiusMil =
|
|
343
|
+
radiusRaw === null
|
|
344
|
+
? null
|
|
345
|
+
: PcbBoardRegionSemanticsParser.#toMil(radiusRaw)
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
index,
|
|
349
|
+
raw,
|
|
350
|
+
angleDeg,
|
|
351
|
+
radiusRaw,
|
|
352
|
+
radiusMil,
|
|
353
|
+
affectedWidthMil:
|
|
354
|
+
angleDeg === null || radiusMil === null
|
|
355
|
+
? null
|
|
356
|
+
: PcbBoardRegionSemanticsParser.#roundMil(
|
|
357
|
+
(Math.abs(angleDeg) / 360) * 2 * Math.PI * radiusMil
|
|
358
|
+
),
|
|
359
|
+
foldIndex: PcbBoardRegionSemanticsParser.#parseOptionalInteger(
|
|
360
|
+
tokens[2]
|
|
361
|
+
),
|
|
362
|
+
x1Raw: PcbBoardRegionSemanticsParser.#parseOptionalInteger(
|
|
363
|
+
tokens[3]
|
|
364
|
+
),
|
|
365
|
+
y1Raw: PcbBoardRegionSemanticsParser.#parseOptionalInteger(
|
|
366
|
+
tokens[4]
|
|
367
|
+
),
|
|
368
|
+
x2Raw: PcbBoardRegionSemanticsParser.#parseOptionalInteger(
|
|
369
|
+
tokens[5]
|
|
370
|
+
),
|
|
371
|
+
y2Raw: PcbBoardRegionSemanticsParser.#parseOptionalInteger(
|
|
372
|
+
tokens[6]
|
|
373
|
+
),
|
|
374
|
+
x1: PcbBoardRegionSemanticsParser.#toMilOrNull(tokens[3]),
|
|
375
|
+
y1: PcbBoardRegionSemanticsParser.#toMilOrNull(tokens[4]),
|
|
376
|
+
x2: PcbBoardRegionSemanticsParser.#toMilOrNull(tokens[5]),
|
|
377
|
+
y2: PcbBoardRegionSemanticsParser.#toMilOrNull(tokens[6])
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Parses one optional boolean field.
|
|
383
|
+
* @param {Record<string, string | string[]>} fields
|
|
384
|
+
* @param {string} key
|
|
385
|
+
* @returns {boolean | null}
|
|
386
|
+
*/
|
|
387
|
+
static #parseOptionalBoolean(fields, key) {
|
|
388
|
+
const raw = getField(fields, key)
|
|
389
|
+
|
|
390
|
+
if (!raw) {
|
|
391
|
+
return null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return parseBoolean(raw)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Parses one optional integer field.
|
|
399
|
+
* @param {Record<string, string | string[]>} fields
|
|
400
|
+
* @param {string} key
|
|
401
|
+
* @returns {number | null}
|
|
402
|
+
*/
|
|
403
|
+
static #parseIntegerField(fields, key) {
|
|
404
|
+
const parsed = parseNumericField(fields, key)
|
|
405
|
+
|
|
406
|
+
return parsed === null ? null : Math.trunc(parsed)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Parses one optional number token.
|
|
411
|
+
* @param {string | undefined} raw
|
|
412
|
+
* @returns {number | null}
|
|
413
|
+
*/
|
|
414
|
+
static #parseOptionalNumber(raw) {
|
|
415
|
+
if (raw === undefined || raw === '') {
|
|
416
|
+
return null
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const parsed = Number(raw)
|
|
420
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Parses one optional integer token.
|
|
425
|
+
* @param {string | undefined} raw
|
|
426
|
+
* @returns {number | null}
|
|
427
|
+
*/
|
|
428
|
+
static #parseOptionalInteger(raw) {
|
|
429
|
+
const parsed = PcbBoardRegionSemanticsParser.#parseOptionalNumber(raw)
|
|
430
|
+
|
|
431
|
+
return parsed === null ? null : Math.trunc(parsed)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Converts one internal Altium coordinate token to mils.
|
|
436
|
+
* @param {string | undefined} raw
|
|
437
|
+
* @returns {number | null}
|
|
438
|
+
*/
|
|
439
|
+
static #toMilOrNull(raw) {
|
|
440
|
+
const value = PcbBoardRegionSemanticsParser.#parseOptionalInteger(raw)
|
|
441
|
+
|
|
442
|
+
return value === null
|
|
443
|
+
? null
|
|
444
|
+
: PcbBoardRegionSemanticsParser.#toMil(value)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Converts one internal Altium coordinate to mils.
|
|
449
|
+
* @param {number} value
|
|
450
|
+
* @returns {number}
|
|
451
|
+
*/
|
|
452
|
+
static #toMil(value) {
|
|
453
|
+
return PcbBoardRegionSemanticsParser.#roundMil(
|
|
454
|
+
Number(value || 0) / 10000
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Rounds a mil value for stable JSON output.
|
|
460
|
+
* @param {number} value
|
|
461
|
+
* @returns {number}
|
|
462
|
+
*/
|
|
463
|
+
static #roundMil(value) {
|
|
464
|
+
return Math.round(Number(value || 0) * 1000000) / 1000000
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Returns the last text value from one field payload.
|
|
469
|
+
* @param {string | string[] | undefined} raw
|
|
470
|
+
* @returns {string}
|
|
471
|
+
*/
|
|
472
|
+
static #stringValue(raw) {
|
|
473
|
+
const values = Array.isArray(raw) ? raw : [raw]
|
|
474
|
+
|
|
475
|
+
return String(values.findLast((value) => value !== undefined) || '')
|
|
476
|
+
}
|
|
477
|
+
}
|