altium-toolkit 1.1.35 → 1.1.37

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.
Files changed (35) hide show
  1. package/AGENTS.md +17 -2
  2. package/package.json +1 -1
  3. package/src/core/altium/PrintableTextDecoder.mjs +20 -1
  4. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +288 -7
  5. package/src/core/altium/SchematicTextParser.mjs +18 -253
  6. package/src/core/altium/SchematicTitleBlockParser.mjs +410 -0
  7. package/src/core/altium/SelectedPartAltiumExportAdapter.mjs +215 -0
  8. package/src/parser.mjs +1 -0
  9. package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +245 -12
  10. package/src/ui/AltiumScene3dAuthoredConnectorYawPolicy.mjs +176 -0
  11. package/src/ui/AltiumScene3dBottomSourceHalfTurnPolicy.mjs +93 -0
  12. package/src/ui/AltiumScene3dDisplayModuleYawPolicy.mjs +284 -0
  13. package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +521 -23
  14. package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +766 -19
  15. package/src/ui/AltiumScene3dQfnFootprintDetector.mjs +307 -0
  16. package/src/ui/AltiumScene3dRepeatedFullFootprintBodyCollapse.mjs +518 -0
  17. package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +121 -17
  18. package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +286 -135
  19. package/src/ui/AltiumScene3dShapeStackOwnerConflictPolicy.mjs +299 -0
  20. package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +22 -0
  21. package/src/ui/PcbScene3dBuilder.mjs +613 -117
  22. package/src/ui/PcbScene3dModelRegistry.mjs +222 -10
  23. package/src/ui/PcbScene3dPackages.mjs +16 -1
  24. package/src/ui/PcbScene3dPlacementSideResolver.mjs +262 -22
  25. package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +984 -0
  26. package/src/ui/PcbScene3dStaticBodyPadOwnerPromotion.mjs +378 -0
  27. package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +169 -23
  28. package/src/ui/PcbScene3dStaticBodyPrototypeRecovery.mjs +445 -0
  29. package/src/ui/PcbScene3dStaticBodyRecovery.mjs +7 -1
  30. package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +199 -2
  31. package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +439 -0
  32. package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +23 -2
  33. package/src/ui/SchematicColorResolver.mjs +44 -0
  34. package/src/ui/SchematicLineColorResolver.mjs +1 -1
  35. package/src/ui/SchematicShapeRenderer.mjs +13 -13
@@ -0,0 +1,410 @@
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
+ /**
8
+ * Helpers for recovering Altium schematic title-block footer metadata.
9
+ */
10
+ export class SchematicTitleBlockParser {
11
+ /**
12
+ * Extracts footer metadata used for the synthesized title block.
13
+ * @param {{ fields: Record<string, string | string[]> }[]} records
14
+ * @param {Record<string, string>} metadata
15
+ * @param {number} sheetWidth
16
+ * @param {Record<string, { size: number, family: string, bold: boolean, italic?: boolean, rotation: number }>} fonts
17
+ * @returns {{ title: string, revision: string, documentNumber: string, sheetNumber: string, sheetTotal: string, date: string, drawnBy: string, footerHints: Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }>> }}
18
+ */
19
+ static extract(records, metadata, sheetWidth, fonts) {
20
+ const footerTexts = records
21
+ .filter((record) =>
22
+ SchematicTitleBlockParser.isFooterRecord(
23
+ record.fields,
24
+ sheetWidth
25
+ )
26
+ )
27
+ .map((record) =>
28
+ SchematicTitleBlockParser.#normalizeFooterRecord(
29
+ record.fields,
30
+ fonts
31
+ )
32
+ )
33
+ .filter(Boolean)
34
+ .sort((left, right) => right.y - left.y || left.x - right.x)
35
+ const footerHints =
36
+ SchematicTitleBlockParser.#collectFooterHints(footerTexts)
37
+ const numericFooterTexts = footerTexts.filter((record) =>
38
+ /^\d+$/.test(record.text)
39
+ )
40
+ const footerDrawnBy = SchematicTitleBlockParser.#extractDrawnBy(
41
+ footerTexts,
42
+ metadata
43
+ )
44
+
45
+ return {
46
+ title:
47
+ SchematicTitleBlockParser.#resolveFooterValue(
48
+ footerHints.title?.text,
49
+ metadata
50
+ ) ||
51
+ SchematicTitleBlockParser.#cleanMetadataValue(metadata.title),
52
+ revision:
53
+ SchematicTitleBlockParser.#resolveFooterValue(
54
+ footerHints.revision?.text,
55
+ metadata
56
+ ) ||
57
+ SchematicTitleBlockParser.#cleanMetadataValue(
58
+ metadata.revision
59
+ ),
60
+ documentNumber: SchematicTitleBlockParser.#cleanMetadataValue(
61
+ SchematicTitleBlockParser.#resolveFooterValue(
62
+ footerHints.documentNumber?.text,
63
+ metadata
64
+ ) || metadata.documentnumber
65
+ ),
66
+ sheetNumber:
67
+ SchematicTitleBlockParser.#resolveFooterValue(
68
+ footerHints.sheetNumber?.text,
69
+ metadata
70
+ ) ||
71
+ numericFooterTexts[0]?.text ||
72
+ SchematicTitleBlockParser.#cleanMetadataValue(
73
+ metadata.sheetnumber
74
+ ),
75
+ sheetTotal:
76
+ SchematicTitleBlockParser.#resolveFooterValue(
77
+ footerHints.sheetTotal?.text,
78
+ metadata
79
+ ) ||
80
+ numericFooterTexts[1]?.text ||
81
+ SchematicTitleBlockParser.#cleanMetadataValue(
82
+ metadata.sheettotal
83
+ ),
84
+ date: SchematicTitleBlockParser.#cleanMetadataValue(
85
+ metadata.currentdate || metadata.date
86
+ ),
87
+ drawnBy:
88
+ SchematicTitleBlockParser.#cleanMetadataValue(
89
+ metadata.drawnby
90
+ ) || footerDrawnBy,
91
+ footerHints: SchematicTitleBlockParser.#stripHintText(footerHints)
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Returns true when the text primitive belongs to the page footer template.
97
+ * @param {Record<string, string | string[]>} fields
98
+ * @param {number} sheetWidth
99
+ * @returns {boolean}
100
+ */
101
+ static isFooterRecord(fields, sheetWidth) {
102
+ const recordType = ParserUtils.getField(fields, 'RECORD')
103
+ const x = ParserUtils.parseNumericField(fields, 'Location.X')
104
+ const y = ParserUtils.parseNumericField(fields, 'Location.Y')
105
+
106
+ return (
107
+ recordType === '4' &&
108
+ x !== null &&
109
+ y !== null &&
110
+ x >= sheetWidth * 0.55 &&
111
+ y <= 100
112
+ )
113
+ }
114
+
115
+ /**
116
+ * Normalizes one visible footer text record into a title-block layout hint.
117
+ * @param {Record<string, string | string[]>} fields
118
+ * @param {Record<string, { size: number, family: string, bold: boolean, italic?: boolean, rotation: number }} fonts
119
+ * @returns {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string } | null}
120
+ */
121
+ static #normalizeFooterRecord(fields, fonts) {
122
+ const text = ParserUtils.getDisplayText(fields)
123
+ const x = ParserUtils.parseNumericField(fields, 'Location.X')
124
+ const y = ParserUtils.parseNumericField(fields, 'Location.Y')
125
+
126
+ if (!text || x === null || y === null) {
127
+ return null
128
+ }
129
+
130
+ const font =
131
+ (fonts || {})[ParserUtils.getField(fields, 'FontID')] ||
132
+ SchematicTitleBlockParser.#defaultFont()
133
+
134
+ return {
135
+ text,
136
+ x,
137
+ y,
138
+ color: ParserUtils.toColor(fields.Color, '#2c3134'),
139
+ fontSize: font.size,
140
+ fontFamily: font.family,
141
+ fontWeight: font.bold ? 700 : 400,
142
+ ...(font.italic ? { fontStyle: 'italic' } : {})
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Maps visible footer rows onto title-block fields.
148
+ * @param {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }[]} footerTexts
149
+ * @returns {Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }>>}
150
+ */
151
+ static #collectFooterHints(footerTexts) {
152
+ const rows = SchematicTitleBlockParser.#groupRows(footerTexts)
153
+ const topRow = rows[0] || []
154
+ const middleRow = rows.length > 2 ? rows[1] || [] : []
155
+ const sheetRow =
156
+ [...rows]
157
+ .reverse()
158
+ .find(
159
+ (row) =>
160
+ row.filter((record) => /^\d+$/.test(record.text))
161
+ .length >= 2
162
+ ) || []
163
+ const numericSheetRow = sheetRow.filter((record) =>
164
+ /^\d+$/.test(record.text)
165
+ )
166
+ const topRowHasVisibleTitleText = topRow.some(
167
+ (record) => /^\d+$/.test(record.text) === false
168
+ )
169
+ const topRowDataRecords = topRow.filter(
170
+ (record) => !SchematicTitleBlockParser.#isLabelText(record.text)
171
+ )
172
+ const middleRowDataRecords = middleRow.filter(
173
+ (record) => !SchematicTitleBlockParser.#isLabelText(record.text)
174
+ )
175
+ const hints = {}
176
+ const titleHint = SchematicTitleBlockParser.#findSpecialStringHint(
177
+ footerTexts,
178
+ ['title']
179
+ )
180
+ const documentNumberHint =
181
+ SchematicTitleBlockParser.#findSpecialStringHint(footerTexts, [
182
+ 'documentnumber'
183
+ ])
184
+ const revisionHint = SchematicTitleBlockParser.#findSpecialStringHint(
185
+ footerTexts,
186
+ ['revision']
187
+ )
188
+ const sheetNumberHint =
189
+ SchematicTitleBlockParser.#findSpecialStringHint(footerTexts, [
190
+ 'sheetnumber'
191
+ ])
192
+ const sheetTotalHint = SchematicTitleBlockParser.#findSpecialStringHint(
193
+ footerTexts,
194
+ ['sheettotal']
195
+ )
196
+
197
+ if (titleHint) {
198
+ hints.title = titleHint
199
+ }
200
+
201
+ if (documentNumberHint) {
202
+ hints.documentNumber = documentNumberHint
203
+ }
204
+
205
+ if (revisionHint) {
206
+ hints.revision = revisionHint
207
+ }
208
+
209
+ if (sheetNumberHint) {
210
+ hints.sheetNumber = sheetNumberHint
211
+ }
212
+
213
+ if (sheetTotalHint) {
214
+ hints.sheetTotal = sheetTotalHint
215
+ }
216
+
217
+ if (
218
+ !hints.title &&
219
+ topRowDataRecords.length &&
220
+ topRowHasVisibleTitleText
221
+ ) {
222
+ hints.title = topRowDataRecords[0]
223
+ }
224
+
225
+ if (
226
+ !hints.documentNumber &&
227
+ topRowDataRecords.length > 1 &&
228
+ topRowHasVisibleTitleText
229
+ ) {
230
+ hints.documentNumber = topRowDataRecords.at(-1)
231
+ }
232
+
233
+ if (!hints.revision && middleRowDataRecords.length) {
234
+ hints.revision = middleRowDataRecords.at(-1)
235
+ }
236
+
237
+ if (!hints.sheetNumber && numericSheetRow.length) {
238
+ hints.sheetNumber = numericSheetRow[0]
239
+ }
240
+
241
+ if (!hints.sheetTotal && numericSheetRow.length) {
242
+ hints.sheetTotal = numericSheetRow.at(-1)
243
+ }
244
+
245
+ return hints
246
+ }
247
+
248
+ /**
249
+ * Finds the first footer text that directly references one special string.
250
+ * @param {{ text: string }[]} footerTexts Footer text rows.
251
+ * @param {string[]} names Lowercase special-string names.
252
+ * @returns {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string } | undefined}
253
+ */
254
+ static #findSpecialStringHint(footerTexts, names) {
255
+ const acceptedNames = new Set(names)
256
+
257
+ return (footerTexts || []).find((record) =>
258
+ acceptedNames.has(
259
+ SchematicTitleBlockParser.#specialStringName(record.text)
260
+ )
261
+ )
262
+ }
263
+
264
+ /**
265
+ * Extracts a direct Altium special-string name from footer text.
266
+ * @param {string} text Footer text.
267
+ * @returns {string}
268
+ */
269
+ static #specialStringName(text) {
270
+ const match = String(text || '')
271
+ .trim()
272
+ .match(/^[.=]([A-Za-z_][\w.-]*)$/u)
273
+
274
+ return match ? match[1].toLowerCase() : ''
275
+ }
276
+
277
+ /**
278
+ * Returns true for static native title-block label cells.
279
+ * @param {string} text Footer text.
280
+ * @returns {boolean}
281
+ */
282
+ static #isLabelText(text) {
283
+ return /^(title|size|number|revision|date|file|sheet|of|drawn by|project):?$/iu.test(
284
+ String(text || '').trim()
285
+ )
286
+ }
287
+
288
+ /**
289
+ * Groups footer texts by their shared baseline row.
290
+ * @param {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }[]} footerTexts
291
+ * @returns {Array<{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }[]>}
292
+ */
293
+ static #groupRows(footerTexts) {
294
+ const tolerance = 8
295
+ const rows = []
296
+
297
+ for (const record of footerTexts) {
298
+ const currentRow = rows.at(-1)
299
+
300
+ if (
301
+ currentRow &&
302
+ Math.abs(currentRow[0].y - record.y) <= tolerance
303
+ ) {
304
+ currentRow.push(record)
305
+ currentRow.sort((left, right) => left.x - right.x)
306
+ continue
307
+ }
308
+
309
+ rows.push([record])
310
+ }
311
+
312
+ return rows
313
+ }
314
+
315
+ /**
316
+ * Resolves one visible footer placeholder against hidden sheet metadata.
317
+ * @param {string | undefined} text
318
+ * @param {Record<string, string>} metadata
319
+ * @returns {string}
320
+ */
321
+ static #resolveFooterValue(text, metadata) {
322
+ const resolved = SchematicTitleBlockParser.#resolveTemplateText(
323
+ text,
324
+ metadata
325
+ )
326
+
327
+ if (String(resolved || '').startsWith('=')) {
328
+ return ''
329
+ }
330
+
331
+ return SchematicTitleBlockParser.#cleanMetadataValue(resolved)
332
+ }
333
+
334
+ /**
335
+ * Extracts a visible footer `Drawn By` value from the bottom-most footer
336
+ * row when hidden metadata does not provide one.
337
+ * @param {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }[]} footerTexts
338
+ * @param {Record<string, string>} metadata
339
+ * @returns {string}
340
+ */
341
+ static #extractDrawnBy(footerTexts, metadata) {
342
+ const bottomRow =
343
+ SchematicTitleBlockParser.#groupRows(footerTexts).at(-1) || []
344
+ const candidates = bottomRow
345
+ .map((record) =>
346
+ SchematicTitleBlockParser.#resolveFooterValue(
347
+ record.text,
348
+ metadata
349
+ )
350
+ )
351
+ .filter(
352
+ (value) => value && /^\d+$/.test(String(value).trim()) === false
353
+ )
354
+
355
+ return candidates.at(-1) || ''
356
+ }
357
+
358
+ /**
359
+ * Removes the non-rendered text payload from stored footer hints.
360
+ * @param {Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }>>} footerHints
361
+ * @returns {Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }>>}
362
+ */
363
+ static #stripHintText(footerHints) {
364
+ return Object.fromEntries(
365
+ Object.entries(footerHints).map(([key, value]) => {
366
+ const { text: _text, ...hint } = value
367
+ return [key, hint]
368
+ })
369
+ )
370
+ }
371
+
372
+ /**
373
+ * Resolves visible title-block placeholders from hidden sheet metadata.
374
+ * @param {string | undefined} text
375
+ * @param {Record<string, string>} metadata
376
+ * @returns {string}
377
+ */
378
+ static #resolveTemplateText(text, metadata) {
379
+ const normalized = String(text || '').trim()
380
+ if (!normalized.startsWith('=')) {
381
+ return normalized
382
+ }
383
+
384
+ const replacement = metadata[normalized.slice(1).toLowerCase()]
385
+ return replacement ? replacement : normalized
386
+ }
387
+
388
+ /**
389
+ * Normalizes placeholder metadata values.
390
+ * @param {string | undefined} value
391
+ * @returns {string}
392
+ */
393
+ static #cleanMetadataValue(value) {
394
+ return value && value !== '*' ? value : ''
395
+ }
396
+
397
+ /**
398
+ * Returns the default schematic font when no sheet font entry exists.
399
+ * @returns {{ size: number, family: string, bold: boolean, italic: boolean, rotation: number }}
400
+ */
401
+ static #defaultFont() {
402
+ return {
403
+ size: 10,
404
+ family: 'Times New Roman',
405
+ bold: false,
406
+ italic: false,
407
+ rotation: 0
408
+ }
409
+ }
410
+ }
@@ -0,0 +1,215 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { AltiumPcbLibExporter } from './AltiumPcbLibExporter.mjs'
6
+ import { AltiumSchLibExporter } from './AltiumSchLibExporter.mjs'
7
+ import { SourceBundleExporter } from './SourceBundleExporter.mjs'
8
+
9
+ /**
10
+ * Builds Altium library exports from normalized selected-part data.
11
+ */
12
+ export class SelectedPartAltiumExportAdapter {
13
+ /**
14
+ * Exports one selected-part bundle to Altium library entries.
15
+ * @param {object} selectedPart Selected part data.
16
+ * @param {{ models?: object[], partName?: string, libraryBasePath?: string, sourceBasePath?: string }} [options] Export options.
17
+ * @returns {{ bundle: object, entries: { path: string, bytes: Uint8Array, contentType: string }[] }}
18
+ */
19
+ static export(selectedPart, options = {}) {
20
+ const partName = SelectedPartAltiumExportAdapter.#safeFileName(
21
+ options.partName ||
22
+ selectedPart?.footprint?.name ||
23
+ selectedPart?.symbol?.name ||
24
+ selectedPart?.designator ||
25
+ 'selected-part'
26
+ )
27
+ const bundle = SelectedPartAltiumExportAdapter.buildBundle(
28
+ selectedPart,
29
+ options.models,
30
+ partName
31
+ )
32
+ const libraryBasePath = SelectedPartAltiumExportAdapter.#basePath(
33
+ options.libraryBasePath,
34
+ 'altium'
35
+ )
36
+ const sourceBasePath = SelectedPartAltiumExportAdapter.#basePath(
37
+ options.sourceBasePath,
38
+ 'source'
39
+ )
40
+ const sourceEntries = SourceBundleExporter.export(bundle, {
41
+ includeModels: true
42
+ }).entries.map((entry) => ({
43
+ ...entry,
44
+ path:
45
+ entry.path === 'manifest.json'
46
+ ? SelectedPartAltiumExportAdapter.#joinPath(
47
+ sourceBasePath,
48
+ 'manifest.json'
49
+ )
50
+ : entry.path
51
+ }))
52
+
53
+ return {
54
+ bundle,
55
+ entries: [
56
+ {
57
+ path: SelectedPartAltiumExportAdapter.#joinPath(
58
+ libraryBasePath,
59
+ partName + '.SchLib'
60
+ ),
61
+ bytes: AltiumSchLibExporter.export([bundle]),
62
+ contentType: 'application/octet-stream'
63
+ },
64
+ {
65
+ path: SelectedPartAltiumExportAdapter.#joinPath(
66
+ libraryBasePath,
67
+ partName + '.PcbLib'
68
+ ),
69
+ bytes: AltiumPcbLibExporter.export([bundle]),
70
+ contentType: 'application/octet-stream'
71
+ },
72
+ ...sourceEntries
73
+ ]
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Builds a source-compatible component bundle from selected-part data.
79
+ * @param {object} selectedPart Selected part data.
80
+ * @param {object[]} [models] Packaged 3D model assets.
81
+ * @param {string} [partName] Export artifact name.
82
+ * @returns {object}
83
+ */
84
+ static buildBundle(selectedPart, models = [], partName = '') {
85
+ const bundleName =
86
+ partName || selectedPart?.designator || 'Selected part'
87
+
88
+ return {
89
+ id: selectedPart?.designator || 'selected-part',
90
+ name: bundleName,
91
+ metadata: {
92
+ name: bundleName,
93
+ partNumber: selectedPart?.symbol?.value || ''
94
+ },
95
+ symbol: {
96
+ name:
97
+ selectedPart?.symbol?.name ||
98
+ selectedPart?.designator ||
99
+ 'Selected part',
100
+ pins: selectedPart?.symbol?.pins || [],
101
+ primitives: SelectedPartAltiumExportAdapter.#symbolPrimitives(
102
+ selectedPart?.symbol
103
+ ),
104
+ raw: selectedPart?.symbol?.raw || {}
105
+ },
106
+ footprint: {
107
+ name:
108
+ selectedPart?.footprint?.name ||
109
+ selectedPart?.designator ||
110
+ 'Selected part',
111
+ component: selectedPart?.footprint?.component || {},
112
+ pads: selectedPart?.footprint?.pads || [],
113
+ tracks: selectedPart?.footprint?.tracks || [],
114
+ arcs: selectedPart?.footprint?.arcs || [],
115
+ fills: selectedPart?.footprint?.fills || [],
116
+ texts: selectedPart?.footprint?.texts || [],
117
+ primitives: [],
118
+ raw: selectedPart?.footprint?.raw || {}
119
+ },
120
+ models: SelectedPartAltiumExportAdapter.#array(models),
121
+ sourceJson: {
122
+ selectedPart
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Builds Altium schematic primitive rows from the selected symbol.
129
+ * @param {object} symbol Selected symbol.
130
+ * @returns {object[]}
131
+ */
132
+ static #symbolPrimitives(symbol = {}) {
133
+ return [
134
+ ...SelectedPartAltiumExportAdapter.#typedPrimitives(
135
+ symbol?.rectangles,
136
+ 'rectangle'
137
+ ),
138
+ ...SelectedPartAltiumExportAdapter.#typedPrimitives(
139
+ symbol?.lines,
140
+ 'line'
141
+ ),
142
+ ...SelectedPartAltiumExportAdapter.#typedPrimitives(
143
+ symbol?.arcs,
144
+ 'arc'
145
+ ),
146
+ ...SelectedPartAltiumExportAdapter.#typedPrimitives(
147
+ symbol?.ellipses,
148
+ 'ellipse'
149
+ ),
150
+ ...SelectedPartAltiumExportAdapter.#typedPrimitives(
151
+ symbol?.polygons,
152
+ 'polygon'
153
+ )
154
+ ]
155
+ }
156
+
157
+ /**
158
+ * Adds a primitive type to each entry.
159
+ * @param {unknown} primitives Primitive candidates.
160
+ * @param {string} type Primitive type.
161
+ * @returns {object[]}
162
+ */
163
+ static #typedPrimitives(primitives, type) {
164
+ return SelectedPartAltiumExportAdapter.#array(primitives).map(
165
+ (primitive) => ({
166
+ type,
167
+ ...primitive
168
+ })
169
+ )
170
+ }
171
+
172
+ /**
173
+ * Normalizes an export base path.
174
+ * @param {unknown} value Candidate path.
175
+ * @param {string} fallback Fallback path.
176
+ * @returns {string}
177
+ */
178
+ static #basePath(value, fallback) {
179
+ return String(value || fallback)
180
+ .replace(/^\/+/u, '')
181
+ .replace(/\/+$/u, '')
182
+ }
183
+
184
+ /**
185
+ * Joins one base path and file path.
186
+ * @param {string} basePath Base path.
187
+ * @param {string} filePath File path.
188
+ * @returns {string}
189
+ */
190
+ static #joinPath(basePath, filePath) {
191
+ const cleanFilePath = String(filePath || '').replace(/^\/+/u, '')
192
+ return basePath ? basePath + '/' + cleanFilePath : cleanFilePath
193
+ }
194
+
195
+ /**
196
+ * Normalizes a possible array.
197
+ * @param {unknown} value Candidate array.
198
+ * @returns {object[]}
199
+ */
200
+ static #array(value) {
201
+ return Array.isArray(value) ? value : []
202
+ }
203
+
204
+ /**
205
+ * Creates a safe file name.
206
+ * @param {unknown} value Raw value.
207
+ * @returns {string}
208
+ */
209
+ static #safeFileName(value) {
210
+ return String(value || 'selected-part').replace(
211
+ /[\\/:\u0000-\u001f]/gu,
212
+ '_'
213
+ )
214
+ }
215
+ }
package/src/parser.mjs CHANGED
@@ -11,6 +11,7 @@ export { AltiumLibraryBatchExporter } from './core/altium/AltiumLibraryBatchExpo
11
11
  export { AltiumLibraryRecordBuilder } from './core/altium/AltiumLibraryRecordBuilder.mjs'
12
12
  export { AltiumPcbLibExporter } from './core/altium/AltiumPcbLibExporter.mjs'
13
13
  export { AltiumSchLibExporter } from './core/altium/AltiumSchLibExporter.mjs'
14
+ export { SelectedPartAltiumExportAdapter } from './core/altium/SelectedPartAltiumExportAdapter.mjs'
14
15
  export { AltiumUnits } from './core/altium/AltiumUnits.mjs'
15
16
  export { SourceBundleExporter } from './core/altium/SourceBundleExporter.mjs'
16
17
  export { SourceComponentBundleNormalizer } from './core/altium/SourceComponentBundleNormalizer.mjs'