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.
- package/README.md +324 -37
- package/dist/lib/OptionsValidation.d.ts +16 -0
- package/dist/lib/OptionsValidation.js +172 -0
- package/dist/lib/Smith.d.ts +20 -5
- package/dist/lib/Smith.js +114 -15
- package/dist/lib/appearance/Theme.js +5 -0
- package/dist/lib/grid/CircleLayerControl.d.ts +1 -1
- package/dist/lib/grid/CircleLayerControl.js +10 -8
- package/dist/lib/grid/ConstQCircles.d.ts +1 -0
- package/dist/lib/grid/ConstQCircles.js +5 -0
- package/dist/lib/grid/ConstSwrCircles.d.ts +1 -0
- package/dist/lib/grid/ConstSwrCircles.js +5 -0
- package/dist/lib/grid/GridLayerControl.js +5 -4
- package/dist/lib/grid/LayerControl.d.ts +1 -2
- package/dist/lib/grid/LayerControl.js +1 -10
- package/dist/lib/index.d.ts +3 -2
- package/dist/lib/io/Touchstone.d.ts +13 -7
- package/dist/lib/io/Touchstone.js +146 -46
- package/dist/lib/io/types.d.ts +17 -0
- package/dist/lib/io/types.js +1 -0
- package/dist/lib/layers.d.ts +7 -1
- package/dist/lib/measurements.d.ts +2 -0
- package/dist/lib/options.d.ts +58 -0
- package/dist/lib/options.js +1 -0
- package/dist/lib/rendering/SvgChartRenderer.d.ts +6 -3
- package/dist/lib/rendering/SvgChartRenderer.js +58 -11
- package/dist/lib/rf/RfCalculations.d.ts +3 -1
- package/dist/lib/rf/RfCalculations.js +23 -6
- package/dist/lib/samples.d.ts +5 -1
- package/dist/lib/scales/SmithPeripheralScales.d.ts +5 -0
- package/dist/lib/scales/SmithPeripheralScales.js +20 -1
- package/dist/lib/svg/SvgExporter.d.ts +1 -1
- package/dist/lib/svg/SvgExporter.js +4 -3
- package/dist/lib/svg/export.d.ts +3 -1
- package/dist/lib/traces/SmithData.d.ts +10 -4
- package/dist/lib/traces/SmithData.js +14 -4
- package/dist/lib/traces/TraceBuffer.d.ts +19 -0
- package/dist/lib/traces/TraceBuffer.js +176 -0
- package/dist/lib/traces/TraceLineDetail.d.ts +8 -0
- package/dist/lib/traces/TraceLineDetail.js +52 -0
- package/dist/lib/traces/TraceModel.d.ts +6 -8
- package/dist/lib/traces/TraceModel.js +24 -49
- package/dist/lib/traces/TraceRenderer.d.ts +13 -3
- package/dist/lib/traces/TraceRenderer.js +83 -23
- package/package.json +4 -2
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import type { TraceSamples } from '../samples.js';
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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('
|
|
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 ||
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
fail('Expected frequency and two finite S11 components.');
|
|
105
|
+
if (fields !== width) {
|
|
106
|
+
continue;
|
|
53
107
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 (
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
141
|
+
previousFrequency = record[0];
|
|
142
|
+
count++;
|
|
143
|
+
fields = 0;
|
|
69
144
|
}
|
|
70
|
-
if (
|
|
71
|
-
throw new Error(
|
|
145
|
+
if (fields) {
|
|
146
|
+
throw new Error(`Line ${recordLine}: Incomplete ${ports}-port record; expected ${width} values. Noise data is not supported.`);
|
|
72
147
|
}
|
|
73
|
-
|
|
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 {};
|
package/dist/lib/layers.d.ts
CHANGED
|
@@ -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:
|
|
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 {
|
|
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:
|
|
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 =
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
123
|
-
if (!Number.isFinite(
|
|
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(
|
|
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
|
}
|
package/dist/lib/samples.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
/** One
|
|
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. */
|