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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altium-toolkit",
3
- "version": "1.0.2",
3
+ "version": "1.0.8",
4
4
  "description": "Altium document parsing and non-interactive rendering utilities",
5
5
  "keywords": [
6
6
  "altium",
@@ -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
- const lines = [
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 polygons =
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)) continue
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 || explicitCount === pins.length) {
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(fields.Color, '#a44a1b'),
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(fields.Color, '#a44a1b'),
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(fields.Color, '#a44a1b'),
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
- points.push({ x: pointX, y: pointY })
528
- previousX = pointX
529
- previousY = pointY
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: '#0000ff',
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.some((pin) => String(pin.designator || '').trim())
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: toColor(record.fields.Color, '#a44a1b'),
102
- fill: toColor(record.fields.AreaColor, '#ffe16f'),
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 || y === 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
- points.push({ x, y })
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(