altium-toolkit 1.1.2 → 1.1.22

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.
Files changed (57) hide show
  1. package/docs/api.md +37 -0
  2. package/docs/model-format.md +18 -0
  3. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
  4. package/docs/testing.md +5 -0
  5. package/package.json +1 -1
  6. package/spec/library-scope.md +5 -0
  7. package/src/core/altium/AltiumLayoutParser.mjs +275 -13
  8. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  9. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  10. package/src/core/altium/AltiumParser.mjs +245 -10
  11. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  12. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  13. package/src/core/altium/AsciiRecordParser.mjs +43 -11
  14. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  15. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
  16. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  17. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  18. package/src/core/altium/PrintableTextDecoder.mjs +133 -13
  19. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
  20. package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
  21. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  22. package/src/core/altium/SchematicImageParser.mjs +291 -6
  23. package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
  24. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
  25. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  26. package/src/core/altium/SchematicPinParser.mjs +262 -24
  27. package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
  28. package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
  29. package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
  30. package/src/core/altium/SchematicTextParser.mjs +125 -11
  31. package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
  32. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  33. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  34. package/src/core/altium/SourceComponentClient.mjs +239 -0
  35. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  36. package/src/parser.mjs +8 -0
  37. package/src/styles/altium-renderers.css +6 -6
  38. package/src/ui/PcbArcUtils.mjs +19 -2
  39. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  40. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  41. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  42. package/src/ui/SchematicColorResolver.mjs +263 -0
  43. package/src/ui/SchematicContentLayout.mjs +58 -1
  44. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  45. package/src/ui/SchematicImageRenderer.mjs +125 -10
  46. package/src/ui/SchematicJunctionRenderer.mjs +1 -1
  47. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  48. package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
  49. package/src/ui/SchematicNoteRenderer.mjs +87 -7
  50. package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
  51. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  52. package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
  53. package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
  54. package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
  55. package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
  56. package/src/ui/SchematicShapeRenderer.mjs +109 -24
  57. package/src/ui/SchematicSvgRenderer.mjs +1210 -71
@@ -22,6 +22,9 @@ export class SchematicPinSvgRenderer {
22
22
  * @param {Set<string>} rotatedVerticalNumberOwners
23
23
  * @param {Set<string>} explicitOwnerPinNameLabels
24
24
  * @param {Map<string, number>} explicitOwnerPinLabelOffsets
25
+ * @param {Map<string, 'left' | 'right'>} compactExternalNumberLabelSides
26
+ * @param {Map<string, { left: number, right: number }>} internalNumberLabelBoxes
27
+ * @param {Set<string>} overlappingExternalNumberLabelKeys
25
28
  * @returns {string}
26
29
  */
27
30
  static buildMarkup(
@@ -30,7 +33,10 @@ export class SchematicPinSvgRenderer {
30
33
  sheet,
31
34
  rotatedVerticalNumberOwners,
32
35
  explicitOwnerPinNameLabels,
33
- explicitOwnerPinLabelOffsets
36
+ explicitOwnerPinLabelOffsets,
37
+ compactExternalNumberLabelSides = new Map(),
38
+ internalNumberLabelBoxes = new Map(),
39
+ overlappingExternalNumberLabelKeys = new Set()
34
40
  ) {
35
41
  const geometry =
36
42
  SchematicPinSvgRenderer.#projectSchematicPinGeometry(pin)
@@ -49,6 +55,11 @@ export class SchematicPinSvgRenderer {
49
55
  const labelMode = pin.labelMode || 'name-and-number'
50
56
  const outerMarkerStyle =
51
57
  SchematicPinSvgRenderer.#resolveSchematicOuterPinMarkerStyle(pin)
58
+ const markerStyle =
59
+ outerMarkerStyle ||
60
+ SchematicPinSvgRenderer.#resolveSchematicElectricalPinMarkerStyle(
61
+ pin
62
+ )
52
63
  const rotateTopNumber =
53
64
  pin.orientation === 'top' &&
54
65
  rotatedVerticalNumberOwners.has(String(pin.ownerIndex || ''))
@@ -60,14 +71,47 @@ export class SchematicPinSvgRenderer {
60
71
  const hasExplicitOwnerPinName =
61
72
  Boolean(pin.name) &&
62
73
  explicitOwnerPinNameLabels.has(ownerPinLabelKey)
74
+ const compactExternalNumberLabelSide =
75
+ compactExternalNumberLabelSides.get(String(pin.ownerIndex || '')) ||
76
+ null
77
+ const internalNumberLabelBox =
78
+ internalNumberLabelBoxes.get(String(pin.ownerIndex || '')) || null
79
+ const shouldRenderExternalNumber =
80
+ !overlappingExternalNumberLabelKeys.has(
81
+ SchematicOwnerPinLabelLayout.buildOwnerPinLabelKey(
82
+ pin.ownerIndex,
83
+ pin.designator
84
+ )
85
+ )
63
86
 
64
87
  if (pin.orientation === 'left') {
65
88
  if (labelMode !== 'hidden' && labelMode !== 'name-only') {
89
+ const internalNumberPlacement =
90
+ SchematicPinSvgRenderer.#resolveInternalHorizontalNumberPlacement(
91
+ pin,
92
+ internalNumberLabelBox
93
+ )
94
+ if (internalNumberPlacement) {
95
+ texts.push(
96
+ SchematicPinSvgRenderer.#buildPinNameTextMarkup(
97
+ 'schematic-pin-name',
98
+ internalNumberPlacement.x,
99
+ projectedY + 3,
100
+ pin,
101
+ labelColor,
102
+ internalNumberPlacement.anchor,
103
+ textOptions
104
+ )
105
+ )
106
+ }
107
+
66
108
  const defaultNumberX =
67
- geometry.bodyX -
68
- SchematicPinSvgRenderer.#resolveHorizontalPinNumberClearance(
69
- outerMarkerStyle,
70
- pin
109
+ SchematicPinSvgRenderer.#resolveExternalHorizontalNumberX(
110
+ pin,
111
+ geometry,
112
+ markerStyle,
113
+ compactExternalNumberLabelSide,
114
+ internalNumberLabelBox
71
115
  )
72
116
  const numberX = hasExplicitOwnerPinName
73
117
  ? SchematicOwnerPinLabelLayout.resolveExplicitOwnerPinNumberX(
@@ -76,25 +120,31 @@ export class SchematicPinSvgRenderer {
76
120
  explicitOwnerPinLabelOffsets
77
121
  )
78
122
  : defaultNumberX
79
- texts.push(
80
- createSvgText(
81
- 'schematic-pin-number',
82
- numberX,
83
- projectedY - 1,
84
- pin.designator,
85
- labelColor,
86
- 'end',
87
- textOptions
123
+ if (shouldRenderExternalNumber) {
124
+ texts.push(
125
+ createSvgText(
126
+ 'schematic-pin-number',
127
+ numberX,
128
+ projectedY - 1,
129
+ pin.designator,
130
+ labelColor,
131
+ SchematicPinSvgRenderer.#resolveExternalHorizontalNumberAnchor(
132
+ pin,
133
+ markerStyle
134
+ ),
135
+ textOptions
136
+ )
88
137
  )
89
- )
138
+ }
90
139
  }
91
140
 
92
141
  if (
93
142
  labelMode !== 'hidden' &&
94
143
  labelMode !== 'number-only' &&
95
- pin.name &&
96
- pin.name !== pin.designator &&
97
- !hasExplicitOwnerPinName
144
+ SchematicPinSvgRenderer.#shouldRenderHorizontalPinName(
145
+ pin,
146
+ hasExplicitOwnerPinName
147
+ )
98
148
  ) {
99
149
  texts.push(
100
150
  SchematicPinSvgRenderer.#buildPinNameTextMarkup(
@@ -116,11 +166,32 @@ export class SchematicPinSvgRenderer {
116
166
 
117
167
  if (pin.orientation === 'right') {
118
168
  if (labelMode !== 'hidden' && labelMode !== 'name-only') {
169
+ const internalNumberPlacement =
170
+ SchematicPinSvgRenderer.#resolveInternalHorizontalNumberPlacement(
171
+ pin,
172
+ internalNumberLabelBox
173
+ )
174
+ if (internalNumberPlacement) {
175
+ texts.push(
176
+ SchematicPinSvgRenderer.#buildPinNameTextMarkup(
177
+ 'schematic-pin-name',
178
+ internalNumberPlacement.x,
179
+ projectedY + 3,
180
+ pin,
181
+ labelColor,
182
+ internalNumberPlacement.anchor,
183
+ textOptions
184
+ )
185
+ )
186
+ }
187
+
119
188
  const defaultNumberX =
120
- geometry.bodyX +
121
- SchematicPinSvgRenderer.#resolveHorizontalPinNumberClearance(
122
- outerMarkerStyle,
123
- pin
189
+ SchematicPinSvgRenderer.#resolveExternalHorizontalNumberX(
190
+ pin,
191
+ geometry,
192
+ markerStyle,
193
+ compactExternalNumberLabelSide,
194
+ internalNumberLabelBox
124
195
  )
125
196
  const numberX = hasExplicitOwnerPinName
126
197
  ? SchematicOwnerPinLabelLayout.resolveExplicitOwnerPinNumberX(
@@ -129,25 +200,31 @@ export class SchematicPinSvgRenderer {
129
200
  explicitOwnerPinLabelOffsets
130
201
  )
131
202
  : defaultNumberX
132
- texts.push(
133
- createSvgText(
134
- 'schematic-pin-number',
135
- numberX,
136
- projectedY - 1,
137
- pin.designator,
138
- labelColor,
139
- 'start',
140
- textOptions
203
+ if (shouldRenderExternalNumber) {
204
+ texts.push(
205
+ createSvgText(
206
+ 'schematic-pin-number',
207
+ numberX,
208
+ projectedY - 1,
209
+ pin.designator,
210
+ labelColor,
211
+ SchematicPinSvgRenderer.#resolveExternalHorizontalNumberAnchor(
212
+ pin,
213
+ markerStyle
214
+ ),
215
+ textOptions
216
+ )
141
217
  )
142
- )
218
+ }
143
219
  }
144
220
 
145
221
  if (
146
222
  labelMode !== 'hidden' &&
147
223
  labelMode !== 'number-only' &&
148
- pin.name &&
149
- pin.name !== pin.designator &&
150
- !hasExplicitOwnerPinName
224
+ SchematicPinSvgRenderer.#shouldRenderHorizontalPinName(
225
+ pin,
226
+ hasExplicitOwnerPinName
227
+ )
151
228
  ) {
152
229
  texts.push(
153
230
  SchematicPinSvgRenderer.#buildPinNameTextMarkup(
@@ -175,7 +252,12 @@ export class SchematicPinSvgRenderer {
175
252
  texts.push(
176
253
  createSvgText(
177
254
  'schematic-pin-number',
178
- geometry.bodyX - 2,
255
+ compactExternalNumberLabelSide
256
+ ? SchematicPinSvgRenderer.#resolveCompactExternalVerticalNumberX(
257
+ geometry,
258
+ compactExternalNumberLabelSide
259
+ )
260
+ : geometry.bodyX - 2,
179
261
  pin.orientation === 'top'
180
262
  ? projectedInnerY - 6
181
263
  : projectedInnerY + 7,
@@ -230,12 +312,7 @@ export class SchematicPinSvgRenderer {
230
312
  '" y2="' +
231
313
  formatNumber(projectedOuterY) +
232
314
  '" stroke="' +
233
- escapeHtml(
234
- SchematicColorResolver.resolveColor(
235
- pin.color,
236
- '--schematic-accent-ink-color'
237
- )
238
- ) +
315
+ escapeHtml(SchematicPinSvgRenderer.#resolvePinStrokeColor(pin)) +
239
316
  '" />' +
240
317
  markerMarkup +
241
318
  texts.join('') +
@@ -262,6 +339,19 @@ export class SchematicPinSvgRenderer {
262
339
  )
263
340
  }
264
341
 
342
+ const electricalMarkerStyle =
343
+ SchematicPinSvgRenderer.#resolveSchematicElectricalPinMarkerStyle(
344
+ pin
345
+ )
346
+ if (electricalMarkerStyle) {
347
+ return SchematicPinSvgRenderer.#buildOuterPinMarkerMarkup(
348
+ pin,
349
+ geometry,
350
+ sheetHeight,
351
+ electricalMarkerStyle
352
+ )
353
+ }
354
+
265
355
  if (Number(pin.electrical || 0) !== 1) {
266
356
  return ''
267
357
  }
@@ -324,7 +414,7 @@ export class SchematicPinSvgRenderer {
324
414
  * @param {{ orientation: 'left' | 'right' | 'top' | 'bottom', labelColor?: string, color: string }} pin
325
415
  * @param {{ bodyX: number, bodyY: number }} geometry
326
416
  * @param {number} sheetHeight
327
- * @param {'single-in' | 'single-out' | 'double'} markerStyle
417
+ * @param {'single-in' | 'single-out' | 'double' | 'cross'} markerStyle
328
418
  * @returns {string}
329
419
  */
330
420
  static #buildOuterPinMarkerMarkup(pin, geometry, sheetHeight, markerStyle) {
@@ -339,6 +429,15 @@ export class SchematicPinSvgRenderer {
339
429
  '--schematic-text-color'
340
430
  )
341
431
 
432
+ if (markerStyle === 'cross') {
433
+ return SchematicPinSvgRenderer.#buildOuterPinCrossMarkerMarkup(
434
+ geometry.bodyX,
435
+ projectedY,
436
+ pin.orientation,
437
+ strokeColor
438
+ )
439
+ }
440
+
342
441
  const polygons = SchematicPinSvgRenderer.#buildOuterPinMarkerPolygons(
343
442
  geometry.bodyX,
344
443
  projectedY,
@@ -374,22 +473,51 @@ export class SchematicPinSvgRenderer {
374
473
 
375
474
  /**
376
475
  * Returns the horizontal pin-number clearance needed by the pin geometry.
377
- * @param {'single-in' | 'single-out' | 'double' | null} markerStyle
378
- * @param {{ length?: number }} pin
476
+ * @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle
477
+ * @param {{ length?: number, electrical?: number }} pin
379
478
  * @returns {number}
380
479
  */
381
480
  static #resolveHorizontalPinNumberClearance(markerStyle, pin) {
382
481
  switch (markerStyle) {
383
482
  case 'double':
384
- return 17
483
+ return 21
484
+ case 'cross':
485
+ return 9
385
486
  case 'single-in':
386
487
  case 'single-out':
387
488
  return 8
388
489
  default:
490
+ if (Number(pin?.electrical || 0) === 1) {
491
+ return 16
492
+ }
493
+
389
494
  return SchematicPinSvgRenderer.#resolveLongPinInset(pin, 2)
390
495
  }
391
496
  }
392
497
 
498
+ /**
499
+ * Returns whether a horizontal pin name should be rendered inside the body.
500
+ * Numeric name/designator matches are duplicate pin numbers; nonnumeric
501
+ * matches such as PAD are valid body labels.
502
+ * @param {{ name?: string, designator?: string }} pin Pin primitive.
503
+ * @param {boolean} hasExplicitOwnerPinName Whether an explicit owner text already draws the label.
504
+ * @returns {boolean}
505
+ */
506
+ static #shouldRenderHorizontalPinName(pin, hasExplicitOwnerPinName) {
507
+ const name = String(pin?.name || '').trim()
508
+ const designator = String(pin?.designator || '').trim()
509
+
510
+ if (!name || hasExplicitOwnerPinName) {
511
+ return false
512
+ }
513
+
514
+ if (name !== designator) {
515
+ return true
516
+ }
517
+
518
+ return !/^\d+$/u.test(name)
519
+ }
520
+
393
521
  /**
394
522
  * Returns the horizontal pin-name inset used inside the symbol body.
395
523
  * @param {{ length?: number }} pin
@@ -401,7 +529,7 @@ export class SchematicPinSvgRenderer {
401
529
  return 10
402
530
  }
403
531
 
404
- return SchematicPinSvgRenderer.#resolveLongPinInset(pin, 4)
532
+ return SchematicPinSvgRenderer.#resolveLongPinInset(pin, 7)
405
533
  }
406
534
 
407
535
  /**
@@ -420,6 +548,192 @@ export class SchematicPinSvgRenderer {
420
548
  return fallback === 2 ? 10 : 8
421
549
  }
422
550
 
551
+ /**
552
+ * Places synthetic horizontal pin numbers outside the symbol body. Owner
553
+ * bodies with internal numeric labels need a stub-side offset so the
554
+ * outside contact number does not collapse onto the inside duplicate.
555
+ * @param {{ length?: number, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number }} pin
556
+ * @param {{ bodyX: number }} geometry
557
+ * @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle
558
+ * @param {'left' | 'right' | null} compactExternalNumberLabelSide
559
+ * @param {{ left: number, right: number } | null} internalNumberLabelBox
560
+ * @returns {number}
561
+ */
562
+ static #resolveExternalHorizontalNumberX(
563
+ pin,
564
+ geometry,
565
+ markerStyle,
566
+ compactExternalNumberLabelSide,
567
+ internalNumberLabelBox
568
+ ) {
569
+ const clearance =
570
+ SchematicPinSvgRenderer.#resolveHorizontalPinNumberClearance(
571
+ markerStyle,
572
+ pin
573
+ )
574
+ const offset =
575
+ compactExternalNumberLabelSide || internalNumberLabelBox
576
+ ? Math.max(
577
+ clearance,
578
+ SchematicPinSvgRenderer.#resolveCompactExternalHorizontalNumberOffset(
579
+ pin
580
+ )
581
+ )
582
+ : clearance
583
+
584
+ if (pin.orientation === 'left') {
585
+ return geometry.bodyX - offset
586
+ }
587
+
588
+ if (pin.orientation === 'right') {
589
+ return geometry.bodyX + offset
590
+ }
591
+
592
+ return geometry.bodyX
593
+ }
594
+
595
+ /**
596
+ * Resolves the text edge used for external horizontal pin numbers.
597
+ * Double/electrical marker labels are authored from the route side back
598
+ * toward the marker, while ordinary pins extend away from the body.
599
+ * @param {{ orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number }} pin Pin primitive.
600
+ * @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle Marker style.
601
+ * @returns {'start' | 'end'}
602
+ */
603
+ static #resolveExternalHorizontalNumberAnchor(pin, markerStyle) {
604
+ const routeFacing =
605
+ markerStyle === 'double' ||
606
+ (!markerStyle && Number(pin?.electrical || 0) === 1)
607
+
608
+ if (routeFacing) {
609
+ return pin.orientation === 'left' ? 'start' : 'end'
610
+ }
611
+
612
+ return pin.orientation === 'left' ? 'end' : 'start'
613
+ }
614
+
615
+ /**
616
+ * Places numeric-only labels just inside a rectangular owner body.
617
+ * @param {{ name?: string, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
618
+ * @param {{ left: number, right: number } | null} box
619
+ * @returns {{ x: number, anchor: 'middle' } | null}
620
+ */
621
+ static #resolveInternalHorizontalNumberPlacement(pin, box) {
622
+ if (
623
+ !box ||
624
+ (pin.orientation !== 'left' && pin.orientation !== 'right') ||
625
+ !/^\d+$/u.test(String(pin.name || '').trim())
626
+ ) {
627
+ return null
628
+ }
629
+
630
+ const left = Number(box.left)
631
+ const right = Number(box.right)
632
+
633
+ if (
634
+ !Number.isFinite(left) ||
635
+ !Number.isFinite(right) ||
636
+ right <= left
637
+ ) {
638
+ return null
639
+ }
640
+
641
+ const inset = Math.min(10, Math.max(6, (right - left) / 6))
642
+
643
+ return {
644
+ x: pin.orientation === 'left' ? left + inset : right - inset,
645
+ anchor: 'middle'
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Places compact owner-drawn side pin numbers near the external pin stub.
651
+ * @param {{ length?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
652
+ * @param {{ bodyX: number }} geometry
653
+ * @returns {number}
654
+ */
655
+ static #resolveCompactExternalHorizontalNumberX(pin, geometry) {
656
+ const offset =
657
+ SchematicPinSvgRenderer.#resolveCompactExternalHorizontalNumberOffset(
658
+ pin
659
+ )
660
+
661
+ if (pin.orientation === 'left') {
662
+ return geometry.bodyX - offset
663
+ }
664
+
665
+ if (pin.orientation === 'right') {
666
+ return geometry.bodyX + offset
667
+ }
668
+
669
+ return geometry.bodyX
670
+ }
671
+
672
+ /**
673
+ * Resolves a stub-side offset for compact external horizontal numbers.
674
+ * @param {{ length?: number }} pin
675
+ * @returns {number}
676
+ */
677
+ static #resolveCompactExternalHorizontalNumberOffset(pin) {
678
+ const length = Math.abs(Number(pin.length || 0))
679
+
680
+ return Math.max(8, Math.min(length - 6, 12))
681
+ }
682
+
683
+ /**
684
+ * Places compact owner-drawn vertical pin numbers on the same side as the
685
+ * horizontal terminal contacts.
686
+ * @param {{ bodyX: number }} geometry
687
+ * @param {'left' | 'right'} side
688
+ * @returns {number}
689
+ */
690
+ static #resolveCompactExternalVerticalNumberX(geometry, side) {
691
+ return geometry.bodyX + (side === 'left' ? -10 : 10)
692
+ }
693
+
694
+ /**
695
+ * Builds an authored outer pin cross marker.
696
+ * @param {number} bodyX
697
+ * @param {number} projectedY
698
+ * @param {'left' | 'right' | 'top' | 'bottom'} orientation
699
+ * @param {string} strokeColor
700
+ * @returns {string}
701
+ */
702
+ static #buildOuterPinCrossMarkerMarkup(
703
+ bodyX,
704
+ projectedY,
705
+ orientation,
706
+ strokeColor
707
+ ) {
708
+ const direction = orientation === 'left' ? 1 : -1
709
+ const centerX = bodyX - direction * 8
710
+ const halfSize = 3
711
+
712
+ return (
713
+ '<g class="schematic-pin-marker"><line x1="' +
714
+ formatNumber(centerX - halfSize) +
715
+ '" y1="' +
716
+ formatNumber(projectedY - halfSize) +
717
+ '" x2="' +
718
+ formatNumber(centerX + halfSize) +
719
+ '" y2="' +
720
+ formatNumber(projectedY + halfSize) +
721
+ '" stroke="' +
722
+ escapeHtml(strokeColor) +
723
+ '" stroke-width="0.75" vector-effect="non-scaling-stroke" /><line x1="' +
724
+ formatNumber(centerX - halfSize) +
725
+ '" y1="' +
726
+ formatNumber(projectedY + halfSize) +
727
+ '" x2="' +
728
+ formatNumber(centerX + halfSize) +
729
+ '" y2="' +
730
+ formatNumber(projectedY - halfSize) +
731
+ '" stroke="' +
732
+ escapeHtml(strokeColor) +
733
+ '" stroke-width="0.75" vector-effect="non-scaling-stroke" /></g>'
734
+ )
735
+ }
736
+
423
737
  /**
424
738
  * Builds one or two authored outer-marker polygons for one horizontal pin.
425
739
  * @param {number} bodyX
@@ -469,7 +783,7 @@ export class SchematicPinSvgRenderer {
469
783
  /**
470
784
  * Resolves one authored outer pin marker style from the stored symbol flag.
471
785
  * @param {{ symbolOuter?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
472
- * @returns {'single-in' | 'single-out' | 'double' | null}
786
+ * @returns {'single-in' | 'single-out' | 'double' | 'cross' | null}
473
787
  */
474
788
  static #resolveSchematicOuterPinMarkerStyle(pin) {
475
789
  if (pin.orientation !== 'left' && pin.orientation !== 'right') {
@@ -483,6 +797,8 @@ export class SchematicPinSvgRenderer {
483
797
  return 'single-out'
484
798
  case 2:
485
799
  return 'single-in'
800
+ case 6:
801
+ return 'cross'
486
802
  case 34:
487
803
  return 'double'
488
804
  default:
@@ -490,6 +806,39 @@ export class SchematicPinSvgRenderer {
490
806
  }
491
807
  }
492
808
 
809
+ /**
810
+ * Resolves marker styles derived from the pin electrical type. The
811
+ * bidirectional type keeps its legacy two-triangle geometry path.
812
+ * @param {{ electrical?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
813
+ * @returns {'single-in' | 'single-out' | null}
814
+ */
815
+ static #resolveSchematicElectricalPinMarkerStyle(pin) {
816
+ if (pin.orientation !== 'left' && pin.orientation !== 'right') {
817
+ return null
818
+ }
819
+
820
+ switch (Number(pin.electrical)) {
821
+ case 0:
822
+ return 'single-in'
823
+ case 2:
824
+ return 'single-out'
825
+ default:
826
+ return null
827
+ }
828
+ }
829
+
830
+ /**
831
+ * Resolves the visible stroke for a schematic pin stub.
832
+ * @param {{ color?: string }} pin
833
+ * @returns {string}
834
+ */
835
+ static #resolvePinStrokeColor(pin) {
836
+ return SchematicColorResolver.resolveNonTextColor(
837
+ pin?.color,
838
+ '--schematic-accent-ink-color'
839
+ )
840
+ }
841
+
493
842
  /**
494
843
  * Builds one pin-name text element, including overline tspans when needed.
495
844
  * @param {string} className