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.
@@ -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)