smithkit 0.2.0 → 0.4.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 (48) hide show
  1. package/README.md +318 -25
  2. package/dist/lib/OptionsValidation.d.ts +16 -0
  3. package/dist/lib/OptionsValidation.js +168 -0
  4. package/dist/lib/Smith.d.ts +24 -39
  5. package/dist/lib/Smith.js +175 -287
  6. package/dist/lib/appearance/SvgTheme.d.ts +6 -0
  7. package/dist/lib/appearance/SvgTheme.js +21 -0
  8. package/dist/lib/appearance/Theme.d.ts +8 -0
  9. package/dist/lib/appearance/Theme.js +125 -0
  10. package/dist/lib/appearance/types.d.ts +55 -0
  11. package/dist/lib/appearance/types.js +1 -0
  12. package/dist/lib/grid/CircleLayerControl.d.ts +1 -1
  13. package/dist/lib/grid/CircleLayerControl.js +10 -8
  14. package/dist/lib/grid/ConstCircles.d.ts +0 -3
  15. package/dist/lib/grid/ConstCircles.js +6 -20
  16. package/dist/lib/grid/ConstQCircles.d.ts +1 -0
  17. package/dist/lib/grid/ConstQCircles.js +6 -1
  18. package/dist/lib/grid/ConstSwrCircles.d.ts +1 -0
  19. package/dist/lib/grid/ConstSwrCircles.js +6 -1
  20. package/dist/lib/grid/GridDefinitions.d.ts +7 -1
  21. package/dist/lib/grid/GridDefinitions.js +29 -0
  22. package/dist/lib/grid/GridLayerControl.d.ts +5 -4
  23. package/dist/lib/grid/GridLayerControl.js +13 -3
  24. package/dist/lib/grid/LayerControl.d.ts +1 -2
  25. package/dist/lib/grid/LayerControl.js +1 -10
  26. package/dist/lib/grid/SmithGridLayer.d.ts +7 -1
  27. package/dist/lib/grid/SmithGridLayer.js +35 -5
  28. package/dist/lib/index.d.ts +4 -1
  29. package/dist/lib/interaction/SmithCursor.d.ts +0 -17
  30. package/dist/lib/interaction/SmithCursor.js +6 -14
  31. package/dist/lib/layers.d.ts +7 -1
  32. package/dist/lib/options.d.ts +54 -0
  33. package/dist/lib/options.js +1 -0
  34. package/dist/lib/rendering/SvgChartRenderer.d.ts +69 -0
  35. package/dist/lib/rendering/SvgChartRenderer.js +364 -0
  36. package/dist/lib/scales/SmithPeripheralScales.js +11 -11
  37. package/dist/lib/scales/SmithScales.d.ts +10 -3
  38. package/dist/lib/scales/SmithScales.js +73 -39
  39. package/dist/lib/svg/ImageExporter.d.ts +18 -0
  40. package/dist/lib/svg/ImageExporter.js +211 -0
  41. package/dist/lib/svg/MarkerLegend.d.ts +12 -0
  42. package/dist/lib/svg/MarkerLegend.js +63 -0
  43. package/dist/lib/svg/SvgExporter.d.ts +6 -1
  44. package/dist/lib/svg/SvgExporter.js +50 -5
  45. package/dist/lib/svg/export.d.ts +36 -0
  46. package/dist/lib/svg/export.js +1 -0
  47. package/dist/lib/traces/SmithMarker.js +8 -5
  48. package/package.json +1 -1
@@ -8,11 +8,6 @@ export class SmithCursor {
8
8
  scaler;
9
9
  epsilon = 1.5e-4;
10
10
  pendingMove;
11
- drawingOpts = {
12
- point: { radius: 5, color: 'red' },
13
- impedance: { width: 1, color: 'red' },
14
- admittance: { width: 1, color: 'green' },
15
- };
16
11
  calcs = new SmithConstantCircle();
17
12
  zClipCircle = this.calcs.resistanceCircle(0);
18
13
  yClipCircle = this.calcs.conductanceCircle(0);
@@ -27,8 +22,8 @@ export class SmithCursor {
27
22
  this.group = new SmithGroup().attr('pointer-events', 'none');
28
23
  this.impedance = {
29
24
  group: new SmithGroup({
30
- stroke: this.drawingOpts.impedance.color,
31
- strokeWidth: this.drawingOpts.impedance.width.toString(),
25
+ stroke: 'var(--smithkit-cursor-impedanceColor)',
26
+ strokeWidth: 'var(--smithkit-cursor-lineWidth)',
32
27
  fill: 'none',
33
28
  }),
34
29
  resistance: { circle: new SmithCircle({ p: [0, 0], r: 1 }) },
@@ -42,8 +37,8 @@ export class SmithCursor {
42
37
  this.impedance.reactance.line.nonScalingStroke();
43
38
  this.admittance = {
44
39
  group: new SmithGroup({
45
- stroke: this.drawingOpts.admittance.color,
46
- strokeWidth: this.drawingOpts.admittance.width.toString(),
40
+ stroke: 'var(--smithkit-cursor-admittanceColor)',
41
+ strokeWidth: 'var(--smithkit-cursor-lineWidth)',
47
42
  fill: 'none',
48
43
  }),
49
44
  conductance: { circle: new SmithCircle({ p: [0, 0], r: 1 }) },
@@ -56,8 +51,8 @@ export class SmithCursor {
56
51
  this.admittance.susceptance.arc.nonScalingStroke();
57
52
  this.admittance.susceptance.line.nonScalingStroke();
58
53
  this.point = new SmithLine([0, 0], [0, 0], {
59
- stroke: this.drawingOpts.point.color,
60
- strokeWidth: this.drawingOpts.point.radius.toString(),
54
+ stroke: 'var(--smithkit-cursor-pointColor)',
55
+ strokeWidth: 'var(--smithkit-cursor-pointSize)',
61
56
  fill: 'none',
62
57
  });
63
58
  this.point.nonScalingStroke();
@@ -180,7 +175,4 @@ export class SmithCursor {
180
175
  this.cancelPendingMove();
181
176
  }
182
177
  }
183
- setDrawOptions(opts) {
184
- this.drawingOpts = opts;
185
- }
186
178
  }
@@ -15,15 +15,21 @@ export interface CircleStyle {
15
15
  /** Non-scaling stroke width in CSS pixels. */
16
16
  strokeWidth: number;
17
17
  }
18
+ export type GridDetail = 'basic' | 'standard' | 'detailed';
18
19
  /** Grid styling uses SVG user units, except non-scaling stroke widths in screen pixels. */
19
20
  export interface GridLayer {
20
21
  setVisible(visible: boolean): void;
21
- setMinorVisible(visible: boolean): void;
22
+ /** Show or hide labels independently of grid lines. Default: true. */
23
+ setLabelsVisible(visible: boolean): void;
24
+ /** Default: standard. Basic shows only the principal normalized values. */
25
+ setDetail(detail: GridDetail): void;
22
26
  setStyle(style: Partial<GridStyle>): void;
23
27
  }
24
28
  export interface CircleLayer {
25
29
  setVisible(visible: boolean): void;
26
30
  setStyle(style: Partial<CircleStyle>): void;
31
+ /** Replace the complete set after validation; duplicates are ignored and empty clears it. */
32
+ setValues(values: readonly number[]): void;
27
33
  addValue(value: number): void;
28
34
  removeValue(value: number): void;
29
35
  }
@@ -0,0 +1,54 @@
1
+ import type { SmithAppearance } from './appearance/types.js';
2
+ import type { CircleStyle, GridDetail, GridStyle } from './layers.js';
3
+ /** Settings for an individual impedance/admittance grid layer. */
4
+ export interface GridLayerOptions {
5
+ visible?: boolean;
6
+ detail?: GridDetail;
7
+ labelsVisible?: boolean;
8
+ style?: Partial<GridStyle>;
9
+ }
10
+ /** Shared settings for all four grids, followed by per-layer overrides. */
11
+ export interface GridOptions {
12
+ /** Default: standard. */
13
+ detail?: GridDetail;
14
+ /** Default: true. */
15
+ labelsVisible?: boolean;
16
+ style?: Partial<GridStyle>;
17
+ layers?: {
18
+ resistance?: GridLayerOptions;
19
+ reactance?: GridLayerOptions;
20
+ conductance?: GridLayerOptions;
21
+ susceptance?: GridLayerOptions;
22
+ };
23
+ }
24
+ export interface CircleLayerOptions {
25
+ /** Default: false. */
26
+ visible?: boolean;
27
+ style?: Partial<CircleStyle>;
28
+ /** Replace the default values. Empty hides all circles; duplicate values are ignored. */
29
+ values?: readonly number[];
30
+ }
31
+ export interface CircleOptions {
32
+ q?: CircleLayerOptions;
33
+ vswr?: CircleLayerOptions;
34
+ }
35
+ export interface InteractionOptions {
36
+ /** Enable wheel/pan/pinch gestures. Default: false. */
37
+ zoom?: boolean;
38
+ /** Enable cursor overlay and pointer readings. Default: false. */
39
+ cursor?: boolean;
40
+ }
41
+ export interface PeripheralScalesOptions {
42
+ /** Show the peripheral rulers as one group. Default: false. */
43
+ visible?: boolean;
44
+ }
45
+ export interface SmithOptions {
46
+ /** Positive real reference impedance in ohms. Default: 50. */
47
+ referenceImpedanceOhms?: number;
48
+ /** Default: the light preset. Use overrides for fonts, colors, and marker/cursor styling. */
49
+ appearance?: SmithAppearance;
50
+ interaction?: InteractionOptions;
51
+ grid?: GridOptions;
52
+ circles?: CircleOptions;
53
+ peripheralScales?: PeripheralScalesOptions;
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import type { SmithTheme } from '../appearance/types.js';
2
+ import { SmithData } from '../traces/SmithData.js';
3
+ import type { SmithMarker } from '../traces/SmithMarker.js';
4
+ import { TraceSamples } from '../samples.js';
5
+ import { Complex } from '../math/Complex.js';
6
+ import type { ChartLayers, PeripheralScales } from '../layers.js';
7
+ import type { TraceStyle } from '../measurements.js';
8
+ /** Internal SVG backend. Owns chart elements, view transforms, and SVG gestures. */
9
+ export declare class SvgChartRenderer {
10
+ private onCursorChange;
11
+ private calcs;
12
+ private scalers;
13
+ private readonly defaultTransform;
14
+ private transform;
15
+ private zoomEnabled;
16
+ private cursorEnabled;
17
+ private applyingView;
18
+ private zoomBehavior;
19
+ private svg;
20
+ private resizeObserver;
21
+ private readonly labelLayout;
22
+ private container;
23
+ private dataContainer;
24
+ private markerContainer;
25
+ private reactanceAxis;
26
+ private constResistance;
27
+ private constReactance;
28
+ private constConductance;
29
+ private constSusceptance;
30
+ private constSwrCircles;
31
+ private constQCircles;
32
+ private cursor;
33
+ private peripheralScaleRenderer;
34
+ private readonly traces;
35
+ private destroyed;
36
+ private mouseGesture;
37
+ private markerDragging;
38
+ private cursorBeforeMarkerDrag;
39
+ readonly layers: ChartLayers;
40
+ readonly peripheralScales: PeripheralScales;
41
+ constructor(onCursorChange: ((position: Complex | undefined) => void) | null);
42
+ setTheme(theme: SmithTheme): void;
43
+ draw(target: string | HTMLElement): void;
44
+ /** Export the current mounted view as standalone SVG with its configured background. */
45
+ toSvg(): string;
46
+ /** Remove this chart and release its event handlers. Safe to call more than once. */
47
+ destroy(): void;
48
+ private updateViewportScale;
49
+ private assertAlive;
50
+ private createScalers;
51
+ private cursorMove;
52
+ setMarkerDragging(dragging: boolean): void;
53
+ hideCursor(): void;
54
+ /** Disable tracking and cancel queued readings; enabling waits for the next pointer move. */
55
+ setCursorEnabled(enabled: boolean): void;
56
+ private initCursor;
57
+ get cursorPosition(): Complex;
58
+ private initializeZoom;
59
+ /** Enable or disable wheel, double-click, and mouse/touch zoom/pan. Keeps the current view. */
60
+ setZoomEnabled(enabled: boolean): void;
61
+ private applyView;
62
+ resetView(): void;
63
+ private onZoom;
64
+ private cursorContainer;
65
+ private drawReactanceAxis;
66
+ /** Construct trace rendering without exposing chart containers or scalers to Smith. */
67
+ createTrace(values: TraceSamples, color: string, style: Partial<TraceStyle>, onMarkerDrag: (marker: SmithMarker, dragging: boolean) => void): SmithData;
68
+ removeTrace(trace: SmithData): void;
69
+ }
@@ -0,0 +1,364 @@
1
+ import { SvgTheme } from '../appearance/SvgTheme.js';
2
+ import * as d3 from 'd3';
3
+ import { MouseGesture } from '../interaction/MouseGesture.js';
4
+ import { SmithSvg } from '../svg/SmithSvg.js';
5
+ import { LabelLayout } from '../svg/LabelLayout.js';
6
+ import { SvgExporter } from '../svg/SvgExporter.js';
7
+ import { SmithGroup } from '../svg/SmithGroup.js';
8
+ import { SmithCircle } from '../svg/SmithCircle.js';
9
+ import { SmithData } from '../traces/SmithData.js';
10
+ import { SmithCursor } from '../interaction/SmithCursor.js';
11
+ import { ConstResistance } from '../grid/ConstResistance.js';
12
+ import { ConstReactance } from '../grid/ConstReactance.js';
13
+ import { ConstConductance } from '../grid/ConstConductance.js';
14
+ import { ConstSusceptance } from '../grid/ConstSusceptance.js';
15
+ import { ConstQCircles } from '../grid/ConstQCircles.js';
16
+ import { ConstSwrCircles } from '../grid/ConstSwrCircles.js';
17
+ import { SmithScaler } from '../svg/SmithScaler.js';
18
+ import { SmithConstantCircle } from '../rf/SmithConstantCircle.js';
19
+ import { GridDefinitions } from '../grid/GridDefinitions.js';
20
+ import { Complex } from '../math/Complex.js';
21
+ import { SmithPeripheralScales } from '../scales/SmithPeripheralScales.js';
22
+ import { GridLayerControl } from '../grid/GridLayerControl.js';
23
+ import { CircleLayerControl } from '../grid/CircleLayerControl.js';
24
+ /** Internal SVG backend. Owns chart elements, view transforms, and SVG gestures. */
25
+ export class SvgChartRenderer {
26
+ onCursorChange;
27
+ calcs = new SmithConstantCircle();
28
+ scalers;
29
+ defaultTransform = d3.zoomIdentity.translate(62.5, 62.5).scale(0.75);
30
+ transform = d3.zoomIdentity;
31
+ zoomEnabled = false;
32
+ cursorEnabled = false;
33
+ applyingView = false;
34
+ zoomBehavior = d3.zoom();
35
+ svg;
36
+ resizeObserver;
37
+ labelLayout;
38
+ container;
39
+ dataContainer;
40
+ markerContainer;
41
+ reactanceAxis;
42
+ constResistance;
43
+ constReactance;
44
+ constConductance;
45
+ constSusceptance;
46
+ constSwrCircles;
47
+ constQCircles;
48
+ cursor;
49
+ peripheralScaleRenderer = new SmithPeripheralScales();
50
+ traces = new Set();
51
+ destroyed = false;
52
+ mouseGesture = new MouseGesture();
53
+ markerDragging = false;
54
+ cursorBeforeMarkerDrag = null;
55
+ layers;
56
+ peripheralScales;
57
+ constructor(onCursorChange) {
58
+ this.onCursorChange = onCursorChange;
59
+ const viewBoxSize = 500;
60
+ const gridData = GridDefinitions.create();
61
+ this.scalers = this.createScalers(viewBoxSize);
62
+ this.svg = new SmithSvg(viewBoxSize);
63
+ this.container = new SmithGroup();
64
+ this.constResistance = new ConstResistance({
65
+ data: gridData,
66
+ scaler: this.scalers.default,
67
+ detail: 'standard',
68
+ });
69
+ this.constResistance.show();
70
+ this.constReactance = new ConstReactance({
71
+ data: gridData,
72
+ scaler: this.scalers.default,
73
+ detail: 'standard',
74
+ });
75
+ this.constReactance.show();
76
+ this.constConductance = new ConstConductance({
77
+ data: gridData,
78
+ scaler: this.scalers.default,
79
+ detail: 'standard',
80
+ });
81
+ this.constConductance.hide();
82
+ this.constSusceptance = new ConstSusceptance({
83
+ data: gridData,
84
+ scaler: this.scalers.default,
85
+ detail: 'standard',
86
+ });
87
+ this.constSusceptance.hide();
88
+ this.constQCircles = new ConstQCircles(this.scalers.default);
89
+ this.constQCircles.hide();
90
+ this.constSwrCircles = new ConstSwrCircles(this.scalers.default);
91
+ this.constSwrCircles.hide();
92
+ this.cursor = this.initCursor();
93
+ const cursorContainer = this.cursorContainer();
94
+ this.reactanceAxis = this.drawReactanceAxis({
95
+ stroke: 'var(--smithkit-boundary-stroke)',
96
+ strokeWidth: 'var(--smithkit-boundary-width)',
97
+ fill: 'none',
98
+ });
99
+ this.dataContainer = new SmithGroup().attr('data-layer', 'samples');
100
+ this.markerContainer = new SmithGroup().attr('data-layer', 'markers');
101
+ // build chart
102
+ this.svg.append(this.container);
103
+ this.container.append(this.constConductance.draw().attr('data-layer', 'conductance'));
104
+ this.container.append(this.constSusceptance.draw().attr('data-layer', 'susceptance'));
105
+ this.container.append(this.constResistance.draw().attr('data-layer', 'resistance'));
106
+ this.container.append(this.constReactance.draw().attr('data-layer', 'reactance'));
107
+ this.container.append(this.constQCircles.draw());
108
+ this.container.append(this.constSwrCircles.draw());
109
+ this.container.append(this.peripheralScaleRenderer);
110
+ this.container.append(this.cursor.Group);
111
+ this.container.append(this.reactanceAxis);
112
+ this.container.append(cursorContainer);
113
+ this.container.append(this.dataContainer);
114
+ const labels = new SmithGroup().attr('data-layer', 'labels').attr('pointer-events', 'none');
115
+ for (const [name, layer] of [
116
+ ['conductance', this.constConductance],
117
+ ['susceptance', this.constSusceptance],
118
+ ['resistance', this.constResistance],
119
+ ['reactance', this.constReactance],
120
+ ]) {
121
+ labels.append(layer.labels.attr('data-label-layer', name));
122
+ }
123
+ labels.append(this.peripheralScaleRenderer.labels);
124
+ this.container.append(labels);
125
+ this.container.append(this.markerContainer);
126
+ this.labelLayout = new LabelLayout(this.svg.Node, labels.Node, this.defaultTransform);
127
+ const assertAlive = () => this.assertAlive();
128
+ this.layers = {
129
+ resistance: new GridLayerControl(this.constResistance, assertAlive, () => this.labelLayout.update(true)),
130
+ reactance: new GridLayerControl(this.constReactance, assertAlive, () => this.labelLayout.update(true)),
131
+ conductance: new GridLayerControl(this.constConductance, assertAlive, () => this.labelLayout.update(true)),
132
+ susceptance: new GridLayerControl(this.constSusceptance, assertAlive, () => this.labelLayout.update(true)),
133
+ q: new CircleLayerControl(this.constQCircles, 0, assertAlive),
134
+ vswr: new CircleLayerControl(this.constSwrCircles, 1, assertAlive),
135
+ };
136
+ this.peripheralScales = {
137
+ setVisible: (visible) => {
138
+ assertAlive();
139
+ if (typeof visible !== 'boolean') {
140
+ throw new TypeError('Peripheral scale visibility must be a boolean.');
141
+ }
142
+ if (visible) {
143
+ this.peripheralScaleRenderer.show();
144
+ }
145
+ else {
146
+ this.peripheralScaleRenderer.hide();
147
+ }
148
+ this.labelLayout.update(true);
149
+ },
150
+ update: (gamma) => {
151
+ assertAlive();
152
+ this.peripheralScaleRenderer.update(gamma);
153
+ },
154
+ };
155
+ this.peripheralScaleRenderer.hide();
156
+ this.initializeZoom();
157
+ this.resizeObserver = new ResizeObserver(() => this.updateViewportScale());
158
+ this.resizeObserver.observe(this.svg.Node);
159
+ }
160
+ setTheme(theme) {
161
+ this.assertAlive();
162
+ SvgTheme.apply(this.svg.Node, theme);
163
+ this.labelLayout.update(true);
164
+ }
165
+ draw(target) {
166
+ this.assertAlive();
167
+ const host = typeof target === 'string' ? document.querySelector(target) : target;
168
+ if (!host) {
169
+ throw new Error('Chart container was not found.');
170
+ }
171
+ host.appendChild(this.svg.Node);
172
+ this.updateViewportScale();
173
+ }
174
+ /** Export the current mounted view as standalone SVG with its configured background. */
175
+ toSvg() {
176
+ this.assertAlive();
177
+ return SvgExporter.chart(this.svg.Node);
178
+ }
179
+ /** Remove this chart and release its event handlers. Safe to call more than once. */
180
+ destroy() {
181
+ if (this.destroyed) {
182
+ return;
183
+ }
184
+ this.resizeObserver.disconnect();
185
+ this.traces.forEach((trace) => trace.destroy());
186
+ this.traces.clear();
187
+ this.destroyed = true;
188
+ this.cursor.setMoveHandler(null);
189
+ this.onCursorChange = null;
190
+ this.mouseGesture.destroy();
191
+ this.zoomBehavior.on('start', null).on('zoom', null);
192
+ this.svg.Element.interrupt().on('.zoom', null);
193
+ this.svg.Element.selectAll('*').interrupt().on('.smithkit', null).on('.drag', null);
194
+ this.svg.Element.remove();
195
+ }
196
+ updateViewportScale() {
197
+ if (this.destroyed) {
198
+ return;
199
+ }
200
+ const matrix = this.svg.Node.getScreenCTM();
201
+ if (matrix) {
202
+ const scale = Math.hypot(matrix.a, matrix.b);
203
+ this.traces.forEach((trace) => trace.setViewportScale(scale));
204
+ }
205
+ this.labelLayout.update();
206
+ }
207
+ assertAlive() {
208
+ if (this.destroyed) {
209
+ throw new Error('This Smith chart has been destroyed. Create a new instance.');
210
+ }
211
+ }
212
+ createScalers(size) {
213
+ const impedance = new SmithScaler(d3.scaleLinear().domain([-1, 1]).range([0, size]), d3.scaleLinear().domain([1, -1]).range([0, size]), d3
214
+ .scaleLinear()
215
+ .domain([0, 1])
216
+ .range([0, size / 2]));
217
+ const admittance = new SmithScaler(d3.scaleLinear().domain([1, -1]).range([0, size]), d3.scaleLinear().domain([-1, 1]).range([0, size]), d3
218
+ .scaleLinear()
219
+ .domain([0, 1])
220
+ .range([0, size / 2]));
221
+ return { default: impedance, impedance, admittance };
222
+ }
223
+ cursorMove(p) {
224
+ if (!this.cursorEnabled || this.markerDragging) {
225
+ return;
226
+ }
227
+ this.cursor.Position = Complex.from(this.scalers.default.pointInvert(p));
228
+ }
229
+ setMarkerDragging(dragging) {
230
+ this.markerDragging = dragging;
231
+ if (dragging) {
232
+ this.cursorBeforeMarkerDrag = this.svg.Node.style.getPropertyValue('cursor') || null;
233
+ this.svg.Element.style('cursor', 'grabbing').style('--smithkit-marker-cursor', 'grabbing');
234
+ this.hideCursor();
235
+ }
236
+ else {
237
+ this.svg.Element.style('cursor', () => this.cursorBeforeMarkerDrag).style('--smithkit-marker-cursor', null);
238
+ this.cursorBeforeMarkerDrag = null;
239
+ }
240
+ }
241
+ hideCursor() {
242
+ this.cursor.hide();
243
+ if (this.cursorEnabled) {
244
+ this.onCursorChange?.(undefined);
245
+ }
246
+ }
247
+ /** Disable tracking and cancel queued readings; enabling waits for the next pointer move. */
248
+ setCursorEnabled(enabled) {
249
+ this.assertAlive();
250
+ if (typeof enabled !== 'boolean') {
251
+ throw new TypeError('Cursor enabled must be a boolean.');
252
+ }
253
+ if (this.cursorEnabled === enabled) {
254
+ return;
255
+ }
256
+ this.cursorEnabled = enabled;
257
+ if (!enabled) {
258
+ this.cursor.hide();
259
+ this.onCursorChange?.(undefined);
260
+ }
261
+ }
262
+ initCursor() {
263
+ const cursor = new SmithCursor(this.scalers.default);
264
+ cursor.Group.attr('class', 'smith-cursor');
265
+ cursor.setMoveHandler(() => {
266
+ this.onCursorChange?.(this.cursor.Position);
267
+ });
268
+ return cursor;
269
+ }
270
+ get cursorPosition() {
271
+ return this.cursor.Position;
272
+ }
273
+ initializeZoom() {
274
+ const zoom = this.zoomBehavior
275
+ .scaleExtent([0.6, 1000])
276
+ .filter((event) => this.zoomEnabled &&
277
+ !this.markerDragging &&
278
+ (!event.ctrlKey || event.type === 'wheel') &&
279
+ !event.button)
280
+ .on('start', (event) => {
281
+ if (event.sourceEvent) {
282
+ this.mouseGesture.capture(event.sourceEvent, 'zoom');
283
+ }
284
+ })
285
+ .on('zoom', (event) => {
286
+ if ((!this.zoomEnabled || this.markerDragging) && !this.applyingView) {
287
+ // An already active mouse/touch gesture still needs its normal end event.
288
+ // Restore D3's view through its public API while that gesture finishes.
289
+ this.applyView(this.transform);
290
+ return;
291
+ }
292
+ this.onZoom(event.transform);
293
+ });
294
+ this.svg.Element.call(zoom);
295
+ this.resetView();
296
+ }
297
+ /** Enable or disable wheel, double-click, and mouse/touch zoom/pan. Keeps the current view. */
298
+ setZoomEnabled(enabled) {
299
+ this.assertAlive();
300
+ if (typeof enabled !== 'boolean') {
301
+ throw new TypeError('Zoom enabled must be a boolean.');
302
+ }
303
+ if (this.zoomEnabled === enabled) {
304
+ return;
305
+ }
306
+ this.zoomEnabled = enabled;
307
+ if (!enabled) {
308
+ this.svg.Element.interrupt();
309
+ }
310
+ }
311
+ applyView(transform) {
312
+ this.applyingView = true;
313
+ try {
314
+ this.svg.Element.call(this.zoomBehavior.transform, transform);
315
+ }
316
+ finally {
317
+ this.applyingView = false;
318
+ }
319
+ }
320
+ resetView() {
321
+ this.assertAlive();
322
+ const transform = this.defaultTransform;
323
+ this.applyView(transform);
324
+ }
325
+ onZoom(transform) {
326
+ if (this.destroyed) {
327
+ return;
328
+ }
329
+ this.transform = transform;
330
+ this.container.Element.attr('transform', transform.toString());
331
+ this.traces.forEach((d) => d.zoom(transform));
332
+ }
333
+ cursorContainer() {
334
+ const shape = this.drawReactanceAxis({ fill: 'transparent', stroke: 'none' });
335
+ shape.Element.style('pointer-events', 'all')
336
+ .on('pointermove.smithkit', (event) => {
337
+ this.cursorMove(d3.pointer(event));
338
+ })
339
+ .on('pointerleave.smithkit', () => {
340
+ this.hideCursor();
341
+ });
342
+ return shape;
343
+ }
344
+ drawReactanceAxis(opts) {
345
+ const c = this.calcs.resistanceCircle(0);
346
+ c.p[0] = this.scalers.default.x(c.p[0]);
347
+ c.p[1] = this.scalers.default.y(c.p[1]);
348
+ c.r = this.scalers.default.r(c.r);
349
+ return new SmithCircle(c, opts);
350
+ }
351
+ /** Construct trace rendering without exposing chart containers or scalers to Smith. */
352
+ createTrace(values, color, style, onMarkerDrag) {
353
+ this.assertAlive();
354
+ const trace = new SmithData(values, color, this.transform, this.dataContainer, this.scalers.default, onMarkerDrag, this.markerContainer, style);
355
+ this.traces.add(trace);
356
+ this.updateViewportScale();
357
+ return trace;
358
+ }
359
+ removeTrace(trace) {
360
+ // Release gestures while Smith can still resolve the marker's public snapshot.
361
+ trace.destroy();
362
+ this.traces.delete(trace);
363
+ }
364
+ }
@@ -42,7 +42,7 @@ export class SmithPeripheralScales extends SmithGroup {
42
42
  .attr('cy', 250)
43
43
  .attr('r', (radius) => radius)
44
44
  .attr('fill', 'none')
45
- .attr('stroke', '#334155')
45
+ .attr('stroke', 'var(--smithkit-scales-boundaryColor)')
46
46
  .attr('stroke-width', 1.1)
47
47
  .attr('vector-effect', 'non-scaling-stroke');
48
48
  this.axes.each(function (scale, index) {
@@ -60,7 +60,7 @@ export class SmithPeripheralScales extends SmithGroup {
60
60
  .attr('cy', 250)
61
61
  .attr('r', radius)
62
62
  .attr('fill', 'none')
63
- .attr('stroke', '#475569')
63
+ .attr('stroke', 'var(--smithkit-scales-stroke)')
64
64
  .attr('stroke-width', 0.6)
65
65
  .attr('vector-effect', 'non-scaling-stroke');
66
66
  }
@@ -75,7 +75,7 @@ export class SmithPeripheralScales extends SmithGroup {
75
75
  .attr('y1', y1)
76
76
  .attr('x2', x2)
77
77
  .attr('y2', y2)
78
- .attr('stroke', '#475569')
78
+ .attr('stroke', 'var(--smithkit-scales-stroke)')
79
79
  .attr('stroke-width', major ? 0.8 : 0.5)
80
80
  .attr('vector-effect', 'non-scaling-stroke');
81
81
  const captionDistance = Math.abs(((angle - captionAngle + 540) % 360) - 180);
@@ -89,9 +89,9 @@ export class SmithPeripheralScales extends SmithGroup {
89
89
  .attr('transform', `rotate(${rotation},${x},${y})`)
90
90
  .attr('text-anchor', 'middle')
91
91
  .attr('dominant-baseline', 'central')
92
- .attr('font-family', 'system-ui, sans-serif')
93
- .attr('font-size', 10.5)
94
- .attr('fill', '#334155')
92
+ .attr('font-family', 'var(--smithkit-fontFamily)')
93
+ .attr('font-size', 'var(--smithkit-scales-peripheralFontSize)')
94
+ .attr('fill', 'var(--smithkit-scales-textColor)')
95
95
  .text(scale.unit === 'λ' ? value.toFixed(2) : String(value));
96
96
  }
97
97
  }
@@ -114,11 +114,11 @@ export class SmithPeripheralScales extends SmithGroup {
114
114
  .append('text')
115
115
  .attr('class', 'peripheral-caption')
116
116
  .attr('text-anchor', 'middle')
117
- .attr('font-family', 'system-ui, sans-serif')
118
- .attr('font-size', 10.5)
117
+ .attr('font-family', 'var(--smithkit-fontFamily)')
118
+ .attr('font-size', 'var(--smithkit-scales-peripheralFontSize)')
119
119
  .attr('font-weight', 500)
120
120
  .attr('letter-spacing', 0.3)
121
- .attr('fill', '#334155')
121
+ .attr('fill', 'var(--smithkit-scales-textColor)')
122
122
  .append('textPath')
123
123
  .attr('href', `#${pathId}`)
124
124
  .attr('startOffset', '50%')
@@ -128,8 +128,8 @@ export class SmithPeripheralScales extends SmithGroup {
128
128
  .attr('class', 'peripheral-indicator')
129
129
  .attr('r', 3)
130
130
  .attr('visibility', 'hidden')
131
- .attr('fill', '#dc2626')
132
- .attr('stroke', 'white')
131
+ .attr('fill', 'var(--smithkit-scales-indicatorColor)')
132
+ .attr('stroke', 'var(--smithkit-scales-indicatorOutline)')
133
133
  .attr('stroke-width', 1);
134
134
  });
135
135
  }
@@ -1,3 +1,5 @@
1
+ import type { ScaleImageExportOptions } from '../svg/export.js';
2
+ import type { SmithAppearance } from '../appearance/types.js';
1
3
  import type { Complex } from '../math/Complex.js';
2
4
  /** Independent parameter scales. Connect update() to a chart's cursor events. */
3
5
  export declare class SmithScales {
@@ -7,12 +9,17 @@ export declare class SmithScales {
7
9
  private readonly start;
8
10
  private readonly length;
9
11
  private readonly axes;
10
- constructor();
12
+ constructor(appearance?: SmithAppearance);
13
+ /** Replace the preset and overrides without changing readings or layout. */
14
+ setAppearance(appearance: SmithAppearance): void;
11
15
  draw(target: string | HTMLElement): void;
12
16
  /** Show a passive-load reflection coefficient, or hide indicators with null. */
13
17
  update(gamma: Complex | null): void;
14
- /** Export all mounted scales in their current layout as a standalone SVG. */
15
- toSvg(): string;
18
+ private updateAxis;
19
+ /** Export mounted scales, optionally with a separate reading and its description. */
20
+ toSvg(options?: ScaleImageExportOptions): string;
21
+ /** Export mounted scales as PNG without changing their live readings. */
22
+ toPng(options?: ScaleImageExportOptions): Promise<Blob>;
16
23
  destroy(): void;
17
24
  private assertAlive;
18
25
  }