smithkit 0.3.0 → 0.5.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 (45) hide show
  1. package/README.md +324 -37
  2. package/dist/lib/OptionsValidation.d.ts +16 -0
  3. package/dist/lib/OptionsValidation.js +172 -0
  4. package/dist/lib/Smith.d.ts +20 -5
  5. package/dist/lib/Smith.js +114 -15
  6. package/dist/lib/appearance/Theme.js +5 -0
  7. package/dist/lib/grid/CircleLayerControl.d.ts +1 -1
  8. package/dist/lib/grid/CircleLayerControl.js +10 -8
  9. package/dist/lib/grid/ConstQCircles.d.ts +1 -0
  10. package/dist/lib/grid/ConstQCircles.js +5 -0
  11. package/dist/lib/grid/ConstSwrCircles.d.ts +1 -0
  12. package/dist/lib/grid/ConstSwrCircles.js +5 -0
  13. package/dist/lib/grid/GridLayerControl.js +5 -4
  14. package/dist/lib/grid/LayerControl.d.ts +1 -2
  15. package/dist/lib/grid/LayerControl.js +1 -10
  16. package/dist/lib/index.d.ts +3 -2
  17. package/dist/lib/io/Touchstone.d.ts +13 -7
  18. package/dist/lib/io/Touchstone.js +146 -46
  19. package/dist/lib/io/types.d.ts +17 -0
  20. package/dist/lib/io/types.js +1 -0
  21. package/dist/lib/layers.d.ts +7 -1
  22. package/dist/lib/measurements.d.ts +2 -0
  23. package/dist/lib/options.d.ts +58 -0
  24. package/dist/lib/options.js +1 -0
  25. package/dist/lib/rendering/SvgChartRenderer.d.ts +6 -3
  26. package/dist/lib/rendering/SvgChartRenderer.js +58 -11
  27. package/dist/lib/rf/RfCalculations.d.ts +3 -1
  28. package/dist/lib/rf/RfCalculations.js +23 -6
  29. package/dist/lib/samples.d.ts +5 -1
  30. package/dist/lib/scales/SmithPeripheralScales.d.ts +5 -0
  31. package/dist/lib/scales/SmithPeripheralScales.js +20 -1
  32. package/dist/lib/svg/SvgExporter.d.ts +1 -1
  33. package/dist/lib/svg/SvgExporter.js +4 -3
  34. package/dist/lib/svg/export.d.ts +3 -1
  35. package/dist/lib/traces/SmithData.d.ts +10 -4
  36. package/dist/lib/traces/SmithData.js +14 -4
  37. package/dist/lib/traces/TraceBuffer.d.ts +19 -0
  38. package/dist/lib/traces/TraceBuffer.js +176 -0
  39. package/dist/lib/traces/TraceLineDetail.d.ts +8 -0
  40. package/dist/lib/traces/TraceLineDetail.js +52 -0
  41. package/dist/lib/traces/TraceModel.d.ts +6 -8
  42. package/dist/lib/traces/TraceModel.js +24 -49
  43. package/dist/lib/traces/TraceRenderer.d.ts +13 -3
  44. package/dist/lib/traces/TraceRenderer.js +83 -23
  45. package/package.json +4 -2
@@ -1,10 +1,16 @@
1
- import type { TraceSamples } from '../samples.js';
2
- export interface TouchstoneData {
3
- samples: TraceSamples;
4
- referenceImpedanceOhms: number;
5
- }
1
+ import type { TraceInput, TraceSamples } from '../samples.js';
2
+ import type { TouchstoneData, TouchstoneParseOptions, TouchstoneWriteOptions } from './types.js';
3
+ /** DOM-independent Touchstone 1.x reflection-trace import and one-port export. */
6
4
  export declare class Touchstone {
7
5
  private constructor();
8
- /** Read one-port Touchstone 1.x data; frequencies in the result are always Hz. */
9
- static parse(text: string): TouchstoneData;
6
+ static parse(text: string, options: TouchstoneParseOptions & {
7
+ output: 'packed';
8
+ }): TouchstoneData<Float64Array>;
9
+ static parse(text: string, options?: TouchstoneParseOptions & {
10
+ output?: 'objects';
11
+ }): TouchstoneData;
12
+ static parse(text: string, options: TouchstoneParseOptions): TouchstoneData<TraceSamples | Float64Array>;
13
+ /** Serialize a reflection trace as one-port Touchstone, in Hz/RI without rounding or renormalization. */
14
+ static stringify(samples: TraceInput, options?: TouchstoneWriteOptions): string;
15
+ private static isNumber;
10
16
  }
@@ -1,75 +1,175 @@
1
+ import { TraceBuffer } from '../traces/TraceBuffer.js';
2
+ /** DOM-independent Touchstone 1.x reflection-trace import and one-port export. */
1
3
  export class Touchstone {
2
4
  constructor() { }
3
- /** Read one-port Touchstone 1.x data; frequencies in the result are always Hz. */
4
- static parse(text) {
5
+ /** Frequencies are returned in Hz. Two-port input must be explicitly selected. */
6
+ static parse(text, options = {}) {
7
+ const { ports = 1, parameter = 'S11', output = 'objects' } = options;
8
+ if (![1, 2].includes(ports) ||
9
+ !['S11', 'S22'].includes(parameter) ||
10
+ !['objects', 'packed'].includes(output) ||
11
+ (ports === 1 && parameter !== 'S11')) {
12
+ throw new TypeError('Expected ports 1 or 2, S11 (or S22 for two ports), and objects or packed output.');
13
+ }
5
14
  const units = { HZ: 1, KHZ: 1e3, MHZ: 1e6, GHZ: 1e9 };
6
15
  let frequencyScale = 1e9;
7
16
  let format = 'MA';
8
17
  let referenceImpedanceOhms = 50;
9
18
  let optionsSeen = false;
19
+ let count = 0;
20
+ let lineNumber = 0;
21
+ let recordLine = 0;
22
+ let fields = 0;
23
+ let previousFrequency = -1;
24
+ const width = ports === 1 ? 3 : 9;
25
+ const record = new Float64Array(width);
10
26
  const samples = [];
11
- for (const [index, raw] of text
12
- .replace(/^\uFEFF/, '')
13
- .split(/\r?\n/)
14
- .entries()) {
15
- const line = raw.split('!')[0].trim();
27
+ let packed = new Float64Array(output === 'packed' ? 3072 : 0);
28
+ const fail = (message) => {
29
+ throw new Error(`Line ${lineNumber}: ${message}`);
30
+ };
31
+ // Scan lines lazily: do not retain a line array or allocate sample objects for packed output.
32
+ for (const match of text.matchAll(/[^\r\n]*(?:\r\n|\r|\n|$)/g)) {
33
+ lineNumber++;
34
+ const line = match[0].split('!', 1)[0].trim();
16
35
  if (!line) {
17
36
  continue;
18
37
  }
19
- const fail = (message) => {
20
- throw new Error(`Line ${index + 1}: ${message}`);
21
- };
22
38
  if (line.startsWith('[')) {
23
- fail('Only Touchstone 1.x one-port files (.s1p) are supported.');
39
+ fail('Touchstone 2.x keywords are not supported; use a 1.x .s1p or .s2p file.');
24
40
  }
25
41
  if (line.startsWith('#')) {
26
- if (optionsSeen || samples.length) {
42
+ if (optionsSeen || count || fields) {
27
43
  fail('The option line must appear once, before the data.');
28
44
  }
29
45
  optionsSeen = true;
30
- const options = line.slice(1).trim().toUpperCase().split(/\s+/).filter(Boolean);
31
- if (!options.length) {
32
- continue;
46
+ const tokens = line.slice(1).trim().toUpperCase().split(/\s+/).filter(Boolean);
47
+ const seen = new Set();
48
+ for (let i = 0; i < tokens.length; i++) {
49
+ const token = tokens[i];
50
+ const kind = Object.hasOwn(units, token)
51
+ ? 'unit'
52
+ : ['RI', 'MA', 'DB'].includes(token)
53
+ ? 'format'
54
+ : token;
55
+ if (!['unit', 'format', 'S', 'R'].includes(kind) || seen.has(kind)) {
56
+ fail('Expected Hz|kHz|MHz|GHz, S, RI|MA|DB and one R impedance.');
57
+ }
58
+ seen.add(kind);
59
+ if (kind === 'unit') {
60
+ frequencyScale = units[token];
61
+ }
62
+ else if (kind === 'format') {
63
+ format = token;
64
+ }
65
+ else if (kind === 'R') {
66
+ const value = tokens[++i];
67
+ if (!value || !Touchstone.isNumber(value)) {
68
+ fail('Reference impedance must be a positive finite number.');
69
+ }
70
+ referenceImpedanceOhms = Number(value);
71
+ if (!Number.isFinite(referenceImpedanceOhms) || referenceImpedanceOhms <= 0) {
72
+ fail('Reference impedance must be a positive finite number.');
73
+ }
74
+ }
33
75
  }
34
- const [unit = 'GHZ', parameter = 'S', representation = 'MA', r = 'R', impedance = '50'] = options;
35
- if (!(unit in units) ||
36
- parameter !== 'S' ||
37
- !['RI', 'MA', 'DB'].includes(representation) ||
38
- r !== 'R' ||
39
- options.length > 5) {
40
- fail('Expected: # Hz|kHz|MHz|GHz S RI|MA|DB R impedance.');
76
+ continue;
77
+ }
78
+ for (const token of line.matchAll(/\S+/g)) {
79
+ if (fields === width) {
80
+ fail('Each frequency record must begin on a new line.');
41
81
  }
42
- frequencyScale = units[unit];
43
- format = representation;
44
- referenceImpedanceOhms = Number(impedance);
45
- if (!Number.isFinite(referenceImpedanceOhms) || referenceImpedanceOhms <= 0) {
46
- fail('Reference impedance must be positive.');
82
+ if (!Touchstone.isNumber(token[0])) {
83
+ fail('Expected finite decimal numbers.');
84
+ }
85
+ const value = Number(token[0]);
86
+ if (!Number.isFinite(value)) {
87
+ fail('Expected finite decimal numbers.');
88
+ }
89
+ if (fields === 0) {
90
+ recordLine = lineNumber;
91
+ const frequencyHz = value * frequencyScale;
92
+ if (frequencyHz < 0 || !Number.isFinite(frequencyHz)) {
93
+ fail('Frequency must be finite and non-negative.');
94
+ }
95
+ // Legacy noise blocks restart the frequency sweep. Never interpret them as S data.
96
+ if (ports === 2 && frequencyHz <= previousFrequency) {
97
+ fail('Two-port frequencies must increase strictly; noise data is not supported.');
98
+ }
99
+ record[fields++] = frequencyHz;
100
+ }
101
+ else {
102
+ record[fields++] = value;
47
103
  }
48
- continue;
49
104
  }
50
- const fields = line.split(/\s+/).map(Number);
51
- if (fields.length !== 3 || !fields.every(Number.isFinite)) {
52
- fail('Expected frequency and two finite S11 components.');
105
+ if (fields !== width) {
106
+ continue;
53
107
  }
54
- const [frequency, a, b] = fields;
55
- const frequencyHz = frequency * frequencyScale;
56
- if (frequencyHz < 0 || !Number.isFinite(frequencyHz)) {
57
- fail('Frequency must be finite and non-negative.');
108
+ let real = 0;
109
+ let imaginary = 0;
110
+ for (let offset = 1; offset < width; offset += 2) {
111
+ const a = record[offset];
112
+ const b = record[offset + 1];
113
+ if (format === 'MA' && a < 0) {
114
+ fail('Magnitude must be non-negative.');
115
+ }
116
+ const magnitude = format === 'DB' ? 10 ** (a / 20) : a;
117
+ const radians = ((b % 360) * Math.PI) / 180;
118
+ const re = format === 'RI' ? a : magnitude * Math.cos(radians);
119
+ const im = format === 'RI' ? b : magnitude * Math.sin(radians);
120
+ if (!Number.isFinite(re) || !Number.isFinite(im)) {
121
+ fail('An S-parameter is outside the supported numeric range.');
122
+ }
123
+ if (offset === (parameter === 'S11' ? 1 : 7)) {
124
+ real = re;
125
+ imaginary = im;
126
+ }
58
127
  }
59
- if (format === 'MA' && a < 0) {
60
- fail('Magnitude must be non-negative.');
128
+ if (output === 'packed') {
129
+ if (count * 3 === packed.length) {
130
+ const grown = new Float64Array(packed.length * 2);
131
+ grown.set(packed);
132
+ packed = grown;
133
+ }
134
+ packed[count * 3] = record[0];
135
+ packed[count * 3 + 1] = real;
136
+ packed[count * 3 + 2] = imaginary;
61
137
  }
62
- const magnitude = format === 'DB' ? 10 ** (a / 20) : a;
63
- const radians = (b * Math.PI) / 180;
64
- const reflectionCoefficient = format === 'RI' ? [a, b] : [magnitude * Math.cos(radians), magnitude * Math.sin(radians)];
65
- if (!reflectionCoefficient.every(Number.isFinite)) {
66
- fail('S11 is outside the supported numeric range.');
138
+ else {
139
+ samples.push({ frequencyHz: record[0], reflectionCoefficient: [real, imaginary] });
67
140
  }
68
- samples.push({ frequencyHz, reflectionCoefficient });
141
+ previousFrequency = record[0];
142
+ count++;
143
+ fields = 0;
69
144
  }
70
- if (!samples.length) {
71
- throw new Error('The file contains no S11 samples.');
145
+ if (fields) {
146
+ throw new Error(`Line ${recordLine}: Incomplete ${ports}-port record; expected ${width} values. Noise data is not supported.`);
72
147
  }
73
- return { samples, referenceImpedanceOhms };
148
+ if (!count) {
149
+ throw new Error(`The file contains no ${parameter} samples.`);
150
+ }
151
+ return {
152
+ samples: output === 'packed' ? packed.slice(0, count * 3) : samples,
153
+ referenceImpedanceOhms,
154
+ };
155
+ }
156
+ /** Serialize a reflection trace as one-port Touchstone, in Hz/RI without rounding or renormalization. */
157
+ static stringify(samples, options = {}) {
158
+ const { referenceImpedanceOhms = 50 } = options;
159
+ if (!Number.isFinite(referenceImpedanceOhms) || referenceImpedanceOhms <= 0) {
160
+ throw new RangeError('Reference impedance must be positive and finite.');
161
+ }
162
+ const data = TraceBuffer.from(samples);
163
+ const lines = [`# Hz S RI R ${referenceImpedanceOhms}`];
164
+ for (let i = 0; i < data.length; i++) {
165
+ if (i > 0 && data.frequency(i) <= data.frequency(i - 1)) {
166
+ throw new RangeError('Touchstone export requires strictly increasing frequencies.');
167
+ }
168
+ lines.push(`${data.frequency(i)} ${data.real(i)} ${data.imaginary(i)}`);
169
+ }
170
+ return lines.join('\n') + '\n';
171
+ }
172
+ static isNumber(token) {
173
+ return /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(token);
74
174
  }
75
175
  }
@@ -0,0 +1,17 @@
1
+ import type { TraceSamples } from '../samples.js';
2
+ export interface TouchstoneData<T extends TraceSamples | Float64Array = TraceSamples> {
3
+ samples: T;
4
+ referenceImpedanceOhms: number;
5
+ }
6
+ export interface TouchstoneParseOptions {
7
+ /** Source port count, normally taken from the filename extension. Default: 1. */
8
+ ports?: 1 | 2;
9
+ /** Reflection parameter to extract. S22 requires ports: 2. Default: S11. */
10
+ parameter?: 'S11' | 'S22';
11
+ /** Packed output contains f/re/im triples in Hz, without intermediate sample objects. */
12
+ output?: 'objects' | 'packed';
13
+ }
14
+ export interface TouchstoneWriteOptions {
15
+ /** Reference of the supplied data, in ohms. Does not renormalize it. Default: 50. */
16
+ referenceImpedanceOhms?: number;
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -21,13 +21,15 @@ export interface GridLayer {
21
21
  setVisible(visible: boolean): void;
22
22
  /** Show or hide labels independently of grid lines. Default: true. */
23
23
  setLabelsVisible(visible: boolean): void;
24
- /** Default: detailed. Basic shows only the principal normalized values. */
24
+ /** Default: standard. Basic shows only the principal normalized values. */
25
25
  setDetail(detail: GridDetail): void;
26
26
  setStyle(style: Partial<GridStyle>): void;
27
27
  }
28
28
  export interface CircleLayer {
29
29
  setVisible(visible: boolean): void;
30
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;
31
33
  addValue(value: number): void;
32
34
  removeValue(value: number): void;
33
35
  }
@@ -41,6 +43,10 @@ export interface ChartLayers {
41
43
  }
42
44
  export interface PeripheralScales {
43
45
  setVisible(visible: boolean): void;
46
+ /** Show or hide axis captions independently of numeric tick labels. Default: true. */
47
+ setCaptionsVisible(visible: boolean): void;
48
+ /** Show or hide numeric tick labels independently of captions. Default: true. */
49
+ setTickLabelsVisible(visible: boolean): void;
44
50
  /** Pass null to clear the indicators. */
45
51
  update(reflectionCoefficient: Complex | null): void;
46
52
  }
@@ -7,6 +7,8 @@ export interface TraceStyle {
7
7
  lineWidth: number;
8
8
  /** Point radius in CSS pixels; remains constant under chart zoom and container resize. Default: 2. */
9
9
  pointRadius: number;
10
+ /** Maximum additional line simplification error in CSS pixels. Zero disables it (default). */
11
+ lineTolerancePx: number;
10
12
  }
11
13
  export interface TraceOptions extends Partial<TraceStyle> {
12
14
  name?: string;
@@ -0,0 +1,58 @@
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
+ /** Show curved axis captions. Default: true. */
45
+ captionsVisible?: boolean;
46
+ /** Show numeric tick labels. Default: true. */
47
+ tickLabelsVisible?: boolean;
48
+ }
49
+ export interface SmithOptions {
50
+ /** Positive real reference impedance in ohms. Default: 50. */
51
+ referenceImpedanceOhms?: number;
52
+ /** Default: the light preset. Use overrides for fonts, colors, and marker/cursor styling. */
53
+ appearance?: SmithAppearance;
54
+ interaction?: InteractionOptions;
55
+ grid?: GridOptions;
56
+ circles?: CircleOptions;
57
+ peripheralScales?: PeripheralScalesOptions;
58
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,7 +1,7 @@
1
1
  import type { SmithTheme } from '../appearance/types.js';
2
2
  import { SmithData } from '../traces/SmithData.js';
3
3
  import type { SmithMarker } from '../traces/SmithMarker.js';
4
- import { TraceSamples } from '../samples.js';
4
+ import { TraceInput } from '../samples.js';
5
5
  import { Complex } from '../math/Complex.js';
6
6
  import type { ChartLayers, PeripheralScales } from '../layers.js';
7
7
  import type { TraceStyle } from '../measurements.js';
@@ -13,6 +13,7 @@ export declare class SvgChartRenderer {
13
13
  private readonly defaultTransform;
14
14
  private transform;
15
15
  private zoomEnabled;
16
+ private cursorEnabled;
16
17
  private applyingView;
17
18
  private zoomBehavior;
18
19
  private svg;
@@ -41,7 +42,7 @@ export declare class SvgChartRenderer {
41
42
  setTheme(theme: SmithTheme): void;
42
43
  draw(target: string | HTMLElement): void;
43
44
  /** Export the current mounted view as standalone SVG with its configured background. */
44
- toSvg(): string;
45
+ toSvg(lineDetail?: 'full' | 'view'): string;
45
46
  /** Remove this chart and release its event handlers. Safe to call more than once. */
46
47
  destroy(): void;
47
48
  private updateViewportScale;
@@ -50,6 +51,8 @@ export declare class SvgChartRenderer {
50
51
  private cursorMove;
51
52
  setMarkerDragging(dragging: boolean): void;
52
53
  hideCursor(): void;
54
+ /** Disable tracking and cancel queued readings; enabling waits for the next pointer move. */
55
+ setCursorEnabled(enabled: boolean): void;
53
56
  private initCursor;
54
57
  get cursorPosition(): Complex;
55
58
  private initializeZoom;
@@ -61,6 +64,6 @@ export declare class SvgChartRenderer {
61
64
  private cursorContainer;
62
65
  private drawReactanceAxis;
63
66
  /** Construct trace rendering without exposing chart containers or scalers to Smith. */
64
- createTrace(values: TraceSamples, color: string, style: Partial<TraceStyle>, onMarkerDrag: (marker: SmithMarker, dragging: boolean) => void): SmithData;
67
+ createTrace(values: TraceInput, color: string, style: Partial<TraceStyle>, onMarkerDrag: (marker: SmithMarker, dragging: boolean) => void): SmithData;
65
68
  removeTrace(trace: SmithData): void;
66
69
  }
@@ -1,3 +1,4 @@
1
+ import { OptionsValidation } from '../OptionsValidation.js';
1
2
  import { SvgTheme } from '../appearance/SvgTheme.js';
2
3
  import * as d3 from 'd3';
3
4
  import { MouseGesture } from '../interaction/MouseGesture.js';
@@ -28,7 +29,8 @@ export class SvgChartRenderer {
28
29
  scalers;
29
30
  defaultTransform = d3.zoomIdentity.translate(62.5, 62.5).scale(0.75);
30
31
  transform = d3.zoomIdentity;
31
- zoomEnabled = true;
32
+ zoomEnabled = false;
33
+ cursorEnabled = false;
32
34
  applyingView = false;
33
35
  zoomBehavior = d3.zoom();
34
36
  svg;
@@ -63,25 +65,25 @@ export class SvgChartRenderer {
63
65
  this.constResistance = new ConstResistance({
64
66
  data: gridData,
65
67
  scaler: this.scalers.default,
66
- detail: 'detailed',
68
+ detail: 'standard',
67
69
  });
68
70
  this.constResistance.show();
69
71
  this.constReactance = new ConstReactance({
70
72
  data: gridData,
71
73
  scaler: this.scalers.default,
72
- detail: 'detailed',
74
+ detail: 'standard',
73
75
  });
74
76
  this.constReactance.show();
75
77
  this.constConductance = new ConstConductance({
76
78
  data: gridData,
77
79
  scaler: this.scalers.default,
78
- detail: 'detailed',
80
+ detail: 'standard',
79
81
  });
80
82
  this.constConductance.hide();
81
83
  this.constSusceptance = new ConstSusceptance({
82
84
  data: gridData,
83
85
  scaler: this.scalers.default,
84
- detail: 'detailed',
86
+ detail: 'standard',
85
87
  });
86
88
  this.constSusceptance.hide();
87
89
  this.constQCircles = new ConstQCircles(this.scalers.default);
@@ -135,6 +137,9 @@ export class SvgChartRenderer {
135
137
  this.peripheralScales = {
136
138
  setVisible: (visible) => {
137
139
  assertAlive();
140
+ if (typeof visible !== 'boolean') {
141
+ throw new TypeError('Peripheral scale visibility must be a boolean.');
142
+ }
138
143
  if (visible) {
139
144
  this.peripheralScaleRenderer.show();
140
145
  }
@@ -143,11 +148,22 @@ export class SvgChartRenderer {
143
148
  }
144
149
  this.labelLayout.update(true);
145
150
  },
151
+ setCaptionsVisible: (visible) => {
152
+ assertAlive();
153
+ OptionsValidation.boolean(visible, 'Peripheral scale caption visibility');
154
+ this.peripheralScaleRenderer.setCaptionsVisible(visible);
155
+ },
156
+ setTickLabelsVisible: (visible) => {
157
+ assertAlive();
158
+ OptionsValidation.boolean(visible, 'Peripheral scale tick label visibility');
159
+ this.peripheralScaleRenderer.setTickLabelsVisible(visible);
160
+ },
146
161
  update: (gamma) => {
147
162
  assertAlive();
148
163
  this.peripheralScaleRenderer.update(gamma);
149
164
  },
150
165
  };
166
+ this.peripheralScaleRenderer.hide();
151
167
  this.initializeZoom();
152
168
  this.resizeObserver = new ResizeObserver(() => this.updateViewportScale());
153
169
  this.resizeObserver.observe(this.svg.Node);
@@ -167,9 +183,23 @@ export class SvgChartRenderer {
167
183
  this.updateViewportScale();
168
184
  }
169
185
  /** Export the current mounted view as standalone SVG with its configured background. */
170
- toSvg() {
186
+ toSvg(lineDetail = 'full') {
171
187
  this.assertAlive();
172
- return SvgExporter.chart(this.svg.Node);
188
+ const replacements = new Map();
189
+ if (lineDetail === 'full') {
190
+ for (const trace of this.traces) {
191
+ const replacement = trace.fullExportLine();
192
+ if (replacement) {
193
+ replacements.set(replacement.node, replacement.path);
194
+ }
195
+ }
196
+ }
197
+ return SvgExporter.chart(this.svg.Node, (original, copy) => {
198
+ const replacement = replacements.get(original);
199
+ if (replacement !== undefined) {
200
+ copy.setAttribute('d', replacement);
201
+ }
202
+ });
173
203
  }
174
204
  /** Remove this chart and release its event handlers. Safe to call more than once. */
175
205
  destroy() {
@@ -216,7 +246,7 @@ export class SvgChartRenderer {
216
246
  return { default: impedance, impedance, admittance };
217
247
  }
218
248
  cursorMove(p) {
219
- if (this.markerDragging) {
249
+ if (!this.cursorEnabled || this.markerDragging) {
220
250
  return;
221
251
  }
222
252
  this.cursor.Position = Complex.from(this.scalers.default.pointInvert(p));
@@ -226,7 +256,7 @@ export class SvgChartRenderer {
226
256
  if (dragging) {
227
257
  this.cursorBeforeMarkerDrag = this.svg.Node.style.getPropertyValue('cursor') || null;
228
258
  this.svg.Element.style('cursor', 'grabbing').style('--smithkit-marker-cursor', 'grabbing');
229
- this.cursor.hide();
259
+ this.hideCursor();
230
260
  }
231
261
  else {
232
262
  this.svg.Element.style('cursor', () => this.cursorBeforeMarkerDrag).style('--smithkit-marker-cursor', null);
@@ -235,6 +265,24 @@ export class SvgChartRenderer {
235
265
  }
236
266
  hideCursor() {
237
267
  this.cursor.hide();
268
+ if (this.cursorEnabled) {
269
+ this.onCursorChange?.(undefined);
270
+ }
271
+ }
272
+ /** Disable tracking and cancel queued readings; enabling waits for the next pointer move. */
273
+ setCursorEnabled(enabled) {
274
+ this.assertAlive();
275
+ if (typeof enabled !== 'boolean') {
276
+ throw new TypeError('Cursor enabled must be a boolean.');
277
+ }
278
+ if (this.cursorEnabled === enabled) {
279
+ return;
280
+ }
281
+ this.cursorEnabled = enabled;
282
+ if (!enabled) {
283
+ this.cursor.hide();
284
+ this.onCursorChange?.(undefined);
285
+ }
238
286
  }
239
287
  initCursor() {
240
288
  const cursor = new SmithCursor(this.scalers.default);
@@ -314,8 +362,7 @@ export class SvgChartRenderer {
314
362
  this.cursorMove(d3.pointer(event));
315
363
  })
316
364
  .on('pointerleave.smithkit', () => {
317
- this.cursor.hide();
318
- this.onCursorChange?.(undefined);
365
+ this.hideCursor();
319
366
  });
320
367
  return shape;
321
368
  }
@@ -54,8 +54,10 @@ export declare class RfCalculations {
54
54
  * unrepresentable result is undefined; non-finite inputs throw RangeError.
55
55
  */
56
56
  static renormalizeReflection(gamma: Complex, fromOhms: number, toOhms: number): Complex | undefined;
57
- /** Copy and renormalize every sample; reject invalid/singular data without modifying the input. */
57
+ /** Copy and renormalize samples, preserving object or packed representation. */
58
+ static renormalizeSamples(samples: Float64Array, fromOhms: number, toOhms: number): Float64Array;
58
59
  static renormalizeSamples(samples: TraceSamples, fromOhms: number, toOhms: number): TraceSamples;
60
+ static renormalizeSamples(samples: TraceSamples | Float64Array, fromOhms: number, toOhms: number): TraceSamples | Float64Array;
59
61
  /**
60
62
  * Convert reactance in ohms at a frequency in Hz to an ideal series L or C.
61
63
  * Zero frequency, zero reactance, or an unrepresentable component returns undefined.
@@ -115,20 +115,37 @@ export class RfCalculations {
115
115
  (inverseScale / denominatorScale);
116
116
  return Number.isFinite(re) && Number.isFinite(im) ? Complex.from(re, im) : undefined;
117
117
  }
118
- /** Copy and renormalize every sample; reject invalid/singular data without modifying the input. */
119
118
  static renormalizeSamples(samples, fromOhms, toOhms) {
120
119
  RfCalculations.validateReference(fromOhms);
121
120
  RfCalculations.validateReference(toOhms);
122
- return samples.map((sample, index) => {
123
- if (!Number.isFinite(sample.frequencyHz) ||
124
- sample.frequencyHz < 0 ||
125
- sample.reflectionCoefficient.length !== 2) {
121
+ const convert = (frequencyHz, re, im, index) => {
122
+ if (!Number.isFinite(frequencyHz) || frequencyHz < 0) {
126
123
  throw new RangeError(`Invalid sample at index ${index}.`);
127
124
  }
128
- const gamma = RfCalculations.renormalizeReflection(Complex.from(...sample.reflectionCoefficient), fromOhms, toOhms);
125
+ const gamma = RfCalculations.renormalizeReflection(Complex.from(re, im), fromOhms, toOhms);
129
126
  if (!gamma) {
130
127
  throw new RangeError(`Renormalization is singular or outside the numeric range at sample ${index}.`);
131
128
  }
129
+ return gamma;
130
+ };
131
+ if (samples instanceof Float64Array) {
132
+ if (samples.length % 3 !== 0) {
133
+ throw new RangeError('Packed samples must contain complete f/re/im triples.');
134
+ }
135
+ const result = new Float64Array(samples.length);
136
+ for (let offset = 0; offset < samples.length; offset += 3) {
137
+ const gamma = convert(samples[offset], samples[offset + 1], samples[offset + 2], offset / 3);
138
+ result[offset] = samples[offset];
139
+ result[offset + 1] = gamma.re;
140
+ result[offset + 2] = gamma.im;
141
+ }
142
+ return result;
143
+ }
144
+ return samples.map((sample, index) => {
145
+ if (sample.reflectionCoefficient.length !== 2) {
146
+ throw new RangeError(`Invalid sample at index ${index}.`);
147
+ }
148
+ const gamma = convert(sample.frequencyHz, ...sample.reflectionCoefficient, index);
132
149
  return { frequencyHz: sample.frequencyHz, reflectionCoefficient: gamma.toVector() };
133
150
  });
134
151
  }
@@ -1,6 +1,10 @@
1
- /** One S11 sample. The reflection coefficient is [real, imaginary]. */
1
+ /** One reflection sample. The reflection coefficient is [real, imaginary]. */
2
2
  export interface TraceSample {
3
3
  readonly frequencyHz: number;
4
4
  readonly reflectionCoefficient: readonly [number, number];
5
5
  }
6
6
  export type TraceSamples = readonly TraceSample[];
7
+ /** One reflection sample: frequency in Hz, followed by the real and imaginary parts of Γ. */
8
+ export type TraceTuple = readonly [frequencyHz: number, re: number, im: number];
9
+ /** Homogeneous object samples, tuples, or packed f/re/im triples. Inputs are copied. */
10
+ export type TraceInput = TraceSamples | readonly TraceTuple[] | Float64Array;
@@ -2,12 +2,17 @@ import type { Complex } from '../math/Complex.js';
2
2
  import { SmithGroup } from '../svg/SmithGroup.js';
3
3
  /** Peripheral rulers in the chart's 500-unit coordinate system. */
4
4
  export declare class SmithPeripheralScales extends SmithGroup {
5
+ private captionsVisible;
6
+ private tickLabelsVisible;
5
7
  private static point;
6
8
  private static rulerRadius;
7
9
  readonly labels: SmithGroup;
8
10
  private readonly scales;
9
11
  private readonly axes;
10
12
  constructor();
13
+ setCaptionsVisible(visible: boolean): void;
14
+ setTickLabelsVisible(visible: boolean): void;
15
+ private updateLabelVisibility;
11
16
  show(): SmithGroup;
12
17
  hide(): SmithGroup;
13
18
  /** Connect to the same cursor or marker data as the independent parameter scales. */