altium-toolkit 1.4.9 → 1.4.11
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/release-notes-v1.4.10.md +29 -0
- package/docs/release-notes-v1.4.11.md +34 -0
- package/package.json +1 -1
- package/src/convergence/AltiumSchematicFidelityNormalizer.mjs +661 -0
- package/src/convergence/AltiumSchematicNativeFooterOwnerAligner.mjs +351 -0
- package/src/convergence/SchematicSvgRenderer.mjs +37 -2
- package/src/ui/SchematicHarnessRenderer.mjs +346 -0
- package/src/ui/SchematicRotatedOwnerTextPlacement.mjs +55 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import { ParserUtils } from '../core/altium/ParserUtils.mjs'
|
|
5
|
+
import { SchematicTypography } from '../ui/SchematicTypography.mjs'
|
|
6
|
+
import { SchematicRotatedOwnerTextPlacement } from '../ui/SchematicRotatedOwnerTextPlacement.mjs'
|
|
7
|
+
|
|
8
|
+
const PRIMITIVE_FAMILIES = Object.freeze([
|
|
9
|
+
'lines',
|
|
10
|
+
'polygons',
|
|
11
|
+
'rectangles',
|
|
12
|
+
'roundedRectangles',
|
|
13
|
+
'ellipses',
|
|
14
|
+
'arcs',
|
|
15
|
+
'pies'
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Repairs render-only schematic fidelity from the native ownership sidecar
|
|
20
|
+
* without changing the preserved historical parser or renderer contracts.
|
|
21
|
+
*/
|
|
22
|
+
export class AltiumSchematicFidelityNormalizer {
|
|
23
|
+
/**
|
|
24
|
+
* Builds a shallow normalized view for convergence rendering.
|
|
25
|
+
* @param {Record<string, any>} documentModel Native renderer document.
|
|
26
|
+
* @returns {Record<string, any>} Fidelity-normalized render document.
|
|
27
|
+
*/
|
|
28
|
+
static normalize(documentModel) {
|
|
29
|
+
const schematic = documentModel?.schematic
|
|
30
|
+
if (!schematic) return documentModel
|
|
31
|
+
|
|
32
|
+
const records = schematic.ownership?.records || []
|
|
33
|
+
const sheet = AltiumSchematicFidelityNormalizer.#normalizeSheet(
|
|
34
|
+
schematic,
|
|
35
|
+
records
|
|
36
|
+
)
|
|
37
|
+
const ownerBounds =
|
|
38
|
+
AltiumSchematicFidelityNormalizer.#collectOwnerBounds(schematic)
|
|
39
|
+
const footerTexts =
|
|
40
|
+
AltiumSchematicFidelityNormalizer.#resolveFooterTexts(
|
|
41
|
+
schematic.texts || [],
|
|
42
|
+
records,
|
|
43
|
+
sheet
|
|
44
|
+
)
|
|
45
|
+
const texts = AltiumSchematicFidelityNormalizer.#placeOwnerTexts(
|
|
46
|
+
footerTexts,
|
|
47
|
+
ownerBounds
|
|
48
|
+
).filter((text) => String(text?.recordType || '') !== '217')
|
|
49
|
+
const harnesses = AltiumSchematicFidelityNormalizer.#normalizeHarnesses(
|
|
50
|
+
schematic.harnesses,
|
|
51
|
+
records
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
sheet === schematic.sheet &&
|
|
56
|
+
texts === schematic.texts &&
|
|
57
|
+
harnesses === schematic.harnesses
|
|
58
|
+
) {
|
|
59
|
+
return documentModel
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...documentModel,
|
|
64
|
+
schematic: {
|
|
65
|
+
...schematic,
|
|
66
|
+
sheet,
|
|
67
|
+
texts,
|
|
68
|
+
...(harnesses ? { harnesses } : {})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Restores a proven embedded native frame from source sheet dimensions.
|
|
75
|
+
* @param {Record<string, any>} schematic Native schematic model.
|
|
76
|
+
* @param {Record<string, any>[]} records Ownership records.
|
|
77
|
+
* @returns {Record<string, any>} Original or normalized sheet.
|
|
78
|
+
*/
|
|
79
|
+
static #normalizeSheet(schematic, records) {
|
|
80
|
+
const sheet = schematic.sheet || {}
|
|
81
|
+
const sourceWidth = Number(sheet.sourceWidth || 0)
|
|
82
|
+
const sourceHeight = Number(sheet.sourceHeight || 0)
|
|
83
|
+
const margin = Math.max(Number(sheet.marginWidth || 20), 20)
|
|
84
|
+
const sheetRecord = records.find(
|
|
85
|
+
(record) =>
|
|
86
|
+
AltiumSchematicFidelityNormalizer.#recordType(record) === '31'
|
|
87
|
+
)
|
|
88
|
+
const sheetStyle = Number(
|
|
89
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
90
|
+
sheetRecord?.fields,
|
|
91
|
+
'SheetStyle'
|
|
92
|
+
) || 0
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
sheetStyle !== 1 ||
|
|
97
|
+
!sheet.borderOn ||
|
|
98
|
+
!sheet.paperSize ||
|
|
99
|
+
sourceWidth <= margin * 2 ||
|
|
100
|
+
sourceHeight <= margin * 2 ||
|
|
101
|
+
Number(sheet.width) >= Number(sheet.height) !==
|
|
102
|
+
sourceWidth >= sourceHeight ||
|
|
103
|
+
!AltiumSchematicFidelityNormalizer.#hasNativeFrameEdge(
|
|
104
|
+
schematic,
|
|
105
|
+
sourceWidth,
|
|
106
|
+
sourceHeight,
|
|
107
|
+
margin
|
|
108
|
+
)
|
|
109
|
+
) {
|
|
110
|
+
return sheet
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
Number(sheet.width) === sourceWidth &&
|
|
115
|
+
Number(sheet.height) === sourceHeight
|
|
116
|
+
) {
|
|
117
|
+
return sheet
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
...sheet,
|
|
122
|
+
width: sourceWidth,
|
|
123
|
+
height: sourceHeight,
|
|
124
|
+
sourceWidth,
|
|
125
|
+
sourceHeight
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Returns true when owner chrome reaches the stored native frame edge and
|
|
131
|
+
* all authored geometry remains inside that frame.
|
|
132
|
+
* @param {Record<string, any>} schematic Native schematic model.
|
|
133
|
+
* @param {number} sourceWidth Stored sheet width.
|
|
134
|
+
* @param {number} sourceHeight Stored sheet height.
|
|
135
|
+
* @param {number} margin Sheet margin.
|
|
136
|
+
* @returns {boolean} Whether the source frame is structurally proven.
|
|
137
|
+
*/
|
|
138
|
+
static #hasNativeFrameEdge(schematic, sourceWidth, sourceHeight, margin) {
|
|
139
|
+
const primitives = PRIMITIVE_FAMILIES.flatMap(
|
|
140
|
+
(family) => schematic[family] || []
|
|
141
|
+
)
|
|
142
|
+
const bounds = primitives
|
|
143
|
+
.map((primitive) => ({
|
|
144
|
+
ownerIndex: String(primitive?.ownerIndex || '').trim(),
|
|
145
|
+
bounds: AltiumSchematicFidelityNormalizer.#bounds(primitive)
|
|
146
|
+
}))
|
|
147
|
+
.filter((entry) => entry.bounds)
|
|
148
|
+
const frameEdge = sourceWidth - margin
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
bounds.some(
|
|
152
|
+
(entry) =>
|
|
153
|
+
entry.ownerIndex &&
|
|
154
|
+
Math.abs(entry.bounds.maxX - frameEdge) <= 0.01
|
|
155
|
+
) &&
|
|
156
|
+
bounds.every(
|
|
157
|
+
(entry) =>
|
|
158
|
+
entry.bounds.maxX <= sourceWidth - margin + 0.01 &&
|
|
159
|
+
entry.bounds.maxY <= sourceHeight - margin + 0.01
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Resolves every text placeholder in a native footer owner group.
|
|
166
|
+
* @param {Record<string, any>[]} texts Normalized visible texts.
|
|
167
|
+
* @param {Record<string, any>[]} records Ownership records.
|
|
168
|
+
* @param {Record<string, any>} sheet Normalized sheet.
|
|
169
|
+
* @returns {Record<string, any>[]} Original or resolved texts.
|
|
170
|
+
*/
|
|
171
|
+
static #resolveFooterTexts(texts, records, sheet) {
|
|
172
|
+
const metadata = new Map()
|
|
173
|
+
for (const record of records) {
|
|
174
|
+
if (AltiumSchematicFidelityNormalizer.#owner(record)) continue
|
|
175
|
+
const name = AltiumSchematicFidelityNormalizer.#field(
|
|
176
|
+
record.fields,
|
|
177
|
+
'Name'
|
|
178
|
+
)
|
|
179
|
+
const value = AltiumSchematicFidelityNormalizer.#field(
|
|
180
|
+
record.fields,
|
|
181
|
+
'Text'
|
|
182
|
+
)
|
|
183
|
+
if (name && value) {
|
|
184
|
+
metadata.set(name.toLowerCase(), value)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const footerOwners = new Set(
|
|
189
|
+
records
|
|
190
|
+
.filter((record) =>
|
|
191
|
+
AltiumSchematicFidelityNormalizer.#isFooterSeedRecord(
|
|
192
|
+
record,
|
|
193
|
+
sheet
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
.map((record) =>
|
|
197
|
+
AltiumSchematicFidelityNormalizer.#owner(record)
|
|
198
|
+
)
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
)
|
|
201
|
+
if (!footerOwners.size || !metadata.size) return texts
|
|
202
|
+
|
|
203
|
+
let changed = false
|
|
204
|
+
const resolvedTexts = texts.map((text) => {
|
|
205
|
+
const sourceText = String(text?.text || '').trim()
|
|
206
|
+
if (
|
|
207
|
+
!footerOwners.has(String(text?.ownerIndex || '').trim()) ||
|
|
208
|
+
!sourceText.startsWith('=')
|
|
209
|
+
) {
|
|
210
|
+
return text
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const replacement = metadata.get(sourceText.slice(1).toLowerCase())
|
|
214
|
+
if (!replacement) return text
|
|
215
|
+
|
|
216
|
+
changed = true
|
|
217
|
+
return { ...text, text: replacement }
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
return changed ? resolvedTexts : texts
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Returns true when an ownership record seeds the lower-right footer.
|
|
225
|
+
* @param {Record<string, any>} record Ownership record.
|
|
226
|
+
* @param {Record<string, any>} sheet Sheet metadata.
|
|
227
|
+
* @returns {boolean} Whether the record belongs to a footer owner.
|
|
228
|
+
*/
|
|
229
|
+
static #isFooterSeedRecord(record, sheet) {
|
|
230
|
+
if (AltiumSchematicFidelityNormalizer.#recordType(record) !== '4') {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const x = Number(
|
|
235
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
236
|
+
record.fields,
|
|
237
|
+
'Location.X'
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
const y = Number(
|
|
241
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
242
|
+
record.fields,
|
|
243
|
+
'Location.Y'
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
Number.isFinite(x) &&
|
|
249
|
+
Number.isFinite(y) &&
|
|
250
|
+
x >= Number(sheet?.width || 0) * 0.55 &&
|
|
251
|
+
y <= 100
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Moves right-side vertical component parameters clear of owner bodies.
|
|
257
|
+
* @param {Record<string, any>[]} texts Schematic text rows.
|
|
258
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerBounds Owner bounds.
|
|
259
|
+
* @returns {Record<string, any>[]} Original or placed texts.
|
|
260
|
+
*/
|
|
261
|
+
static #placeOwnerTexts(texts, ownerBounds) {
|
|
262
|
+
let changed = false
|
|
263
|
+
const placedTexts = texts.map((text) => {
|
|
264
|
+
const viewerFontSize =
|
|
265
|
+
SchematicTypography.resolveViewerFontSize(text?.fontSize) || 0
|
|
266
|
+
const x = SchematicRotatedOwnerTextPlacement.resolveX(
|
|
267
|
+
text,
|
|
268
|
+
text?.x,
|
|
269
|
+
viewerFontSize,
|
|
270
|
+
ownerBounds
|
|
271
|
+
)
|
|
272
|
+
if (x === Number(text?.x)) return text
|
|
273
|
+
|
|
274
|
+
changed = true
|
|
275
|
+
return { ...text, x }
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
return changed ? placedTexts : texts
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Completes harness child ownership from explicit and additional-list rows.
|
|
283
|
+
* @param {Record<string, any> | null | undefined} harnesses Harness model.
|
|
284
|
+
* @param {Record<string, any>[]} records Ownership records.
|
|
285
|
+
* @returns {Record<string, any> | null | undefined} Completed harness model.
|
|
286
|
+
*/
|
|
287
|
+
static #normalizeHarnesses(harnesses, records) {
|
|
288
|
+
if (!harnesses?.connectors?.length) return harnesses
|
|
289
|
+
|
|
290
|
+
let changed = false
|
|
291
|
+
const connectors = harnesses.connectors.map((connector) => {
|
|
292
|
+
const connectorRecord = records.find(
|
|
293
|
+
(record) =>
|
|
294
|
+
record.key === connector.recordKey ||
|
|
295
|
+
String(record.recordId || '') ===
|
|
296
|
+
String(connector.recordId || '')
|
|
297
|
+
)
|
|
298
|
+
if (!connectorRecord) return connector
|
|
299
|
+
|
|
300
|
+
const children = AltiumSchematicFidelityNormalizer.#harnessChildren(
|
|
301
|
+
connectorRecord,
|
|
302
|
+
records
|
|
303
|
+
)
|
|
304
|
+
const entryRecords = children.filter(
|
|
305
|
+
(record) =>
|
|
306
|
+
AltiumSchematicFidelityNormalizer.#recordType(record) ===
|
|
307
|
+
'216'
|
|
308
|
+
)
|
|
309
|
+
const typeRecord = children.find(
|
|
310
|
+
(record) =>
|
|
311
|
+
AltiumSchematicFidelityNormalizer.#recordType(record) ===
|
|
312
|
+
'217'
|
|
313
|
+
)
|
|
314
|
+
if (!entryRecords.length && !typeRecord) return connector
|
|
315
|
+
|
|
316
|
+
changed = true
|
|
317
|
+
return {
|
|
318
|
+
...connector,
|
|
319
|
+
entries: entryRecords.map((record) =>
|
|
320
|
+
AltiumSchematicFidelityNormalizer.#harnessEntry(record)
|
|
321
|
+
),
|
|
322
|
+
...(typeRecord
|
|
323
|
+
? {
|
|
324
|
+
typeLabel:
|
|
325
|
+
AltiumSchematicFidelityNormalizer.#harnessTypeLabel(
|
|
326
|
+
typeRecord
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
: {})
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
if (!changed) return harnesses
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
...harnesses,
|
|
336
|
+
connectors,
|
|
337
|
+
bundleLinks: (harnesses.bundleLinks || []).map((link, index) => {
|
|
338
|
+
const connector = connectors[index]
|
|
339
|
+
return {
|
|
340
|
+
...link,
|
|
341
|
+
harnessType:
|
|
342
|
+
connector?.typeLabel?.text ||
|
|
343
|
+
connector?.entries?.find((entry) => entry.harnessType)
|
|
344
|
+
?.harnessType,
|
|
345
|
+
entries: (connector?.entries || []).map(
|
|
346
|
+
(entry) => entry.name
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Collects connector children using native explicit and list ownership.
|
|
355
|
+
* @param {Record<string, any>} connectorRecord Connector ownership row.
|
|
356
|
+
* @param {Record<string, any>[]} records Ownership records.
|
|
357
|
+
* @returns {Record<string, any>[]} Child rows.
|
|
358
|
+
*/
|
|
359
|
+
static #harnessChildren(connectorRecord, records) {
|
|
360
|
+
const position = records.indexOf(connectorRecord)
|
|
361
|
+
const ownerKeys = new Set([
|
|
362
|
+
String(connectorRecord.recordIndex ?? ''),
|
|
363
|
+
String(Number(connectorRecord.recordIndex ?? -1) + 1),
|
|
364
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
365
|
+
connectorRecord.fields,
|
|
366
|
+
'IndexInSheet'
|
|
367
|
+
),
|
|
368
|
+
String(
|
|
369
|
+
Number(
|
|
370
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
371
|
+
connectorRecord.fields,
|
|
372
|
+
'IndexInSheet'
|
|
373
|
+
) || -1
|
|
374
|
+
) + 1
|
|
375
|
+
)
|
|
376
|
+
])
|
|
377
|
+
const children = records.filter((record) => {
|
|
378
|
+
const recordType =
|
|
379
|
+
AltiumSchematicFidelityNormalizer.#recordType(record)
|
|
380
|
+
return (
|
|
381
|
+
(recordType === '216' || recordType === '217') &&
|
|
382
|
+
ownerKeys.has(AltiumSchematicFidelityNormalizer.#owner(record))
|
|
383
|
+
)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
for (let index = position + 1; position >= 0; index += 1) {
|
|
387
|
+
const record = records[index]
|
|
388
|
+
const recordType =
|
|
389
|
+
AltiumSchematicFidelityNormalizer.#recordType(record)
|
|
390
|
+
if (recordType !== '216' && recordType !== '217') break
|
|
391
|
+
if (
|
|
392
|
+
AltiumSchematicFidelityNormalizer.#owner(record) ||
|
|
393
|
+
!ParserUtils.parseBoolean(
|
|
394
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
395
|
+
record.fields,
|
|
396
|
+
'OwnerIndexAdditionalList'
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
) {
|
|
400
|
+
break
|
|
401
|
+
}
|
|
402
|
+
if (!children.includes(record)) children.push(record)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return children
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Builds one normalized harness entry.
|
|
410
|
+
* @param {Record<string, any>} record Ownership row.
|
|
411
|
+
* @returns {Record<string, any>} Harness entry.
|
|
412
|
+
*/
|
|
413
|
+
static #harnessEntry(record) {
|
|
414
|
+
const fields = record.fields || {}
|
|
415
|
+
const whole = Number(
|
|
416
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
417
|
+
fields,
|
|
418
|
+
'DistanceFromTop'
|
|
419
|
+
) || 0
|
|
420
|
+
)
|
|
421
|
+
const fraction = Number(
|
|
422
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
423
|
+
fields,
|
|
424
|
+
'DistanceFromTop_Frac1'
|
|
425
|
+
) || 0
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
return AltiumSchematicFidelityNormalizer.#stripEmpty({
|
|
429
|
+
key: 'harness-entry-' + String(record.recordIndex ?? 0),
|
|
430
|
+
recordKey: record.key,
|
|
431
|
+
name: AltiumSchematicFidelityNormalizer.#field(fields, 'Name'),
|
|
432
|
+
side: AltiumSchematicFidelityNormalizer.#side(
|
|
433
|
+
AltiumSchematicFidelityNormalizer.#field(fields, 'Side')
|
|
434
|
+
),
|
|
435
|
+
distanceFromTop: Number(
|
|
436
|
+
(whole * 10 + fraction / 100000).toFixed(4)
|
|
437
|
+
),
|
|
438
|
+
harnessType: AltiumSchematicFidelityNormalizer.#field(
|
|
439
|
+
fields,
|
|
440
|
+
'HarnessType'
|
|
441
|
+
),
|
|
442
|
+
textStyle: AltiumSchematicFidelityNormalizer.#textStyle(
|
|
443
|
+
AltiumSchematicFidelityNormalizer.#field(fields, 'TextStyle')
|
|
444
|
+
),
|
|
445
|
+
textColor: ParserUtils.toColor(fields.TEXTCOLOR, '#000000')
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Builds one normalized harness type label.
|
|
451
|
+
* @param {Record<string, any>} record Ownership row.
|
|
452
|
+
* @returns {Record<string, any>} Harness type label.
|
|
453
|
+
*/
|
|
454
|
+
static #harnessTypeLabel(record) {
|
|
455
|
+
const fields = record.fields || {}
|
|
456
|
+
return {
|
|
457
|
+
key: 'harness-type-' + String(record.recordIndex ?? 0),
|
|
458
|
+
recordKey: record.key,
|
|
459
|
+
text: AltiumSchematicFidelityNormalizer.#field(fields, 'Text'),
|
|
460
|
+
x: Number(
|
|
461
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
462
|
+
fields,
|
|
463
|
+
'Location.X'
|
|
464
|
+
) || 0
|
|
465
|
+
),
|
|
466
|
+
y: Number(
|
|
467
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
468
|
+
fields,
|
|
469
|
+
'Location.Y'
|
|
470
|
+
) || 0
|
|
471
|
+
),
|
|
472
|
+
color: ParserUtils.toColor(fields.COLOR, '#000000')
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Collects owner envelopes for rotated parameter placement.
|
|
478
|
+
* @param {Record<string, any>} schematic Native schematic model.
|
|
479
|
+
* @returns {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>}
|
|
480
|
+
*/
|
|
481
|
+
static #collectOwnerBounds(schematic) {
|
|
482
|
+
const ownerBounds = new Map()
|
|
483
|
+
for (const primitive of PRIMITIVE_FAMILIES.flatMap(
|
|
484
|
+
(family) => schematic[family] || []
|
|
485
|
+
)) {
|
|
486
|
+
const owner = String(primitive?.ownerIndex || '').trim()
|
|
487
|
+
const bounds = AltiumSchematicFidelityNormalizer.#bounds(primitive)
|
|
488
|
+
if (!owner || !bounds) continue
|
|
489
|
+
|
|
490
|
+
const current = ownerBounds.get(owner)
|
|
491
|
+
if (!current) {
|
|
492
|
+
ownerBounds.set(owner, { ...bounds })
|
|
493
|
+
continue
|
|
494
|
+
}
|
|
495
|
+
current.minX = Math.min(current.minX, bounds.minX)
|
|
496
|
+
current.minY = Math.min(current.minY, bounds.minY)
|
|
497
|
+
current.maxX = Math.max(current.maxX, bounds.maxX)
|
|
498
|
+
current.maxY = Math.max(current.maxY, bounds.maxY)
|
|
499
|
+
}
|
|
500
|
+
return ownerBounds
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Resolves primitive coordinate bounds.
|
|
505
|
+
* @param {Record<string, any>} primitive Primitive row.
|
|
506
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
507
|
+
*/
|
|
508
|
+
static #bounds(primitive) {
|
|
509
|
+
const points = []
|
|
510
|
+
if (Array.isArray(primitive?.points)) points.push(...primitive.points)
|
|
511
|
+
if (
|
|
512
|
+
Number.isFinite(Number(primitive?.x1)) &&
|
|
513
|
+
Number.isFinite(Number(primitive?.y1)) &&
|
|
514
|
+
Number.isFinite(Number(primitive?.x2)) &&
|
|
515
|
+
Number.isFinite(Number(primitive?.y2))
|
|
516
|
+
) {
|
|
517
|
+
points.push(
|
|
518
|
+
{ x: Number(primitive.x1), y: Number(primitive.y1) },
|
|
519
|
+
{ x: Number(primitive.x2), y: Number(primitive.y2) }
|
|
520
|
+
)
|
|
521
|
+
} else if (
|
|
522
|
+
Number.isFinite(Number(primitive?.x)) &&
|
|
523
|
+
Number.isFinite(Number(primitive?.y)) &&
|
|
524
|
+
Number.isFinite(Number(primitive?.width)) &&
|
|
525
|
+
Number.isFinite(Number(primitive?.height))
|
|
526
|
+
) {
|
|
527
|
+
points.push(
|
|
528
|
+
{ x: Number(primitive.x), y: Number(primitive.y) },
|
|
529
|
+
{
|
|
530
|
+
x: Number(primitive.x) + Number(primitive.width),
|
|
531
|
+
y: Number(primitive.y) + Number(primitive.height)
|
|
532
|
+
}
|
|
533
|
+
)
|
|
534
|
+
} else if (
|
|
535
|
+
Number.isFinite(Number(primitive?.x)) &&
|
|
536
|
+
Number.isFinite(Number(primitive?.y)) &&
|
|
537
|
+
(Number.isFinite(Number(primitive?.radius)) ||
|
|
538
|
+
Number.isFinite(Number(primitive?.radiusX)))
|
|
539
|
+
) {
|
|
540
|
+
const radiusX = Math.abs(
|
|
541
|
+
Number(primitive.radiusX ?? primitive.radius)
|
|
542
|
+
)
|
|
543
|
+
const radiusY = Math.abs(
|
|
544
|
+
Number(primitive.radiusY ?? primitive.radius)
|
|
545
|
+
)
|
|
546
|
+
points.push(
|
|
547
|
+
{
|
|
548
|
+
x: Number(primitive.x) - radiusX,
|
|
549
|
+
y: Number(primitive.y) - radiusY
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
x: Number(primitive.x) + radiusX,
|
|
553
|
+
y: Number(primitive.y) + radiusY
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const finitePoints = points.filter(
|
|
559
|
+
(point) =>
|
|
560
|
+
Number.isFinite(Number(point?.x)) &&
|
|
561
|
+
Number.isFinite(Number(point?.y))
|
|
562
|
+
)
|
|
563
|
+
if (!finitePoints.length) return null
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
minX: Math.min(...finitePoints.map((point) => Number(point.x))),
|
|
567
|
+
minY: Math.min(...finitePoints.map((point) => Number(point.y))),
|
|
568
|
+
maxX: Math.max(...finitePoints.map((point) => Number(point.x))),
|
|
569
|
+
maxY: Math.max(...finitePoints.map((point) => Number(point.y)))
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Resolves one ownership record type.
|
|
575
|
+
* @param {Record<string, any> | undefined} record Ownership row.
|
|
576
|
+
* @returns {string} Record type.
|
|
577
|
+
*/
|
|
578
|
+
static #recordType(record) {
|
|
579
|
+
return String(
|
|
580
|
+
record?.recordType ??
|
|
581
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
582
|
+
record?.fields,
|
|
583
|
+
'Record'
|
|
584
|
+
) ??
|
|
585
|
+
''
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Resolves one ownership key.
|
|
591
|
+
* @param {Record<string, any> | undefined} record Ownership row.
|
|
592
|
+
* @returns {string} Owner key.
|
|
593
|
+
*/
|
|
594
|
+
static #owner(record) {
|
|
595
|
+
return String(
|
|
596
|
+
record?.ownerIndex ??
|
|
597
|
+
AltiumSchematicFidelityNormalizer.#field(
|
|
598
|
+
record?.fields,
|
|
599
|
+
'OwnerIndex'
|
|
600
|
+
) ??
|
|
601
|
+
''
|
|
602
|
+
).trim()
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Reads one case-insensitive raw field.
|
|
607
|
+
* @param {Record<string, any> | undefined} fields Raw fields.
|
|
608
|
+
* @param {string} name Field name.
|
|
609
|
+
* @returns {string} Field value.
|
|
610
|
+
*/
|
|
611
|
+
static #field(fields, name) {
|
|
612
|
+
if (!fields) return ''
|
|
613
|
+
const key = Object.keys(fields).find(
|
|
614
|
+
(candidate) => candidate.toLowerCase() === name.toLowerCase()
|
|
615
|
+
)
|
|
616
|
+
const value = key ? fields[key] : ''
|
|
617
|
+
return String(Array.isArray(value) ? value.at(-1) || '' : value || '')
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Resolves native side codes.
|
|
622
|
+
* @param {string | number} value Native side code.
|
|
623
|
+
* @returns {'left' | 'right' | 'top' | 'bottom'} Side label.
|
|
624
|
+
*/
|
|
625
|
+
static #side(value) {
|
|
626
|
+
return ['left', 'right', 'top', 'bottom'][Number(value)] || 'left'
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Resolves native harness entry text style.
|
|
631
|
+
* @param {string} value Native style.
|
|
632
|
+
* @returns {string} Normalized style.
|
|
633
|
+
*/
|
|
634
|
+
static #textStyle(value) {
|
|
635
|
+
const normalized = String(value || '').toLowerCase()
|
|
636
|
+
if (normalized === '1' || normalized === 'abbreviated') {
|
|
637
|
+
return 'abbreviated'
|
|
638
|
+
}
|
|
639
|
+
if (normalized === '2' || normalized === 'short') return 'short'
|
|
640
|
+
return 'full'
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Removes empty fields while retaining false and zero.
|
|
645
|
+
* @param {Record<string, any>} value Candidate record.
|
|
646
|
+
* @returns {Record<string, any>} Compact record.
|
|
647
|
+
*/
|
|
648
|
+
static #stripEmpty(value) {
|
|
649
|
+
return Object.fromEntries(
|
|
650
|
+
Object.entries(value).filter(
|
|
651
|
+
([, fieldValue]) =>
|
|
652
|
+
fieldValue !== null &&
|
|
653
|
+
fieldValue !== undefined &&
|
|
654
|
+
fieldValue !== ''
|
|
655
|
+
)
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
Object.freeze(AltiumSchematicFidelityNormalizer.prototype)
|
|
661
|
+
Object.freeze(AltiumSchematicFidelityNormalizer)
|