smithkit 0.1.1 → 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 +119 -40
  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 +68 -36
  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 +4 -6
  11. package/dist/lib/{draw → grid}/ConstCircles.js +2 -2
  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 +4 -2
  51. package/dist/lib/scales/SmithPeripheralScales.js +46 -26
  52. package/dist/lib/scales/SmithScales.d.ts +3 -1
  53. package/dist/lib/scales/SmithScales.js +8 -2
  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
@@ -1,48 +1,33 @@
1
- import { SmithGroup } from './SmithGroup.js';
1
+ import { SmithGroup } from '../svg/SmithGroup.js';
2
2
  import type { TraceStyle, MarkerSelectionStrategy } from '../measurements.js';
3
3
  import { SmithMarker } from './SmithMarker.js';
4
- import { SmithScaler } from './SmithScaler.js';
5
- import { TraceSamples, TraceSample } from '../samples.js';
6
- interface Marker {
7
- marker: SmithMarker;
8
- selectedPoint: TraceSample;
9
- number: number;
10
- }
11
- interface Transform {
12
- x: number;
13
- y: number;
14
- k: number;
15
- }
4
+ import { SmithScaler } from '../svg/SmithScaler.js';
5
+ import type { TraceSamples, TraceSample } from '../samples.js';
6
+ import { type TraceTransform } from './TraceRenderer.js';
7
+ /** Coordinates trace rendering, draggable markers, and deferred marker events. */
16
8
  export declare class SmithData {
17
- private data;
18
9
  private color;
19
10
  private transform;
20
- private fgContainer;
21
11
  private scaler;
22
12
  private markerDragHandler?;
23
13
  private markerContainer;
24
- private style;
25
- private viewportScale;
14
+ private readonly model;
15
+ private readonly renderer;
16
+ private readonly markers;
17
+ private readonly pendingEvents;
26
18
  private destroyed;
19
+ private viewportScale;
20
+ private name;
21
+ private selectHandler;
27
22
  private visible;
28
- private pendingEvents;
29
- private group;
30
- private markersCount;
31
- private markers;
32
23
  private handler;
33
- constructor(data: TraceSamples, color: string, transform: Transform, fgContainer: SmithGroup, scaler: SmithScaler, markerDragHandler?: ((marker: SmithMarker, dragging: boolean) => void) | undefined, markerContainer?: SmithGroup, style?: Partial<TraceStyle>);
34
- private drawTrace;
35
- private renderPoints;
24
+ constructor(data: TraceSamples, color: string, transform: TraceTransform, container: SmithGroup, scaler: SmithScaler, markerDragHandler?: ((marker: SmithMarker, dragging: boolean) => void) | undefined, markerContainer?: SmithGroup, style?: Partial<TraceStyle>);
36
25
  setViewportScale(scale: number): void;
37
- zoom(transform: Transform): void;
38
- private zoomDataPoints;
39
- private zoomAllMarkers;
26
+ zoom(transform: TraceTransform): void;
40
27
  addMarker(sampleIndex?: number): number;
41
28
  removeMarker(index: number): boolean;
42
29
  setMarkerSample(index: number, sampleIndex: number): boolean;
43
30
  setMarkerFrequency(index: number, frequencyHz: number): boolean;
44
- private nearestFrequencyIndex;
45
- private validateSampleIndex;
46
31
  get Samples(): TraceSamples;
47
32
  get SampleCount(): number;
48
33
  get Color(): string;
@@ -51,16 +36,20 @@ export declare class SmithData {
51
36
  setColor(color: string): void;
52
37
  get Style(): TraceStyle;
53
38
  setStyle(options: Partial<TraceStyle>): void;
54
- private redraw;
55
39
  setVisible(visible: boolean): void;
56
- private copySamples;
40
+ private moveMarker;
41
+ private updateReading;
42
+ setName(name: string): void;
43
+ setMarkerSelectHandler(handler: (marker: SmithMarker) => void): void;
57
44
  private notifyMarker;
58
45
  private cancelEvents;
59
46
  update(values: TraceSamples, strategy?: MarkerSelectionStrategy): void;
60
47
  destroy(): void;
61
- getMarker(index: number): Marker | undefined;
62
- get Markers(): Marker[];
48
+ get Markers(): {
49
+ number: number;
50
+ selectedPoint: TraceSample;
51
+ marker: SmithMarker;
52
+ }[];
63
53
  setMarkerMoveHandler(handler: (marker: number, data: TraceSample) => void): void;
64
- private findClosestPointTo;
54
+ private assertAlive;
65
55
  }
66
- export {};
@@ -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
+ }