altium-toolkit 1.1.3 → 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 (39) 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/AltiumLibraryBatchExporter.mjs +206 -0
  8. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  9. package/src/core/altium/AltiumParser.mjs +5 -2
  10. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  11. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  12. package/src/core/altium/AsciiRecordParser.mjs +43 -11
  13. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  14. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  15. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  16. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  17. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  18. package/src/core/altium/SchematicPinParser.mjs +87 -20
  19. package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
  20. package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
  21. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  22. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  23. package/src/core/altium/SourceComponentClient.mjs +239 -0
  24. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  25. package/src/parser.mjs +8 -0
  26. package/src/styles/altium-renderers.css +6 -6
  27. package/src/ui/PcbArcUtils.mjs +19 -2
  28. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  29. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  30. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  31. package/src/ui/SchematicColorResolver.mjs +185 -0
  32. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  33. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  34. package/src/ui/SchematicNoteRenderer.mjs +5 -1
  35. package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
  36. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  37. package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
  38. package/src/ui/SchematicShapeRenderer.mjs +37 -11
  39. package/src/ui/SchematicSvgRenderer.mjs +944 -51
@@ -11,7 +11,7 @@ export class SchematicOwnerPinLabelLayout {
11
11
  * Resolves one native-facing pin text placement in renderer coordinates
12
12
  * before sheet Y projection. The returned `yOffset` is applied after
13
13
  * projection, matching SVG text baseline behavior.
14
- * @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', symbolOuter?: number }} pin
14
+ * @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', symbolOuter?: number, electrical?: number }} pin
15
15
  * @param {'name' | 'number'} labelKind
16
16
  * @param {{ labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', rotateTopNumber?: boolean }} [options]
17
17
  * @returns {{ x: number, yOffset: number, anchor: 'start' | 'middle' | 'end', rotation: number } | null}
@@ -19,7 +19,7 @@ export class SchematicOwnerPinLabelLayout {
19
19
  static resolveNativePinTextPlacement(pin, labelKind, options = {}) {
20
20
  const labelMode = options.labelMode || 'name-and-number'
21
21
  const markerStyle =
22
- SchematicOwnerPinLabelLayout.#resolveOuterPinMarkerStyle(pin)
22
+ SchematicOwnerPinLabelLayout.#resolvePinMarkerStyle(pin)
23
23
 
24
24
  if (labelKind === 'number') {
25
25
  return SchematicOwnerPinLabelLayout.#resolveNumberPlacement(
@@ -200,6 +200,67 @@ export class SchematicOwnerPinLabelLayout {
200
200
  return boxes
201
201
  }
202
202
 
203
+ /**
204
+ * Collects internally numbered pins whose external number would overlap a
205
+ * visible route text label on the same horizontal lane.
206
+ * @param {{ ownerIndex?: string, name?: string, designator?: string, length?: number, x: number, y: number, orientation: 'left' | 'right' | 'top' | 'bottom', labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', electrical?: number, symbolOuter?: number }[]} pins
207
+ * @param {{ ownerIndex?: string, text?: string, resolvedText?: string, x?: number, y?: number, anchor?: 'start' | 'middle' | 'end', fontSize?: number, hidden?: boolean, rotation?: number }[]} texts
208
+ * @param {Map<string, { left: number, right: number }>} internalNumberLabelBoxes
209
+ * @returns {Set<string>}
210
+ */
211
+ static collectOverlappingExternalNumberLabelKeys(
212
+ pins,
213
+ texts,
214
+ internalNumberLabelBoxes
215
+ ) {
216
+ const textBounds = (texts || [])
217
+ .map((text) =>
218
+ SchematicOwnerPinLabelLayout.#estimateHorizontalTextBounds(text)
219
+ )
220
+ .filter(Boolean)
221
+ const keys = new Set()
222
+
223
+ if (textBounds.length === 0) {
224
+ return keys
225
+ }
226
+
227
+ for (const pin of pins || []) {
228
+ const ownerIndex = String(pin.ownerIndex || '').trim()
229
+ if (!ownerIndex || !internalNumberLabelBoxes.has(ownerIndex)) {
230
+ continue
231
+ }
232
+
233
+ if (!SchematicOwnerPinLabelLayout.#isInternalNumberLabelPin(pin)) {
234
+ continue
235
+ }
236
+
237
+ const numberBounds =
238
+ SchematicOwnerPinLabelLayout.#estimateExternalNumberBounds(pin)
239
+ if (!numberBounds) {
240
+ continue
241
+ }
242
+
243
+ if (
244
+ textBounds.some((bounds) =>
245
+ SchematicOwnerPinLabelLayout.#boundsOverlap(
246
+ numberBounds,
247
+ bounds,
248
+ 1.5
249
+ )
250
+ )
251
+ ) {
252
+ keys.add(
253
+ SchematicOwnerPinLabelLayout.buildOwnerPinLabelKey(
254
+ ownerIndex,
255
+ pin.designator
256
+ )
257
+ )
258
+ }
259
+ }
260
+
261
+ return keys
262
+ }
263
+
203
264
  /**
204
265
  * Resolves the final SVG text anchor for one schematic free-text label.
205
266
  * Mirrored rotated owner pin-name labels need the opposite text edge so
@@ -455,6 +516,133 @@ export class SchematicOwnerPinLabelLayout {
455
516
  return /^(?:[DS]|[GS]\d*)$/i.test(String(name || '').trim())
456
517
  }
457
518
 
519
+ /**
520
+ * Estimates a route text label's source-coordinate visual bounds.
521
+ * @param {{ ownerIndex?: string, text?: string, resolvedText?: string, x?: number, y?: number, anchor?: 'start' | 'middle' | 'end', fontSize?: number, hidden?: boolean, rotation?: number } | null} text
522
+ * @returns {{ minX: number, maxX: number, minY: number, maxY: number } | null}
523
+ */
524
+ static #estimateHorizontalTextBounds(text) {
525
+ if (!text || text.hidden || text.ownerIndex) return null
526
+ if (Number(text.rotation || 0) !== 0) return null
527
+
528
+ const label = String(text.resolvedText ?? text.text ?? '').trim()
529
+ const x = Number(text.x)
530
+ const y = Number(text.y)
531
+ if (!label || !Number.isFinite(x) || !Number.isFinite(y)) return null
532
+
533
+ const fontSize = SchematicOwnerPinLabelLayout.#resolveViewerFontSize(
534
+ text.fontSize
535
+ )
536
+ const width = SchematicOwnerPinLabelLayout.#estimateTextWidth(
537
+ label,
538
+ fontSize
539
+ )
540
+ const anchor = text.anchor || 'start'
541
+ const minX =
542
+ anchor === 'end'
543
+ ? x - width
544
+ : anchor === 'middle'
545
+ ? x - width / 2
546
+ : x
547
+ const maxX =
548
+ anchor === 'end'
549
+ ? x
550
+ : anchor === 'middle'
551
+ ? x + width / 2
552
+ : x + width
553
+
554
+ return {
555
+ minX,
556
+ maxX,
557
+ minY: y - fontSize * 0.7,
558
+ maxY: y + fontSize * 0.35
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Estimates one external pin number's source-coordinate bounds.
564
+ * @param {{ designator?: string, length?: number, x: number, y: number, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number }} pin
565
+ * @returns {{ minX: number, maxX: number, minY: number, maxY: number } | null}
566
+ */
567
+ static #estimateExternalNumberBounds(pin) {
568
+ if (pin.orientation !== 'left' && pin.orientation !== 'right') {
569
+ return null
570
+ }
571
+
572
+ const label = String(pin.designator || '').trim()
573
+ const x = Number(pin.x)
574
+ const y = Number(pin.y)
575
+ if (!label || !Number.isFinite(x) || !Number.isFinite(y)) return null
576
+
577
+ const markerStyle =
578
+ SchematicOwnerPinLabelLayout.#resolvePinMarkerStyle(pin)
579
+ const clearance =
580
+ SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberClearance(
581
+ markerStyle,
582
+ pin
583
+ )
584
+ const offset = Math.max(
585
+ clearance,
586
+ SchematicOwnerPinLabelLayout.#resolveCompactExternalHorizontalNumberOffset(
587
+ pin
588
+ )
589
+ )
590
+ const numberX = pin.orientation === 'left' ? x - offset : x + offset
591
+ const anchor =
592
+ SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberAnchor(
593
+ pin,
594
+ markerStyle
595
+ )
596
+ const fontSize = SchematicOwnerPinLabelLayout.#resolveViewerFontSize()
597
+ const width = SchematicOwnerPinLabelLayout.#estimateTextWidth(
598
+ label,
599
+ fontSize
600
+ )
601
+
602
+ return {
603
+ minX: anchor === 'end' ? numberX - width : numberX,
604
+ maxX: anchor === 'end' ? numberX : numberX + width,
605
+ minY: y - fontSize * 0.7,
606
+ maxY: y + fontSize * 0.35
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Returns the viewer-adjusted schematic font size used for collision
612
+ * estimates.
613
+ * @param {number | undefined} fontSize Source font size.
614
+ * @returns {number}
615
+ */
616
+ static #resolveViewerFontSize(fontSize = 10) {
617
+ return Math.max(Number(fontSize || 10) - 1, 6)
618
+ }
619
+
620
+ /**
621
+ * Estimates one rendered text run width.
622
+ * @param {string} text Text content.
623
+ * @param {number} fontSize Viewer font size.
624
+ * @returns {number}
625
+ */
626
+ static #estimateTextWidth(text, fontSize) {
627
+ return Math.max(String(text || '').length * fontSize * 0.62, fontSize)
628
+ }
629
+
630
+ /**
631
+ * Returns true when two source-coordinate boxes overlap.
632
+ * @param {{ minX: number, maxX: number, minY: number, maxY: number }} first First bounds.
633
+ * @param {{ minX: number, maxX: number, minY: number, maxY: number }} second Second bounds.
634
+ * @param {number} tolerance Coordinate tolerance.
635
+ * @returns {boolean}
636
+ */
637
+ static #boundsOverlap(first, second, tolerance) {
638
+ return (
639
+ first.minX <= second.maxX + tolerance &&
640
+ first.maxX >= second.minX - tolerance &&
641
+ first.minY <= second.maxY + tolerance &&
642
+ first.maxY >= second.minY - tolerance
643
+ )
644
+ }
645
+
458
646
  /**
459
647
  * Moves left/right pin numbers outward by the same horizontal correction
460
648
  * already applied to their explicit owner pin-name labels.
@@ -506,7 +694,10 @@ export class SchematicOwnerPinLabelLayout {
506
694
  pin
507
695
  ),
508
696
  yOffset: -1,
509
- anchor: 'end',
697
+ anchor: SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberAnchor(
698
+ pin,
699
+ markerStyle
700
+ ),
510
701
  rotation: 0
511
702
  }
512
703
  case 'right':
@@ -518,7 +709,10 @@ export class SchematicOwnerPinLabelLayout {
518
709
  pin
519
710
  ),
520
711
  yOffset: -1,
521
- anchor: 'start',
712
+ anchor: SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberAnchor(
713
+ pin,
714
+ markerStyle
715
+ ),
522
716
  rotation: 0
523
717
  }
524
718
  case 'top':
@@ -594,23 +788,45 @@ export class SchematicOwnerPinLabelLayout {
594
788
  /**
595
789
  * Returns the horizontal pin-number clearance needed by the pin geometry.
596
790
  * @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle
597
- * @param {{ length?: number }} pin
791
+ * @param {{ length?: number, electrical?: number }} pin
598
792
  * @returns {number}
599
793
  */
600
794
  static #resolveHorizontalPinNumberClearance(markerStyle, pin) {
601
795
  switch (markerStyle) {
602
796
  case 'double':
603
- return 17
797
+ return 21
604
798
  case 'cross':
605
- return 12
799
+ return 9
606
800
  case 'single-in':
607
801
  case 'single-out':
608
802
  return 8
609
803
  default:
804
+ if (Number(pin?.electrical || 0) === 1) {
805
+ return 16
806
+ }
807
+
610
808
  return SchematicOwnerPinLabelLayout.#resolveLongPinInset(pin, 2)
611
809
  }
612
810
  }
613
811
 
812
+ /**
813
+ * Resolves the text edge used for horizontal pin numbers.
814
+ * @param {{ orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number }} pin Pin primitive.
815
+ * @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle Marker style.
816
+ * @returns {'start' | 'end'}
817
+ */
818
+ static #resolveHorizontalPinNumberAnchor(pin, markerStyle) {
819
+ const routeFacing =
820
+ markerStyle === 'double' ||
821
+ (!markerStyle && Number(pin?.electrical || 0) === 1)
822
+
823
+ if (routeFacing) {
824
+ return pin.orientation === 'left' ? 'start' : 'end'
825
+ }
826
+
827
+ return pin.orientation === 'left' ? 'end' : 'start'
828
+ }
829
+
614
830
  /**
615
831
  * Returns the horizontal pin-name inset used inside the symbol body.
616
832
  * @param {{ length?: number }} pin
@@ -622,7 +838,7 @@ export class SchematicOwnerPinLabelLayout {
622
838
  return 10
623
839
  }
624
840
 
625
- return SchematicOwnerPinLabelLayout.#resolveLongPinInset(pin, 4)
841
+ return SchematicOwnerPinLabelLayout.#resolveLongPinInset(pin, 7)
626
842
  }
627
843
 
628
844
  /**
@@ -641,6 +857,18 @@ export class SchematicOwnerPinLabelLayout {
641
857
  return fallback === 2 ? 10 : 8
642
858
  }
643
859
 
860
+ /**
861
+ * Resolves the stub-side offset used when a pin already has an internal
862
+ * numeric label in its owner body.
863
+ * @param {{ length?: number }} pin Pin primitive.
864
+ * @returns {number}
865
+ */
866
+ static #resolveCompactExternalHorizontalNumberOffset(pin) {
867
+ const length = Math.abs(Number(pin.length || 0))
868
+
869
+ return Math.max(8, Math.min(length - 6, 12))
870
+ }
871
+
644
872
  /**
645
873
  * Resolves one authored outer pin marker style from the stored symbol flag.
646
874
  * @param {{ symbolOuter?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
@@ -665,4 +893,37 @@ export class SchematicOwnerPinLabelLayout {
665
893
  return null
666
894
  }
667
895
  }
896
+
897
+ /**
898
+ * Resolves the marker style that contributes to native text clearance.
899
+ * @param {{ symbolOuter?: number, electrical?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
900
+ * @returns {'single-in' | 'single-out' | 'double' | 'cross' | null}
901
+ */
902
+ static #resolvePinMarkerStyle(pin) {
903
+ return (
904
+ SchematicOwnerPinLabelLayout.#resolveOuterPinMarkerStyle(pin) ||
905
+ SchematicOwnerPinLabelLayout.#resolveElectricalPinMarkerStyle(pin)
906
+ )
907
+ }
908
+
909
+ /**
910
+ * Resolves electrical pin marker styles. Bidirectional pins keep the
911
+ * existing route-facing number placement through the explicit check.
912
+ * @param {{ electrical?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
913
+ * @returns {'single-in' | 'single-out' | null}
914
+ */
915
+ static #resolveElectricalPinMarkerStyle(pin) {
916
+ if (pin.orientation !== 'left' && pin.orientation !== 'right') {
917
+ return null
918
+ }
919
+
920
+ switch (Number(pin.electrical)) {
921
+ case 0:
922
+ return 'single-in'
923
+ case 2:
924
+ return 'single-out'
925
+ default:
926
+ return null
927
+ }
928
+ }
668
929
  }
@@ -0,0 +1,155 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ const COMPACT_PIN_MAX_LENGTH = 15
6
+ const MARKER_LINE_BASE_TOLERANCE = 6
7
+ const MARKER_LINE_BASE_MAX_LENGTH = 8
8
+ const MARKER_LINE_COLOR = 'var(--schematic-accent-ink-color)'
9
+
10
+ /**
11
+ * Normalizes compact owner-authored marker strokes around hidden pin stubs.
12
+ */
13
+ export class SchematicOwnerPinMarkerLineThemer {
14
+ /**
15
+ * Applies schematic pin color to compact owner marker strokes.
16
+ * @param {{ x1?: number, y1?: number, x2?: number, y2?: number, color?: string, ownerIndex?: string }[]} lines Line primitives.
17
+ * @param {{ x?: number, y?: number, length?: number, name?: string, designator?: string, electrical?: number, ownerIndex?: string }[]} pins Pin primitives.
18
+ * @returns {{ x1?: number, y1?: number, x2?: number, y2?: number, color?: string, ownerIndex?: string }[]}
19
+ */
20
+ static theme(lines, pins) {
21
+ const anchorsByOwner =
22
+ SchematicOwnerPinMarkerLineThemer.#buildCompactPinAnchors(pins)
23
+
24
+ if (anchorsByOwner.size === 0) {
25
+ return lines || []
26
+ }
27
+
28
+ return (lines || []).map((line) =>
29
+ SchematicOwnerPinMarkerLineThemer.#isCompactMarkerLine(
30
+ line,
31
+ anchorsByOwner
32
+ )
33
+ ? { ...line, color: MARKER_LINE_COLOR }
34
+ : line
35
+ )
36
+ }
37
+
38
+ /**
39
+ * Builds lookup anchors for compact unnamed owner pins.
40
+ * @param {{ x?: number, y?: number, length?: number, name?: string, designator?: string, electrical?: number, ownerIndex?: string }[]} pins Pin primitives.
41
+ * @returns {Map<string, { x: number, y: number, tolerance: number, maxLineLength: number }[]>}
42
+ */
43
+ static #buildCompactPinAnchors(pins) {
44
+ const anchorsByOwner = new Map()
45
+
46
+ for (const pin of pins || []) {
47
+ if (!SchematicOwnerPinMarkerLineThemer.#isCompactMarkerPin(pin)) {
48
+ continue
49
+ }
50
+
51
+ const ownerIndex = String(pin.ownerIndex || '').trim()
52
+ const length = Math.abs(Number(pin.length || 0))
53
+ const anchor = {
54
+ x: Number(pin.x),
55
+ y: Number(pin.y),
56
+ tolerance: Math.max(MARKER_LINE_BASE_TOLERANCE, length * 0.6),
57
+ maxLineLength: Math.max(
58
+ MARKER_LINE_BASE_MAX_LENGTH,
59
+ length * 0.8
60
+ )
61
+ }
62
+
63
+ if (!anchorsByOwner.has(ownerIndex)) {
64
+ anchorsByOwner.set(ownerIndex, [])
65
+ }
66
+ anchorsByOwner.get(ownerIndex).push(anchor)
67
+ }
68
+
69
+ return anchorsByOwner
70
+ }
71
+
72
+ /**
73
+ * Returns true for compact unnamed owner pins that are marker anchors.
74
+ * @param {{ x?: number, y?: number, length?: number, name?: string, designator?: string, electrical?: number, ownerIndex?: string } | null | undefined} pin Pin primitive.
75
+ * @returns {boolean}
76
+ */
77
+ static #isCompactMarkerPin(pin) {
78
+ const ownerIndex = String(pin?.ownerIndex || '').trim()
79
+ const name = String(pin?.name || '').trim()
80
+ const designator = String(pin?.designator || '').trim()
81
+ const length = Math.abs(Number(pin?.length || 0))
82
+ const electrical = Number(pin?.electrical)
83
+ const x = Number(pin?.x)
84
+ const y = Number(pin?.y)
85
+
86
+ return (
87
+ Boolean(ownerIndex) &&
88
+ !name &&
89
+ /^\d+$/.test(designator) &&
90
+ Number.isFinite(x) &&
91
+ Number.isFinite(y) &&
92
+ length > 0 &&
93
+ length <= COMPACT_PIN_MAX_LENGTH &&
94
+ (!Number.isFinite(electrical) || electrical === 4)
95
+ )
96
+ }
97
+
98
+ /**
99
+ * Returns true when one owner line is compact marker linework near a pin.
100
+ * @param {{ x1?: number, y1?: number, x2?: number, y2?: number, ownerIndex?: string } | null | undefined} line Line primitive.
101
+ * @param {Map<string, { x: number, y: number, tolerance: number, maxLineLength: number }[]>} anchorsByOwner Marker anchors by owner.
102
+ * @returns {boolean}
103
+ */
104
+ static #isCompactMarkerLine(line, anchorsByOwner) {
105
+ const ownerIndex = String(line?.ownerIndex || '').trim()
106
+ const anchors = anchorsByOwner.get(ownerIndex)
107
+
108
+ if (!anchors?.length) {
109
+ return false
110
+ }
111
+
112
+ const pointA = {
113
+ x: Number(line?.x1),
114
+ y: Number(line?.y1)
115
+ }
116
+ const pointB = {
117
+ x: Number(line?.x2),
118
+ y: Number(line?.y2)
119
+ }
120
+
121
+ if (
122
+ !Number.isFinite(pointA.x) ||
123
+ !Number.isFinite(pointA.y) ||
124
+ !Number.isFinite(pointB.x) ||
125
+ !Number.isFinite(pointB.y)
126
+ ) {
127
+ return false
128
+ }
129
+
130
+ const lineLength = SchematicOwnerPinMarkerLineThemer.#distance(
131
+ pointA,
132
+ pointB
133
+ )
134
+
135
+ return anchors.some(
136
+ (anchor) =>
137
+ lineLength > 0 &&
138
+ lineLength <= anchor.maxLineLength &&
139
+ SchematicOwnerPinMarkerLineThemer.#distance(pointA, anchor) <=
140
+ anchor.tolerance &&
141
+ SchematicOwnerPinMarkerLineThemer.#distance(pointB, anchor) <=
142
+ anchor.tolerance
143
+ )
144
+ }
145
+
146
+ /**
147
+ * Measures planar distance between two schematic points.
148
+ * @param {{ x: number, y: number }} pointA First point.
149
+ * @param {{ x: number, y: number }} pointB Second point.
150
+ * @returns {number}
151
+ */
152
+ static #distance(pointA, pointB) {
153
+ return Math.hypot(pointA.x - pointB.x, pointA.y - pointB.y)
154
+ }
155
+ }