smithkit 0.1.0 → 0.2.0

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 (132) hide show
  1. package/README.md +124 -59
  2. package/dist/lib/MarkerMeasurements.d.ts +6 -0
  3. package/dist/lib/MarkerMeasurements.js +22 -0
  4. package/dist/lib/Smith.d.ts +9 -2
  5. package/dist/lib/Smith.js +78 -35
  6. package/dist/lib/SmithFormatter.d.ts +10 -0
  7. package/dist/lib/SmithFormatter.js +19 -0
  8. package/dist/lib/grid/CircleLayerControl.d.ts +15 -0
  9. package/dist/lib/grid/CircleLayerControl.js +40 -0
  10. package/dist/lib/{draw → grid}/ConstCircles.d.ts +6 -6
  11. package/dist/lib/{draw → grid}/ConstCircles.js +12 -5
  12. package/dist/lib/{draw → grid}/ConstQCircles.d.ts +2 -2
  13. package/dist/lib/{draw → grid}/ConstQCircles.js +3 -3
  14. package/dist/lib/{draw → grid}/ConstSwrCircles.d.ts +2 -2
  15. package/dist/lib/{draw → grid}/ConstSwrCircles.js +3 -3
  16. package/dist/lib/grid/GridDefinitions.d.ts +12 -0
  17. package/dist/lib/grid/GridDefinitions.js +138 -0
  18. package/dist/lib/grid/GridGeometry.d.ts +26 -0
  19. package/dist/lib/grid/GridGeometry.js +58 -0
  20. package/dist/lib/grid/GridLabels.d.ts +8 -0
  21. package/dist/lib/grid/GridLabels.js +45 -0
  22. package/dist/lib/grid/GridLayerControl.d.ts +12 -0
  23. package/dist/lib/grid/GridLayerControl.js +39 -0
  24. package/dist/lib/grid/LayerControl.d.ts +6 -0
  25. package/dist/lib/grid/LayerControl.js +16 -0
  26. package/dist/lib/{draw → grid}/SmithGridLayer.d.ts +5 -4
  27. package/dist/lib/{draw → grid}/SmithGridLayer.js +26 -9
  28. package/dist/lib/grid/types.d.ts +1 -1
  29. package/dist/lib/index.d.ts +8 -10
  30. package/dist/lib/index.js +5 -7
  31. package/dist/lib/{draw → interaction}/SmithCursor.d.ts +3 -3
  32. package/dist/lib/{draw → interaction}/SmithCursor.js +6 -6
  33. package/dist/lib/io/Touchstone.d.ts +10 -0
  34. package/dist/lib/io/Touchstone.js +75 -0
  35. package/dist/lib/layers.d.ts +2 -8
  36. package/dist/lib/layers.js +1 -73
  37. package/dist/lib/{complex → math}/Complex.d.ts +1 -0
  38. package/dist/lib/{complex → math}/Complex.js +1 -1
  39. package/dist/lib/math/geometry.d.ts +14 -0
  40. package/dist/lib/measurements.d.ts +2 -4
  41. package/dist/lib/measurements.js +1 -19
  42. package/dist/lib/rf/RfCalculations.d.ts +65 -0
  43. package/dist/lib/rf/RfCalculations.js +161 -0
  44. package/dist/lib/{SmithConstantCircle.d.ts → rf/SmithConstantCircle.d.ts} +2 -3
  45. package/dist/lib/{SmithConstantCircle.js → rf/SmithConstantCircle.js} +3 -4
  46. package/dist/lib/scales/PeripheralScaleDefinitions.d.ts +16 -0
  47. package/dist/lib/scales/PeripheralScaleDefinitions.js +54 -0
  48. package/dist/lib/scales/RadialScaleDefinitions.d.ts +15 -0
  49. package/dist/lib/scales/RadialScaleDefinitions.js +112 -0
  50. package/dist/lib/scales/SmithPeripheralScales.d.ts +7 -2
  51. package/dist/lib/scales/SmithPeripheralScales.js +65 -28
  52. package/dist/lib/scales/SmithScales.d.ts +3 -1
  53. package/dist/lib/scales/SmithScales.js +20 -6
  54. package/dist/lib/svg/LabelLayout.d.ts +17 -0
  55. package/dist/lib/svg/LabelLayout.js +145 -0
  56. package/dist/lib/{draw → svg}/SmithArc.d.ts +1 -1
  57. package/dist/lib/{draw → svg}/SmithCircle.d.ts +1 -1
  58. package/dist/lib/{draw → svg}/SmithLine.d.ts +1 -1
  59. package/dist/lib/{draw → svg}/SmithScaler.d.ts +1 -4
  60. package/dist/lib/{draw → svg}/SmithText.d.ts +1 -1
  61. package/dist/lib/svg/SvgExporter.d.ts +14 -0
  62. package/dist/lib/svg/SvgExporter.js +137 -0
  63. package/dist/lib/{draw → traces}/SmithData.d.ts +24 -35
  64. package/dist/lib/traces/SmithData.js +195 -0
  65. package/dist/lib/{draw → traces}/SmithMarker.d.ts +14 -3
  66. package/dist/lib/traces/SmithMarker.js +237 -0
  67. package/dist/lib/traces/TraceModel.d.ts +31 -0
  68. package/dist/lib/traces/TraceModel.js +123 -0
  69. package/dist/lib/traces/TraceRenderer.d.ts +33 -0
  70. package/dist/lib/traces/TraceRenderer.js +152 -0
  71. package/package.json +4 -1
  72. package/dist/lib/SmithArcsDefs.d.ts +0 -10
  73. package/dist/lib/SmithArcsDefs.js +0 -10
  74. package/dist/lib/components.d.ts +0 -14
  75. package/dist/lib/components.js +0 -26
  76. package/dist/lib/draw/SmithData.js +0 -366
  77. package/dist/lib/draw/SmithMarker.js +0 -122
  78. package/dist/lib/formatting.d.ts +0 -7
  79. package/dist/lib/formatting.js +0 -16
  80. package/dist/lib/grid/definitions.d.ts +0 -3
  81. package/dist/lib/grid/definitions.js +0 -123
  82. package/dist/lib/grid/geometry.d.ts +0 -20
  83. package/dist/lib/grid/geometry.js +0 -55
  84. package/dist/lib/grid/labels.d.ts +0 -3
  85. package/dist/lib/grid/labels.js +0 -41
  86. package/dist/lib/io/touchstone.d.ts +0 -7
  87. package/dist/lib/io/touchstone.js +0 -72
  88. package/dist/lib/math/complexRatio.d.ts +0 -3
  89. package/dist/lib/math/complexRatio.js +0 -30
  90. package/dist/lib/renormalization.d.ts +0 -10
  91. package/dist/lib/renormalization.js +0 -62
  92. package/dist/lib/rf.d.ts +0 -35
  93. package/dist/lib/rf.js +0 -74
  94. package/dist/lib/scales/peripheralScales.d.ts +0 -12
  95. package/dist/lib/scales/peripheralScales.js +0 -49
  96. package/dist/lib/scales/radialScales.d.ts +0 -12
  97. package/dist/lib/scales/radialScales.js +0 -109
  98. package/dist/lib/shapes/Arc.d.ts +0 -6
  99. package/dist/lib/shapes/Circle.d.ts +0 -5
  100. package/dist/lib/shapes/Circle.js +0 -1
  101. package/dist/lib/shapes/Line.d.ts +0 -5
  102. package/dist/lib/shapes/Line.js +0 -1
  103. package/dist/lib/shapes/Point.d.ts +0 -1
  104. package/dist/lib/shapes/Point.js +0 -1
  105. /package/dist/lib/{draw → grid}/ConstConductance.d.ts +0 -0
  106. /package/dist/lib/{draw → grid}/ConstConductance.js +0 -0
  107. /package/dist/lib/{draw → grid}/ConstReactance.d.ts +0 -0
  108. /package/dist/lib/{draw → grid}/ConstReactance.js +0 -0
  109. /package/dist/lib/{draw → grid}/ConstResistance.d.ts +0 -0
  110. /package/dist/lib/{draw → grid}/ConstResistance.js +0 -0
  111. /package/dist/lib/{draw → grid}/ConstSusceptance.d.ts +0 -0
  112. /package/dist/lib/{draw → grid}/ConstSusceptance.js +0 -0
  113. /package/dist/lib/{arcs → grid}/Tick.d.ts +0 -0
  114. /package/dist/lib/{arcs → grid}/Tick.js +0 -0
  115. /package/dist/lib/{draw → interaction}/MouseGesture.d.ts +0 -0
  116. /package/dist/lib/{draw → interaction}/MouseGesture.js +0 -0
  117. /package/dist/lib/{draw/SmithDrawOptions.js → math/geometry.js} +0 -0
  118. /package/dist/lib/{draw → svg}/SmithArc.js +0 -0
  119. /package/dist/lib/{draw → svg}/SmithCircle.js +0 -0
  120. /package/dist/lib/{draw → svg}/SmithDrawOptions.d.ts +0 -0
  121. /package/dist/lib/{shapes/Arc.js → svg/SmithDrawOptions.js} +0 -0
  122. /package/dist/lib/{draw → svg}/SmithGroup.d.ts +0 -0
  123. /package/dist/lib/{draw → svg}/SmithGroup.js +0 -0
  124. /package/dist/lib/{draw → svg}/SmithLine.js +0 -0
  125. /package/dist/lib/{draw → svg}/SmithPath.d.ts +0 -0
  126. /package/dist/lib/{draw → svg}/SmithPath.js +0 -0
  127. /package/dist/lib/{draw → svg}/SmithScaler.js +0 -0
  128. /package/dist/lib/{draw → svg}/SmithShape.d.ts +0 -0
  129. /package/dist/lib/{draw → svg}/SmithShape.js +0 -0
  130. /package/dist/lib/{draw → svg}/SmithSvg.d.ts +0 -0
  131. /package/dist/lib/{draw → svg}/SmithSvg.js +0 -0
  132. /package/dist/lib/{draw → svg}/SmithText.js +0 -0
@@ -0,0 +1,195 @@
1
+ import { SmithMarker } from './SmithMarker.js';
2
+ import { TraceModel } from './TraceModel.js';
3
+ import { TraceRenderer } from './TraceRenderer.js';
4
+ /** Coordinates trace rendering, draggable markers, and deferred marker events. */
5
+ export class SmithData {
6
+ color;
7
+ transform;
8
+ scaler;
9
+ markerDragHandler;
10
+ markerContainer;
11
+ model;
12
+ renderer;
13
+ markers = new Map();
14
+ pendingEvents = new Map();
15
+ destroyed = false;
16
+ viewportScale = 1;
17
+ name = 'Trace';
18
+ selectHandler = null;
19
+ visible = true;
20
+ handler = null;
21
+ constructor(data, color, transform, container, scaler, markerDragHandler, markerContainer = container, style = {}) {
22
+ this.color = color;
23
+ this.transform = transform;
24
+ this.scaler = scaler;
25
+ this.markerDragHandler = markerDragHandler;
26
+ this.markerContainer = markerContainer;
27
+ this.model = new TraceModel(data);
28
+ this.renderer = new TraceRenderer(this.model.Samples, color, transform, container, scaler, style);
29
+ }
30
+ setViewportScale(scale) {
31
+ this.viewportScale = scale;
32
+ this.renderer.setViewportScale(scale);
33
+ this.markers.forEach((marker) => marker.zoom(this.transform.k * scale));
34
+ }
35
+ zoom(transform) {
36
+ if (this.destroyed) {
37
+ return;
38
+ }
39
+ this.transform = transform;
40
+ this.renderer.zoom(transform);
41
+ this.markers.forEach((marker) => marker.zoom(transform.k * this.viewportScale));
42
+ }
43
+ addMarker(sampleIndex = 0) {
44
+ this.assertAlive();
45
+ const entry = this.model.addMarker(sampleIndex);
46
+ const marker = new SmithMarker(entry.number, this.color, (dragging) => this.markerDragHandler?.(marker, dragging));
47
+ this.markers.set(entry, marker);
48
+ marker.Element.attr('data-role', 'marker');
49
+ marker.setSampleHandler((sampleIndex) => this.setMarkerSample(this.model.markerIndex(entry), sampleIndex));
50
+ marker.setSelectHandler(() => this.selectHandler?.(marker));
51
+ this.markerContainer.append(marker);
52
+ marker.setDragHandler((point) => {
53
+ if (this.model.selectNearestPoint(entry, this.scaler.pointInvert(point))) {
54
+ this.moveMarker(entry);
55
+ marker.zoom(this.transform.k * this.viewportScale);
56
+ }
57
+ });
58
+ marker.zoom(this.transform.k * this.viewportScale);
59
+ marker.show();
60
+ marker.Element.style('display', () => (this.visible ? null : 'none'));
61
+ this.moveMarker(entry);
62
+ return this.model.markerIndex(entry);
63
+ }
64
+ removeMarker(index) {
65
+ const entry = this.model.getMarker(index);
66
+ if (!entry) {
67
+ return false;
68
+ }
69
+ clearTimeout(this.pendingEvents.get(entry));
70
+ this.pendingEvents.delete(entry);
71
+ // Ending a drag must still be able to read the marker snapshot.
72
+ this.markers.get(entry).destroy();
73
+ this.markers.delete(entry);
74
+ this.model.removeMarker(index);
75
+ return true;
76
+ }
77
+ setMarkerSample(index, sampleIndex) {
78
+ const entry = this.model.setMarkerSample(index, sampleIndex);
79
+ if (!entry) {
80
+ return false;
81
+ }
82
+ this.moveMarker(entry);
83
+ return true;
84
+ }
85
+ setMarkerFrequency(index, frequencyHz) {
86
+ const entry = this.model.setMarkerFrequency(index, frequencyHz);
87
+ if (!entry) {
88
+ return false;
89
+ }
90
+ this.moveMarker(entry);
91
+ return true;
92
+ }
93
+ get Samples() {
94
+ return this.model.Samples;
95
+ }
96
+ get SampleCount() {
97
+ return this.model.Samples.length;
98
+ }
99
+ get Color() {
100
+ return this.color;
101
+ }
102
+ get Visible() {
103
+ return this.visible;
104
+ }
105
+ markerSampleIndex(index) {
106
+ return this.model.markerSampleIndex(index);
107
+ }
108
+ setColor(color) {
109
+ this.color = color;
110
+ this.renderer.setColor(color);
111
+ this.markers.forEach((marker) => marker.setColor(color));
112
+ }
113
+ get Style() {
114
+ return this.renderer.Style;
115
+ }
116
+ setStyle(options) {
117
+ this.renderer.setStyle(options);
118
+ }
119
+ setVisible(visible) {
120
+ this.visible = visible;
121
+ this.renderer.setVisible(visible);
122
+ this.markers.forEach((marker) => {
123
+ if (!visible) {
124
+ marker.cancelDrag();
125
+ marker.Node?.blur();
126
+ }
127
+ marker.Element.style('display', () => (visible ? null : 'none'));
128
+ });
129
+ }
130
+ moveMarker(entry) {
131
+ const marker = this.markers.get(entry);
132
+ marker.move(this.scaler.point(entry.selectedPoint.reflectionCoefficient));
133
+ this.updateReading(entry, marker);
134
+ this.notifyMarker(entry);
135
+ }
136
+ updateReading(entry, marker) {
137
+ marker.setReading(`${this.name}, marker ${entry.number}`, this.model.markerSampleIndex(this.model.markerIndex(entry)), this.SampleCount, entry.selectedPoint.frequencyHz);
138
+ }
139
+ setName(name) {
140
+ this.name = name;
141
+ this.markers.forEach((marker, entry) => this.updateReading(entry, marker));
142
+ }
143
+ setMarkerSelectHandler(handler) {
144
+ this.selectHandler = handler;
145
+ }
146
+ notifyMarker(entry) {
147
+ if (this.pendingEvents.has(entry)) {
148
+ return;
149
+ }
150
+ const timer = setTimeout(() => {
151
+ this.pendingEvents.delete(entry);
152
+ const index = this.model.markerIndex(entry);
153
+ if (!this.destroyed && index >= 0) {
154
+ this.handler?.(index, entry.selectedPoint);
155
+ }
156
+ }, 0);
157
+ this.pendingEvents.set(entry, timer);
158
+ }
159
+ cancelEvents() {
160
+ this.pendingEvents.forEach((timer) => clearTimeout(timer));
161
+ this.pendingEvents.clear();
162
+ }
163
+ update(values, strategy = 'frequency') {
164
+ this.assertAlive();
165
+ this.model.update(values, strategy);
166
+ this.cancelEvents();
167
+ this.renderer.update(this.model.Samples);
168
+ this.model.Markers.forEach((entry) => this.moveMarker(entry));
169
+ }
170
+ destroy() {
171
+ if (this.destroyed) {
172
+ return;
173
+ }
174
+ this.destroyed = true;
175
+ this.cancelEvents();
176
+ this.handler = null;
177
+ this.selectHandler = null;
178
+ this.markers.forEach((marker) => marker.destroy());
179
+ this.markers.clear();
180
+ this.model.clear();
181
+ this.markerDragHandler = undefined;
182
+ this.renderer.destroy();
183
+ }
184
+ get Markers() {
185
+ return this.model.Markers.map((entry) => ({ ...entry, marker: this.markers.get(entry) }));
186
+ }
187
+ setMarkerMoveHandler(handler) {
188
+ this.handler = handler;
189
+ }
190
+ assertAlive() {
191
+ if (this.destroyed) {
192
+ throw new Error('This dataset has been removed.');
193
+ }
194
+ }
195
+ }
@@ -1,25 +1,36 @@
1
- import { SmithShape } from './SmithShape.js';
2
- import { Point } from '../shapes/Point.js';
1
+ import { SmithShape } from '../svg/SmithShape.js';
2
+ import { Point } from '../math/geometry.js';
3
3
  export declare class SmithMarker extends SmithShape {
4
4
  private dragStateHandler;
5
5
  private readonly size;
6
6
  private mouseGesture;
7
7
  private activeDrags;
8
8
  private destroyed;
9
+ private sampleIndex;
10
+ private sampleCount;
11
+ private scale;
9
12
  private triangle;
10
13
  private inner;
11
14
  private text;
15
+ private hitArea;
16
+ private focusRing;
12
17
  private rc;
13
18
  private dragHandler;
19
+ private sampleHandler;
20
+ private selectHandler;
14
21
  constructor(marker: number, color: string, dragStateHandler?: ((dragging: boolean) => void) | null);
22
+ focus(): boolean;
23
+ setReading(name: string, index: number, count: number, frequencyHz: number): void;
24
+ private keydown;
15
25
  destroy(): void;
16
26
  cancelDrag(): void;
17
27
  setColor(color: string): void;
18
- private onDrag;
19
28
  get Position(): Point;
20
29
  move(p: Point): void;
21
30
  zoom(k: number): void;
22
31
  show(): void;
23
32
  hide(): void;
24
33
  setDragHandler(handler: (p: Point) => void): void;
34
+ setSampleHandler(handler: (index: number) => void): void;
35
+ setSelectHandler(handler: () => void): void;
25
36
  }
@@ -0,0 +1,237 @@
1
+ import * as d3 from 'd3';
2
+ import { MouseGesture } from '../interaction/MouseGesture.js';
3
+ import { SmithShape } from '../svg/SmithShape.js';
4
+ import { SmithFormatter } from '../SmithFormatter.js';
5
+ export class SmithMarker extends SmithShape {
6
+ dragStateHandler;
7
+ size = 18;
8
+ mouseGesture = new MouseGesture();
9
+ activeDrags = new Set();
10
+ destroyed = false;
11
+ sampleIndex = 0;
12
+ sampleCount = 1;
13
+ scale = 1;
14
+ triangle;
15
+ inner;
16
+ text;
17
+ hitArea;
18
+ focusRing;
19
+ rc = [0, 0];
20
+ dragHandler = null;
21
+ sampleHandler = null;
22
+ selectHandler = null;
23
+ constructor(marker, color, dragStateHandler = null) {
24
+ super(d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'g')));
25
+ this.dragStateHandler = dragStateHandler;
26
+ const g = this.Element.attr('tabindex', 0)
27
+ .attr('role', 'slider')
28
+ .attr('aria-orientation', 'horizontal')
29
+ .attr('aria-description', 'Arrow keys move one sample; Shift+Arrow or Page Up/Down moves ten. Home and End select the first and last sample.')
30
+ .style('outline', 'none')
31
+ .style('touch-action', 'none')
32
+ .style('cursor', 'var(--smithkit-marker-cursor, grab)')
33
+ .on('focus.smithkit', () => {
34
+ this.focusRing.attr('visibility', null);
35
+ if (this.activeDrags.size === 0) {
36
+ this.selectHandler?.();
37
+ }
38
+ })
39
+ .on('blur.smithkit', () => this.focusRing.attr('visibility', 'hidden'))
40
+ .on('keydown.smithkit', (event) => this.keydown(event))
41
+ .on('touchstart.smithkit', (event) => event.stopPropagation());
42
+ this.hitArea = g
43
+ .append('circle')
44
+ .attr('class', 'marker-hit-area')
45
+ .attr('fill', 'none')
46
+ .attr('pointer-events', 'all')
47
+ .attr('stroke', 'none')
48
+ .attr('aria-hidden', 'true');
49
+ this.focusRing = g
50
+ .append('g')
51
+ .attr('class', 'marker-focus')
52
+ .attr('visibility', 'hidden')
53
+ .attr('pointer-events', 'none')
54
+ .attr('aria-hidden', 'true');
55
+ for (const [stroke, width] of [
56
+ ['white', 5],
57
+ ['#1d4ed8', 2.5],
58
+ ]) {
59
+ this.focusRing
60
+ .append('circle')
61
+ .attr('fill', 'none')
62
+ .attr('stroke', stroke)
63
+ .attr('stroke-width', width)
64
+ .attr('vector-effect', 'non-scaling-stroke');
65
+ }
66
+ this.triangle = g.append('polygon').attr('stroke', 'none').attr('fill', 'gray');
67
+ this.inner = g.append('polygon').attr('stroke', 'none').attr('fill', color);
68
+ this.text = g
69
+ .append('text')
70
+ .attr('pointer-events', 'none')
71
+ .attr('aria-hidden', 'true')
72
+ .attr('font-family', 'Verdana')
73
+ .attr('font-weight', 'normal')
74
+ .attr('text-anchor', 'middle')
75
+ .attr('dominant-baseline', 'middle')
76
+ .attr('fill', 'white')
77
+ .text(marker.toString());
78
+ this.zoom(1);
79
+ const drag = d3
80
+ .drag()
81
+ .container(() => this.Node.parentNode)
82
+ .touchable(() => true)
83
+ .filter((event) => !this.destroyed && !event.ctrlKey && !event.button && this.activeDrags.size === 0)
84
+ .on('start', (event) => {
85
+ if (this.destroyed || this.activeDrags.size > 0) {
86
+ return;
87
+ }
88
+ this.mouseGesture.capture(event.sourceEvent, 'drag');
89
+ const wasIdle = this.activeDrags.size === 0;
90
+ this.activeDrags.add(event.identifier);
91
+ this.Node.focus({ preventScroll: true });
92
+ if (wasIdle) {
93
+ this.dragStateHandler?.(true);
94
+ this.selectHandler?.();
95
+ }
96
+ })
97
+ .on('drag', (event) => {
98
+ if (!this.destroyed && this.activeDrags.has(event.identifier)) {
99
+ this.dragHandler?.([event.x, event.y]);
100
+ }
101
+ })
102
+ .on('end', (event) => {
103
+ if (this.activeDrags.delete(event.identifier) && this.activeDrags.size === 0) {
104
+ this.dragStateHandler?.(false);
105
+ }
106
+ });
107
+ g.call(drag);
108
+ }
109
+ focus() {
110
+ if (this.destroyed || this.Element.style('display') === 'none' || !this.Node.isConnected) {
111
+ return false;
112
+ }
113
+ this.Node.focus({ preventScroll: true });
114
+ return this.Node.ownerDocument.activeElement === this.Node;
115
+ }
116
+ setReading(name, index, count, frequencyHz) {
117
+ this.sampleIndex = index;
118
+ this.sampleCount = count;
119
+ this.Element.attr('aria-label', name)
120
+ .attr('aria-valuemin', 0)
121
+ .attr('aria-valuemax', count - 1)
122
+ .attr('aria-valuenow', index)
123
+ .attr('aria-valuetext', `${SmithFormatter.number(frequencyHz)}Hz, sample ${index + 1} of ${count}`)
124
+ .attr('aria-disabled', count <= 1 ? 'true' : null);
125
+ }
126
+ keydown(event) {
127
+ if (this.destroyed ||
128
+ event.altKey ||
129
+ event.ctrlKey ||
130
+ event.metaKey ||
131
+ this.activeDrags.size > 0) {
132
+ return;
133
+ }
134
+ const step = event.shiftKey ? 10 : 1;
135
+ let next;
136
+ switch (event.key) {
137
+ case 'ArrowRight':
138
+ case 'ArrowUp':
139
+ next = this.sampleIndex + step;
140
+ break;
141
+ case 'ArrowLeft':
142
+ case 'ArrowDown':
143
+ next = this.sampleIndex - step;
144
+ break;
145
+ case 'PageUp':
146
+ next = this.sampleIndex + 10;
147
+ break;
148
+ case 'PageDown':
149
+ next = this.sampleIndex - 10;
150
+ break;
151
+ case 'Home':
152
+ next = 0;
153
+ break;
154
+ case 'End':
155
+ next = this.sampleCount - 1;
156
+ break;
157
+ default:
158
+ return;
159
+ }
160
+ event.preventDefault();
161
+ event.stopPropagation();
162
+ this.selectHandler?.();
163
+ next = Math.max(0, Math.min(this.sampleCount - 1, next));
164
+ if (next !== this.sampleIndex) {
165
+ this.sampleHandler?.(next);
166
+ }
167
+ }
168
+ destroy() {
169
+ if (this.destroyed) {
170
+ return;
171
+ }
172
+ this.destroyed = true;
173
+ this.dragHandler = null;
174
+ this.sampleHandler = null;
175
+ this.selectHandler = null;
176
+ this.cancelDrag();
177
+ this.dragStateHandler = null;
178
+ this.Element.on('.drag', null).on('.smithkit', null).interrupt();
179
+ this.Element.selectAll('*').on('.drag', null).interrupt();
180
+ this.Element.remove();
181
+ }
182
+ cancelDrag() {
183
+ if (this.activeDrags.size > 0) {
184
+ this.activeDrags.clear();
185
+ this.dragStateHandler?.(false);
186
+ }
187
+ this.mouseGesture.destroy();
188
+ }
189
+ setColor(color) {
190
+ this.inner.attr('fill', color);
191
+ }
192
+ get Position() {
193
+ return this.rc;
194
+ }
195
+ move(p) {
196
+ this.rc = p;
197
+ this.triangle.attr('transform', `translate(${p[0]},${p[1]})`);
198
+ this.inner.attr('transform', `translate(${p[0]},${p[1]})`);
199
+ this.text.attr('x', p[0]).attr('y', p[1]);
200
+ const centerY = p[1] - this.size / this.scale / 2;
201
+ this.hitArea.attr('cx', p[0]).attr('cy', centerY);
202
+ this.focusRing.selectAll('circle').attr('cx', p[0]).attr('cy', centerY);
203
+ }
204
+ zoom(k) {
205
+ if (!Number.isFinite(k) || k <= 0) {
206
+ return;
207
+ }
208
+ this.scale = k;
209
+ const size = this.size / k;
210
+ const h = size / 12;
211
+ const alpha = Math.atan(1 / 2);
212
+ const beta = Math.PI / 2 - alpha;
213
+ const x = h / Math.sin(alpha);
214
+ const y = h / Math.sin(beta) + h / Math.tan(beta);
215
+ this.triangle.attr('points', `0,0 ${size / 2},${-size} ${-size / 2},${-size}`);
216
+ this.inner.attr('points', `0,${-x} ${size / 2 - y},${-size + h} ${-size / 2 + y},${-size + h}`);
217
+ this.text.attr('font-size', size / 1.8).attr('dy', -size / 1.6);
218
+ this.hitArea.attr('r', 22 / k);
219
+ this.focusRing.selectAll('circle').attr('r', 15 / k);
220
+ this.move(this.rc);
221
+ }
222
+ show() {
223
+ this.Element.attr('opacity', null);
224
+ }
225
+ hide() {
226
+ this.Element.attr('opacity', '0');
227
+ }
228
+ setDragHandler(handler) {
229
+ this.dragHandler = handler;
230
+ }
231
+ setSampleHandler(handler) {
232
+ this.sampleHandler = handler;
233
+ }
234
+ setSelectHandler(handler) {
235
+ this.selectHandler = handler;
236
+ }
237
+ }
@@ -0,0 +1,31 @@
1
+ import type { MarkerSelectionStrategy } from '../measurements.js';
2
+ import type { Point } from '../math/geometry.js';
3
+ import type { TraceSample, TraceSamples } from '../samples.js';
4
+ export interface TraceMarker {
5
+ readonly number: number;
6
+ selectedPoint: TraceSample;
7
+ }
8
+ /** Owns validated samples and marker selections without depending on SVG or D3. */
9
+ export declare class TraceModel {
10
+ private samples;
11
+ private markers;
12
+ private markerCount;
13
+ constructor(samples: TraceSamples);
14
+ get Samples(): TraceSamples;
15
+ get Markers(): readonly TraceMarker[];
16
+ addMarker(sampleIndex: number): TraceMarker;
17
+ removeMarker(index: number): TraceMarker | undefined;
18
+ getMarker(index: number): TraceMarker | undefined;
19
+ markerIndex(marker: TraceMarker): number;
20
+ markerSampleIndex(index: number): number;
21
+ setMarkerSample(index: number, sampleIndex: number): TraceMarker | undefined;
22
+ setMarkerFrequency(index: number, frequencyHz: number): TraceMarker | undefined;
23
+ /** Returns false when dragging still selects the same sample. */
24
+ selectNearestPoint(marker: TraceMarker, point: Readonly<Point>): boolean;
25
+ update(values: TraceSamples, strategy: MarkerSelectionStrategy): void;
26
+ clear(): void;
27
+ private nearestFrequencyIndex;
28
+ private nearestPoint;
29
+ private validateSampleIndex;
30
+ private copySamples;
31
+ }
@@ -0,0 +1,123 @@
1
+ /** Owns validated samples and marker selections without depending on SVG or D3. */
2
+ export class TraceModel {
3
+ samples;
4
+ markers = [];
5
+ markerCount = 0;
6
+ constructor(samples) {
7
+ this.samples = this.copySamples(samples);
8
+ }
9
+ get Samples() {
10
+ return this.samples;
11
+ }
12
+ get Markers() {
13
+ return this.markers.slice();
14
+ }
15
+ addMarker(sampleIndex) {
16
+ this.validateSampleIndex(sampleIndex);
17
+ const marker = { number: ++this.markerCount, selectedPoint: this.samples[sampleIndex] };
18
+ this.markers.push(marker);
19
+ return marker;
20
+ }
21
+ removeMarker(index) {
22
+ const marker = this.markers[index];
23
+ if (marker) {
24
+ this.markers.splice(index, 1);
25
+ }
26
+ return marker;
27
+ }
28
+ getMarker(index) {
29
+ return this.markers[index];
30
+ }
31
+ markerIndex(marker) {
32
+ return this.markers.indexOf(marker);
33
+ }
34
+ markerSampleIndex(index) {
35
+ const marker = this.markers[index];
36
+ return marker ? this.samples.indexOf(marker.selectedPoint) : -1;
37
+ }
38
+ setMarkerSample(index, sampleIndex) {
39
+ const marker = this.markers[index];
40
+ if (!marker) {
41
+ return;
42
+ }
43
+ this.validateSampleIndex(sampleIndex);
44
+ marker.selectedPoint = this.samples[sampleIndex];
45
+ return marker;
46
+ }
47
+ setMarkerFrequency(index, frequencyHz) {
48
+ if (!this.markers[index]) {
49
+ return;
50
+ }
51
+ if (!Number.isFinite(frequencyHz) || frequencyHz < 0) {
52
+ throw new RangeError('Marker frequency must be finite and non-negative.');
53
+ }
54
+ return this.setMarkerSample(index, this.nearestFrequencyIndex(frequencyHz));
55
+ }
56
+ /** Returns false when dragging still selects the same sample. */
57
+ selectNearestPoint(marker, point) {
58
+ if (!this.markers.includes(marker)) {
59
+ return false;
60
+ }
61
+ const sample = this.nearestPoint(point);
62
+ if (sample === marker.selectedPoint) {
63
+ return false;
64
+ }
65
+ marker.selectedPoint = sample;
66
+ return true;
67
+ }
68
+ update(values, strategy) {
69
+ // Validate and copy before mutating either samples or marker selections.
70
+ const samples = this.copySamples(values);
71
+ const selectedIndices = this.markers.map((entry) => this.samples.indexOf(entry.selectedPoint));
72
+ this.samples = samples;
73
+ this.markers.forEach((entry, index) => {
74
+ if (strategy === 'sample-index') {
75
+ entry.selectedPoint = samples[Math.min(selectedIndices[index], samples.length - 1)];
76
+ }
77
+ else if (strategy === 'reflection') {
78
+ entry.selectedPoint = this.nearestPoint(entry.selectedPoint.reflectionCoefficient);
79
+ }
80
+ else {
81
+ entry.selectedPoint = samples[this.nearestFrequencyIndex(entry.selectedPoint.frequencyHz)];
82
+ }
83
+ });
84
+ }
85
+ clear() {
86
+ this.samples = [];
87
+ this.markers = [];
88
+ }
89
+ nearestFrequencyIndex(frequencyHz) {
90
+ let closest = 0;
91
+ let distance = Math.abs(this.samples[0].frequencyHz - frequencyHz);
92
+ for (let i = 1; i < this.samples.length; i++) {
93
+ const nextDistance = Math.abs(this.samples[i].frequencyHz - frequencyHz);
94
+ if (nextDistance < distance) {
95
+ closest = i;
96
+ distance = nextDistance;
97
+ }
98
+ }
99
+ return closest;
100
+ }
101
+ nearestPoint(point) {
102
+ const distance = (sample) => Math.hypot(point[0] - sample.reflectionCoefficient[0], point[1] - sample.reflectionCoefficient[1]);
103
+ return this.samples.reduce((previous, current) => distance(previous) <= distance(current) ? previous : current);
104
+ }
105
+ validateSampleIndex(index) {
106
+ if (!Number.isInteger(index) || index < 0 || index >= this.samples.length) {
107
+ throw new RangeError('Sample index is outside this trace.');
108
+ }
109
+ }
110
+ copySamples(values) {
111
+ if (!values.length ||
112
+ values.some(({ frequencyHz, reflectionCoefficient }) => !Number.isFinite(frequencyHz) ||
113
+ frequencyHz < 0 ||
114
+ reflectionCoefficient.length !== 2 ||
115
+ !reflectionCoefficient.every(Number.isFinite))) {
116
+ throw new RangeError('A dataset requires samples with a non-negative finite frequency and two finite coordinates.');
117
+ }
118
+ return values.map(({ frequencyHz, reflectionCoefficient }) => ({
119
+ frequencyHz,
120
+ reflectionCoefficient: [reflectionCoefficient[0], reflectionCoefficient[1]],
121
+ }));
122
+ }
123
+ }
@@ -0,0 +1,33 @@
1
+ import { SmithGroup } from '../svg/SmithGroup.js';
2
+ import { SmithScaler } from '../svg/SmithScaler.js';
3
+ import type { TraceStyle } from '../measurements.js';
4
+ import type { TraceSamples } from '../samples.js';
5
+ export interface TraceTransform {
6
+ x: number;
7
+ y: number;
8
+ k: number;
9
+ }
10
+ /** Draws a trace; sample validation and marker selection belong to TraceModel. */
11
+ export declare class TraceRenderer {
12
+ private data;
13
+ private color;
14
+ private transform;
15
+ private scaler;
16
+ private style;
17
+ private viewportScale;
18
+ private visible;
19
+ private group;
20
+ constructor(data: TraceSamples, color: string, transform: TraceTransform, container: SmithGroup, scaler: SmithScaler, style?: Partial<TraceStyle>);
21
+ private drawTrace;
22
+ private renderPoints;
23
+ setViewportScale(scale: number): void;
24
+ zoom(transform: TraceTransform): void;
25
+ private zoomDataPoints;
26
+ setColor(color: string): void;
27
+ get Style(): TraceStyle;
28
+ setStyle(options: Partial<TraceStyle>): void;
29
+ private redraw;
30
+ setVisible(visible: boolean): void;
31
+ update(data: TraceSamples): void;
32
+ destroy(): void;
33
+ }