smithkit 0.4.0 → 0.6.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 (39) hide show
  1. package/README.md +47 -774
  2. package/dist/lib/OptionsValidation.js +7 -3
  3. package/dist/lib/Smith.d.ts +7 -1
  4. package/dist/lib/Smith.js +17 -9
  5. package/dist/lib/grid/SmithGridLayer.js +3 -1
  6. package/dist/lib/index.d.ts +2 -2
  7. package/dist/lib/io/Touchstone.d.ts +13 -7
  8. package/dist/lib/io/Touchstone.js +146 -46
  9. package/dist/lib/io/types.d.ts +17 -0
  10. package/dist/lib/io/types.js +1 -0
  11. package/dist/lib/layers.d.ts +4 -0
  12. package/dist/lib/math/Complex.d.ts +1 -1
  13. package/dist/lib/math/Complex.js +2 -2
  14. package/dist/lib/measurements.d.ts +2 -0
  15. package/dist/lib/options.d.ts +4 -0
  16. package/dist/lib/rendering/SvgChartRenderer.d.ts +3 -3
  17. package/dist/lib/rendering/SvgChartRenderer.js +28 -3
  18. package/dist/lib/rf/RfCalculations.d.ts +3 -1
  19. package/dist/lib/rf/RfCalculations.js +23 -6
  20. package/dist/lib/samples.d.ts +5 -1
  21. package/dist/lib/scales/SmithPeripheralScales.d.ts +5 -0
  22. package/dist/lib/scales/SmithPeripheralScales.js +20 -1
  23. package/dist/lib/scales/SmithScales.js +2 -2
  24. package/dist/lib/svg/LabelLayout.js +9 -2
  25. package/dist/lib/svg/SvgExporter.d.ts +1 -1
  26. package/dist/lib/svg/SvgExporter.js +4 -3
  27. package/dist/lib/svg/export.d.ts +2 -0
  28. package/dist/lib/traces/SmithData.d.ts +10 -4
  29. package/dist/lib/traces/SmithData.js +14 -4
  30. package/dist/lib/traces/SmithMarker.js +1 -0
  31. package/dist/lib/traces/TraceBuffer.d.ts +19 -0
  32. package/dist/lib/traces/TraceBuffer.js +176 -0
  33. package/dist/lib/traces/TraceLineDetail.d.ts +8 -0
  34. package/dist/lib/traces/TraceLineDetail.js +52 -0
  35. package/dist/lib/traces/TraceModel.d.ts +6 -8
  36. package/dist/lib/traces/TraceModel.js +24 -49
  37. package/dist/lib/traces/TraceRenderer.d.ts +13 -3
  38. package/dist/lib/traces/TraceRenderer.js +83 -23
  39. package/package.json +25 -6
@@ -96,9 +96,13 @@ export class OptionsValidation {
96
96
  }
97
97
  }
98
98
  if (options.peripheralScales !== undefined) {
99
- OptionsValidation.object(options.peripheralScales, 'Peripheral scales', ['visible']);
100
- if (options.peripheralScales.visible !== undefined) {
101
- OptionsValidation.boolean(options.peripheralScales.visible, 'Peripheral scale visibility');
99
+ const keys = ['visible', 'captionsVisible', 'tickLabelsVisible'];
100
+ OptionsValidation.object(options.peripheralScales, 'Peripheral scales', keys);
101
+ for (const key of keys) {
102
+ const value = options.peripheralScales[key];
103
+ if (value !== undefined) {
104
+ OptionsValidation.boolean(value, `Peripheral scales ${key}`);
105
+ }
102
106
  }
103
107
  }
104
108
  if (options.grid !== undefined) {
@@ -1,7 +1,7 @@
1
1
  import type { SmithOptions } from './options.js';
2
2
  import type { SmithImageExportOptions } from './svg/export.js';
3
3
  import type { SmithAppearance } from './appearance/types.js';
4
- import type { TraceSamples } from './samples.js';
4
+ import type { TraceInput, TraceSamples, TraceTuple } from './samples.js';
5
5
  import type { ChartLayers, GridDetail, PeripheralScales } from './layers.js';
6
6
  import type { SmithReading } from './rf/RfCalculations.js';
7
7
  import type { TraceOptions, TraceUpdateOptions, TraceInfo, MarkerSnapshot, MarkerComparison } from './measurements.js';
@@ -62,6 +62,9 @@ export declare class Smith {
62
62
  resetView(): void;
63
63
  /** Add a named trace without markers. Use addMarker to create markers explicitly. Returns a chart-local, stable ID. */
64
64
  addTrace(values: TraceSamples, options?: TraceOptions): string;
65
+ addTrace(values: readonly TraceTuple[], options?: TraceOptions): string;
66
+ addTrace(values: Float64Array, options?: TraceOptions): string;
67
+ addTrace(values: TraceInput, options?: TraceOptions): string;
65
68
  /** Detached metadata snapshots; IDs and marker display numbers survive removals. */
66
69
  getTraces(): TraceInfo[];
67
70
  private traceIndex;
@@ -71,6 +74,9 @@ export declare class Smith {
71
74
  setTraceOptions(id: string, options: TraceOptions): boolean;
72
75
  /** Replace samples; by default each marker follows its nearest measured frequency. */
73
76
  updateTrace(id: string, values: TraceSamples, options?: TraceUpdateOptions): boolean;
77
+ updateTrace(id: string, values: readonly TraceTuple[], options?: TraceUpdateOptions): boolean;
78
+ updateTrace(id: string, values: Float64Array, options?: TraceUpdateOptions): boolean;
79
+ updateTrace(id: string, values: TraceInput, options?: TraceUpdateOptions): boolean;
74
80
  removeTrace(id: string): boolean;
75
81
  /** Place a marker on an existing sample; no interpolation is performed. */
76
82
  addMarker(traceId: string, sampleIndex?: number): string | undefined;
package/dist/lib/Smith.js CHANGED
@@ -80,6 +80,12 @@ export class Smith {
80
80
  if (options.peripheralScales?.visible !== undefined) {
81
81
  this.peripheralScales.setVisible(options.peripheralScales.visible);
82
82
  }
83
+ if (options.peripheralScales?.captionsVisible !== undefined) {
84
+ this.peripheralScales.setCaptionsVisible(options.peripheralScales.captionsVisible);
85
+ }
86
+ if (options.peripheralScales?.tickLabelsVisible !== undefined) {
87
+ this.peripheralScales.setTickLabelsVisible(options.peripheralScales.tickLabelsVisible);
88
+ }
83
89
  for (const name of ['resistance', 'reactance', 'conductance', 'susceptance']) {
84
90
  const layer = this.layers[name];
85
91
  for (const settings of [options.grid, options.grid?.layers?.[name]]) {
@@ -143,13 +149,10 @@ export class Smith {
143
149
  }
144
150
  }
145
151
  /** Export the current mounted view as standalone SVG with its configured background. */
146
- toSvg(options) {
152
+ toSvg(options = {}) {
147
153
  this.assertAlive();
148
154
  this.validateExportOptions(options);
149
- const source = this.renderer.toSvg();
150
- if (options === undefined) {
151
- return source;
152
- }
155
+ const source = this.renderer.toSvg(options.lineDetail ?? 'full');
153
156
  const sources = [source];
154
157
  if (options.scales) {
155
158
  sources.push(options.scales.toSvg({ readout: options.scaleReadout }));
@@ -157,6 +160,9 @@ export class Smith {
157
160
  return ImageExporter.svg(sources, options, this.exportLegend(options));
158
161
  }
159
162
  validateExportOptions(options) {
163
+ if (options?.lineDetail !== undefined && !['full', 'view'].includes(options.lineDetail)) {
164
+ throw new TypeError('Export line detail must be full or view.');
165
+ }
160
166
  if (options?.scaleReadout !== undefined && !options.scales) {
161
167
  throw new TypeError('Scale readout requires included radial scales.');
162
168
  }
@@ -178,7 +184,7 @@ export class Smith {
178
184
  async toPng(options = {}) {
179
185
  this.assertAlive();
180
186
  this.validateExportOptions(options);
181
- const sources = [this.toSvg()];
187
+ const sources = [this.renderer.toSvg(options.lineDetail ?? 'full')];
182
188
  if (options.scales) {
183
189
  sources.push(options.scales.toSvg({ readout: options.scaleReadout }));
184
190
  }
@@ -231,7 +237,6 @@ export class Smith {
231
237
  this.assertAlive();
232
238
  this.renderer.resetView();
233
239
  }
234
- /** Add a named trace without markers. Use addMarker to create markers explicitly. Returns a chart-local, stable ID. */
235
240
  addTrace(values, options = {}) {
236
241
  this.assertAlive();
237
242
  this.validateTraceOptions(options);
@@ -285,6 +290,10 @@ export class Smith {
285
290
  if (options.mode !== undefined && !['points', 'line', 'both'].includes(options.mode)) {
286
291
  throw new TypeError('Trace mode must be points, line, or both.');
287
292
  }
293
+ if (options.lineTolerancePx !== undefined &&
294
+ (!Number.isFinite(options.lineTolerancePx) || options.lineTolerancePx < 0)) {
295
+ throw new RangeError('Line tolerance must be finite and non-negative.');
296
+ }
288
297
  for (const key of ['lineWidth', 'pointRadius']) {
289
298
  const value = options[key];
290
299
  if (value !== undefined && (!Number.isFinite(value) || value <= 0)) {
@@ -323,7 +332,6 @@ export class Smith {
323
332
  }
324
333
  return true;
325
334
  }
326
- /** Replace samples; by default each marker follows its nearest measured frequency. */
327
335
  updateTrace(id, values, options = {}) {
328
336
  this.assertAlive();
329
337
  const data = this.data[this.traceIndex(id)];
@@ -419,7 +427,7 @@ export class Smith {
419
427
  if (referenceImpedanceOhms === this.referenceOhms) {
420
428
  return;
421
429
  }
422
- const samples = this.data.map((data) => RfCalculations.renormalizeSamples(data.Samples, this.referenceOhms, referenceImpedanceOhms));
430
+ const samples = this.data.map((data) => data.Samples.renormalize(this.referenceOhms, referenceImpedanceOhms));
423
431
  this.data.forEach((data) => data.Markers.forEach((entry) => entry.marker.cancelDrag()));
424
432
  this.referenceOhms = referenceImpedanceOhms;
425
433
  this.data.forEach((data, index) => data.update(samples[index], 'sample-index'));
@@ -31,7 +31,9 @@ export class SmithGridLayer extends ConstCircles {
31
31
  const label = GridGeometry.label(kind, tick.definition);
32
32
  const text = new SmithText(this.scaler.point(label.point), label.text, {
33
33
  rotate: label.rotate,
34
- dx: this.scaler.r(label.dx).toString(),
34
+ // Keep the zero clear of the opposite grid's high-value labels even
35
+ // when minimum font sizing or browser font metrics enlarge it.
36
+ dx: real && tick.definition.point.r === 0 ? '0.9em' : this.scaler.r(label.dx).toString(),
35
37
  dy: this.scaler.r(label.dy).toString(),
36
38
  textAnchor: label.textAnchor,
37
39
  dominantBaseline: label.dominantBaseline,
@@ -1,9 +1,9 @@
1
1
  export { Smith, SmithEventType } from './Smith.js';
2
2
  export type { SmithEvent } from './Smith.js';
3
3
  export { Complex } from './math/Complex.js';
4
- export type { TraceSamples, TraceSample } from './samples.js';
4
+ export type { TraceSamples, TraceSample, TraceTuple, TraceInput } from './samples.js';
5
5
  export { Touchstone } from './io/Touchstone.js';
6
- export type { TouchstoneData } from './io/Touchstone.js';
6
+ export type { TouchstoneData, TouchstoneParseOptions, TouchstoneWriteOptions } from './io/types.js';
7
7
  export { SmithScales } from './scales/SmithScales.js';
8
8
  export { MarkerMeasurements } from './MarkerMeasurements.js';
9
9
  export type { TraceOptions, TraceUpdateOptions, MarkerSelectionStrategy, TraceStyle, TraceRenderMode, TraceInfo, MarkerInfo, MarkerSnapshot, MarkerComparison, } from './measurements.js';
@@ -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 {};
@@ -43,6 +43,10 @@ export interface ChartLayers {
43
43
  }
44
44
  export interface PeripheralScales {
45
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;
46
50
  /** Pass null to clear the indicators. */
47
51
  update(reflectionCoefficient: Complex | null): void;
48
52
  }
@@ -117,7 +117,7 @@ export declare class Complex {
117
117
  private static hyperbolicProduct;
118
118
  private static hyperbolicReciprocal;
119
119
  /**
120
- * Elliptic coordinates for principal inverse functions (x,y >= 0).
120
+ * Elliptic coordinates for principal inverse functions (non-negative x and y).
121
121
  * With a = (|z+1| + |z-1|)/2, return acosh(a) and sqrt(a²-x²).
122
122
  * Rationalized hypot differences retain tiny components near the branch cuts.
123
123
  * Principal branches: https://dlmf.nist.gov/4.37
@@ -34,7 +34,7 @@ export class Complex {
34
34
  }
35
35
  if (args.length === 2) {
36
36
  const arg0 = args[0];
37
- const arg1 = args[1];
37
+ const arg1 = args[1] === undefined ? 0 : args[1];
38
38
  if (typeof arg0 === 'number' && typeof arg1 === 'number') {
39
39
  return new Complex(arg0, arg1);
40
40
  }
@@ -598,7 +598,7 @@ export class Complex {
598
598
  return Complex.reciprocal(denominator).mul(Math.exp(Math.LN2 - x));
599
599
  }
600
600
  /**
601
- * Elliptic coordinates for principal inverse functions (x,y >= 0).
601
+ * Elliptic coordinates for principal inverse functions (non-negative x and y).
602
602
  * With a = (|z+1| + |z-1|)/2, return acosh(a) and sqrt(a²-x²).
603
603
  * Rationalized hypot differences retain tiny components near the branch cuts.
604
604
  * Principal branches: https://dlmf.nist.gov/4.37
@@ -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;
@@ -41,6 +41,10 @@ export interface InteractionOptions {
41
41
  export interface PeripheralScalesOptions {
42
42
  /** Show the peripheral rulers as one group. Default: false. */
43
43
  visible?: boolean;
44
+ /** Show curved axis captions. Default: true. */
45
+ captionsVisible?: boolean;
46
+ /** Show numeric tick labels. Default: true. */
47
+ tickLabelsVisible?: boolean;
44
48
  }
45
49
  export interface SmithOptions {
46
50
  /** Positive real reference impedance in ohms. Default: 50. */
@@ -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';
@@ -42,7 +42,7 @@ export declare class SvgChartRenderer {
42
42
  setTheme(theme: SmithTheme): void;
43
43
  draw(target: string | HTMLElement): void;
44
44
  /** Export the current mounted view as standalone SVG with its configured background. */
45
- toSvg(): string;
45
+ toSvg(lineDetail?: 'full' | 'view'): string;
46
46
  /** Remove this chart and release its event handlers. Safe to call more than once. */
47
47
  destroy(): void;
48
48
  private updateViewportScale;
@@ -64,6 +64,6 @@ export declare class SvgChartRenderer {
64
64
  private cursorContainer;
65
65
  private drawReactanceAxis;
66
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;
67
+ createTrace(values: TraceInput, color: string, style: Partial<TraceStyle>, onMarkerDrag: (marker: SmithMarker, dragging: boolean) => void): SmithData;
68
68
  removeTrace(trace: SmithData): void;
69
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';
@@ -147,6 +148,16 @@ export class SvgChartRenderer {
147
148
  }
148
149
  this.labelLayout.update(true);
149
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
+ },
150
161
  update: (gamma) => {
151
162
  assertAlive();
152
163
  this.peripheralScaleRenderer.update(gamma);
@@ -172,9 +183,23 @@ export class SvgChartRenderer {
172
183
  this.updateViewportScale();
173
184
  }
174
185
  /** Export the current mounted view as standalone SVG with its configured background. */
175
- toSvg() {
186
+ toSvg(lineDetail = 'full') {
176
187
  this.assertAlive();
177
- 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
+ });
178
203
  }
179
204
  /** Remove this chart and release its event handlers. Safe to call more than once. */
180
205
  destroy() {
@@ -331,7 +356,7 @@ export class SvgChartRenderer {
331
356
  this.traces.forEach((d) => d.zoom(transform));
332
357
  }
333
358
  cursorContainer() {
334
- const shape = this.drawReactanceAxis({ fill: 'transparent', stroke: 'none' });
359
+ const shape = this.drawReactanceAxis({ fill: 'transparent', stroke: 'none', strokeWidth: '0' });
335
360
  shape.Element.style('pointer-events', 'all')
336
361
  .on('pointermove.smithkit', (event) => {
337
362
  this.cursorMove(d3.pointer(event));
@@ -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;