altium-toolkit 1.0.2 → 1.0.8
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/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +17 -6
- package/src/core/altium/AsciiRecordParser.mjs +28 -1
- package/src/core/altium/SchematicPinDesignatorInferer.mjs +36 -3
- package/src/core/altium/SchematicPinParser.mjs +139 -14
- package/src/core/altium/SchematicPrimitiveParser.mjs +75 -4
- package/src/core/altium/SchematicTextParser.mjs +22 -18
- package/src/core/altium/SchematicTextPostProcessor.mjs +127 -10
- package/src/core/altium/SchematicWireNormalizer.mjs +1162 -0
- package/src/renderers.mjs +3 -0
- package/src/styles/altium-renderers.css +6 -0
- package/src/ui/PcbInteractionGeometry.mjs +350 -0
- package/src/ui/PcbInteractionIndex.mjs +588 -0
- package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
- package/src/ui/PcbInteractionLayerModel.mjs +99 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
- package/src/ui/PcbScene3dBuilder.mjs +32 -4
- package/src/ui/PcbSvgRenderer.mjs +2 -2
- package/src/ui/PcbTextPrimitiveRenderer.mjs +58 -2
- package/src/ui/SchematicContentLayout.mjs +124 -22
- package/src/ui/SchematicJunctionRenderer.mjs +21 -3
- package/src/ui/SchematicNoteRenderer.mjs +75 -10
- package/src/ui/SchematicPinSvgRenderer.mjs +48 -7
- package/src/ui/SchematicPowerPortRenderer.mjs +53 -160
- package/src/ui/SchematicSheetChromeRenderer.mjs +29 -15
- package/src/ui/SchematicSvgRenderer.mjs +341 -39
- package/src/ui/SchematicSvgUtils.mjs +9 -2
- package/src/ui/SchematicTypography.mjs +13 -10
package/package.json
CHANGED
|
@@ -27,6 +27,7 @@ import { SchematicImageParser } from './SchematicImageParser.mjs'
|
|
|
27
27
|
import { SchematicNetlistBuilder } from './SchematicNetlistBuilder.mjs'
|
|
28
28
|
import { SchematicComponentTextResolver } from './SchematicComponentTextResolver.mjs'
|
|
29
29
|
import { SchematicStreamExtractor } from './SchematicStreamExtractor.mjs'
|
|
30
|
+
import { SchematicWireNormalizer } from './SchematicWireNormalizer.mjs'
|
|
30
31
|
import { CircuitJsonModelAdapter } from '../circuit-json/CircuitJsonModelAdapter.mjs'
|
|
31
32
|
const {
|
|
32
33
|
countMatchingKeys,
|
|
@@ -281,7 +282,7 @@ export class AltiumParser {
|
|
|
281
282
|
)
|
|
282
283
|
}
|
|
283
284
|
|
|
284
|
-
|
|
285
|
+
let lines = [
|
|
285
286
|
...lineRecords.map((record, index) => ({
|
|
286
287
|
x1: parseNumericField(record.fields, 'Location.X') || 0,
|
|
287
288
|
y1: parseNumericField(record.fields, 'Location.Y') || 0,
|
|
@@ -325,8 +326,22 @@ export class AltiumParser {
|
|
|
325
326
|
)
|
|
326
327
|
)
|
|
327
328
|
]
|
|
328
|
-
const
|
|
329
|
+
const pins = parseSchematicPins(pinRecords)
|
|
330
|
+
const junctions = SchematicJunctionParser.parseSchematicJunctions(
|
|
331
|
+
recordIndexAwareRecords
|
|
332
|
+
)
|
|
333
|
+
lines = SchematicWireNormalizer.extendCollapsedPolylineEndpoints(
|
|
334
|
+
lines,
|
|
335
|
+
pins,
|
|
336
|
+
junctions
|
|
337
|
+
)
|
|
338
|
+
let polygons =
|
|
329
339
|
SchematicPrimitiveParser.parseSchematicPolygons(polygonRecords)
|
|
340
|
+
;({ lines, polygons } =
|
|
341
|
+
SchematicWireNormalizer.normalizeStandaloneCalloutArrowheads(
|
|
342
|
+
lines,
|
|
343
|
+
polygons
|
|
344
|
+
))
|
|
330
345
|
const arcs = SchematicPrimitiveParser.parseSchematicArcs(arcRecords)
|
|
331
346
|
const ellipses =
|
|
332
347
|
SchematicPrimitiveParser.parseSchematicEllipses(ellipseRecords)
|
|
@@ -348,9 +363,6 @@ export class AltiumParser {
|
|
|
348
363
|
const { sheetSymbols, sheetEntries } = SchematicSheetParser.parse(
|
|
349
364
|
recordIndexAwareRecords
|
|
350
365
|
)
|
|
351
|
-
const junctions = SchematicJunctionParser.parseSchematicJunctions(
|
|
352
|
-
recordIndexAwareRecords
|
|
353
|
-
)
|
|
354
366
|
const busEntries = SchematicBusEntryParser.parseSchematicBusEntries(
|
|
355
367
|
recordIndexAwareRecords
|
|
356
368
|
)
|
|
@@ -360,7 +372,6 @@ export class AltiumParser {
|
|
|
360
372
|
arrayBuffer
|
|
361
373
|
)
|
|
362
374
|
|
|
363
|
-
const pins = parseSchematicPins(pinRecords)
|
|
364
375
|
const ports = parseSchematicPorts(portRecords, lines)
|
|
365
376
|
const crosses = parseSchematicCrosses(crossRecords)
|
|
366
377
|
let texts = drawableTextRecords
|
|
@@ -26,7 +26,16 @@ export class AsciiRecordParser {
|
|
|
26
26
|
|
|
27
27
|
for (const chunk of chunks) {
|
|
28
28
|
const candidate = chunk.trim()
|
|
29
|
-
if (!AsciiRecordParser.#isRecordCandidate(candidate))
|
|
29
|
+
if (!AsciiRecordParser.#isRecordCandidate(candidate)) {
|
|
30
|
+
if (
|
|
31
|
+
AsciiRecordParser.#isRecordFieldPrefixFragment(
|
|
32
|
+
candidate
|
|
33
|
+
)
|
|
34
|
+
) {
|
|
35
|
+
pendingPrefix += candidate
|
|
36
|
+
}
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
30
39
|
|
|
31
40
|
const headerPrefix =
|
|
32
41
|
AsciiRecordParser.#extractHeaderFieldPrefix(candidate)
|
|
@@ -65,6 +74,24 @@ export class AsciiRecordParser {
|
|
|
65
74
|
return candidate.split('|').length >= 4
|
|
66
75
|
}
|
|
67
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Returns true when a short printable fragment contains fields that belong
|
|
79
|
+
* to the next record marker in the same run.
|
|
80
|
+
* @param {string} candidate
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
static #isRecordFieldPrefixFragment(candidate) {
|
|
84
|
+
if (!candidate.startsWith('|')) return false
|
|
85
|
+
if (!candidate.includes('=')) return false
|
|
86
|
+
if (AsciiRecordParser.#hasRecordMarker(candidate)) return false
|
|
87
|
+
|
|
88
|
+
const segments = candidate.split('|').filter(Boolean)
|
|
89
|
+
return segments.every((segment) => {
|
|
90
|
+
const separatorIndex = segment.indexOf('=')
|
|
91
|
+
return separatorIndex > 0
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
/**
|
|
69
96
|
* Returns true when a printable fragment contains its marker field.
|
|
70
97
|
* @param {string} candidate
|
|
@@ -9,8 +9,8 @@ export class SchematicPinDesignatorInferer {
|
|
|
9
9
|
/**
|
|
10
10
|
* Infers omitted source-order pin numbers for compact four-pin symbols
|
|
11
11
|
* whose printable records keep enough numeric hints to prove the sequence.
|
|
12
|
-
* @param {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
13
|
-
* @returns {{ x: number, y: number, length: number, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[] | null}
|
|
12
|
+
* @param {{ x: number, y: number, length: number, name?: string, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
13
|
+
* @returns {{ x: number, y: number, length: number, name?: string, designator: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[] | null}
|
|
14
14
|
*/
|
|
15
15
|
static inferSequentialCompactFourPinDesignators(pins) {
|
|
16
16
|
if (
|
|
@@ -40,7 +40,16 @@ export class SchematicPinDesignatorInferer {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
if (explicitCount < 2
|
|
43
|
+
if (explicitCount < 2) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
explicitCount === pins.length &&
|
|
49
|
+
!SchematicPinDesignatorInferer.#hasRepeatedCompactTerminalNames(
|
|
50
|
+
pins
|
|
51
|
+
)
|
|
52
|
+
) {
|
|
44
53
|
return null
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -50,6 +59,30 @@ export class SchematicPinDesignatorInferer {
|
|
|
50
59
|
}))
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Returns true when a compact four-pin owner repeats internal terminal
|
|
64
|
+
* names, so visible pin numbers are the useful external labels.
|
|
65
|
+
* @param {{ name?: string }[]} pins
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
static #hasRepeatedCompactTerminalNames(pins) {
|
|
69
|
+
const names = pins.map((pin) => String(pin.name || '').trim())
|
|
70
|
+
|
|
71
|
+
if (names.some((name) => !name)) {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const counts = new Map()
|
|
76
|
+
for (const name of names) {
|
|
77
|
+
counts.set(name, (counts.get(name) || 0) + 1)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
counts.size < pins.length &&
|
|
82
|
+
[...counts.values()].every((count) => count > 1)
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
53
86
|
/**
|
|
54
87
|
* Infers omitted numeric labels for compact two-column owners whose
|
|
55
88
|
* physical side geometry implies the same sequence Altium displays.
|
|
@@ -71,6 +71,11 @@ export class SchematicPinParser {
|
|
|
71
71
|
record.fields,
|
|
72
72
|
'SymBol_Outer'
|
|
73
73
|
) || undefined,
|
|
74
|
+
color: ParserUtils.toColor(record.fields.Color, '#000000'),
|
|
75
|
+
labelColor: ParserUtils.toColor(
|
|
76
|
+
record.fields.TextColor,
|
|
77
|
+
'#1f1f1f'
|
|
78
|
+
),
|
|
74
79
|
ownerIndex
|
|
75
80
|
})
|
|
76
81
|
}
|
|
@@ -397,7 +402,7 @@ export class SchematicPinParser {
|
|
|
397
402
|
* Expands a schematic polyline record into drawable line segments.
|
|
398
403
|
* @param {Record<string, string | string[]>} fields
|
|
399
404
|
* @param {{ isBus?: boolean, recordType?: string }} [options]
|
|
400
|
-
* @returns {{ x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle: number, isBus?: boolean, recordType?: string }[]}
|
|
405
|
+
* @returns {{ x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle: number, isBus?: boolean, recordType?: string, omittedEndpointAxis?: 'x' | 'y', sourceLocationCount?: number }[]}
|
|
401
406
|
*/
|
|
402
407
|
static parseSchematicPolyline(fields, options = {}) {
|
|
403
408
|
const points = SchematicPinParser.#collectSchematicPointList(fields)
|
|
@@ -408,17 +413,27 @@ export class SchematicPinParser {
|
|
|
408
413
|
for (let index = 1; index < points.length; index += 1) {
|
|
409
414
|
const previous = points[index - 1]
|
|
410
415
|
const current = points[index]
|
|
416
|
+
const omittedEndpointAxis =
|
|
417
|
+
SchematicPinParser.#resolveOmittedPointAxis(current)
|
|
411
418
|
|
|
412
419
|
segments.push({
|
|
413
420
|
x1: previous.x,
|
|
414
421
|
y1: previous.y,
|
|
415
422
|
x2: current.x,
|
|
416
423
|
y2: current.y,
|
|
417
|
-
color: ParserUtils.toColor(
|
|
424
|
+
color: ParserUtils.toColor(
|
|
425
|
+
fields.Color,
|
|
426
|
+
SchematicPinParser.#resolveDefaultPolylineColor(
|
|
427
|
+
fields,
|
|
428
|
+
options.recordType
|
|
429
|
+
)
|
|
430
|
+
),
|
|
418
431
|
width: ParserUtils.parseNumericField(fields, 'LineWidth') || 1,
|
|
419
432
|
lineStyle,
|
|
420
433
|
isBus: options.isBus === true ? true : undefined,
|
|
421
|
-
recordType: options.recordType || undefined
|
|
434
|
+
recordType: options.recordType || undefined,
|
|
435
|
+
omittedEndpointAxis: omittedEndpointAxis || undefined,
|
|
436
|
+
sourceLocationCount: points.length
|
|
422
437
|
})
|
|
423
438
|
}
|
|
424
439
|
|
|
@@ -449,7 +464,10 @@ export class SchematicPinParser {
|
|
|
449
464
|
y1: previous.y,
|
|
450
465
|
x2: current.x,
|
|
451
466
|
y2: current.y,
|
|
452
|
-
color: ParserUtils.toColor(
|
|
467
|
+
color: ParserUtils.toColor(
|
|
468
|
+
fields.Color,
|
|
469
|
+
SchematicPinParser.#resolveDefaultPolylineColor(fields, '7')
|
|
470
|
+
),
|
|
453
471
|
width: ParserUtils.parseNumericField(fields, 'LineWidth') || 1,
|
|
454
472
|
lineStyle
|
|
455
473
|
})
|
|
@@ -463,7 +481,10 @@ export class SchematicPinParser {
|
|
|
463
481
|
y1: lastPoint.y,
|
|
464
482
|
x2: firstPoint.x,
|
|
465
483
|
y2: firstPoint.y,
|
|
466
|
-
color: ParserUtils.toColor(
|
|
484
|
+
color: ParserUtils.toColor(
|
|
485
|
+
fields.Color,
|
|
486
|
+
SchematicPinParser.#resolveDefaultPolylineColor(fields, '7')
|
|
487
|
+
),
|
|
467
488
|
width: ParserUtils.parseNumericField(fields, 'LineWidth') || 1,
|
|
468
489
|
lineStyle
|
|
469
490
|
})
|
|
@@ -489,6 +510,21 @@ export class SchematicPinParser {
|
|
|
489
510
|
return ParserUtils.parseNumericField(fields, 'LineStyle') || 0
|
|
490
511
|
}
|
|
491
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Resolves the fallback stroke color for schematic drawing primitives.
|
|
515
|
+
* @param {Record<string, string | string[]>} fields
|
|
516
|
+
* @param {string | undefined} recordType
|
|
517
|
+
* @returns {string}
|
|
518
|
+
*/
|
|
519
|
+
static #resolveDefaultPolylineColor(fields, recordType) {
|
|
520
|
+
const resolvedRecordType =
|
|
521
|
+
recordType || ParserUtils.getField(fields, 'RECORD')
|
|
522
|
+
|
|
523
|
+
return resolvedRecordType === '6' || resolvedRecordType === '7'
|
|
524
|
+
? '#000000'
|
|
525
|
+
: '#a44a1b'
|
|
526
|
+
}
|
|
527
|
+
|
|
492
528
|
/**
|
|
493
529
|
* Collects a schematic point list, carrying forward a missing coordinate
|
|
494
530
|
* axis from the preceding point when Altium omitted an unchanged value.
|
|
@@ -500,6 +536,7 @@ export class SchematicPinParser {
|
|
|
500
536
|
fields,
|
|
501
537
|
'LocationCount'
|
|
502
538
|
)
|
|
539
|
+
const closesPolygon = ParserUtils.getField(fields, 'RECORD') === '7'
|
|
503
540
|
|
|
504
541
|
if (locationCount === null || locationCount < 2) {
|
|
505
542
|
return []
|
|
@@ -524,17 +561,92 @@ export class SchematicPinParser {
|
|
|
524
561
|
break
|
|
525
562
|
}
|
|
526
563
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
564
|
+
const point =
|
|
565
|
+
closesPolygon && index === locationCount
|
|
566
|
+
? SchematicPinParser.#resolveCollapsedFinalPolygonPoint(
|
|
567
|
+
x,
|
|
568
|
+
y,
|
|
569
|
+
pointX,
|
|
570
|
+
pointY,
|
|
571
|
+
points
|
|
572
|
+
)
|
|
573
|
+
: { x: pointX, y: pointY }
|
|
574
|
+
|
|
575
|
+
points.push({ ...point, sourceX: x, sourceY: y })
|
|
576
|
+
previousX = point.x
|
|
577
|
+
previousY = point.y
|
|
530
578
|
}
|
|
531
579
|
|
|
532
580
|
return points
|
|
533
581
|
}
|
|
534
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Resolves which coordinate axis was omitted on one source point.
|
|
585
|
+
* @param {{ sourceX?: number | null, sourceY?: number | null }} point
|
|
586
|
+
* @returns {'x' | 'y' | null}
|
|
587
|
+
*/
|
|
588
|
+
static #resolveOmittedPointAxis(point) {
|
|
589
|
+
if (point.sourceX === null && point.sourceY !== null) {
|
|
590
|
+
return 'x'
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (point.sourceX !== null && point.sourceY === null) {
|
|
594
|
+
return 'y'
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return null
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Recovers a closed polygon's final omitted axis when carrying the previous
|
|
602
|
+
* point would collapse the last side into a duplicate point.
|
|
603
|
+
* @param {number | null} sourceX
|
|
604
|
+
* @param {number | null} sourceY
|
|
605
|
+
* @param {number} pointX
|
|
606
|
+
* @param {number} pointY
|
|
607
|
+
* @param {{ x: number, y: number }[]} points
|
|
608
|
+
* @returns {{ x: number, y: number }}
|
|
609
|
+
*/
|
|
610
|
+
static #resolveCollapsedFinalPolygonPoint(
|
|
611
|
+
sourceX,
|
|
612
|
+
sourceY,
|
|
613
|
+
pointX,
|
|
614
|
+
pointY,
|
|
615
|
+
points
|
|
616
|
+
) {
|
|
617
|
+
const previousPoint = points.at(-1)
|
|
618
|
+
const firstPoint = points[0]
|
|
619
|
+
|
|
620
|
+
if (!previousPoint || !firstPoint) {
|
|
621
|
+
return { x: pointX, y: pointY }
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (
|
|
625
|
+
sourceY === null &&
|
|
626
|
+
sourceX !== null &&
|
|
627
|
+
firstPoint.y !== previousPoint.y
|
|
628
|
+
) {
|
|
629
|
+
if (pointX === previousPoint.x && pointY === previousPoint.y) {
|
|
630
|
+
return { x: pointX, y: firstPoint.y }
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (
|
|
635
|
+
sourceX === null &&
|
|
636
|
+
sourceY !== null &&
|
|
637
|
+
firstPoint.x !== previousPoint.x
|
|
638
|
+
) {
|
|
639
|
+
if (pointX === previousPoint.x && pointY === previousPoint.y) {
|
|
640
|
+
return { x: firstPoint.x, y: pointY }
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return { x: pointX, y: pointY }
|
|
645
|
+
}
|
|
646
|
+
|
|
535
647
|
/**
|
|
536
648
|
* Deduces the visible pins for one schematic symbol owner.
|
|
537
|
-
* @param {{ x: number, y: number, length: number, conglomerate?: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, ownerIndex: string }[]} pins
|
|
649
|
+
* @param {{ x: number, y: number, length: number, conglomerate?: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, color?: string, labelColor?: string, ownerIndex: string }[]} pins
|
|
538
650
|
* @returns {{ x: number, y: number, length: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, color: string, labelColor: string, labelMode: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', ownerIndex: string }[]}
|
|
539
651
|
*/
|
|
540
652
|
static #normalizeSchematicPinGroup(pins) {
|
|
@@ -626,8 +738,8 @@ export class SchematicPinParser {
|
|
|
626
738
|
|
|
627
739
|
return normalizedPins.map(({ conglomerate, ...pin }) => ({
|
|
628
740
|
...pin,
|
|
629
|
-
color: '#
|
|
630
|
-
labelColor: '#1f1f1f',
|
|
741
|
+
color: pin.color || '#000000',
|
|
742
|
+
labelColor: pin.labelColor || '#1f1f1f',
|
|
631
743
|
labelMode
|
|
632
744
|
}))
|
|
633
745
|
}
|
|
@@ -649,7 +761,7 @@ export class SchematicPinParser {
|
|
|
649
761
|
pins.length < 3 ||
|
|
650
762
|
pins.length > 4 ||
|
|
651
763
|
orientationCount < 3 ||
|
|
652
|
-
pins
|
|
764
|
+
!SchematicPinParser.#hasOptionalNumericPinDesignators(pins)
|
|
653
765
|
) {
|
|
654
766
|
return false
|
|
655
767
|
}
|
|
@@ -663,6 +775,19 @@ export class SchematicPinParser {
|
|
|
663
775
|
)
|
|
664
776
|
}
|
|
665
777
|
|
|
778
|
+
/**
|
|
779
|
+
* Returns true when compact owner-drawn terminal glyph pins have either no
|
|
780
|
+
* external designators or ordinary numeric pin numbers.
|
|
781
|
+
* @param {{ designator: string }[]} pins
|
|
782
|
+
* @returns {boolean}
|
|
783
|
+
*/
|
|
784
|
+
static #hasOptionalNumericPinDesignators(pins) {
|
|
785
|
+
return pins.every((pin) => {
|
|
786
|
+
const designator = String(pin.designator || '').trim()
|
|
787
|
+
return !designator || /^\d+$/.test(designator)
|
|
788
|
+
})
|
|
789
|
+
}
|
|
790
|
+
|
|
666
791
|
/**
|
|
667
792
|
* Returns true for one-letter terminal glyphs commonly drawn inside
|
|
668
793
|
* transistor-style schematic symbols.
|
|
@@ -726,8 +851,8 @@ export class SchematicPinParser {
|
|
|
726
851
|
|
|
727
852
|
/**
|
|
728
853
|
* Removes duplicate pin records emitted for alternate display modes.
|
|
729
|
-
* @param {{ x: number, y: number, length: number, conglomerate?: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, ownerIndex: string }[]} pins
|
|
730
|
-
* @returns {{ x: number, y: number, length: number, conglomerate?: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, ownerIndex: string }[]}
|
|
854
|
+
* @param {{ x: number, y: number, length: number, conglomerate?: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, color?: string, labelColor?: string, ownerIndex: string }[]} pins
|
|
855
|
+
* @returns {{ x: number, y: number, length: number, conglomerate?: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, color?: string, labelColor?: string, ownerIndex: string }[]}
|
|
731
856
|
*/
|
|
732
857
|
static #dedupeSchematicPins(pins) {
|
|
733
858
|
const seen = new Set()
|
|
@@ -96,10 +96,12 @@ export class SchematicPrimitiveParser {
|
|
|
96
96
|
return null
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
const color = toColor(record.fields.Color, '#000000')
|
|
100
|
+
|
|
99
101
|
return {
|
|
100
102
|
points,
|
|
101
|
-
color
|
|
102
|
-
fill: toColor(record.fields.AreaColor,
|
|
103
|
+
color,
|
|
104
|
+
fill: toColor(record.fields.AreaColor, color),
|
|
103
105
|
isSolid: parseBoolean(record.fields.IsSolid),
|
|
104
106
|
transparent: parseBoolean(record.fields.Transparent),
|
|
105
107
|
lineWidth:
|
|
@@ -699,18 +701,87 @@ export class SchematicPrimitiveParser {
|
|
|
699
701
|
}
|
|
700
702
|
|
|
701
703
|
const points = []
|
|
704
|
+
let previousX = null
|
|
705
|
+
let previousY = null
|
|
702
706
|
|
|
703
707
|
for (let index = 1; index <= locationCount; index += 1) {
|
|
704
708
|
const x = parseNumericField(fields, 'X' + index)
|
|
705
709
|
const y = parseNumericField(fields, 'Y' + index)
|
|
706
710
|
|
|
707
|
-
if (x === null
|
|
711
|
+
if (x === null && y === null) {
|
|
712
|
+
break
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const pointX = x === null ? previousX : x
|
|
716
|
+
const pointY = y === null ? previousY : y
|
|
717
|
+
|
|
718
|
+
if (pointX === null || pointY === null) {
|
|
708
719
|
break
|
|
709
720
|
}
|
|
710
721
|
|
|
711
|
-
|
|
722
|
+
const point =
|
|
723
|
+
index === locationCount
|
|
724
|
+
? SchematicPrimitiveParser.#resolveCollapsedFinalPolygonPoint(
|
|
725
|
+
x,
|
|
726
|
+
y,
|
|
727
|
+
pointX,
|
|
728
|
+
pointY,
|
|
729
|
+
points
|
|
730
|
+
)
|
|
731
|
+
: { x: pointX, y: pointY }
|
|
732
|
+
|
|
733
|
+
points.push(point)
|
|
734
|
+
previousX = point.x
|
|
735
|
+
previousY = point.y
|
|
712
736
|
}
|
|
713
737
|
|
|
714
738
|
return points
|
|
715
739
|
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Recovers a closed polygon's final omitted axis when carrying the previous
|
|
743
|
+
* point would collapse the last side into a duplicate point.
|
|
744
|
+
* @param {number | null} sourceX
|
|
745
|
+
* @param {number | null} sourceY
|
|
746
|
+
* @param {number} pointX
|
|
747
|
+
* @param {number} pointY
|
|
748
|
+
* @param {{ x: number, y: number }[]} points
|
|
749
|
+
* @returns {{ x: number, y: number }}
|
|
750
|
+
*/
|
|
751
|
+
static #resolveCollapsedFinalPolygonPoint(
|
|
752
|
+
sourceX,
|
|
753
|
+
sourceY,
|
|
754
|
+
pointX,
|
|
755
|
+
pointY,
|
|
756
|
+
points
|
|
757
|
+
) {
|
|
758
|
+
const previousPoint = points.at(-1)
|
|
759
|
+
const firstPoint = points[0]
|
|
760
|
+
|
|
761
|
+
if (!previousPoint || !firstPoint) {
|
|
762
|
+
return { x: pointX, y: pointY }
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (
|
|
766
|
+
sourceY === null &&
|
|
767
|
+
sourceX !== null &&
|
|
768
|
+
firstPoint.y !== previousPoint.y
|
|
769
|
+
) {
|
|
770
|
+
if (pointX === previousPoint.x && pointY === previousPoint.y) {
|
|
771
|
+
return { x: pointX, y: firstPoint.y }
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (
|
|
776
|
+
sourceX === null &&
|
|
777
|
+
sourceY !== null &&
|
|
778
|
+
firstPoint.x !== previousPoint.x
|
|
779
|
+
) {
|
|
780
|
+
if (pointX === previousPoint.x && pointY === previousPoint.y) {
|
|
781
|
+
return { x: firstPoint.x, y: pointY }
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return { x: pointX, y: pointY }
|
|
786
|
+
}
|
|
716
787
|
}
|
|
@@ -33,7 +33,7 @@ export class SchematicTextParser {
|
|
|
33
33
|
/**
|
|
34
34
|
* Builds a font table from the sheet header.
|
|
35
35
|
* @param {Record<string, string | string[]> | undefined} fields
|
|
36
|
-
* @returns {Record<string, { size: number, family: string, bold: boolean, rotation: number }>}
|
|
36
|
+
* @returns {Record<string, { size: number, family: string, bold: boolean, italic: boolean, rotation: number }>}
|
|
37
37
|
*/
|
|
38
38
|
static extractSchematicFonts(fields) {
|
|
39
39
|
const count = ParserUtils.parseNumericField(fields, 'FontIdCount') || 0
|
|
@@ -47,6 +47,7 @@ export class SchematicTextParser {
|
|
|
47
47
|
ParserUtils.getField(fields, 'FontName' + index)
|
|
48
48
|
),
|
|
49
49
|
bold: ParserUtils.parseBoolean(fields?.['Bold' + index]),
|
|
50
|
+
italic: ParserUtils.parseBoolean(fields?.['Italic' + index]),
|
|
50
51
|
rotation:
|
|
51
52
|
ParserUtils.parseNumericField(fields, 'Rotation' + index) ||
|
|
52
53
|
0
|
|
@@ -61,8 +62,8 @@ export class SchematicTextParser {
|
|
|
61
62
|
* @param {Record<string, string | string[]>} fields
|
|
62
63
|
* @param {Record<string, string>} metadata
|
|
63
64
|
* @param {{ width: number, marginWidth: number, titleBlockOn?: boolean }} sheet
|
|
64
|
-
* @param {Record<string, { size: number, family: string, bold: boolean, rotation: number }>} fonts
|
|
65
|
-
* @returns {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] } | null}
|
|
65
|
+
* @param {Record<string, { size: number, family: string, bold: boolean, italic?: boolean, rotation: number }>} fonts
|
|
66
|
+
* @returns {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] } | null}
|
|
66
67
|
*/
|
|
67
68
|
static normalizeSchematicTextRecord(fields, metadata, sheet, fonts) {
|
|
68
69
|
const x = ParserUtils.parseNumericField(fields, 'Location.X')
|
|
@@ -121,6 +122,7 @@ export class SchematicTextParser {
|
|
|
121
122
|
fontSize: SchematicTextParser.#toSvgFontSize(font.size),
|
|
122
123
|
fontFamily: font.family,
|
|
123
124
|
fontWeight: font.bold ? 700 : 400,
|
|
125
|
+
...(font.italic ? { fontStyle: 'italic' } : {}),
|
|
124
126
|
rotation,
|
|
125
127
|
sourceOrientation:
|
|
126
128
|
sourceOrientation === null ? undefined : sourceOrientation,
|
|
@@ -155,8 +157,8 @@ export class SchematicTextParser {
|
|
|
155
157
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
156
158
|
* @param {Record<string, string>} metadata
|
|
157
159
|
* @param {number} sheetWidth
|
|
158
|
-
* @param {Record<string, { size: number, family: string, bold: boolean, rotation: number }>} fonts
|
|
159
|
-
* @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 }>> }}
|
|
160
|
+
* @param {Record<string, { size: number, family: string, bold: boolean, italic?: boolean, rotation: number }>} fonts
|
|
161
|
+
* @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 }>> }}
|
|
160
162
|
*/
|
|
161
163
|
static extractSchematicTitleBlock(records, metadata, sheetWidth, fonts) {
|
|
162
164
|
const footerTexts = records
|
|
@@ -248,8 +250,8 @@ export class SchematicTextParser {
|
|
|
248
250
|
/**
|
|
249
251
|
* Normalizes one visible footer text record into a title-block layout hint.
|
|
250
252
|
* @param {Record<string, string | string[]>} fields
|
|
251
|
-
* @param {Record<string, { size: number, family: string, bold: boolean, rotation: number }>} fonts
|
|
252
|
-
* @returns {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number } | null}
|
|
253
|
+
* @param {Record<string, { size: number, family: string, bold: boolean, italic?: boolean, rotation: number }>} fonts
|
|
254
|
+
* @returns {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string } | null}
|
|
253
255
|
*/
|
|
254
256
|
static #normalizeTitleBlockFooterRecord(fields, fonts) {
|
|
255
257
|
const text = ParserUtils.getDisplayText(fields)
|
|
@@ -274,14 +276,15 @@ export class SchematicTextParser {
|
|
|
274
276
|
),
|
|
275
277
|
fontSize: font.size,
|
|
276
278
|
fontFamily: font.family,
|
|
277
|
-
fontWeight: font.bold ? 700 : 400
|
|
279
|
+
fontWeight: font.bold ? 700 : 400,
|
|
280
|
+
...(font.italic ? { fontStyle: 'italic' } : {})
|
|
278
281
|
}
|
|
279
282
|
}
|
|
280
283
|
|
|
281
284
|
/**
|
|
282
285
|
* Maps visible footer rows onto title-block fields.
|
|
283
|
-
* @param {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }[]} footerTexts
|
|
284
|
-
* @returns {Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }>>}
|
|
286
|
+
* @param {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }[]} footerTexts
|
|
287
|
+
* @returns {Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }>>}
|
|
285
288
|
*/
|
|
286
289
|
static #collectSchematicTitleBlockFooterHints(footerTexts) {
|
|
287
290
|
const rows = SchematicTextParser.#groupTitleBlockFooterRows(footerTexts)
|
|
@@ -325,8 +328,8 @@ export class SchematicTextParser {
|
|
|
325
328
|
|
|
326
329
|
/**
|
|
327
330
|
* Groups footer texts by their shared baseline row.
|
|
328
|
-
* @param {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }[]} footerTexts
|
|
329
|
-
* @returns {Array<{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }[]>}
|
|
331
|
+
* @param {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }[]} footerTexts
|
|
332
|
+
* @returns {Array<{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }[]>}
|
|
330
333
|
*/
|
|
331
334
|
static #groupTitleBlockFooterRows(footerTexts) {
|
|
332
335
|
const tolerance = 8
|
|
@@ -372,7 +375,7 @@ export class SchematicTextParser {
|
|
|
372
375
|
/**
|
|
373
376
|
* Extracts a visible footer `Drawn By` value from the bottom-most footer
|
|
374
377
|
* row when hidden metadata does not provide one.
|
|
375
|
-
* @param {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }[]} footerTexts
|
|
378
|
+
* @param {{ text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }[]} footerTexts
|
|
376
379
|
* @param {Record<string, string>} metadata
|
|
377
380
|
* @returns {string}
|
|
378
381
|
*/
|
|
@@ -397,8 +400,8 @@ export class SchematicTextParser {
|
|
|
397
400
|
|
|
398
401
|
/**
|
|
399
402
|
* Removes the non-rendered text payload from stored footer hints.
|
|
400
|
-
* @param {Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { text: string, x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }>>} footerHints
|
|
401
|
-
* @returns {Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number }>>}
|
|
403
|
+
* @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
|
|
404
|
+
* @returns {Partial<Record<'title' | 'documentNumber' | 'revision' | 'sheetNumber' | 'sheetTotal', { x: number, y: number, color: string, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }>>}
|
|
402
405
|
*/
|
|
403
406
|
static #stripSchematicTitleBlockHintText(footerHints) {
|
|
404
407
|
return Object.fromEntries(
|
|
@@ -622,13 +625,14 @@ export class SchematicTextParser {
|
|
|
622
625
|
|
|
623
626
|
/**
|
|
624
627
|
* Returns the default schematic font when no sheet font entry exists.
|
|
625
|
-
* @returns {{ size: number, family: string, bold: boolean, rotation: number }}
|
|
628
|
+
* @returns {{ size: number, family: string, bold: boolean, italic: boolean, rotation: number }}
|
|
626
629
|
*/
|
|
627
630
|
static #defaultSchematicFont() {
|
|
628
631
|
return {
|
|
629
632
|
size: 10,
|
|
630
633
|
family: 'Times New Roman',
|
|
631
634
|
bold: false,
|
|
635
|
+
italic: false,
|
|
632
636
|
rotation: 0
|
|
633
637
|
}
|
|
634
638
|
}
|
|
@@ -670,9 +674,9 @@ export class SchematicTextParser {
|
|
|
670
674
|
|
|
671
675
|
/**
|
|
672
676
|
* Adds note box metadata to one decoded schematic note record.
|
|
673
|
-
* @param {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end' }} textRecord
|
|
677
|
+
* @param {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end' }} textRecord
|
|
674
678
|
* @param {Record<string, string | string[]>} fields
|
|
675
|
-
* @returns {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }}
|
|
679
|
+
* @returns {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }}
|
|
676
680
|
*/
|
|
677
681
|
static #normalizeSchematicNoteRecord(textRecord, fields) {
|
|
678
682
|
const noteLines = SchematicTextParser.#decodeSchematicNoteLines(
|