smithkit 0.4.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 +179 -15
- package/dist/lib/OptionsValidation.js +7 -3
- package/dist/lib/Smith.d.ts +7 -1
- package/dist/lib/Smith.js +16 -5
- package/dist/lib/index.d.ts +2 -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 +4 -0
- package/dist/lib/measurements.d.ts +2 -0
- package/dist/lib/options.d.ts +4 -0
- package/dist/lib/rendering/SvgChartRenderer.d.ts +3 -3
- package/dist/lib/rendering/SvgChartRenderer.js +27 -2
- 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 +2 -0
- 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
|
@@ -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
|
@@ -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
|
}
|
|
@@ -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;
|
package/dist/lib/options.d.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
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() {
|
|
@@ -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. */
|
|
@@ -6,6 +6,8 @@ const chartRadius = 250;
|
|
|
6
6
|
const groupWidth = 40;
|
|
7
7
|
/** Peripheral rulers in the chart's 500-unit coordinate system. */
|
|
8
8
|
export class SmithPeripheralScales extends SmithGroup {
|
|
9
|
+
captionsVisible = true;
|
|
10
|
+
tickLabelsVisible = true;
|
|
9
11
|
static point(radius, degrees) {
|
|
10
12
|
const angle = (degrees * Math.PI) / 180;
|
|
11
13
|
return [250 + radius * Math.cos(angle), 250 - radius * Math.sin(angle)];
|
|
@@ -79,11 +81,13 @@ export class SmithPeripheralScales extends SmithGroup {
|
|
|
79
81
|
.attr('stroke-width', major ? 0.8 : 0.5)
|
|
80
82
|
.attr('vector-effect', 'non-scaling-stroke');
|
|
81
83
|
const captionDistance = Math.abs(((angle - captionAngle + 540) % 360) - 180);
|
|
82
|
-
if (major
|
|
84
|
+
if (major) {
|
|
83
85
|
const [x, y] = SmithPeripheralScales.point(radius + side * 11, angle);
|
|
84
86
|
const rotation = angle >= 0 ? 90 - angle : -90 - angle;
|
|
85
87
|
labelAxis
|
|
86
88
|
.append('text')
|
|
89
|
+
.attr('class', 'peripheral-tick-label')
|
|
90
|
+
.classed('peripheral-caption-sector', captionDistance <= 25)
|
|
87
91
|
.attr('x', x)
|
|
88
92
|
.attr('y', y)
|
|
89
93
|
.attr('transform', `rotate(${rotation},${x},${y})`)
|
|
@@ -132,6 +136,21 @@ export class SmithPeripheralScales extends SmithGroup {
|
|
|
132
136
|
.attr('stroke', 'var(--smithkit-scales-indicatorOutline)')
|
|
133
137
|
.attr('stroke-width', 1);
|
|
134
138
|
});
|
|
139
|
+
this.updateLabelVisibility();
|
|
140
|
+
}
|
|
141
|
+
setCaptionsVisible(visible) {
|
|
142
|
+
this.captionsVisible = visible;
|
|
143
|
+
this.updateLabelVisibility();
|
|
144
|
+
}
|
|
145
|
+
setTickLabelsVisible(visible) {
|
|
146
|
+
this.tickLabelsVisible = visible;
|
|
147
|
+
this.updateLabelVisibility();
|
|
148
|
+
}
|
|
149
|
+
updateLabelVisibility() {
|
|
150
|
+
this.labels.Element.selectAll('.peripheral-caption').attr('display', this.captionsVisible ? null : 'none');
|
|
151
|
+
this.labels.Element.selectAll('.peripheral-tick-label').attr('display', this.tickLabelsVisible ? null : 'none');
|
|
152
|
+
// Fill the reserved caption sectors with numeric labels when captions are hidden.
|
|
153
|
+
this.labels.Element.selectAll('.peripheral-caption-sector').attr('display', this.tickLabelsVisible && !this.captionsVisible ? null : 'none');
|
|
135
154
|
}
|
|
136
155
|
show() {
|
|
137
156
|
this.labels.show();
|
|
@@ -4,7 +4,7 @@ export declare class SvgExporter {
|
|
|
4
4
|
private static readonly namespace;
|
|
5
5
|
private static readonly properties;
|
|
6
6
|
private static readonly nonInherited;
|
|
7
|
-
static chart(source: SVGElement): string;
|
|
7
|
+
static chart(source: SVGElement, customize?: (original: SVGElement, copy: SVGElement) => void): string;
|
|
8
8
|
/** Preserve the current CSS layout of separate scale SVGs in one SVG document. */
|
|
9
9
|
static scales(container: HTMLElement, axes: readonly SVGSVGElement[], customize?: (copy: SVGElement, index: number) => void, caption?: {
|
|
10
10
|
text: string;
|
|
@@ -46,9 +46,9 @@ export class SvgExporter {
|
|
|
46
46
|
'baseline-shift',
|
|
47
47
|
'text-decoration',
|
|
48
48
|
]);
|
|
49
|
-
static chart(source) {
|
|
49
|
+
static chart(source, customize) {
|
|
50
50
|
const bounds = SvgExporter.bounds(source);
|
|
51
|
-
const copy = SvgExporter.copy(source);
|
|
51
|
+
const copy = SvgExporter.copy(source, customize);
|
|
52
52
|
SvgExporter.background(copy, source);
|
|
53
53
|
SvgExporter.size(copy, bounds.width, bounds.height);
|
|
54
54
|
return SvgExporter.serialize(copy);
|
|
@@ -120,7 +120,7 @@ export class SvgExporter {
|
|
|
120
120
|
}
|
|
121
121
|
return bounds;
|
|
122
122
|
}
|
|
123
|
-
static copy(source) {
|
|
123
|
+
static copy(source, customize) {
|
|
124
124
|
const copy = source.cloneNode(true);
|
|
125
125
|
const originals = [source, ...source.querySelectorAll('*')];
|
|
126
126
|
const copies = [copy, ...copy.querySelectorAll('*')];
|
|
@@ -167,6 +167,7 @@ export class SvgExporter {
|
|
|
167
167
|
element.removeAttribute(attribute.name);
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
+
customize?.(original, element);
|
|
170
171
|
});
|
|
171
172
|
return copy;
|
|
172
173
|
}
|
package/dist/lib/svg/export.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface MarkerLegendOptions {
|
|
|
25
25
|
fields?: readonly MarkerLegendField[];
|
|
26
26
|
}
|
|
27
27
|
export interface SmithImageExportOptions extends ImageExportOptions {
|
|
28
|
+
/** Full samples (default) or current displayed line geometry. View detail scales with export dimensions. */
|
|
29
|
+
lineDetail?: 'full' | 'view';
|
|
28
30
|
/** Include a footer with the names and colors of visible traces. Default: false. */
|
|
29
31
|
legend?: boolean;
|
|
30
32
|
/** Include a marker legend below the chart/scales, independently of the trace legend. Default: false. */
|
|
@@ -2,7 +2,8 @@ import { SmithGroup } from '../svg/SmithGroup.js';
|
|
|
2
2
|
import type { TraceStyle, MarkerSelectionStrategy } from '../measurements.js';
|
|
3
3
|
import { SmithMarker } from './SmithMarker.js';
|
|
4
4
|
import { SmithScaler } from '../svg/SmithScaler.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type { TraceInput, TraceSample } from '../samples.js';
|
|
6
|
+
import { TraceBuffer } from './TraceBuffer.js';
|
|
6
7
|
import { type TraceTransform } from './TraceRenderer.js';
|
|
7
8
|
/** Coordinates trace rendering, draggable markers, and deferred marker events. */
|
|
8
9
|
export declare class SmithData {
|
|
@@ -21,19 +22,23 @@ export declare class SmithData {
|
|
|
21
22
|
private selectHandler;
|
|
22
23
|
private visible;
|
|
23
24
|
private handler;
|
|
24
|
-
constructor(data:
|
|
25
|
+
constructor(data: TraceInput, color: string, transform: TraceTransform, container: SmithGroup, scaler: SmithScaler, markerDragHandler?: ((marker: SmithMarker, dragging: boolean) => void) | undefined, markerContainer?: SmithGroup, style?: Partial<TraceStyle>);
|
|
25
26
|
setViewportScale(scale: number): void;
|
|
26
27
|
zoom(transform: TraceTransform): void;
|
|
27
28
|
addMarker(sampleIndex?: number): number;
|
|
28
29
|
removeMarker(index: number): boolean;
|
|
29
30
|
setMarkerSample(index: number, sampleIndex: number): boolean;
|
|
30
31
|
setMarkerFrequency(index: number, frequencyHz: number): boolean;
|
|
31
|
-
get Samples():
|
|
32
|
+
get Samples(): TraceBuffer;
|
|
32
33
|
get SampleCount(): number;
|
|
33
34
|
get Color(): string;
|
|
34
35
|
get Visible(): boolean;
|
|
35
36
|
markerSampleIndex(index: number): number;
|
|
36
37
|
setColor(color: string): void;
|
|
38
|
+
fullExportLine(): {
|
|
39
|
+
node: SVGElement;
|
|
40
|
+
path: string;
|
|
41
|
+
} | undefined;
|
|
37
42
|
get Style(): TraceStyle;
|
|
38
43
|
setStyle(options: Partial<TraceStyle>): void;
|
|
39
44
|
setVisible(visible: boolean): void;
|
|
@@ -43,10 +48,11 @@ export declare class SmithData {
|
|
|
43
48
|
setMarkerSelectHandler(handler: (marker: SmithMarker) => void): void;
|
|
44
49
|
private notifyMarker;
|
|
45
50
|
private cancelEvents;
|
|
46
|
-
update(values:
|
|
51
|
+
update(values: TraceInput | TraceBuffer, strategy?: MarkerSelectionStrategy): void;
|
|
47
52
|
destroy(): void;
|
|
48
53
|
get Markers(): {
|
|
49
54
|
number: number;
|
|
55
|
+
sampleIndex: number;
|
|
50
56
|
selectedPoint: TraceSample;
|
|
51
57
|
marker: SmithMarker;
|
|
52
58
|
}[];
|
|
@@ -110,6 +110,9 @@ export class SmithData {
|
|
|
110
110
|
this.renderer.setColor(color);
|
|
111
111
|
this.markers.forEach((marker) => marker.setColor(color));
|
|
112
112
|
}
|
|
113
|
+
fullExportLine() {
|
|
114
|
+
return this.renderer.fullExportLine();
|
|
115
|
+
}
|
|
113
116
|
get Style() {
|
|
114
117
|
return this.renderer.Style;
|
|
115
118
|
}
|
|
@@ -129,12 +132,15 @@ export class SmithData {
|
|
|
129
132
|
}
|
|
130
133
|
moveMarker(entry) {
|
|
131
134
|
const marker = this.markers.get(entry);
|
|
132
|
-
marker.move(this.scaler.point(
|
|
135
|
+
marker.move(this.scaler.point([
|
|
136
|
+
this.model.Samples.real(entry.sampleIndex),
|
|
137
|
+
this.model.Samples.imaginary(entry.sampleIndex),
|
|
138
|
+
]));
|
|
133
139
|
this.updateReading(entry, marker);
|
|
134
140
|
this.notifyMarker(entry);
|
|
135
141
|
}
|
|
136
142
|
updateReading(entry, marker) {
|
|
137
|
-
marker.setReading(`${this.name}, marker ${entry.number}`, this.model.markerSampleIndex(this.model.markerIndex(entry)), this.SampleCount, entry.
|
|
143
|
+
marker.setReading(`${this.name}, marker ${entry.number}`, this.model.markerSampleIndex(this.model.markerIndex(entry)), this.SampleCount, this.model.Samples.frequency(entry.sampleIndex));
|
|
138
144
|
}
|
|
139
145
|
setName(name) {
|
|
140
146
|
this.name = name;
|
|
@@ -151,7 +157,7 @@ export class SmithData {
|
|
|
151
157
|
this.pendingEvents.delete(entry);
|
|
152
158
|
const index = this.model.markerIndex(entry);
|
|
153
159
|
if (!this.destroyed && index >= 0) {
|
|
154
|
-
this.handler?.(index, entry.
|
|
160
|
+
this.handler?.(index, this.model.Samples.sample(entry.sampleIndex));
|
|
155
161
|
}
|
|
156
162
|
}, 0);
|
|
157
163
|
this.pendingEvents.set(entry, timer);
|
|
@@ -182,7 +188,11 @@ export class SmithData {
|
|
|
182
188
|
this.renderer.destroy();
|
|
183
189
|
}
|
|
184
190
|
get Markers() {
|
|
185
|
-
return this.model.Markers.map((entry) => ({
|
|
191
|
+
return this.model.Markers.map((entry) => ({
|
|
192
|
+
...entry,
|
|
193
|
+
selectedPoint: this.model.Samples.sample(entry.sampleIndex),
|
|
194
|
+
marker: this.markers.get(entry),
|
|
195
|
+
}));
|
|
186
196
|
}
|
|
187
197
|
setMarkerMoveHandler(handler) {
|
|
188
198
|
this.handler = handler;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TraceInput, TraceSample } from '../samples.js';
|
|
2
|
+
import type { Point } from '../math/geometry.js';
|
|
3
|
+
/** Owned, packed f/re/im storage. Never exposes the mutable backing buffer. */
|
|
4
|
+
export declare class TraceBuffer {
|
|
5
|
+
private readonly values;
|
|
6
|
+
private readonly sorted;
|
|
7
|
+
private constructor();
|
|
8
|
+
static from(input: TraceInput): TraceBuffer;
|
|
9
|
+
static empty(): TraceBuffer;
|
|
10
|
+
get length(): number;
|
|
11
|
+
frequency(index: number): number;
|
|
12
|
+
real(index: number): number;
|
|
13
|
+
imaginary(index: number): number;
|
|
14
|
+
sample(index: number): TraceSample;
|
|
15
|
+
nearestFrequency(frequencyHz: number): number;
|
|
16
|
+
private lowerBound;
|
|
17
|
+
nearestPoint(point: Readonly<Point>): number;
|
|
18
|
+
renormalize(fromOhms: number, toOhms: number): TraceBuffer;
|
|
19
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Complex } from '../math/Complex.js';
|
|
2
|
+
import { RfCalculations } from '../rf/RfCalculations.js';
|
|
3
|
+
/** Owned, packed f/re/im storage. Never exposes the mutable backing buffer. */
|
|
4
|
+
export class TraceBuffer {
|
|
5
|
+
values;
|
|
6
|
+
sorted;
|
|
7
|
+
constructor(values, sorted) {
|
|
8
|
+
this.values = values;
|
|
9
|
+
this.sorted = sorted;
|
|
10
|
+
}
|
|
11
|
+
static from(input) {
|
|
12
|
+
const packed = input instanceof Float64Array;
|
|
13
|
+
if ((!packed && !Array.isArray(input)) || !input.length || (packed && input.length % 3 !== 0)) {
|
|
14
|
+
throw new RangeError('A trace requires complete samples with frequency, real, and imaginary values.');
|
|
15
|
+
}
|
|
16
|
+
const count = packed ? input.length / 3 : input.length;
|
|
17
|
+
const values = new Float64Array(count * 3);
|
|
18
|
+
const tuples = !packed && Array.isArray(input[0]);
|
|
19
|
+
let sorted = true;
|
|
20
|
+
for (let i = 0; i < count; i++) {
|
|
21
|
+
let f;
|
|
22
|
+
let re;
|
|
23
|
+
let im;
|
|
24
|
+
if (packed) {
|
|
25
|
+
f = input[i * 3];
|
|
26
|
+
re = input[i * 3 + 1];
|
|
27
|
+
im = input[i * 3 + 2];
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const sample = input[i];
|
|
31
|
+
if (tuples) {
|
|
32
|
+
if (!Array.isArray(sample) || sample.length !== 3) {
|
|
33
|
+
throw new RangeError(`Invalid tuple at sample ${i}; expected [frequencyHz, re, im].`);
|
|
34
|
+
}
|
|
35
|
+
[f, re, im] = sample;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
if (!sample ||
|
|
39
|
+
typeof sample !== 'object' ||
|
|
40
|
+
Array.isArray(sample) ||
|
|
41
|
+
!('reflectionCoefficient' in sample) ||
|
|
42
|
+
!Array.isArray(sample.reflectionCoefficient) ||
|
|
43
|
+
sample.reflectionCoefficient.length !== 2) {
|
|
44
|
+
throw new RangeError(`Invalid object at sample ${i}; expected frequencyHz and reflectionCoefficient.`);
|
|
45
|
+
}
|
|
46
|
+
f = sample.frequencyHz;
|
|
47
|
+
[re, im] = sample.reflectionCoefficient;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isFinite(f) || f < 0 || !Number.isFinite(re) || !Number.isFinite(im)) {
|
|
51
|
+
throw new RangeError(`Invalid sample at index ${i}; frequency must be non-negative and all values finite.`);
|
|
52
|
+
}
|
|
53
|
+
values[i * 3] = f;
|
|
54
|
+
values[i * 3 + 1] = re;
|
|
55
|
+
values[i * 3 + 2] = im;
|
|
56
|
+
if (i > 0 && f < values[(i - 1) * 3]) {
|
|
57
|
+
sorted = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return new TraceBuffer(values, sorted);
|
|
61
|
+
}
|
|
62
|
+
static empty() {
|
|
63
|
+
return new TraceBuffer(new Float64Array(), true);
|
|
64
|
+
}
|
|
65
|
+
get length() {
|
|
66
|
+
return this.values.length / 3;
|
|
67
|
+
}
|
|
68
|
+
frequency(index) {
|
|
69
|
+
return this.values[index * 3];
|
|
70
|
+
}
|
|
71
|
+
real(index) {
|
|
72
|
+
return this.values[index * 3 + 1];
|
|
73
|
+
}
|
|
74
|
+
imaginary(index) {
|
|
75
|
+
return this.values[index * 3 + 2];
|
|
76
|
+
}
|
|
77
|
+
sample(index) {
|
|
78
|
+
return {
|
|
79
|
+
frequencyHz: this.frequency(index),
|
|
80
|
+
reflectionCoefficient: [this.real(index), this.imaginary(index)],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
nearestFrequency(frequencyHz) {
|
|
84
|
+
if (this.sorted) {
|
|
85
|
+
const right = this.lowerBound(frequencyHz);
|
|
86
|
+
if (right === 0) {
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
const distance = frequencyHz - this.frequency(right - 1);
|
|
90
|
+
if (right < this.length && this.frequency(right) - frequencyHz < distance) {
|
|
91
|
+
return right;
|
|
92
|
+
}
|
|
93
|
+
// Find the earliest equally close sample, including floating-point rounding
|
|
94
|
+
// ties between distinct frequencies far below the requested frequency.
|
|
95
|
+
let low = 0;
|
|
96
|
+
let high = right - 1;
|
|
97
|
+
while (low < high) {
|
|
98
|
+
const mid = Math.floor((low + high) / 2);
|
|
99
|
+
if (frequencyHz - this.frequency(mid) > distance) {
|
|
100
|
+
low = mid + 1;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
high = mid;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return low;
|
|
107
|
+
}
|
|
108
|
+
let closest = 0;
|
|
109
|
+
let distance = Math.abs(this.frequency(0) - frequencyHz);
|
|
110
|
+
for (let i = 1; i < this.length; i++) {
|
|
111
|
+
const next = Math.abs(this.frequency(i) - frequencyHz);
|
|
112
|
+
if (next < distance) {
|
|
113
|
+
closest = i;
|
|
114
|
+
distance = next;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return closest;
|
|
118
|
+
}
|
|
119
|
+
lowerBound(frequencyHz) {
|
|
120
|
+
let low = 0;
|
|
121
|
+
let high = this.length;
|
|
122
|
+
while (low < high) {
|
|
123
|
+
const mid = Math.floor((low + high) / 2);
|
|
124
|
+
if (this.frequency(mid) < frequencyHz) {
|
|
125
|
+
low = mid + 1;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
high = mid;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return low;
|
|
132
|
+
}
|
|
133
|
+
nearestPoint(point) {
|
|
134
|
+
let closest = 0;
|
|
135
|
+
let distance = Infinity;
|
|
136
|
+
const [real, imaginary] = point;
|
|
137
|
+
// A coordinate difference is a lower bound on Euclidean distance. Reject
|
|
138
|
+
// points outside the current bounding square before evaluating the norm.
|
|
139
|
+
// Keep the boundary inclusive and compare surviving norms with Math.hypot
|
|
140
|
+
// to preserve first-input ties, including ties caused by floating-point rounding.
|
|
141
|
+
for (let offset = 1; offset < this.values.length; offset += 3) {
|
|
142
|
+
const dx = real - this.values[offset];
|
|
143
|
+
if (Math.abs(dx) > distance) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const dy = imaginary - this.values[offset + 1];
|
|
147
|
+
if (Math.abs(dy) > distance) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const next = Math.hypot(dx, dy);
|
|
151
|
+
if (next < distance) {
|
|
152
|
+
closest = (offset - 1) / 3;
|
|
153
|
+
distance = next;
|
|
154
|
+
if (distance === 0) {
|
|
155
|
+
return closest;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return closest;
|
|
160
|
+
}
|
|
161
|
+
renormalize(fromOhms, toOhms) {
|
|
162
|
+
RfCalculations.renormalizeSamples([], fromOhms, toOhms);
|
|
163
|
+
const values = new Float64Array(this.values.length);
|
|
164
|
+
for (let i = 0; i < this.length; i++) {
|
|
165
|
+
const gamma = RfCalculations.renormalizeReflection(Complex.from(this.real(i), this.imaginary(i)), fromOhms, toOhms);
|
|
166
|
+
if (!gamma) {
|
|
167
|
+
throw new RangeError(`Renormalization is singular or outside the numeric range at sample ${i}.`);
|
|
168
|
+
}
|
|
169
|
+
values[i * 3] = this.frequency(i);
|
|
170
|
+
const [re, im] = gamma.toVector();
|
|
171
|
+
values[i * 3 + 1] = re;
|
|
172
|
+
values[i * 3 + 2] = im;
|
|
173
|
+
}
|
|
174
|
+
return new TraceBuffer(values, this.sorted);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TraceBuffer } from './TraceBuffer.js';
|
|
2
|
+
/** Nested radial-distance levels with a conservative accumulated error in Γ units. */
|
|
3
|
+
export declare class TraceLineDetail {
|
|
4
|
+
private readonly levels;
|
|
5
|
+
constructor(data: TraceBuffer);
|
|
6
|
+
/** Undefined means use every sample. No data scan or allocation during view selection. */
|
|
7
|
+
select(tolerance: number): Uint32Array | undefined;
|
|
8
|
+
}
|