altium-toolkit 1.1.35 → 1.1.36
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/AGENTS.md +2 -2
- package/package.json +1 -1
- package/src/core/altium/PrintableTextDecoder.mjs +20 -1
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +288 -7
- package/src/core/altium/SchematicTextParser.mjs +18 -253
- package/src/core/altium/SchematicTitleBlockParser.mjs +410 -0
- package/src/core/altium/SelectedPartAltiumExportAdapter.mjs +215 -0
- package/src/parser.mjs +1 -0
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +149 -20
- package/src/ui/PcbScene3dPackages.mjs +16 -1
- package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +899 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +138 -8
- package/src/ui/PcbScene3dStaticBodyPrototypeRecovery.mjs +445 -0
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +7 -1
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +199 -2
- package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +438 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +23 -2
- package/src/ui/SchematicColorResolver.mjs +44 -0
- package/src/ui/SchematicLineColorResolver.mjs +1 -1
- 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'
|