KiCad PCB EDA Suite
Loading...
Searching...
No Matches
sim_plot_tab.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2016-2023 CERN
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * @author Tomasz Wlostowski <[email protected]>
8 * @author Maciej Suminski <[email protected]>
9 *
10 * This program is free software; you can redistribute it and/or
11 * modify it under the terms of the GNU General Public License
12 * as published by the Free Software Foundation; either version 3
13 * of the License, or (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 */
23
24#include <wx/tokenzr.h>
26#include "sim_plot_colors.h"
27#include "sim_plot_tab.h"
28#include "simulator_frame.h"
29#include "core/kicad_algo.h"
30
31#include <algorithm>
32#include <cmath>
33#include <limits>
34
35
36
37static wxString formatFloat( double x, int nDigits )
38{
39 wxString rv, fmt;
40
41 if( nDigits )
42 fmt.Printf( "%%.0%df", nDigits );
43 else
44 fmt = wxT( "%.0f" );
45
46 rv.Printf( fmt, x );
47
48 return rv;
49}
50
51
52static void getSISuffix( double x, const wxString& unit, int& power, wxString& suffix )
53{
54 const int n_powers = 11;
55
56 const struct
57 {
58 int exponent;
59 char suffix;
60 } powers[] =
61 {
62 { -18, 'a' },
63 { -15, 'f' },
64 { -12, 'p' },
65 { -9, 'n' },
66 { -6, 'u' },
67 { -3, 'm' },
68 { 0, 0 },
69 { 3, 'k' },
70 { 6, 'M' },
71 { 9, 'G' },
72 { 12, 'T' },
73 { 14, 'P' }
74 };
75
76 power = 0;
77 suffix = unit;
78
79 if( x == 0.0 )
80 return;
81
82 for( int i = 0; i < n_powers - 1; i++ )
83 {
84 double r_cur = pow( 10, powers[i].exponent );
85
86 if( fabs( x ) >= r_cur && fabs( x ) < r_cur * 1000.0 )
87 {
88 power = powers[i].exponent;
89
90 if( powers[i].suffix )
91 suffix = wxString( powers[i].suffix ) + unit;
92 else
93 suffix = unit;
94
95 return;
96 }
97 }
98}
99
100
101static int countDecimalDigits( double x, int maxDigits )
102{
103 if( std::isnan( x ) )
104 return 0;
105
106 auto countSignificantDigits =
107 [&]( int64_t k )
108 {
109 while( k && ( k % 10LL ) == 0LL )
110 k /= 10LL;
111
112 int n = 0;
113
114 while( k != 0LL )
115 {
116 n++;
117 k /= 10LL;
118 }
119
120 return n;
121 };
122
123 int64_t k = (int)( ( x - floor( x ) ) * pow( 10.0, (double) maxDigits ) );
124 int n = countSignificantDigits( k );
125
126 // check for trailing 9's
127 n = std::min( n, countSignificantDigits( k + 1 ) );
128
129 return n;
130}
131
132
133template <typename T_PARENT>
134class LIN_SCALE : public T_PARENT
135{
136public:
137 LIN_SCALE( const wxString& name, const wxString& unit, int flags ) :
138 T_PARENT( name, flags, false ),
139 m_unit( unit )
140 {};
141
142 wxString GetUnits() const { return m_unit; }
143
144private:
145 void formatLabels() override
146 {
147 double maxVis = T_PARENT::AbsVisibleMaxValue();
148
149 wxString suffix;
150 int power = 0;
151 int digits = 0;
152 int constexpr MAX_DIGITS = 3;
153 int constexpr MAX_DISAMBIGUATION_DIGITS = 6;
154 bool duplicateLabels = false;
155
156 getSISuffix( maxVis, m_unit, power, suffix );
157
158 double sf = pow( 10.0, power );
159
160 for( mpScaleBase::TICK_LABEL& l : T_PARENT::m_tickLabels )
161 digits = std::max( digits, countDecimalDigits( l.pos / sf, MAX_DIGITS ) );
162
163 do
164 {
165 for( size_t ii = 0; ii < T_PARENT::m_tickLabels.size(); ++ii )
166 {
167 mpScaleBase::TICK_LABEL& l = T_PARENT::m_tickLabels[ii];
168
169 l.label = formatFloat( l.pos / sf, digits );
170 l.visible = true;
171
172 if( ii > 0 && l.label == T_PARENT::m_tickLabels[ii-1].label )
173 duplicateLabels = true;
174 }
175 }
176 while( duplicateLabels && ++digits <= MAX_DISAMBIGUATION_DIGITS );
177
178 if( m_base_axis_label.IsEmpty() )
179 m_base_axis_label = T_PARENT::GetName();
180
181 T_PARENT::SetName( wxString::Format( "%s (%s)", m_base_axis_label, suffix ) );
182 }
183
184private:
185 const wxString m_unit;
187};
188
189
190class TIME_SCALE : public LIN_SCALE<mpScaleX>
191{
192public:
193 TIME_SCALE( const wxString& name, const wxString& unit, int flags ) :
194 LIN_SCALE( name, unit, flags ),
195 m_startTime( 0.0 ),
196 m_endTime( 1.0 )
197 {};
198
199 void ExtendDataRange( double minV, double maxV ) override
200 {
201 LIN_SCALE::ExtendDataRange( minV, maxV );
202
203 // Time is never longer than the simulation itself
204 if( m_minV < m_startTime )
206
207 if( m_maxV > m_endTime )
209 };
210
211 void SetStartAndEnd( double aStartTime, double aEndTime )
212 {
213 m_startTime = aStartTime;
214 m_endTime = aEndTime;
216 }
217
218 void ResetDataRange() override
219 {
222 m_rangeSet = true;
223 }
224
225protected:
227 double m_endTime;
228};
229
230
231template <typename T_PARENT>
232class LOG_SCALE : public T_PARENT
233{
234public:
235 LOG_SCALE( const wxString& name, const wxString& unit, int flags ) :
236 T_PARENT( name, flags, false ),
237 m_unit( unit )
238 {};
239
240 wxString GetUnits() const { return m_unit; }
241
242private:
243 void formatLabels() override
244 {
245 wxString suffix;
246 int power;
247 int constexpr MAX_DIGITS = 3;
248
249 for( mpScaleBase::TICK_LABEL& l : T_PARENT::m_tickLabels )
250 {
251 getSISuffix( l.pos, m_unit, power, suffix );
252 double sf = pow( 10.0, power );
253 int k = countDecimalDigits( l.pos / sf, MAX_DIGITS );
254
255 l.label = formatFloat( l.pos / sf, k ) + suffix;
256 l.visible = true;
257 }
258 }
259
260private:
261 const wxString m_unit;
262};
263
264
265bool SMITH_GRID::GetChartView( mpWindow& aWindow, double aZoom, const wxRealPoint& aPan, SMITH_VIEW& aView )
266{
267 int mL = aWindow.GetMarginLeft(), mT = aWindow.GetMarginTop();
268 int plotW = aWindow.GetScrX() - mL - aWindow.GetMarginRight();
269 int plotH = aWindow.GetScrY() - mT - aWindow.GetMarginBottom();
270 int radius = std::min( plotW, plotH ) / 2 - 15;
271
272 if( radius <= 20 )
273 return false;
274
275 aView.center = wxPoint( mL + plotW / 2, mT + plotH / 2 );
276 aView.radius = radius * aZoom;
277 aView.zoom = aZoom;
278 aView.pan = aPan;
279 aView.plotRect = wxRect( mL, mT, plotW, plotH );
280
281 return true;
282}
283
284
285static bool smithView( mpWindow& aWindow, SMITH_VIEW& aView )
286{
287 // the layers of a Smith chart are drawn on the SIM_VIEW carrying its own pan and zoom
288 SIM_VIEW* view = dynamic_cast<SIM_VIEW*>( &aWindow );
289 double zoom = view ? view->GetSmithZoom() : 1.0;
290 wxRealPoint pan = view ? view->GetSmithPan() : wxRealPoint( 0.0, 0.0 );
291
292 return SMITH_GRID::GetChartView( aWindow, zoom, pan, aView );
293}
294
295
296void SMITH_GRID::Plot( wxDC& aDC, mpWindow& aWindow )
297{
298 if( !m_visible )
299 return;
300
301 SMITH_VIEW view;
302
303 if( !smithView( aWindow, view ) )
304 return;
305
306 const wxRect& plotRect = view.plotRect;
307 int mL = plotRect.x, mT = plotRect.y, plotW = plotRect.width, plotH = plotRect.height;
308
309 aDC.SetClippingRegion( plotRect );
310
311 // enough segments to keep the chord error under a pixel at this circle's screen radius
312 auto segmentsFor =
313 [&]( double aR ) -> int
314 {
315 return std::clamp( KiROUND( M_PI * std::sqrt( aR * view.radius ) ), 64, 4096 );
316 };
317
318 // draw a gamma-plane circle, keeping only the parts inside the unit circle
319 auto drawClippedCircle =
320 [&]( double aCx, double aCy, double aR )
321 {
322 constexpr double LIMIT = 1.0002;
323
324 std::vector<wxPoint> run;
325 int segments = segmentsFor( aR );
326
327 for( int ii = 0; ii <= segments; ii++ )
328 {
329 double angle = 2.0 * M_PI * ii / segments;
330 double re = aCx + aR * cos( angle );
331 double im = aCy + aR * sin( angle );
332
333 if( re * re + im * im <= LIMIT )
334 {
335 run.push_back( view.ToScreen( re, im ) );
336 }
337 else
338 {
339 if( run.size() > 1 )
340 aDC.DrawLines( (int) run.size(), run.data() );
341
342 run.clear();
343 }
344 }
345
346 if( run.size() > 1 )
347 aDC.DrawLines( (int) run.size(), run.data() );
348 };
349
350 auto formatValue =
351 []( double aValue ) -> wxString
352 {
353 return wxString::Format( wxS( "%g" ), aValue );
354 };
355
356 // ohm labels for a single reference impedance, normalized labels without one
357 double labelScale = m_z0 > 0.0 ? m_z0 : 1.0;
358
359 static const std::vector<double> baseVals = { 0.2, 0.5, 1.0, 2.0, 5.0 };
360 std::vector<double> gridVals = baseVals;
361
362 constexpr double FINEST_SCALE = 2.0 / ( 1.0 + 20.0 );
363
364 wxRealPoint lowGamma = view.ToGamma( wxPoint( mL, mT + plotH ) );
365 wxRealPoint highGamma = view.ToGamma( wxPoint( mL + plotW, mT ) );
366
367 auto poleDistance =
368 [&]( double aRe )
369 {
370 return std::hypot( std::max( { lowGamma.x - aRe, aRe - highGamma.x, 0.0 } ),
371 std::max( { lowGamma.y, -highGamma.y, 0.0 } ) );
372 };
373
374 bool zoomedIn = ( plotW / 2.0 ) / view.radius < 1.0;
375 bool converged = zoomedIn && std::min( poleDistance( 1.0 ), poleDistance( -1.0 ) ) < FINEST_SCALE;
376
377 if( view.zoom >= 2.0 && !converged )
378 gridVals.insert( gridVals.end(), { 0.1, 0.3, 0.4, 0.7, 1.5, 3.0, 10.0 } );
379
380 if( view.zoom >= 5.0 && !converged )
381 gridVals.insert( gridVals.end(), { 0.05, 0.15, 0.6, 0.8, 1.2, 1.7, 2.5, 4.0, 7.0, 20.0 } );
382
383 wxPen gridPen = m_pen;
384 gridPen.SetStyle( wxPENSTYLE_DOT );
385
386 aDC.SetBrush( *wxTRANSPARENT_BRUSH );
387 aDC.SetFont( GetPlotFont() );
388 aDC.SetTextForeground( m_pen.GetColour() );
389 aDC.SetPen( gridPen );
390
391 // constant resistance circles, centered on the real axis, tangent at gamma = 1
392 for( double r : gridVals )
393 drawClippedCircle( r / ( 1.0 + r ), 0.0, 1.0 / ( 1.0 + r ) );
394
395 // constant reactance arcs, one per sign, clipped to the unit circle
396 for( double x : gridVals )
397 {
398 drawClippedCircle( 1.0, 1.0 / x, 1.0 / x );
399 drawClippedCircle( 1.0, -1.0 / x, 1.0 / x );
400 }
401
402 aDC.SetPen( m_pen );
403 aDC.DrawLine( view.ToScreen( -1.0, 0.0 ), view.ToScreen( 1.0, 0.0 ) );
404 aDC.DrawCircle( view.ToScreen( 0.0, 0.0 ), KiROUND( view.radius ) );
405
406 // Label each gridline at its axis/rim anchor, or at the plot edge when the anchor is
407 // panned/zoomed out of view.
408 constexpr double LABEL_LIMIT = 1.0002;
409
410 auto anchorPos =
411 [&]( double aRe, double aIm, wxPoint& aOut ) -> bool
412 {
413 if( aRe * aRe + aIm * aIm > LABEL_LIMIT )
414 return false;
415
416 wxPoint p = view.ToScreen( aRe, aIm );
417
418 if( !plotRect.Contains( p ) )
419 return false;
420
421 aOut = p;
422 return true;
423 };
424
425 // anchor off-screen, put the label where the gridline meets the plot edge
426 auto edgePos =
427 [&]( double aCx, double aCy, double aR, wxPoint& aOut ) -> bool
428 {
429 int bestMargin = std::numeric_limits<int>::max();
430 bool found = false;
431 int segments = segmentsFor( aR );
432
433 for( int ii = 0; ii <= segments; ii++ )
434 {
435 double angle = 2.0 * M_PI * ii / segments;
436 double re = aCx + aR * cos( angle );
437 double im = aCy + aR * sin( angle );
438
439 if( re * re + im * im > LABEL_LIMIT )
440 continue;
441
442 wxPoint p = view.ToScreen( re, im );
443
444 if( !plotRect.Contains( p ) )
445 continue;
446
447 int margin = std::min( { p.x - mL, mL + plotW - p.x, p.y - mT, mT + plotH - p.y } );
448
449 if( margin < bestMargin )
450 {
451 bestMargin = margin;
452 aOut = p;
453 found = true;
454 }
455 }
456
457 return found;
458 };
459
460 auto clampToPlot =
461 [&]( const wxPoint& aPos, const wxSize& aExt ) -> wxPoint
462 {
463 return wxPoint( std::clamp( aPos.x, mL + 1, mL + plotW - aExt.x - 1 ),
464 std::clamp( aPos.y, mT + 1, mT + plotH - aExt.y - 1 ) );
465 };
466
467 std::vector<wxRect> placed;
468
469 auto drawLabel =
470 [&]( const wxString& aLabel, const wxPoint& aPos, const wxSize& aExt )
471 {
472 wxPoint pos = clampToPlot( aPos, aExt );
473 wxRect box( pos, aExt );
474
475 box.Inflate( 2, 1 );
476
477 for( const wxRect& seen : placed )
478 {
479 if( seen.Intersects( box ) )
480 return;
481 }
482
483 placed.push_back( box );
484 aDC.DrawText( aLabel, pos );
485 };
486
487 auto drawEdgeLabel =
488 [&]( const wxString& aLabel, const wxPoint& aAt )
489 {
490 wxSize ext = aDC.GetTextExtent( aLabel );
491
492 drawLabel( aLabel, wxPoint( aAt.x - ext.x / 2, aAt.y + 3 ), ext );
493 };
494
495 // push reactance labels just outside the rim, clamped so j50 and -j50 are not clipped
496 auto drawRimLabel = [&]( const wxString& aLabel, const wxPoint& aAt, double aRe, double aIm )
497 {
498 wxSize ext = aDC.GetTextExtent( aLabel );
499 wxPoint pos = aAt;
500
501 pos.x += KiROUND( aRe * 6 );
502 pos.y -= KiROUND( aIm * 6 );
503 pos.x -= KiROUND( ext.x * ( 1.0 - aRe ) / 2.0 );
504 pos.y -= KiROUND( ext.y * ( 1.0 + aIm ) / 2.0 );
505
506 drawLabel( aLabel, pos, ext );
507 };
508
509 wxPoint at;
510
511 // short (r = 0)
512 if( anchorPos( -1.0, 0.0, at ) )
513 drawRimLabel( wxS( "0" ), at, -1.0, 0.0 );
514 else if( edgePos( 0.0, 0.0, 1.0, at ) )
515 drawEdgeLabel( wxS( "0" ), at );
516
517 for( double r : gridVals )
518 {
519 if( anchorPos( ( r - 1.0 ) / ( r + 1.0 ), 0.0, at ) || edgePos( r / ( 1.0 + r ), 0.0, 1.0 / ( 1.0 + r ), at ) )
520 {
521 drawEdgeLabel( formatValue( r * labelScale ), at );
522 }
523 }
524
525 for( double x : gridVals )
526 {
527 double d = x * x + 1.0;
528 double re = ( x * x - 1.0 ) / d;
529 double im = 2.0 * x / d;
530 wxString posLabel = wxS( "j" ) + formatValue( x * labelScale );
531 wxString negLabel = wxS( "-j" ) + formatValue( x * labelScale );
532
533 if( anchorPos( re, im, at ) )
534 drawRimLabel( posLabel, at, re, im );
535 else if( edgePos( 1.0, 1.0 / x, 1.0 / x, at ) )
536 drawEdgeLabel( posLabel, at );
537
538 if( anchorPos( re, -im, at ) )
539 drawRimLabel( negLabel, at, re, -im );
540 else if( edgePos( 1.0, -1.0 / x, 1.0 / x, at ) )
541 drawEdgeLabel( negLabel, at );
542 }
543
544 // the ohm labels mean nothing unless the reference impedance is named
545 wxString note;
546
547 if( m_z0 > 0.0 )
548 note = wxString::Format( wxS( "Z0 = %s Ω" ), formatValue( m_z0 ) );
549 else if( m_mixedReferences )
550 note = _( "Normalized Z/Z0 (ports differ)" );
551 else
552 note = _( "Normalized Z/Z0" );
553
554 wxSize ext = aDC.GetTextExtent( note );
555
556 aDC.DrawText( note, mL + 4, mT + plotH - ext.y - 4 );
557
558 aDC.DestroyClippingRegion();
559}
560
561
562void SMITH_TRACE::Plot( wxDC& aDC, mpWindow& aWindow )
563{
564 if( !m_visible )
565 return;
566
567 SMITH_VIEW view;
568
569 if( !smithView( aWindow, view ) )
570 return;
571
572 const std::vector<double>& xs = GetDataX();
573 const std::vector<double>& ys = GetDataY();
574 size_t count = std::min( xs.size(), ys.size() );
575
576 if( count == 0 )
577 return;
578
579 aDC.SetPen( m_pen );
580 aDC.SetClippingRegion( view.plotRect );
581
582 size_t chunk = GetSweepSize();
583
584 if( GetSweepCount() <= 1 || chunk == std::numeric_limits<size_t>::max() || chunk == 0 )
585 chunk = count;
586
587 std::vector<wxPoint> pts;
588
589 auto flush =
590 [&]()
591 {
592 if( pts.size() > 1 )
593 {
594 aDC.DrawLines( (int) pts.size(), pts.data() );
595 }
596 else if( pts.size() == 1 )
597 {
598 aDC.SetBrush( wxBrush( m_pen.GetColour() ) );
599 aDC.DrawCircle( pts[0], 2 );
600 }
601
602 pts.clear();
603 };
604
605 for( size_t start = 0; start < count; start += chunk )
606 {
607 size_t end = std::min( count, start + chunk );
608
609 for( size_t ii = start; ii < end; ii++ )
610 {
611 // a non-finite sample breaks the locus rather than drawing a bogus segment
612 if( !std::isfinite( xs[ii] ) || !std::isfinite( ys[ii] ) )
613 {
614 flush();
615 continue;
616 }
617
618 pts.emplace_back( view.ToScreen( xs[ii], ys[ii] ) );
619 }
620
621 flush();
622 }
623
624 aDC.DestroyClippingRegion();
625}
626
627
629{
630 const std::vector<double>& re = m_trace->GetDataX();
631 const std::vector<double>& im = m_trace->GetDataY();
632 const std::vector<double>& freqs = static_cast<SMITH_TRACE*>( m_trace )->GetFrequencies();
633
634 size_t count = std::min( re.size(), im.size() );
635
636 if( count == 0 )
637 return;
638
639 m_index = std::clamp( aIndex, 0, (int) count - 1 );
640 m_gamma = wxRealPoint( re[m_index], im[m_index] );
641
642 // no frequency data for this sample, keep the previous x so a saved position stays finite
643 double freq = m_index < (int) freqs.size() ? freqs[m_index] : m_coords.x;
644
645 m_coords = wxRealPoint( freq, std::hypot( m_gamma.x, m_gamma.y ) );
646}
647
648
650{
651 const std::vector<double>& freqs = static_cast<SMITH_TRACE*>( m_trace )->GetFrequencies();
652 const std::vector<double>& re = m_trace->GetDataX();
653 const std::vector<double>& im = m_trace->GetDataY();
654
655 // frequencies repeat identically per run, search only the run the cursor is on
656 // so a frequency-keyed move cannot silently hop to run 0
657 size_t begin = 0;
658 size_t end = freqs.size();
659 size_t chunk = m_trace->GetSweepSize();
660
661 if( m_trace->GetSweepCount() > 1
662 && chunk > 0
663 && chunk != std::numeric_limits<size_t>::max()
664 && m_index >= 0
665 && (size_t) m_index < freqs.size() )
666 {
667 begin = ( (size_t) m_index / chunk ) * chunk;
668 end = std::min( freqs.size(), begin + chunk );
669 }
670
671 int best = -1;
672 double bestDist = std::numeric_limits<double>::max();
673
674 for( size_t ii = begin; ii < end; ii++ )
675 {
676 if( !std::isfinite( freqs[ii] ) )
677 continue;
678
679 if( ii < re.size() && ii < im.size() && ( !std::isfinite( re[ii] ) || !std::isfinite( im[ii] ) ) )
680 continue;
681
682 double dist = std::fabs( freqs[ii] - aFreq );
683
684 if( dist < bestDist )
685 {
686 bestDist = dist;
687 best = (int) ii;
688 }
689 }
690
691 if( best >= 0 )
692 snapToIndex( best );
693}
694
695
696void SMITH_CURSOR::SetCoordX( double aValue )
697{
698 m_requestFreq = aValue;
699
701
702 if( m_window )
703 m_window->Refresh();
704}
705
706
708{
709 if( static_cast<SMITH_TRACE*>( m_trace )->GetFrequencies().empty() )
710 {
711 // no data yet, remember the frequency and resolve it once the sim fills in
713 m_pendingFreq = true;
714 m_updateRequired = false;
715 return;
716 }
717
719 m_pendingFreq = false;
720 m_updateRequired = false;
721 m_updateRef = true;
722}
723
724
725void SMITH_CURSOR::Move( wxPoint aDelta )
726{
727 m_dragging = true;
728 Update();
729 mpInfoLayer::Move( aDelta );
730}
731
732
734{
735 // skip CURSOR's axis reference, the marker follows the locus
737}
738
739
740bool SMITH_CURSOR::Inside( const wxPoint& aPoint ) const
741{
742 if( !m_window || m_index < 0 )
743 return false;
744
745 SMITH_VIEW view;
746
747 if( !smithView( *m_window, view ) )
748 return false;
749
750 wxPoint marker = view.ToScreen( m_gamma.x, m_gamma.y );
751
752 return std::abs( aPoint.x - marker.x ) <= DRAG_MARGIN && std::abs( aPoint.y - marker.y ) <= DRAG_MARGIN;
753}
754
755
756void SMITH_CURSOR::Plot( wxDC& aDC, mpWindow& aWindow )
757{
758 if( !m_window )
759 m_window = &aWindow;
760
761 if( !m_visible )
762 return;
763
764 SMITH_VIEW view;
765
766 if( !smithView( aWindow, view ) )
767 return;
768
769 const std::vector<double>& re = m_trace->GetDataX();
770 const std::vector<double>& im = m_trace->GetDataY();
771 size_t count = std::min( re.size(), im.size() );
772
773 if( count == 0 )
774 return;
775
776 if( m_pendingFreq )
777 {
778 // sim data has arrived, restore the frequency saved from the workbook
780 m_pendingFreq = false;
781 m_updateRequired = false;
782 m_updateRef = true;
783 }
784 else if( m_updateRequired )
785 {
786 if( m_dragging )
787 {
788 // snap to the locus sample closest to the drag position
789 int best = -1;
790 double bestDist = std::numeric_limits<double>::max();
791
792 for( size_t ii = 0; ii < count; ii++ )
793 {
794 if( !std::isfinite( re[ii] ) || !std::isfinite( im[ii] ) )
795 continue;
796
797 wxPoint p = view.ToScreen( re[ii], im[ii] );
798 double dx = (double) p.x - m_dim.x;
799 double dy = (double) p.y - m_dim.y;
800 double dist = dx * dx + dy * dy;
801
802 if( dist < bestDist )
803 {
804 bestDist = dist;
805 best = (int) ii;
806 }
807 }
808
809 if( best >= 0 )
810 {
811 snapToIndex( best );
813 }
814
815 m_dragging = false;
816 }
817 else
818 {
819 // the trace data changed under the cursor, follow the frequency rather than
820 // the screen position so a re-run cannot hop to another point of the locus
822 }
823
824 m_updateRequired = false;
825
826 // Notify the parent window about the changes
827 wxQueueEvent( aWindow.GetParent(), new wxCommandEvent( EVT_SIM_CURSOR_UPDATE ) );
828 }
829 else
830 {
831 if( m_index < 0 )
832 {
833 snapToIndex( (int) count / 2 );
835 }
836
837 m_updateRef = true;
838 }
839
840 wxPoint marker = view.ToScreen( m_gamma.x, m_gamma.y );
841
842 m_dim.SetX( marker.x );
843 m_dim.SetY( marker.y );
844
845 if( m_updateRef )
846 {
848 m_updateRef = false;
849 }
850
851 wxPen pen = GetPen();
852 wxColour fg = aWindow.GetForegroundColour();
853 COLOR4D cursorColor = COLOR4D( m_trace->GetTraceColour() ).Mix( fg, 0.6 );
854
855 pen.SetColour( cursorColor.ToColour() );
856 pen.SetStyle( wxPENSTYLE_SOLID );
857 aDC.SetPen( pen );
858 aDC.SetBrush( *wxTRANSPARENT_BRUSH );
859
860 aDC.DrawCircle( marker, 4 );
861 aDC.DrawLine( marker.x - 8, marker.y, marker.x - 4, marker.y );
862 aDC.DrawLine( marker.x + 4, marker.y, marker.x + 8, marker.y );
863 aDC.DrawLine( marker.x, marker.y - 8, marker.x, marker.y - 4 );
864 aDC.DrawLine( marker.x, marker.y + 4, marker.x, marker.y + 8 );
865
866 double gm = std::hypot( m_gamma.x, m_gamma.y );
867 double z0 = static_cast<SMITH_TRACE*>( m_trace )->GetReferenceImpedance();
868 double freq = m_coords.x;
869 double zr, zi;
870
871 auto formatSI = []( double aValue, const wxString& aUnit ) -> wxString
872 {
873 if( std::isnan( aValue ) )
874 return wxS( "--" );
875
876 int power = 0;
877 wxString suffix;
878
879 getSISuffix( aValue, aUnit, power, suffix );
880
881 double sf = pow( 10.0, power );
882
883 return formatFloat( aValue / sf, 3 ) + wxS( " " ) + suffix;
884 };
885
886 std::vector<wxString> lines;
887
888 lines.push_back( getID() + wxS( ": f = " ) + formatSI( freq, wxS( "Hz" ) ) );
889
890 // ohms need the port impedance, without one only the normalized z is known
891 bool absolute = z0 > 0.0;
892
893 if( !SMITH_MATH::GammaToImpedance( m_gamma.x, m_gamma.y, absolute ? z0 : 1.0, zr, zi ) )
894 {
895 lines.push_back( wxS( "Z = inf" ) );
896 }
897 else if( absolute )
898 {
899 lines.push_back( wxString::Format( wxS( "Z = %s %s j%s" ),
900 formatSI( zr, wxS( "Ω" ) ),
901 zi < 0 ? wxS( "-" ) : wxS( "+" ),
902 formatSI( std::fabs( zi ), wxS( "Ω" ) ) ) );
903
904 // series equivalent of the reactance at the marker frequency
905 if( std::isfinite( freq ) && freq > 0.0 && zi != 0.0 )
906 {
907 if( zi > 0.0 )
908 lines.push_back( wxS( "L = " ) + formatSI( SMITH_MATH::SeriesInductance( zi, freq ), wxS( "H" ) ) );
909 else
910 lines.push_back( wxS( "C = " ) + formatSI( SMITH_MATH::SeriesCapacitance( zi, freq ), wxS( "F" ) ) );
911 }
912 }
913 else
914 {
915 lines.push_back( wxString::Format( wxS( "z = %s %s j%s" ),
916 formatFloat( zr, 3 ),
917 zi < 0 ? wxS( "-" ) : wxS( "+" ),
918 formatFloat( std::fabs( zi ), 3 ) ) );
919 }
920
921 double rl = SMITH_MATH::ReturnLoss( gm );
922 double vswr = SMITH_MATH::VSWR( gm );
923
924 if( std::isfinite( rl ) )
925 lines.push_back( wxString::Format( wxS( "RL = %s dB" ), formatFloat( rl, 1 ) ) );
926 else
927 lines.push_back( wxS( "RL = inf" ) );
928
929 if( std::isfinite( vswr ) )
930 lines.push_back( wxString::Format( wxS( "VSWR = %s" ), formatFloat( vswr, 2 ) ) );
931 else
932 lines.push_back( wxS( "VSWR = inf" ) );
933
934 aDC.SetFont( GetPlotFont() );
935
936 int boxW = 0;
937 int boxH = 0;
938 int lineH = aDC.GetTextExtent( wxS( "M" ) ).y;
939
940 for( const wxString& line : lines )
941 boxW = std::max( boxW, aDC.GetTextExtent( line ).x );
942
943 boxW += 8;
944 boxH = (int) lines.size() * lineH + 6;
945
946 wxPoint boxPos( marker.x + ( marker.x < aWindow.GetScrX() / 2 ? 12 : -12 - boxW ),
947 marker.y + ( marker.y < aWindow.GetScrY() / 2 ? 12 : -12 - boxH ) );
948
949 boxPos.x = std::clamp( boxPos.x, 0, std::max( 0, aWindow.GetScrX() - boxW ) );
950 boxPos.y = std::clamp( boxPos.y, 0, std::max( 0, aWindow.GetScrY() - boxH ) );
951
952 wxBrush labelBrush( aWindow.GetBackgroundColour() );
953
954 aDC.SetBrush( labelBrush );
955 aDC.DrawRectangle( wxRect( boxPos, wxSize( boxW, boxH ) ) );
956 aDC.SetTextForeground( cursorColor.ToColour() );
957
958 for( size_t ii = 0; ii < lines.size(); ii++ )
959 aDC.DrawText( lines[ii], boxPos.x + 4, boxPos.y + 3 + (int) ii * lineH );
960}
961
962
963void CURSOR::SetCoordX( double aValue )
964{
965 wxRealPoint oldCoords = m_coords;
966
967 doSetCoordX( aValue );
968 m_updateRequired = false;
969 m_updateRef = true;
970
971 if( m_window )
972 {
973 wxRealPoint delta = m_coords - oldCoords;
974 mpInfoLayer::Move( wxPoint( m_window->x2p( m_trace->x2s( delta.x ) ),
975 m_window->y2p( m_trace->y2s( delta.y ) ) ) );
976
977 m_window->Refresh();
978 }
979}
980
981
982void CURSOR::Move( wxPoint aDelta )
983{
984 Update();
985
986 if( m_trace->IsMultiRun() && m_window
987 && m_trace->GetSweepCount() > 1
988 && m_trace->GetSweepSize() != std::numeric_limits<size_t>::max() )
989 {
990 int newY = m_reference.y + aDelta.y;
991
992 double plotY = m_window->p2y( newY );
993 m_snapTargetY = m_trace->s2y( plotY );
994 m_snapToNearest = true;
995 }
996
997 mpInfoLayer::Move( aDelta );
998}
999
1000
1001bool CURSOR::OnDoubleClick( const wxPoint& aPoint, mpWindow& aWindow )
1002{
1003 if( !Inside( aPoint ) )
1004 return false;
1005
1006 if( !m_trace->IsMultiRun() )
1007 return false;
1008
1009 int sweepCount = m_trace->GetSweepCount();
1010 size_t sweepSize = m_trace->GetSweepSize();
1011
1012 if( sweepCount <= 1 )
1013 return false;
1014
1015 if( sweepSize == std::numeric_limits<size_t>::max() || sweepSize == 0 )
1016 return false;
1017
1018 if( m_sweepIndex < 0 || m_sweepIndex >= sweepCount )
1019 m_sweepIndex = 0;
1020
1021 m_sweepIndex = ( m_sweepIndex + 1 ) % sweepCount;
1022
1023 Update();
1024 m_updateRef = true;
1025 m_window = &aWindow;
1026 aWindow.Refresh();
1027
1028 return true;
1029}
1030
1031
1032void CURSOR::doSetCoordX( double aValue )
1033{
1034 m_coords.x = aValue;
1035
1036 const std::vector<double>& dataX = m_trace->GetDataX();
1037 const std::vector<double>& dataY = m_trace->GetDataY();
1038
1039 if( dataX.size() <= 1 )
1040 return;
1041
1042 bool snapToNearest = m_snapToNearest;
1043 double snapTargetY = m_snapTargetY;
1044 m_snapToNearest = false;
1045
1046 size_t startIdx = 0;
1047 size_t endIdx = dataX.size();
1048 int sweepCount = m_trace->GetSweepCount();
1049 size_t sweepSize = m_trace->GetSweepSize();
1050
1051 if( snapToNearest && m_trace->IsMultiRun()
1052 && sweepCount > 1
1053 && sweepSize != std::numeric_limits<size_t>::max()
1054 && sweepSize > 0
1055 && std::isfinite( snapTargetY ) )
1056 {
1057 double bestDistance = std::numeric_limits<double>::infinity();
1058 int bestSweep = m_sweepIndex;
1059 bool found = false;
1060
1061 for( int sweepIdx = 0; sweepIdx < sweepCount; ++sweepIdx )
1062 {
1063 size_t candidateStart = static_cast<size_t>( sweepIdx ) * sweepSize;
1064 size_t candidateEnd = std::min( dataX.size(), candidateStart + sweepSize );
1065
1066 if( candidateStart >= candidateEnd )
1067 continue;
1068
1069 auto candidateBegin = dataX.begin() + candidateStart;
1070 auto candidateEndIt = dataX.begin() + candidateEnd;
1071 auto candidateMaxIt = std::upper_bound( candidateBegin, candidateEndIt, m_coords.x );
1072 int candidateMaxIdx = candidateMaxIt - dataX.begin();
1073 int candidateMinIdx = candidateMaxIdx - 1;
1074
1075 if( candidateMinIdx < (int) candidateStart
1076 || candidateMaxIdx >= (int) candidateEnd
1077 || candidateMaxIdx >= (int) dataX.size() )
1078 {
1079 continue;
1080 }
1081
1082 double leftX = dataX[candidateMinIdx];
1083 double rightX = dataX[candidateMaxIdx];
1084
1085 if( leftX == rightX )
1086 continue;
1087
1088 double leftY = dataY[candidateMinIdx];
1089 double rightY = dataY[candidateMaxIdx];
1090 double value = leftY + ( rightY - leftY ) / ( rightX - leftX ) * ( m_coords.x - leftX );
1091 double distance = std::fabs( value - snapTargetY );
1092
1093 if( distance < bestDistance )
1094 {
1095 bestDistance = distance;
1096 bestSweep = sweepIdx;
1097 found = true;
1098 }
1099 }
1100
1101 if( found )
1102 m_sweepIndex = bestSweep;
1103 }
1104
1105 if( m_trace->IsMultiRun()
1106 && sweepCount > 1
1107 && sweepSize != std::numeric_limits<size_t>::max()
1108 && sweepSize > 0 )
1109 {
1110 size_t available = static_cast<size_t>( sweepCount ) * sweepSize;
1111
1112 if( available <= dataX.size() )
1113 {
1114 if( m_sweepIndex < 0 || m_sweepIndex >= sweepCount )
1115 m_sweepIndex = std::max( sweepCount - 1, 0 );
1116
1117 startIdx = static_cast<size_t>( m_sweepIndex ) * sweepSize;
1118 endIdx = std::min( dataX.size(), startIdx + sweepSize );
1119 }
1120 else
1121 {
1122 m_sweepIndex = 0;
1123 }
1124 }
1125 else
1126 {
1127 m_sweepIndex = 0;
1128 }
1129
1130 if( startIdx >= endIdx )
1131 {
1132 m_coords.y = NAN;
1133 return;
1134 }
1135
1136 auto beginIt = dataX.begin() + startIdx;
1137 auto endIt = dataX.begin() + endIdx;
1138
1139 // Find the closest point coordinates
1140 auto maxXIt = std::upper_bound( beginIt, endIt, m_coords.x );
1141 int maxIdx = maxXIt - dataX.begin();
1142 int minIdx = maxIdx - 1;
1143
1144 // Out of bounds checks
1145 if( minIdx < (int) startIdx || maxIdx >= (int) endIdx || maxIdx >= (int) dataX.size() )
1146 {
1147 // Simulation may not be complete yet, or we may have a cursor off the beginning or end
1148 // of the data. Either way, that's where the user put it. Don't second guess them; just
1149 // leave its y value undefined.
1150 m_coords.y = NAN;
1151 return;
1152 }
1153
1154 const double leftX = dataX[minIdx];
1155 const double rightX = dataX[maxIdx];
1156 const double leftY = dataY[minIdx];
1157 const double rightY = dataY[maxIdx];
1158
1159 // Linear interpolation
1160 m_coords.y = leftY + ( rightY - leftY ) / ( rightX - leftX ) * ( m_coords.x - leftX );
1161}
1162
1163
1165{
1166 for( const auto& [ id, cursor ] : m_trace->GetCursors() )
1167 {
1168 if( cursor == this )
1169 return wxString::Format( _( "%d" ), id );
1170 }
1171
1172 return wxEmptyString;
1173}
1174
1175
1176void CURSOR::Plot( wxDC& aDC, mpWindow& aWindow )
1177{
1178 if( !m_window )
1179 m_window = &aWindow;
1180
1181 if( !m_visible || m_trace->GetDataX().size() <= 1 )
1182 return;
1183
1184 if( m_updateRequired )
1185 {
1186 doSetCoordX( m_trace->s2x( aWindow.p2x( m_dim.x ) ) );
1187 m_updateRequired = false;
1188
1189 // Notify the parent window about the changes
1190 wxQueueEvent( aWindow.GetParent(), new wxCommandEvent( EVT_SIM_CURSOR_UPDATE ) );
1191 }
1192 else
1193 {
1194 m_updateRef = true;
1195 }
1196
1197 if( m_updateRef )
1198 {
1200 m_updateRef = false;
1201 }
1202
1203 if( !std::isfinite( m_coords.x ) )
1204 return;
1205
1206 // A silent trace interpolates to no y value at all, and converting that to a pixel is
1207 // undefined behaviour, so carry the x cursor on its own
1208 const bool hasY = std::isfinite( m_coords.y );
1209
1210 // Line length in horizontal and vertical dimensions
1211 const wxPoint cursorPos( aWindow.x2p( m_trace->x2s( m_coords.x ) ),
1212 hasY ? aWindow.y2p( m_trace->y2s( m_coords.y ) ) : 0 );
1213
1214 wxCoord leftPx = aWindow.GetMarginLeft();
1215 wxCoord rightPx = aWindow.GetScrX() - aWindow.GetMarginRight();
1216 wxCoord topPx = aWindow.GetMarginTop();
1217 wxCoord bottomPx = aWindow.GetScrY() - aWindow.GetMarginBottom();
1218
1219 wxPen pen = GetPen();
1220 wxColour fg = aWindow.GetForegroundColour();
1221 COLOR4D cursorColor = COLOR4D( m_trace->GetTraceColour() ).Mix( fg, 0.6 );
1222 COLOR4D textColor = fg;
1223
1224 if( cursorColor.Distance( textColor ) < 0.66 )
1225 textColor.Invert();
1226
1227 pen.SetColour( cursorColor.ToColour() );
1228 pen.SetStyle( m_continuous ? wxPENSTYLE_SOLID : wxPENSTYLE_LONG_DASH );
1229 aDC.SetPen( pen );
1230
1231 if( hasY && topPx < cursorPos.y && cursorPos.y < bottomPx )
1232 aDC.DrawLine( leftPx, cursorPos.y, rightPx, cursorPos.y );
1233
1234 if( leftPx < cursorPos.x && cursorPos.x < rightPx )
1235 {
1236 aDC.DrawLine( cursorPos.x, topPx, cursorPos.x, bottomPx );
1237
1238 wxString id = getID();
1239 wxSize size = aDC.GetTextExtent( wxS( "M" ) );
1240 wxRect textRect( wxPoint( cursorPos.x + 1 - size.x / 2, topPx - 4 - size.y ), size );
1241 wxBrush brush;
1242 wxPoint poly[3];
1243
1244 // Because a "1" looks off-center if it's actually centred.
1245 if( id == "1" )
1246 textRect.x -= 1;
1247
1248 // We want an equalateral triangle, so use size.y for both axes.
1249 size.y += 3;
1250 // Make sure it's an even number so the slopes of the sides will be identical.
1251 size.y = ( size.y / 2 ) * 2;
1252 poly[0] = { cursorPos.x - 1 - size.y / 2, topPx - size.y };
1253 poly[1] = { cursorPos.x + 1 + size.y / 2, topPx - size.y };
1254 poly[2] = { cursorPos.x, topPx };
1255
1256 brush.SetStyle( wxBRUSHSTYLE_SOLID );
1257 brush.SetColour( m_trace->GetTraceColour() );
1258 aDC.SetBrush( brush );
1259 aDC.DrawPolygon( 3, poly );
1260
1261 aDC.SetTextForeground( textColor.ToColour() );
1262 aDC.DrawLabel( id, textRect, wxALIGN_CENTER_HORIZONTAL | wxALIGN_CENTER_VERTICAL );
1263
1264 if( m_trace->IsMultiRun() && m_trace->GetSweepCount() > 1
1265 && m_trace->GetSweepSize() != std::numeric_limits<size_t>::max() )
1266 {
1267 wxString runLabel;
1268 const std::vector<wxString>& labels = m_trace->GetMultiRunLabels();
1269
1270 if( m_sweepIndex >= 0 && m_sweepIndex < (int) labels.size() )
1271 {
1272 runLabel = labels[m_sweepIndex];
1273 }
1274 else
1275 {
1276 runLabel = wxString::Format( _( "Run %d" ), m_sweepIndex + 1 );
1277 }
1278
1279 wxSize runSize = aDC.GetTextExtent( runLabel );
1280 int runX = textRect.GetRight() + 6;
1281 wxRect runRect( wxPoint( runX, textRect.y ), runSize );
1282
1283 runRect.Inflate( 3, 1 );
1284
1285 wxBrush labelBrush( aWindow.GetBackgroundColour() );
1286 wxPen labelPen( cursorColor.ToColour() );
1287
1288 aDC.SetPen( labelPen );
1289 aDC.SetBrush( labelBrush );
1290 aDC.DrawRectangle( runRect );
1291 aDC.SetTextForeground( cursorColor.ToColour() );
1292 aDC.DrawLabel( runLabel, runRect, wxALIGN_CENTER_HORIZONTAL | wxALIGN_CENTER_VERTICAL );
1293 }
1294 }
1295}
1296
1297
1298bool CURSOR::Inside( const wxPoint& aPoint ) const
1299{
1300 if( !m_window || !m_trace )
1301 return false;
1302
1303 // An undefined coordinate draws no line, so it offers nothing to grab
1304 bool nearX = std::isfinite( m_coords.x )
1305 && std::abs( (double) aPoint.x - m_window->x2p( m_trace->x2s( m_coords.x ) ) ) <= DRAG_MARGIN;
1306 bool nearY = std::isfinite( m_coords.y )
1307 && std::abs( (double) aPoint.y - m_window->y2p( m_trace->y2s( m_coords.y ) ) ) <= DRAG_MARGIN;
1308
1309 return nearX || nearY;
1310}
1311
1312
1314{
1315 if( !m_window )
1316 return;
1317
1318 // An undefined coordinate has no pixel, so keep the last good reference for a drag to
1319 // measure against
1320 if( std::isfinite( m_coords.x ) )
1321 m_reference.x = m_window->x2p( m_trace->x2s( m_coords.x ) );
1322
1323 if( std::isfinite( m_coords.y ) )
1324 m_reference.y = m_window->y2p( m_trace->y2s( m_coords.y ) );
1325}
1326
1327
1328SIM_VIEW::SIM_VIEW( SIM_PLOT_TAB* aPlotTab, wxWindow* aParent ) :
1329 mpWindow( aParent, wxID_ANY ),
1330 m_axis_x( nullptr ),
1331 m_axis_y1( nullptr ),
1332 m_axis_y2( nullptr ),
1333 m_axis_y3( nullptr ),
1334 m_legend( nullptr ),
1335 m_plotTab( aPlotTab ),
1336 m_smithGrid( nullptr ),
1337 m_smithChart( false ),
1338 m_smithZoom( 1.0 ),
1339 m_smithPanning( false ),
1340 m_smithLeftSkipped( false )
1341{
1342 // Smith-mode pan/zoom, these run before mpWindow's handlers and skip when not a Smith chart
1343 Bind( wxEVT_MOUSEWHEEL, &SIM_VIEW::onSmithMouseWheel, this );
1344 Bind( wxEVT_MAGNIFY, &SIM_VIEW::onSmithMagnify, this );
1345 Bind( wxEVT_MIDDLE_DOWN, &SIM_VIEW::onSmithMiddleDown, this );
1346 Bind( wxEVT_LEFT_DOWN, &SIM_VIEW::onSmithLeftDown, this );
1347 Bind( wxEVT_MOTION, &SIM_VIEW::onSmithMotion, this );
1348 Bind( wxEVT_LEFT_UP, &SIM_VIEW::onSmithLeftUp, this );
1349 Bind( wxEVT_LEFT_DCLICK, &SIM_VIEW::onSmithDClick, this );
1350 Bind( wxEVT_RIGHT_DOWN, &SIM_VIEW::onSmithRightDown, this );
1351 Bind( wxEVT_RIGHT_UP, &SIM_VIEW::onSmithRightUp, this );
1352
1353 // route the context-menu zoom commands to the Smith view
1354 for( int id : { mpID_ZOOM_IN, mpID_ZOOM_OUT, mpID_FIT, mpID_CENTER } )
1355 Bind( wxEVT_MENU, &SIM_VIEW::onSmithMenuCommand, this, id );
1356}
1357
1358
1360{
1361 if( m_plotTab )
1362 m_plotTab->SyncXView( this );
1363}
1364
1365
1366void SIM_VIEW::SetY1Scale( bool aLock, double aMin, double aMax )
1367{
1368 wxCHECK( m_axis_y1, /* void */ );
1369 m_axis_y1->SetAxisMinMax( aLock, aMin, aMax );
1370}
1371
1372
1373void SIM_VIEW::SetY2Scale( bool aLock, double aMin, double aMax )
1374{
1375 wxCHECK( m_axis_y2, /* void */ );
1376 m_axis_y2->SetAxisMinMax( aLock, aMin, aMax );
1377}
1378
1379
1380void SIM_VIEW::SetY3Scale( bool aLock, double aMin, double aMax )
1381{
1382 wxCHECK( m_axis_y3, /* void */ );
1383 m_axis_y3->SetAxisMinMax( aLock, aMin, aMax );
1384}
1385
1386
1387wxString SIM_VIEW::GetUnitsX() const
1388{
1389 LOG_SCALE<mpScaleXLog>* logScale = dynamic_cast<LOG_SCALE<mpScaleXLog>*>( m_axis_x );
1390 LIN_SCALE<mpScaleX>* linScale = dynamic_cast<LIN_SCALE<mpScaleX>*>( m_axis_x );
1391
1392 if( logScale )
1393 return logScale->GetUnits();
1394 else if( linScale )
1395 return linScale->GetUnits();
1396 else
1397 return wxEmptyString;
1398}
1399
1400
1401wxString SIM_VIEW::GetUnitsY1() const
1402{
1403 LIN_SCALE<mpScaleY>* linScale = dynamic_cast<LIN_SCALE<mpScaleY>*>( m_axis_y1 );
1404
1405 if( linScale )
1406 return linScale->GetUnits();
1407 else
1408 return wxEmptyString;
1409}
1410
1411
1412wxString SIM_VIEW::GetUnitsY2() const
1413{
1414 LIN_SCALE<mpScaleY>* linScale = dynamic_cast<LIN_SCALE<mpScaleY>*>( m_axis_y2 );
1415
1416 if( linScale )
1417 return linScale->GetUnits();
1418 else
1419 return wxEmptyString;
1420}
1421
1422
1423wxString SIM_VIEW::GetUnitsY3() const
1424{
1425 LIN_SCALE<mpScaleY>* linScale = dynamic_cast<LIN_SCALE<mpScaleY>*>( m_axis_y3 );
1426
1427 if( linScale )
1428 return linScale->GetUnits();
1429 else
1430 return wxEmptyString;
1431}
1432
1433
1434wxString SIM_VIEW::GetUnitsForTrace( TRACE* aTrace ) const
1435{
1436 if( m_plotTab->GetSimType() == ST_AC )
1437 {
1438 if( aTrace->GetType() & SPT_AC_PHASE )
1439 return GetUnitsY2();
1440 else
1441 return GetUnitsY1();
1442 }
1443 else
1444 {
1445 if( aTrace->GetType() & SPT_POWER )
1446 return GetUnitsY3();
1447 else if( aTrace->GetType() & SPT_CURRENT )
1448 return GetUnitsY2();
1449 else
1450 return GetUnitsY1();
1451 }
1452}
1453
1454
1455int SIM_VIEW::GetAxisSlot( TRACE* aTrace ) const
1456{
1457 if( ( aTrace->GetType() & SPT_AC_PHASE )
1458 || ( ( m_plotTab->GetSimType() != ST_AC ) && ( aTrace->GetType() & SPT_CURRENT ) ) )
1459 {
1460 return 2;
1461 }
1462 else if( aTrace->GetType() & SPT_POWER )
1463 {
1464 return 3;
1465 }
1466 else
1467 {
1468 return 1;
1469 }
1470}
1471
1472
1474{
1475 switch( aSlot )
1476 {
1477 case 2: return m_axis_y2;
1478 case 3: return m_axis_y3;
1479 default: return m_axis_y1;
1480 }
1481}
1482
1483
1484void SIM_VIEW::updateAxes( int aNewTraceType )
1485{
1486 switch( m_plotTab->GetSimType() )
1487 {
1488 case ST_AC:
1489 if( !m_axis_x )
1490 {
1491 m_axis_x = new LOG_SCALE<mpScaleXLog>( wxEmptyString, wxT( "Hz" ), mpALIGN_BOTTOM );
1492 m_axis_x->SetNameAlign( mpALIGN_BOTTOM );
1493 AddLayer( m_axis_x );
1494
1495 m_axis_y1 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "dB" ), mpALIGN_LEFT );
1496 m_axis_y1->SetNameAlign( mpALIGN_LEFT );
1498
1499 m_axis_y2 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "°" ), mpALIGN_RIGHT );
1500 m_axis_y2->SetNameAlign( mpALIGN_RIGHT );
1501 m_axis_y2->SetMasterScale( m_axis_y1 );
1503 }
1504
1505 m_axis_x->SetName( _( "Frequency" ) );
1506 m_axis_y1->SetName( _( "Gain" ) );
1507 m_axis_y2->SetName( _( "Phase" ) );
1508 break;
1509
1510 case ST_SP:
1511 if( !m_axis_x )
1512 {
1513 m_axis_x = new LOG_SCALE<mpScaleXLog>( wxEmptyString, wxT( "Hz" ), mpALIGN_BOTTOM );
1514 m_axis_x->SetNameAlign( mpALIGN_BOTTOM );
1515 AddLayer( m_axis_x );
1516
1517 m_axis_y1 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "" ), mpALIGN_LEFT );
1518 m_axis_y1->SetNameAlign( mpALIGN_LEFT );
1520
1521 m_axis_y2 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "°" ), mpALIGN_RIGHT );
1522 m_axis_y2->SetNameAlign( mpALIGN_RIGHT );
1523 m_axis_y2->SetMasterScale( m_axis_y1 );
1525 }
1526
1527 m_axis_x->SetName( _( "Frequency" ) );
1528 m_axis_y1->SetName( _( "Amplitude" ) );
1529 m_axis_y2->SetName( _( "Phase" ) );
1530 break;
1531
1532 case ST_DC: prepareDCAxes( aNewTraceType ); break;
1533
1534 case ST_NOISE:
1535 if( !m_axis_x )
1536 {
1537 m_axis_x = new LOG_SCALE<mpScaleXLog>( wxEmptyString, wxT( "Hz" ), mpALIGN_BOTTOM );
1538 m_axis_x->SetNameAlign( mpALIGN_BOTTOM );
1539 AddLayer( m_axis_x );
1540
1541 if( ( aNewTraceType & SPT_CURRENT ) == 0 )
1542 {
1543 m_axis_y1 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "" ), mpALIGN_LEFT );
1544 m_axis_y1->SetNameAlign( mpALIGN_LEFT );
1546 }
1547 else
1548 {
1549 m_axis_y2 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "" ), mpALIGN_RIGHT );
1550 m_axis_y2->SetNameAlign( mpALIGN_RIGHT );
1552 }
1553 }
1554
1555 m_axis_x->SetName( _( "Frequency" ) );
1556
1557 if( m_axis_y1 )
1558 m_axis_y1->SetName( _( "Noise (V/√Hz)" ) );
1559
1560 if( m_axis_y2 )
1561 m_axis_y2->SetName( _( "Noise (A/√Hz)" ) );
1562
1563 break;
1564
1565 case ST_FFT:
1566 if( !m_axis_x )
1567 {
1568 m_axis_x = new LOG_SCALE<mpScaleXLog>( wxEmptyString, wxT( "Hz" ), mpALIGN_BOTTOM );
1569 m_axis_x->SetNameAlign( mpALIGN_BOTTOM );
1570 AddLayer( m_axis_x );
1571
1572 m_axis_y1 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "dB" ), mpALIGN_LEFT );
1573 m_axis_y1->SetNameAlign( mpALIGN_LEFT );
1575 }
1576
1577 m_axis_x->SetName( _( "Frequency" ) );
1578 m_axis_y1->SetName( _( "Intensity" ) );
1579 break;
1580
1581 case ST_TRAN:
1582 if( !m_axis_x )
1583 {
1584 m_axis_x = new TIME_SCALE( wxEmptyString, wxT( "s" ), mpALIGN_BOTTOM );
1585 m_axis_x->SetNameAlign( mpALIGN_BOTTOM );
1586 AddLayer( m_axis_x );
1587
1588 m_axis_y1 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "V" ), mpALIGN_LEFT );
1589 m_axis_y1->SetNameAlign( mpALIGN_LEFT );
1591
1592 m_axis_y2 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "A" ), mpALIGN_RIGHT );
1593 m_axis_y2->SetNameAlign( mpALIGN_RIGHT );
1594 m_axis_y2->SetMasterScale( m_axis_y1 );
1596 }
1597
1598 m_axis_x->SetName( _( "Time" ) );
1599 m_axis_y1->SetName( _( "Voltage" ) );
1600 m_axis_y2->SetName( _( "Current" ) );
1601
1602 if( aNewTraceType & SPT_POWER )
1604
1605 if( m_axis_y3 )
1606 m_axis_y3->SetName( _( "Power" ) );
1607
1608 break;
1609
1610 default:
1611 // suppress warnings
1612 break;
1613 }
1614
1615 if( m_plotTab->GetSimType() == ST_TRAN || m_plotTab->GetSimType() == ST_DC )
1616 {
1617 if( m_axis_y3 )
1618 {
1619 SetMargins( 30, 160, 45, 70 );
1620
1621 if( m_axis_y2 )
1622 m_axis_y2->SetNameAlign( mpALIGN_BORDER_RIGHT );
1623
1624 m_axis_y3->SetAlign( mpALIGN_BORDER_RIGHT );
1625 m_axis_y3->SetNameAlign( mpALIGN_BORDER_RIGHT );
1626 }
1627 else
1628 {
1629 SetMargins( 30, 70, 45, 70 );
1630
1631 if( m_axis_y2 )
1632 m_axis_y2->SetNameAlign( mpALIGN_RIGHT );
1633 }
1634 }
1635
1636 if( m_axis_x )
1637 m_axis_x->SetFont( KIUI::GetStatusFont( this ) );
1638
1639 if( m_axis_y1 )
1640 m_axis_y1->SetFont( KIUI::GetStatusFont( this ) );
1641
1642 if( m_axis_y2 )
1643 m_axis_y2->SetFont( KIUI::GetStatusFont( this ) );
1644
1645 if( m_axis_y3 )
1646 m_axis_y3->SetFont( KIUI::GetStatusFont( this ) );
1647
1649}
1650
1651
1652void SIM_VIEW::prepareDCAxes( int aNewTraceType )
1653{
1654 wxString sim_cmd = m_plotTab->GetSimCommand().Lower();
1655 wxString rem;
1656
1657 if( sim_cmd.StartsWith( ".dc", &rem ) )
1658 {
1659 wxChar ch = 0;
1660
1661 rem.Trim( false );
1662
1663 try
1664 {
1665 ch = rem.GetChar( 0 );
1666 }
1667 catch( ... )
1668 {
1669 // Best efforts
1670 }
1671
1672 switch( ch )
1673 {
1674 // Make sure that we have a reliable default (even if incorrectly labeled)
1675 default:
1676 case 'v':
1677 if( !m_axis_x )
1678 {
1679 m_axis_x = new LIN_SCALE<mpScaleX>( wxEmptyString, wxT( "V" ), mpALIGN_BOTTOM );
1680 m_axis_x->SetNameAlign( mpALIGN_BOTTOM );
1681 AddLayer( m_axis_x );
1682 }
1683
1684 m_axis_x->SetName( _( "Voltage (swept)" ) );
1685 break;
1686
1687 case 'i':
1688 if( !m_axis_x )
1689 {
1690 m_axis_x = new LIN_SCALE<mpScaleX>( wxEmptyString, wxT( "A" ), mpALIGN_BOTTOM );
1691 m_axis_x->SetNameAlign( mpALIGN_BOTTOM );
1692 AddLayer( m_axis_x );
1693 }
1694
1695 m_axis_x->SetName( _( "Current (swept)" ) );
1696 break;
1697
1698 case 'r':
1699 if( !m_axis_x )
1700 {
1701 m_axis_x = new LIN_SCALE<mpScaleX>( wxEmptyString, wxT( "Ω" ), mpALIGN_BOTTOM );
1702 m_axis_x->SetNameAlign( mpALIGN_BOTTOM );
1703 AddLayer( m_axis_x );
1704 }
1705
1706 m_axis_x->SetName( _( "Resistance (swept)" ) );
1707 break;
1708
1709 case 't':
1710 if( !m_axis_x )
1711 {
1712 m_axis_x = new LIN_SCALE<mpScaleX>( wxEmptyString, wxT( "°C" ), mpALIGN_BOTTOM );
1713 m_axis_x->SetNameAlign( mpALIGN_BOTTOM );
1714 AddLayer( m_axis_x );
1715 }
1716
1717 m_axis_x->SetName( _( "Temperature (swept)" ) );
1718 break;
1719 }
1720
1721 if( !m_axis_y1 )
1722 {
1723 m_axis_y1 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "V" ), mpALIGN_LEFT );
1724 m_axis_y1->SetNameAlign( mpALIGN_LEFT );
1726 }
1727
1728 if( !m_axis_y2 )
1729 {
1730 m_axis_y2 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "A" ), mpALIGN_RIGHT );
1731 m_axis_y2->SetNameAlign( mpALIGN_RIGHT );
1733 }
1734
1735 m_axis_y1->SetName( _( "Voltage (measured)" ) );
1736 m_axis_y2->SetName( _( "Current" ) );
1737
1738 if( ( aNewTraceType & SPT_POWER ) )
1740
1741 if( m_axis_y3 )
1742 m_axis_y3->SetName( _( "Power" ) );
1743 }
1744}
1745
1746
1748{
1749 if( !m_axis_y3 )
1750 {
1751 SetMargins( 30, 160, 45, 70 );
1752 m_axis_y3 = new LIN_SCALE<mpScaleY>( wxEmptyString, wxT( "W" ), mpALIGN_BORDER_RIGHT );
1753 m_axis_y3->SetNameAlign( mpALIGN_BORDER_RIGHT );
1754 m_axis_y3->SetMasterScale( m_axis_y1 );
1756 }
1757
1758 if( m_axis_y3 )
1759 {
1760 m_axis_y3->SetAlign( mpALIGN_BORDER_RIGHT );
1761 m_axis_y3->SetNameAlign( mpALIGN_BORDER_RIGHT );
1762 }
1763
1764 if( m_axis_y2 )
1765 m_axis_y2->SetNameAlign( mpALIGN_BORDER_RIGHT );
1766}
1767
1768
1770{
1771 bool hasY1Traces = false;
1772 bool hasY2Traces = false;
1773 bool hasY3Traces = false;
1774
1775 if( !m_smithChart )
1776 {
1777 for( const auto& [name, trace] : m_plotTab->GetTraces() )
1778 {
1779 if( !trace || trace->GetView() != this )
1780 continue;
1781
1782 if( trace->GetType() & SPT_POWER )
1783 {
1784 hasY3Traces = true;
1785 }
1786 else if( ( trace->GetType() & SPT_AC_PHASE )
1787 || ( ( m_plotTab->GetSimType() != ST_AC ) && ( trace->GetType() & SPT_CURRENT ) ) )
1788 {
1789 hasY2Traces = true;
1790 }
1791 else
1792 {
1793 hasY1Traces = true;
1794 }
1795 }
1796 }
1797
1798 bool visibilityChanged = false;
1799
1800 if( m_axis_x && m_axis_x->IsVisible() != !m_smithChart )
1801 {
1802 m_axis_x->SetVisible( !m_smithChart );
1803 visibilityChanged = true;
1804 }
1805
1806 if( m_axis_y1 && m_axis_y1->IsVisible() != hasY1Traces )
1807 {
1808 m_axis_y1->SetVisible( hasY1Traces );
1809 visibilityChanged = true;
1810 }
1811
1812 if( m_axis_y2 && m_axis_y2->IsVisible() != hasY2Traces )
1813 {
1814 m_axis_y2->SetVisible( hasY2Traces );
1815 visibilityChanged = true;
1816 }
1817
1818 if( m_axis_y3 && m_axis_y3->IsVisible() != hasY3Traces )
1819 {
1820 m_axis_y3->SetVisible( hasY3Traces );
1821 visibilityChanged = true;
1822 }
1823
1824 if( visibilityChanged )
1825 UpdateAll();
1826}
1827
1828
1829void SIM_VIEW::ResetScales( bool aIncludeX )
1830{
1831 if( m_axis_x && aIncludeX )
1832 {
1833 m_axis_x->ResetDataRange();
1834
1835 if( m_plotTab->GetSimType() == ST_TRAN )
1836 {
1837 wxStringTokenizer tokenizer( m_plotTab->GetSimCommand(), " \t\r\n", wxTOKEN_STRTOK );
1838 wxString cmd = tokenizer.GetNextToken().Lower();
1839
1840 wxASSERT( cmd == wxS( ".tran" ) );
1841
1842 SPICE_VALUE step;
1843 SPICE_VALUE end( 1.0 );
1844 SPICE_VALUE start( 0.0 );
1845
1846 if( tokenizer.HasMoreTokens() )
1847 step = SPICE_VALUE( tokenizer.GetNextToken() );
1848
1849 if( tokenizer.HasMoreTokens() )
1850 end = SPICE_VALUE( tokenizer.GetNextToken() );
1851
1852 if( tokenizer.HasMoreTokens() )
1853 start = SPICE_VALUE( tokenizer.GetNextToken() );
1854
1855 static_cast<TIME_SCALE*>( m_axis_x )->SetStartAndEnd( start.ToDouble(), end.ToDouble() );
1856 }
1857 }
1858
1859 if( m_axis_y1 )
1860 m_axis_y1->ResetDataRange();
1861
1862 if( m_axis_y2 )
1863 m_axis_y2->ResetDataRange();
1864
1865 if( m_axis_y3 )
1866 m_axis_y3->ResetDataRange();
1867
1868 for( auto& [name, trace] : m_plotTab->GetTraces() )
1869 {
1870 if( trace->GetView() == this )
1871 trace->UpdateScales();
1872 }
1873}
1874
1875
1876void SIM_VIEW::SetSmithChart( bool aEnable )
1877{
1878 if( m_smithChart == aEnable )
1879 return;
1880
1881 m_smithChart = aEnable;
1882
1883 // a mode switch mid-gesture must not leave a pan or a skipped click behind
1884 m_smithPanning = false;
1885 m_smithLeftSkipped = false;
1886
1887 if( aEnable && !m_smithGrid )
1888 {
1889 m_smithGrid = new SMITH_GRID();
1890 m_smithGrid->SetFont( KIUI::GetStatusFont( this ) );
1891 m_smithGrid->SetPen( wxPen( m_plotTab->GetPlotColor( SIM_PLOT_COLORS::COLOR_SET::AXIS ), 1 ) );
1893 }
1894
1895 if( m_smithGrid )
1896 m_smithGrid->SetVisible( aEnable );
1897
1898 if( aEnable )
1900
1902
1904 UpdateAll();
1905}
1906
1907
1909{
1910 if( !m_smithGrid )
1911 return;
1912
1913 double z0 = 0.0;
1914 bool mixed = false;
1915 bool unresolved = false;
1916
1917 for( const auto& [name, trace] : m_plotTab->GetTraces() )
1918 {
1919 SMITH_TRACE* smithTrace = dynamic_cast<SMITH_TRACE*>( trace );
1920
1921 if( !smithTrace || smithTrace->GetView() != this )
1922 continue;
1923
1924 double traceZ0 = smithTrace->GetReferenceImpedance();
1925
1926 if( traceZ0 <= 0.0 )
1927 unresolved = true;
1928 else if( z0 == 0.0 )
1929 z0 = traceZ0;
1930 else if( traceZ0 != z0 )
1931 mixed = true;
1932 }
1933
1934 // a trace whose port impedance is unknown leaves no single reference to name either
1935 m_smithGrid->SetReferenceImpedance( mixed || unresolved ? 0.0 : z0 );
1936 m_smithGrid->SetMixedReferences( mixed );
1937}
1938
1939
1941{
1942 if( m_smithGrid )
1943 m_smithGrid->SetPen( wxPen( m_plotTab->GetPlotColor( SIM_PLOT_COLORS::COLOR_SET::AXIS ), 1 ) );
1944}
1945
1946
1948{
1949 return SMITH_GRID::GetChartView( const_cast<SIM_VIEW&>( *this ), m_smithZoom, m_smithPan, aView );
1950}
1951
1952
1953void SIM_VIEW::SmithZoomAt( const wxPoint& aPos, double aFactor )
1954{
1955 SMITH_VIEW view;
1956
1957 if( !getSmithView( view ) )
1958 return;
1959
1960 double newZoom = std::clamp( m_smithZoom * aFactor, 1.0, 50.0 );
1961
1962 if( newZoom <= 1.0 )
1963 {
1965 Refresh();
1966 return;
1967 }
1968
1969 // keep the point under the cursor fixed while zooming
1970 m_smithPan = SMITH_MATH::ZoomAboutPoint( view, aPos, newZoom );
1971 m_smithZoom = newZoom;
1972
1973 Refresh();
1974}
1975
1976
1977void SIM_VIEW::SmithPanBy( const wxPoint& aDelta )
1978{
1979 SMITH_VIEW view;
1980
1981 if( !getSmithView( view ) )
1982 return;
1983
1984 m_smithPan.x -= aDelta.x / view.radius;
1985 m_smithPan.y += aDelta.y / view.radius;
1986
1987 Refresh();
1988}
1989
1990
1991void SIM_VIEW::onSmithMouseWheel( wxMouseEvent& aEvent )
1992{
1993 if( !m_smithChart )
1994 {
1995 aEvent.Skip();
1996 return;
1997 }
1998
1999 // swallow horizontal scroll too, mpWindow would pan its hidden axes with it
2000 if( aEvent.GetWheelAxis() != wxMOUSE_WHEEL_VERTICAL || aEvent.GetWheelRotation() == 0 )
2001 return;
2002
2003 // do not skip, or mpWindow would also zoom its hidden axes
2004 SmithZoomAt( aEvent.GetPosition(), aEvent.GetWheelRotation() > 0 ? 1.2 : 1.0 / 1.2 );
2005}
2006
2007
2008void SIM_VIEW::onSmithMagnify( wxMouseEvent& aEvent )
2009{
2010 if( !m_smithChart )
2011 {
2012 aEvent.Skip();
2013 return;
2014 }
2015
2016 double factor = aEvent.GetMagnification() + 1.0;
2017
2018 if( factor > 0.0 )
2019 SmithZoomAt( aEvent.GetPosition(), factor );
2020}
2021
2022
2023void SIM_VIEW::onSmithMiddleDown( wxMouseEvent& aEvent )
2024{
2025 // keep mpWindow's middle-button pan off the hidden axes
2026 if( !m_smithChart )
2027 aEvent.Skip();
2028}
2029
2030
2031void SIM_VIEW::onSmithLeftDown( wxMouseEvent& aEvent )
2032{
2033 // clear stale pan state so it cannot hijack a cursor grab
2034 m_smithPanning = false;
2035 m_smithLeftSkipped = false;
2036
2037 wxPoint pos = aEvent.GetPosition();
2038
2039 if( m_smithChart && !IsInsideInfoLayer( pos ) )
2040 {
2041 m_smithPanning = true;
2042 m_smithPanLast = pos;
2043 return;
2044 }
2045
2046 // on a cursor or the legend, let mpWindow drag it, and remember that it saw the click
2047 // so the matching motions and release reach it too
2048 m_smithLeftSkipped = true;
2049 aEvent.Skip();
2050}
2051
2052
2053void SIM_VIEW::onSmithMotion( wxMouseEvent& aEvent )
2054{
2055 if( m_smithChart && aEvent.Dragging() )
2056 {
2057 if( aEvent.LeftIsDown() && !m_smithLeftSkipped )
2058 {
2059 // a left drag mpWindow did not see the start of, pan if one is active, and
2060 // swallow either way so mpWindow cannot rubber-band from a stale click point
2061 if( m_smithPanning )
2062 {
2063 wxPoint pos = aEvent.GetPosition();
2064
2065 SmithPanBy( pos - m_smithPanLast );
2066 m_smithPanLast = pos;
2067 }
2068
2069 return;
2070 }
2071
2072 // keep mpWindow's middle-button pan off the hidden axes
2073 if( aEvent.MiddleIsDown() )
2074 return;
2075 }
2076
2077 aEvent.Skip();
2078}
2079
2080
2081void SIM_VIEW::onSmithLeftUp( wxMouseEvent& aEvent )
2082{
2083 if( m_smithChart )
2084 {
2085 m_smithPanning = false;
2086
2087 // a release mpWindow saw no click for would zoom the hidden axes to the rect
2088 // between its stale click point and this position
2089 if( !m_smithLeftSkipped )
2090 return;
2091
2092 m_smithLeftSkipped = false;
2093 }
2094
2095 aEvent.Skip();
2096}
2097
2098
2099void SIM_VIEW::onSmithDClick( wxMouseEvent& aEvent )
2100{
2101 wxPoint pos = aEvent.GetPosition();
2102
2103 if( m_smithChart && !IsInsideInfoLayer( pos ) )
2104 {
2106 Refresh();
2107 return;
2108 }
2109
2110 aEvent.Skip();
2111}
2112
2113
2114void SIM_VIEW::onSmithRightDown( wxMouseEvent& aEvent )
2115{
2116 // remember where the menu opened, for its zoom commands
2117 if( m_smithChart )
2118 m_smithMenuPos = aEvent.GetPosition();
2119
2120 aEvent.Skip();
2121}
2122
2123
2124void SIM_VIEW::onSmithRightUp( wxMouseEvent& aEvent )
2125{
2126 if( !m_smithChart )
2127 {
2128 aEvent.Skip();
2129 return;
2130 }
2131
2132 // no zoom history here, so grey out Undo/Redo and show the menu ourselves
2133 wxMenu* menu = GetPopupMenu();
2134
2135 menu->Enable( mpID_ZOOM_UNDO, false );
2136 menu->Enable( mpID_ZOOM_REDO, false );
2137
2138 PopupMenu( menu, aEvent.GetPosition() );
2139}
2140
2141
2142void SIM_VIEW::onSmithMenuCommand( wxCommandEvent& aEvent )
2143{
2144 if( !m_smithChart )
2145 {
2146 aEvent.Skip();
2147 return;
2148 }
2149
2150 switch( aEvent.GetId() )
2151 {
2152 case mpID_ZOOM_IN: SmithZoomAt( m_smithMenuPos, 1.5 ); break;
2153 case mpID_ZOOM_OUT: SmithZoomAt( m_smithMenuPos, 1.0 / 1.5 ); break;
2154
2155 case mpID_FIT:
2157 Refresh();
2158 break;
2159
2160 case mpID_CENTER:
2161 {
2162 SMITH_VIEW view;
2163
2164 if( getSmithView( view ) )
2165 {
2167 Refresh();
2168 }
2169
2170 break;
2171 }
2172
2173 default: aEvent.Skip();
2174 }
2175}
2176
2177
2178SIM_PLOT_TAB::SIM_PLOT_TAB( const wxString& aSimCommand, wxWindow* parent ) :
2179 SIM_TAB( aSimCommand, parent ),
2180 m_dotted_cp( false ),
2181 m_syncingXView( false ),
2182 m_smithMode( false )
2183{
2185
2186 wxBoxSizer* outerSizer = new wxBoxSizer( wxVERTICAL );
2187
2188 m_viewsSizer = new wxBoxSizer( wxVERTICAL );
2189 outerSizer->Add( m_viewsSizer, 1, wxEXPAND, 0 );
2190
2191 wxBoxSizer* viewButtonsSizer = new wxBoxSizer( wxHORIZONTAL );
2192
2193 STD_BITMAP_BUTTON* addViewButton = new STD_BITMAP_BUTTON( this, wxID_ANY, wxNullBitmap );
2194 addViewButton->SetBitmap( KiBitmapBundle( BITMAPS::small_plus ) );
2195 addViewButton->SetToolTip( _( "Add a new signal view" ) );
2196
2197 STD_BITMAP_BUTTON* removeViewButton = new STD_BITMAP_BUTTON( this, wxID_ANY, wxNullBitmap );
2198 removeViewButton->SetBitmap( KiBitmapBundle( BITMAPS::small_trash ) );
2199 removeViewButton->SetToolTip( _( "Remove the last signal view" ) );
2200
2201 viewButtonsSizer->Add( addViewButton, 0, wxLEFT, 5 );
2202 viewButtonsSizer->Add( removeViewButton, 0, wxLEFT, 30 );
2203
2204 addViewButton->Bind( wxEVT_BUTTON,
2205 [this]( wxCommandEvent& aEvent )
2206 {
2207 AddView();
2208 } );
2209
2210 removeViewButton->Bind( wxEVT_BUTTON,
2211 [this]( wxCommandEvent& aEvent )
2212 {
2213 const std::vector<SIM_VIEW*>& views = GetViews();
2214
2215 if( !views.empty() )
2216 RemoveView( views.back() );
2217 } );
2218
2219 outerSizer->Add( viewButtonsSizer, 0, wxEXPAND | wxTOP | wxBOTTOM, 3 );
2220
2221 SetSizer( outerSizer );
2222
2223 AddView();
2224}
2225
2226
2228{
2229 // ~mpWindow destroys all the added layers, so there is no need to destroy m_traces contents
2230}
2231
2232
2234{
2236
2237 for( SIM_VIEW* view : m_views )
2238 view->SetMouseWheelActions( m_mouseWheelActions );
2239}
2240
2241
2243{
2244 SIM_VIEW* view = new SIM_VIEW( this, this );
2245
2246 view->LimitView( true );
2247 view->SetMargins( 30, 70, 45, 70 );
2249
2250 view->updateAxes();
2251
2252 // a mpInfoLegend displays the name of traces on the left top panel corner:
2253 mpInfoLegend* legend = new mpInfoLegend( wxRect( 0, 0, 200, 40 ), wxTRANSPARENT_BRUSH );
2254 legend->SetVisible( false );
2255 view->AddLayer( legend );
2256 view->m_legend = legend;
2257 view->m_lastLegendPosition = legend->GetPosition();
2258
2259 view->EnableDoubleBuffer( true );
2260 view->SetSmithChart( m_smithMode );
2261
2262 m_views.push_back( view );
2263 m_viewsSizer->Add( view, 1, wxALL | wxEXPAND, 1 );
2264
2266
2267 if( m_views.size() > 1 )
2268 SyncXView( m_views[0] );
2269
2270 view->UpdateAll();
2271 Layout();
2272
2273 wxQueueEvent( this, new wxCommandEvent( EVT_SIM_VIEWS_CHANGED ) );
2274
2275 return view;
2276}
2277
2278
2280{
2281 if( m_views.size() <= 1 )
2282 return false;
2283
2284 auto it = std::find( m_views.begin(), m_views.end(), aView );
2285
2286 if( it == m_views.end() )
2287 return false;
2288
2289 // The traces routed to this view lose their axes along with it, so drop them rather than
2290 // leaving them behind holding a destroyed view (and its destroyed axis layers). Their
2291 // signals stay in the Signals grid, simply no longer plotted, and can be assigned to one
2292 // of the remaining views.
2293 std::vector<TRACE*> plottedHere;
2294
2295 for( const auto& [name, trace] : m_traces )
2296 {
2297 trace->ClearYScaleViewIf( aView );
2298
2299 if( trace->GetView() == aView )
2300 plottedHere.push_back( trace );
2301 }
2302
2303 // DeleteTrace() erases from m_traces, so it cannot run while iterating it
2304 for( TRACE* trace : plottedHere )
2305 DeleteTrace( trace );
2306
2307 m_viewsSizer->Detach( aView );
2308 m_views.erase( it );
2309 aView->Destroy();
2310
2311 Layout();
2312
2313 // Any Y-scale ranges that were merged with the removed view are now stale; recompute them
2314 // (and refresh) so the remaining views reflect only the traces that are still present.
2315 ResetScales( false );
2316
2317 for( SIM_VIEW* view : m_views )
2318 view->UpdateAll();
2319
2320 wxQueueEvent( this, new wxCommandEvent( EVT_SIM_VIEWS_CHANGED ) );
2321
2322 return true;
2323}
2324
2325
2327{
2328 for( size_t i = 0; i < m_views.size(); ++i )
2329 {
2330 if( m_views[i] == aView )
2331 return (int) i;
2332 }
2333
2334 return -1;
2335}
2336
2337
2339{
2340 if( m_syncingXView || !aSource )
2341 return;
2342
2343 m_syncingXView = true;
2344
2345 double scaleX = aSource->GetScaleX();
2346 double pos = aSource->GetPosX();
2347 double desiredMin = aSource->GetDesiredXmin();
2348 double desiredMax = aSource->GetDesiredXmax();
2349
2350 for( SIM_VIEW* view : m_views )
2351 {
2352 if( view == aSource )
2353 continue;
2354
2355 view->SetScaleX( scaleX );
2356 view->SetXRange( pos, desiredMax, desiredMin );
2357 view->UpdateAll();
2358 }
2359
2360 m_syncingXView = false;
2361}
2362
2363
2365{
2366 SIM_VIEW* view = GetDefaultView();
2367 return view ? view->GetLabelX() : wxString( wxS( "" ) );
2368}
2369
2370
2372{
2373 SIM_VIEW* view = GetDefaultView();
2374 return view ? view->GetUnitsX() : wxString( wxS( "" ) );
2375}
2376
2377
2378wxString SIM_PLOT_TAB::GetUnitsForTrace( TRACE* aTrace ) const
2379{
2380 SIM_VIEW* view = aTrace->GetView() ? aTrace->GetView() : GetDefaultView();
2381 return view ? view->GetUnitsForTrace( aTrace ) : wxString( wxS( "" ) );
2382}
2383
2384
2385void SIM_PLOT_TAB::ShowGrid( bool aEnable )
2386{
2387 for( SIM_VIEW* view : m_views )
2388 view->ShowGrid( aEnable );
2389}
2390
2391
2393{
2394 SIM_VIEW* view = GetDefaultView();
2395 return view && view->IsGridShown();
2396}
2397
2398
2399void SIM_PLOT_TAB::ShowLegend( bool aEnable )
2400{
2401 for( SIM_VIEW* view : m_views )
2402 view->ShowLegend( aEnable );
2403}
2404
2405
2407{
2408 SIM_VIEW* view = GetDefaultView();
2409 return view && view->IsLegendShown();
2410}
2411
2412
2414{
2415 SIM_VIEW* view = GetDefaultView();
2416 return view ? view->GetLegendPosition() : wxPoint();
2417}
2418
2419
2420void SIM_PLOT_TAB::SetLegendPosition( const wxPoint& aPosition )
2421{
2422 if( SIM_VIEW* view = GetDefaultView() )
2423 view->SetLegendPosition( aPosition );
2424
2425 m_LastLegendPosition = aPosition;
2426}
2427
2428
2430{
2431 SIM_VIEW* view = GetDefaultView();
2432 return view ? view->GetLabelY1() : wxString( wxS( "" ) );
2433}
2434
2435
2437{
2438 SIM_VIEW* view = GetDefaultView();
2439 return view ? view->GetLabelY2() : wxString( wxS( "" ) );
2440}
2441
2442
2444{
2445 SIM_VIEW* view = GetDefaultView();
2446 return view ? view->GetLabelY3() : wxString( wxS( "" ) );
2447}
2448
2449
2451{
2452 SIM_VIEW* view = GetDefaultView();
2453 return view ? view->GetUnitsY1() : wxString( wxS( "" ) );
2454}
2455
2456
2458{
2459 SIM_VIEW* view = GetDefaultView();
2460 return view ? view->GetUnitsY2() : wxString( wxS( "" ) );
2461}
2462
2463
2465{
2466 SIM_VIEW* view = GetDefaultView();
2467 return view ? view->GetUnitsY3() : wxString( wxS( "" ) );
2468}
2469
2470
2471bool SIM_PLOT_TAB::GetY1Scale( double* aMin, double* aMax ) const
2472{
2473 SIM_VIEW* view = GetDefaultView();
2474 return view && view->GetY1Scale( aMin, aMax );
2475}
2476
2477
2478bool SIM_PLOT_TAB::GetY2Scale( double* aMin, double* aMax ) const
2479{
2480 SIM_VIEW* view = GetDefaultView();
2481 return view && view->GetY2Scale( aMin, aMax );
2482}
2483
2484
2485bool SIM_PLOT_TAB::GetY3Scale( double* aMin, double* aMax ) const
2486{
2487 SIM_VIEW* view = GetDefaultView();
2488 return view && view->GetY3Scale( aMin, aMax );
2489}
2490
2491
2492void SIM_PLOT_TAB::SetY1Scale( bool aLock, double aMin, double aMax )
2493{
2494 if( SIM_VIEW* view = GetDefaultView() )
2495 view->SetY1Scale( aLock, aMin, aMax );
2496}
2497
2498
2499void SIM_PLOT_TAB::SetY2Scale( bool aLock, double aMin, double aMax )
2500{
2501 if( SIM_VIEW* view = GetDefaultView() )
2502 view->SetY2Scale( aLock, aMin, aMax );
2503}
2504
2505
2506void SIM_PLOT_TAB::SetY3Scale( bool aLock, double aMin, double aMax )
2507{
2508 if( SIM_VIEW* view = GetDefaultView() )
2509 view->SetY3Scale( aLock, aMin, aMax );
2510}
2511
2512
2514{
2515 if( SIM_VIEW* view = GetDefaultView() )
2516 view->EnsureThirdYAxisExists();
2517}
2518
2519
2521{
2522 for( SIM_VIEW* view : m_views )
2523 {
2524 view->SetColourTheme( m_colors.GetPlotColor( SIM_PLOT_COLORS::COLOR_SET::BACKGROUND ),
2527
2528 view->UpdateSmithGridColor();
2529 view->UpdateAll();
2530 }
2531}
2532
2533
2534void SIM_PLOT_TAB::SetSmithMode( bool aEnable )
2535{
2536 // only S-parameter tabs have a Smith view, a stale workbook cannot force one elsewhere
2537 if( aEnable && GetSimType() != ST_SP )
2538 return;
2539
2540 if( m_smithMode == aEnable )
2541 return;
2542
2543 m_smithMode = aEnable;
2544
2545 for( SIM_VIEW* view : m_views )
2546 view->SetSmithChart( aEnable );
2547}
2548
2549
2551{
2552 for( SIM_VIEW* view : m_views )
2553 view->UpdateSmithReferenceImpedance();
2554}
2555
2556
2558{
2559 SIM_VIEW* view = GetDefaultView();
2560 return view ? view->GetSmithZoom() : 1.0;
2561}
2562
2563
2564wxRealPoint SIM_PLOT_TAB::GetSmithPan() const
2565{
2566 SIM_VIEW* view = GetDefaultView();
2567 return view ? view->GetSmithPan() : wxRealPoint( 0.0, 0.0 );
2568}
2569
2570
2572{
2573 for( SIM_VIEW* view : m_views )
2574 {
2575 view->ResetSmithView();
2576 view->Refresh();
2577 }
2578}
2579
2580
2581void SIM_PLOT_TAB::SetSmithView( double aZoom, double aPanX, double aPanY )
2582{
2583 for( SIM_VIEW* view : m_views )
2584 view->SetSmithView( aZoom, aPanX, aPanY );
2585}
2586
2587
2588void SIM_PLOT_TAB::SmithZoomAt( const wxPoint& aPos, double aFactor )
2589{
2590 for( SIM_VIEW* view : m_views )
2591 view->SmithZoomAt( aPos, aFactor );
2592}
2593
2594
2595void SIM_PLOT_TAB::SmithPanBy( const wxPoint& aDelta )
2596{
2597 for( SIM_VIEW* view : m_views )
2598 view->SmithPanBy( aDelta );
2599}
2600
2601
2603{
2604 for( SIM_VIEW* view : m_views )
2605 {
2606 view->updateAxes();
2607 view->UpdateAll();
2608 }
2609}
2610
2611
2613{
2614 int type = trace->GetType();
2615 wxPenStyle penStyle;
2616
2617 if( ( type & SPT_AC_GAIN ) > 0 )
2618 penStyle = wxPENSTYLE_SOLID;
2619 else if( ( type & SPT_AC_PHASE ) > 0 )
2620 penStyle = m_dotted_cp ? wxPENSTYLE_DOT : wxPENSTYLE_SOLID;
2621 else if( ( type & SPT_CURRENT ) > 0 )
2622 penStyle = m_dotted_cp ? wxPENSTYLE_DOT : wxPENSTYLE_SOLID;
2623 else
2624 penStyle = wxPENSTYLE_SOLID;
2625
2626 trace->SetPen( wxPen( trace->GetTraceColour(), 2, penStyle ) );
2627 m_sessionTraceColors[trace->GetName()] = trace->GetTraceColour();
2628}
2629
2630
2631TRACE* SIM_PLOT_TAB::GetOrAddTrace( const wxString& aVectorName, int aType, SIM_VIEW* aView )
2632{
2633 TRACE* trace = GetTrace( aVectorName, aType );
2634
2635 if( !trace && aView )
2636 {
2637 aView->updateAxes( aType );
2638
2639 if( GetSimType() == ST_TRAN || GetSimType() == ST_DC )
2640 {
2641 bool hasVoltageTraces = false;
2642
2643 for( const auto& [id, candidate] : m_traces )
2644 {
2645 if( candidate->GetView() == aView && ( candidate->GetType() & SPT_VOLTAGE ) )
2646 {
2647 hasVoltageTraces = true;
2648 break;
2649 }
2650 }
2651
2652 if( !hasVoltageTraces )
2653 {
2654 if( aView->m_axis_y2 )
2655 aView->m_axis_y2->SetMasterScale( nullptr );
2656
2657 if( aView->m_axis_y3 )
2658 aView->m_axis_y3->SetMasterScale( nullptr );
2659 }
2660 }
2661
2662 if( aType & SPT_SP_SMITH )
2663 trace = new SMITH_TRACE( aVectorName, (SIM_TRACE_TYPE) aType );
2664 else
2665 trace = new TRACE( aVectorName, (SIM_TRACE_TYPE) aType );
2666
2667 if( m_sessionTraceColors.count( aVectorName ) )
2668 trace->SetTraceColour( m_sessionTraceColors[aVectorName] );
2669 else
2670 trace->SetTraceColour( m_colors.GenerateColor( m_sessionTraceColors ) );
2671
2672 UpdateTraceStyle( trace );
2673 m_traces[getTraceId( aVectorName, aType )] = trace;
2674
2675 trace->SetView( aView );
2676 aView->AddLayer( (mpLayer*) trace );
2677 }
2678
2679 return trace;
2680}
2681
2682
2683void SIM_PLOT_TAB::SetTraceData( TRACE* trace, std::vector<double>& aX, std::vector<double>& aY, int aSweepCount,
2684 size_t aSweepSize, bool aIsMultiRun, const std::vector<wxString>& aMultiRunLabels )
2685{
2686 SIM_VIEW* view = trace->GetView();
2687
2688 wxCHECK( view, /* void */ );
2689
2690 // smith traces carry Re/Im of the reflection coefficient, not frequency
2691 bool smithTrace = ( trace->GetType() & SPT_SP_SMITH ) > 0;
2692
2693 if( dynamic_cast<LOG_SCALE<mpScaleXLog>*>( view->m_axis_x ) && !smithTrace )
2694 {
2695 // log( 0 ) is not valid.
2696 if( aX.size() > 0 && aX[0] == 0 )
2697 {
2698 aX.erase( aX.begin() );
2699 aY.erase( aY.begin() );
2700 }
2701 }
2702
2703 if( GetSimType() == ST_AC || GetSimType() == ST_FFT )
2704 {
2705 if( trace->GetType() & SPT_AC_PHASE )
2706 {
2707 for( double& pt : aY )
2708 pt = pt * 180.0 / M_PI; // convert to degrees
2709 }
2710 else
2711 {
2712 for( double& pt : aY )
2713 pt = MagnitudeToDb( pt ); // NaN where there is no signal
2714 }
2715 }
2716
2717 trace->SetData( aX, aY );
2718 trace->SetSweepCount( aSweepCount );
2719 trace->SetSweepSize( aSweepSize );
2720 trace->SetIsMultiRun( aIsMultiRun );
2721 trace->SetMultiRunLabels( aMultiRunLabels );
2722
2723 // Phase and currents on second Y axis, except for AC currents, those use the same axis as voltage
2724 if( smithTrace )
2725 {
2726 // drawn through the chart geometry, not the axis transforms
2727 trace->SetScale( nullptr, nullptr );
2728 }
2729 else if( ( trace->GetType() & SPT_AC_PHASE )
2730 || ( ( GetSimType() != ST_AC ) && ( trace->GetType() & SPT_CURRENT ) ) )
2731 {
2732 trace->SetScale( view->m_axis_x, view->m_axis_y2 );
2733 }
2734 else if( trace->GetType() & SPT_POWER )
2735 {
2736 trace->SetScale( view->m_axis_x, view->m_axis_y3 );
2737 }
2738 else
2739 {
2740 trace->SetScale( view->m_axis_x, view->m_axis_y1 );
2741 }
2742
2743 for( auto& [cursorId, cursor] : trace->GetCursors() )
2744 {
2745 if( cursor )
2746 cursor->UpdateForNewData();
2747 }
2748
2749 view->UpdateAxisVisibility();
2750}
2751
2752
2754{
2755 for( const auto& [name, trace] : m_traces )
2756 {
2757 if( trace == aTrace )
2758 {
2759 m_traces.erase( name );
2760 break;
2761 }
2762 }
2763
2764 if( SIM_VIEW* view = aTrace->GetView() )
2765 {
2766 for( const auto& [id, cursor] : aTrace->GetCursors() )
2767 {
2768 if( cursor )
2769 view->DelLayer( cursor, true );
2770 }
2771
2772 view->DelLayer( aTrace, true, true );
2773 view->UpdateAxisVisibility();
2774 view->UpdateSmithReferenceImpedance();
2775 }
2776
2777 ResetScales( false );
2778}
2779
2780
2781bool SIM_PLOT_TAB::DeleteTrace( const wxString& aVectorName, int aTraceType )
2782{
2783 if( TRACE* trace = GetTrace( aVectorName, aTraceType ) )
2784 {
2785 DeleteTrace( trace );
2786 return true;
2787 }
2788
2789 return false;
2790}
2791
2792
2793void SIM_PLOT_TAB::EnableCursor( TRACE* aTrace, int aCursorId, const wxString& aSignalName )
2794{
2795 SIM_VIEW* view = aTrace->GetView();
2796
2797 wxCHECK( view, /* void */ );
2798
2799 CURSOR* cursor;
2800
2801 if( aTrace->GetType() & SPT_SP_SMITH )
2802 {
2803 SMITH_TRACE* smithTrace = static_cast<SMITH_TRACE*>( aTrace );
2804
2805 cursor = new SMITH_CURSOR( smithTrace, this );
2806
2807 // start somewhere on the locus, biased per cursor id like the rectangular case
2808 const std::vector<double>& freqs = smithTrace->GetFrequencies();
2809
2810 if( !freqs.empty() )
2811 cursor->SetCoordX( freqs[freqs.size() * ( aCursorId == 1 ? 2 : 3 ) / 5] );
2812 }
2813 else
2814 {
2815 int width = view->GetXScreen() - view->GetMarginLeft() - view->GetMarginRight();
2816 int center = view->GetMarginLeft() + KiROUND( width * ( aCursorId == 1 ? 0.4 : 0.6 ) );
2817
2818 cursor = new CURSOR( aTrace, this );
2819
2820 cursor->SetX( center );
2821 }
2822
2823 cursor->SetName( aSignalName );
2824
2825 aTrace->SetCursor( aCursorId, cursor );
2826 view->AddLayer( cursor );
2827
2828 // Notify the parent window about the changes
2829 wxQueueEvent( this, new wxCommandEvent( EVT_SIM_CURSOR_UPDATE ) );
2830}
2831
2832
2833void SIM_PLOT_TAB::DisableCursor( TRACE* aTrace, int aCursorId )
2834{
2835 if( CURSOR* cursor = aTrace->GetCursor( aCursorId ) )
2836 {
2837 aTrace->SetCursor( aCursorId, nullptr );
2838
2839 if( SIM_VIEW* view = aTrace->GetView() )
2840 view->DelLayer( cursor, true );
2841
2842 // Notify the parent window about the changes
2843 wxQueueEvent( this, new wxCommandEvent( EVT_SIM_CURSOR_UPDATE ) );
2844 }
2845}
2846
2847
2848void SIM_PLOT_TAB::ResetScales( bool aIncludeX )
2849{
2850 for( SIM_VIEW* view : m_views )
2851 view->ResetScales( aIncludeX );
2852
2853 // Merge Y-axis auto-fit ranges for traces whose "Y Scale" has been explicitly linked to
2854 // another view, so that the linked axes end up sharing the same numeric range -- as if all
2855 // the involved traces were plotted together in a single view.
2856 std::map<std::pair<SIM_VIEW*, int>, std::vector<TRACE*>> groups;
2857
2858 for( const auto& [name, trace] : m_traces )
2859 {
2860 if( SIM_VIEW* view = trace->GetView() )
2861 groups[{ trace->GetYScaleView(), view->GetAxisSlot( trace ) }].push_back( trace );
2862 }
2863
2864 for( const auto& [key, traces] : groups )
2865 {
2866 std::vector<SIM_VIEW*> actualViews;
2867
2868 for( TRACE* trace : traces )
2869 {
2870 SIM_VIEW* view = trace->GetView();
2871
2872 if( std::find( actualViews.begin(), actualViews.end(), view ) == actualViews.end() )
2873 actualViews.push_back( view );
2874 }
2875
2876 if( actualViews.size() <= 1 )
2877 continue; // All traces already share the same, single axis object.
2878
2879 bool haveRange = false;
2880 double minV = 0.0;
2881 double maxV = 0.0;
2882
2883 for( TRACE* trace : traces )
2884 {
2885 double lo = trace->GetMinY();
2886 double hi = trace->GetMaxY();
2887
2888 if( !std::isfinite( lo ) || !std::isfinite( hi ) )
2889 continue;
2890
2891 if( !haveRange )
2892 {
2893 minV = lo;
2894 maxV = hi;
2895 haveRange = true;
2896 }
2897 else
2898 {
2899 minV = std::min( minV, lo );
2900 maxV = std::max( maxV, hi );
2901 }
2902 }
2903
2904 if( !haveRange )
2905 continue;
2906
2907 for( SIM_VIEW* view : actualViews )
2908 {
2909 if( mpScaleY* axis = view->GetAxisBySlot( key.second ) )
2910 axis->SetDataRange( minV, maxV );
2911 }
2912 }
2913}
2914
2915
2917{
2918 return GetDefaultView();
2919}
2920
2921
2922wxDEFINE_EVENT( EVT_SIM_CURSOR_UPDATE, wxCommandEvent );
2923wxDEFINE_EVENT( EVT_SIM_VIEWS_CHANGED, wxCommandEvent );
const char * name
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition bitmap.cpp:106
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:995
The SIMULATOR_FRAME holds the main user-interface for running simulations.
mpWindow * m_window
void Move(wxPoint aDelta) override
Moves the layer rectangle of given pixel deltas.
wxString getID()
wxRealPoint m_coords
bool m_updateRef
static constexpr int DRAG_MARGIN
bool Inside(const wxPoint &aPoint) const override
Checks whether a point is inside the info box rectangle.
void doSetCoordX(double aValue)
int m_sweepIndex
bool OnDoubleClick(const wxPoint &aPoint, mpWindow &aWindow) override
virtual void SetCoordX(double aValue)
void UpdateReference() override
Updates the rectangle reference point.
bool m_updateRequired
double m_snapTargetY
TRACE * m_trace
void Plot(wxDC &aDC, mpWindow &aWindow) override
Plot method.
bool m_snapToNearest
void Update()
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
COLOR4D & Invert()
Makes the color inverted, alpha remains the same.
Definition color4d.h:239
wxColour ToColour() const
Definition color4d.cpp:221
double Distance(const COLOR4D &other) const
Returns the distance (in RGB space) between two colors.
Definition color4d.cpp:551
COLOR4D Mix(const COLOR4D &aColor, double aFactor) const
Return a color that is mixed with the input by a factor.
Definition color4d.h:292
void formatLabels() override
wxString GetUnits() const
const wxString m_unit
LIN_SCALE(const wxString &name, const wxString &unit, int flags)
wxString m_base_axis_label
wxString GetUnits() const
const wxString m_unit
LOG_SCALE(const wxString &name, const wxString &unit, int flags)
void formatLabels() override
bool DeleteTrace(const wxString &aVectorName, int aTraceType)
double GetSmithZoom() const
void UpdateSmithReferenceImpedance()
The saved/restored pan and zoom track the default view.
wxString GetLabelY1() const
mpWindow * GetPlotWin() const
Get (or create, on aView) the TRACE for a signal.
void SetSmithView(double aZoom, double aPanX, double aPanY)
void ShowGrid(bool aEnable)
void SetTraceData(TRACE *aTrace, std::vector< double > &aX, std::vector< double > &aY, int aSweepCount, size_t aSweepSize, bool aIsMultiRun=false, const std::vector< wxString > &aMultiRunLabels={})
void SyncXView(SIM_VIEW *aSource)
const std::vector< SIM_VIEW * > & GetViews() const
wxString GetUnitsY2() const
void SetY2Scale(bool aLock, double aMin, double aMax)
void SmithZoomAt(const wxPoint &aPos, double aFactor)
TRACE * GetTrace(const wxString &aVecName, int aType) const
void SetSmithMode(bool aEnable)
virtual ~SIM_PLOT_TAB()
wxString GetLabelX() const
bool IsGridShown() const
wxRealPoint GetSmithPan() const
wxString GetLabelY3() const
void SetY1Scale(bool aLock, double aMin, double aMax)
wxPoint GetLegendPosition() const
void SetY3Scale(bool aLock, double aMin, double aMax)
std::map< wxString, TRACE * > m_traces
bool IsLegendShown() const
SIM_VIEW * GetDefaultView() const
Add a new (empty) view, stacked below the existing ones.
SIM_VIEW * AddView()
Remove a view.
wxString GetUnitsForTrace(TRACE *aTrace) const
SIM_PLOT_COLORS m_colors
void UpdateTraceStyle(TRACE *trace)
Update plot colors.
void SetLegendPosition(const wxPoint &aPosition)
TRACE * GetOrAddTrace(const wxString &aVectorName, int aType, SIM_VIEW *aView)
void ResetScales(bool aIncludeX)
Update trace line style.
void UpdatePlotColors()
void ShowLegend(bool aEnable)
bool GetY3Scale(double *aMin, double *aMax) const
SIM_PLOT_TAB(const wxString &aSimCommand, wxWindow *parent)
wxString GetLabelY2() const
void EnableCursor(TRACE *aTrace, int aCursorId, const wxString &aSignalName)
int GetViewIndex(SIM_VIEW *aView) const
wxString GetUnitsX() const
Get the display units (e.g. "V", "A", "dB") for a given trace's own view.
void OnLanguageChanged() override
Getter for the default view's math plot window (back-compat for zoom undo/redo, export).
void EnsureThirdYAxisExists()
wxString getTraceId(const wxString &aVectorName, int aType) const
void ApplyPreferences(const SIM_PREFERENCES &aPrefs) override
wxBoxSizer * m_viewsSizer
mpWindow::MouseWheelActionSet m_mouseWheelActions
bool RemoveView(SIM_VIEW *aView)
Mirror aSource's current X range onto every other view of this tab.
void SmithPanBy(const wxPoint &aDelta)
Traces and cursors set aside while in Smith mode, restored when leaving it.
std::vector< SIM_VIEW * > m_views
wxPoint m_LastLegendPosition
wxString GetUnitsY1() const
std::map< wxString, wxColour > m_sessionTraceColors
static mpWindow::MouseWheelActionSet convertMouseWheelActions(const SIM_MOUSE_WHEEL_ACTION_SET &s)
void DisableCursor(TRACE *aTrace, int aCursorId)
Reset scale ranges to fit the current traces, on every view.
bool GetY2Scale(double *aMin, double *aMax) const
void ResetSmithView()
Restore a saved view, values are validated and clamped.
bool GetY1Scale(double *aMin, double *aMax) const
wxString GetUnitsY3() const
SIM_TAB()
Definition sim_tab.cpp:29
SIM_TYPE GetSimType() const
Definition sim_tab.cpp:71
A single stacked plot area within a SIM_PLOT_TAB.
wxString GetLabelY2() const
void OnXViewChanged() override
Directly set this view's X range (used to mirror another view's zoom/pan onto this one).
void updateAxes(int aNewTraceType=SIM_TRACE_TYPE::SPT_UNKNOWN)
void SmithZoomAt(const wxPoint &aPos, double aFactor)
bool GetY3Scale(double *aMin, double *aMax) const
void onSmithMenuCommand(wxCommandEvent &aEvent)
mpScaleY * m_axis_y3
wxString GetUnitsForTrace(TRACE *aTrace) const
Get the Y-axis slot (1, 2 or 3) a trace of this type is plotted on.
bool m_smithPanning
void UpdateSmithGridColor()
void SetY2Scale(bool aLock, double aMin, double aMax)
void EnsureThirdYAxisExists()
wxPoint m_smithMenuPos
void SmithPanBy(const wxPoint &aDelta)
mpInfoLegend * m_legend
void ResetSmithView()
void onSmithDClick(wxMouseEvent &aEvent)
wxString GetLabelX() const
bool m_smithLeftSkipped
wxPoint m_lastLegendPosition
wxString GetLabelY3() const
wxString GetUnitsY3() const
Get the display units (e.g. "V", "A", "dB") for a given trace plotted on this view.
wxPoint GetLegendPosition() const
void onSmithMagnify(wxMouseEvent &aEvent)
void SetY1Scale(bool aLock, double aMin, double aMax)
const wxRealPoint & GetSmithPan() const
void SetY3Scale(bool aLock, double aMin, double aMax)
void onSmithMiddleDown(wxMouseEvent &aEvent)
void onSmithRightDown(wxMouseEvent &aEvent)
SIM_VIEW(SIM_PLOT_TAB *aPlotTab, wxWindow *aParent)
Mirrors this view's X range onto every other view of the same tab.
bool IsLegendShown() const
mpScaleY * GetAxisBySlot(int aSlot) const
double GetSmithZoom() const
bool getSmithView(SMITH_VIEW &aView) const
bool GetY2Scale(double *aMin, double *aMax) const
bool GetY1Scale(double *aMin, double *aMax) const
void onSmithLeftUp(wxMouseEvent &aEvent)
wxString GetLabelY1() const
void UpdateSmithReferenceImpedance()
Re-read the Smith grid pen from the tab's color theme.
mpScaleY * m_axis_y2
SMITH_GRID * m_smithGrid
void onSmithLeftDown(wxMouseEvent &aEvent)
void ResetScales(bool aIncludeX)
Create/Ensure axes are available for plotting.
mpScaleXBase * m_axis_x
int GetAxisSlot(TRACE *aTrace) const
Get the Y-axis scale object for a given slot (1, 2 or 3), or nullptr if not created yet.
SIM_PLOT_TAB * m_plotTab
mpScaleY * m_axis_y1
wxString GetUnitsX() const
void onSmithRightUp(wxMouseEvent &aEvent)
void SetSmithChart(bool aEnable)
< Show/hide this view's Smith chart overlay.
bool m_smithChart
void UpdateAxisVisibility()
void onSmithMotion(wxMouseEvent &aEvent)
wxString GetUnitsY2() const
wxPoint m_smithPanLast
wxString GetUnitsY1() const
void onSmithMouseWheel(wxMouseEvent &aEvent)
double m_smithZoom
bool IsGridShown() const
wxRealPoint m_smithPan
void prepareDCAxes(int aNewTraceType)
<
Trace hidden while the tab is in Smith mode, kept so leaving the mode restores it.
void UpdateReference() override
Updates the rectangle reference point.
void Plot(wxDC &aDC, mpWindow &aWindow) override
Plot method.
bool Inside(const wxPoint &aPoint) const override
Checks whether a point is inside the info box rectangle.
void SetCoordX(double aValue) override
double m_requestFreq
void snapToIndex(int aIndex)
void snapToFrequency(double aFreq)
void UpdateForNewData() override
wxRealPoint m_gamma
void Move(wxPoint aDelta) override
Moves the layer rectangle of given pixel deltas.
Reflection coefficient locus, Re in X and Im in Y, drawn on the Smith chart.
static bool GetChartView(mpWindow &aWindow, double aZoom, const wxRealPoint &aPan, SMITH_VIEW &aView)
void Plot(wxDC &aDC, mpWindow &aWindow) override
Plot given view of layer to the given device context.
bool m_mixedReferences
Cursor that snaps along a Smith chart locus, keyed by frequency.
void Plot(wxDC &aDC, mpWindow &aWindow) override
Layer plot handler.
const std::vector< double > & GetFrequencies() const
Zero until the response port resolves one, which leaves only normalized values readable.
double GetReferenceImpedance() const
Helper class to recognize Spice formatted values.
Definition spice_value.h:52
double ToDouble() const
A bitmap button widget that behaves like a standard dialog button except with an icon.
void SetBitmap(const wxBitmapBundle &aBmp)
void ResetDataRange() override
void ExtendDataRange(double minV, double maxV) override
void SetStartAndEnd(double aStartTime, double aEndTime)
double m_startTime
TIME_SCALE(const wxString &name, const wxString &unit, int flags)
Overlay layer drawing the Smith chart grid (constant resistance and reactance circles)
void SetIsMultiRun(bool aIsMultiRun)
void SetTraceColour(const wxColour &aColour)
void SetMultiRunLabels(const std::vector< wxString > &aLabels)
std::map< int, CURSOR * > & GetCursors()
SIM_VIEW * GetView() const
void SetView(SIM_VIEW *aView)
The view whose Y-axis scale this trace's axis should match (defaults to its own view).
SIM_TRACE_TYPE GetType() const
void SetData(const std::vector< double > &aX, const std::vector< double > &aY) override
Assigns new data set for the trace.
void SetCursor(int aCursorId, CURSOR *aCursor)
const std::vector< double > & GetDataY() const
wxColour GetTraceColour() const
const std::vector< double > & GetDataX() const
CURSOR * GetCursor(int aCursorId)
void SetSweepSize(size_t aSweepSize)
Definition mathplot.h:1477
size_t GetSweepSize() const
Definition mathplot.h:1478
void SetSweepCount(int aSweepCount)
Definition mathplot.h:1476
int GetSweepCount() const override
Definition mathplot.h:1502
virtual void SetScale(mpScaleBase *scaleX, mpScaleBase *scaleY)
wxPoint m_reference
Definition mathplot.h:409
wxRect m_dim
Definition mathplot.h:407
wxPoint GetPosition() const
Returns the position of the upper left corner of the box (in pixels)
Definition mathplot.cpp:197
virtual void UpdateReference()
Updates the rectangle reference point.
Definition mathplot.cpp:152
virtual void Move(wxPoint delta)
Moves the layer rectangle of given pixel deltas.
Definition mathplot.cpp:145
Implements the legend to be added to the plot This layer allows you to add a legend to describe the p...
Definition mathplot.h:421
const wxString & GetName() const
Get layer name.
Definition mathplot.h:249
bool m_continuous
Definition mathplot.h:338
bool m_visible
Definition mathplot.h:341
const wxPen & GetPen() const
Get pen set for this layer.
Definition mathplot.h:280
void SetVisible(bool show)
Sets layer visibility.
Definition mathplot.h:321
const wxFont & GetPlotFont() const
Get the font to draw this layer with, which is the default one when none was set.
Definition mathplot.h:269
void SetPen(const wxPen &pen)
Set layer pen.
Definition mathplot.h:309
wxPen m_pen
Definition mathplot.h:334
bool m_rangeSet
Definition mathplot.h:788
double m_maxV
Definition mathplot.h:787
double m_minV
Definition mathplot.h:787
Plot layer implementing a y-scale ruler.
Definition mathplot.h:873
void SetMasterScale(mpScaleY *masterScale)
Definition mathplot.h:898
Canvas for plotting mpLayer implementations.
Definition mathplot.h:953
int GetMarginLeft() const
Definition mathplot.h:1264
void SetMargins(int top, int right, int bottom, int left)
Set window margins, creating a blank area where some kinds of layers cannot draw.
int GetScrX() const
Get current view's X dimension in device context units.
Definition mathplot.h:1074
int GetScrY() const
Get current view's Y dimension in device context units.
Definition mathplot.h:1083
void SetMouseWheelActions(const MouseWheelActionSet &s)
Set the pan/zoom actions corresponding to mousewheel/trackpad events.
Definition mathplot.h:1151
double p2x(wxCoord pixelCoordX)
Converts mpWindow (screen) pixel coordinates into graph (floating point) coordinates,...
Definition mathplot.h:1127
void LimitView(bool aEnable)
Enable limiting of zooming & panning to the area used by the plots.
Definition mathplot.h:1303
wxCoord x2p(double x)
Converts graph (floating point) coordinates into mpWindow (screen) pixel coordinates,...
Definition mathplot.h:1135
mpInfoLayer * IsInsideInfoLayer(wxPoint &point)
Check if a given point is inside the area of a mpInfoLayer and eventually returns its pointer.
int GetXScreen() const
Definition mathplot.h:1075
int GetMarginTop() const
Definition mathplot.h:1258
void UpdateAll()
Refresh display.
wxCoord y2p(double y)
Converts graph (floating point) coordinates into mpWindow (screen) pixel coordinates,...
Definition mathplot.h:1139
double GetDesiredXmax() const
Returns the right-border layer coordinate that the user wants the mpWindow to show (it may be not exa...
Definition mathplot.h:1207
wxMenu * GetPopupMenu()
Get reference to context menu of the plot canvas.
Definition mathplot.h:994
double GetDesiredXmin() const
Returns the left-border layer coordinate that the user wants the mpWindow to show (it may be not exac...
Definition mathplot.h:1201
int GetMarginRight() const
Definition mathplot.h:1260
int GetMarginBottom() const
Definition mathplot.h:1262
void EnableDoubleBuffer(bool enabled)
Enable/disable the double-buffering of the window, eliminating the flicker (default=disabled).
Definition mathplot.h:1144
double GetScaleX() const
Get current view's X scale.
Definition mathplot.h:1048
bool AddLayer(mpLayer *layer, bool refreshDisplay=true)
Add a plot layer to the canvas.
double GetPosX() const
Get current view's X position.
Definition mathplot.h:1060
static bool empty(const wxTextEntryBase *aCtrl)
#define _(s)
#define mpALIGN_BORDER_RIGHT
Aligns Y axis to right border.
Definition mathplot.h:483
#define mpALIGN_RIGHT
Aligns label to the right.
Definition mathplot.h:455
#define mpALIGN_LEFT
Aligns label to the left.
Definition mathplot.h:459
@ mpID_ZOOM_REDO
Definition mathplot.h:127
@ mpID_FIT
Definition mathplot.h:125
@ mpID_ZOOM_IN
Definition mathplot.h:128
@ mpID_CENTER
Definition mathplot.h:130
@ mpID_ZOOM_UNDO
Definition mathplot.h:126
@ mpID_ZOOM_OUT
Definition mathplot.h:129
#define mpALIGN_BOTTOM
Aligns label to the bottom.
Definition mathplot.h:463
KICOMMON_API wxFont GetStatusFont(wxWindow *aWindow)
double SeriesCapacitance(double aReactance, double aFreq)
Pan that keeps the gamma point under aPos fixed when the view zooms to aNewZoom.
Definition smith_math.h:101
double SeriesInductance(double aReactance, double aFreq)
Definition smith_math.h:96
double VSWR(double aGammaMag)
Definition smith_math.h:78
wxRealPoint ZoomAboutPoint(const SMITH_VIEW &aView, const wxPoint &aPos, double aNewZoom)
S-parameter vectors are named S_<responsePort>_<drivePort>.
Definition smith_math.h:107
bool GammaToImpedance(double aRe, double aIm, double aZ0, double &aResistance, double &aReactance)
< Impedance of a reflection coefficient, z = z0 ( 1 + gamma ) / ( 1 - gamma ), false at the gamma = 1...
Definition smith_math.h:65
double ReturnLoss(double aGammaMag)
Series equivalent element of a reactance at one frequency, henries for aReactance > 0,...
Definition smith_math.h:86
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
static float distance(const SFVEC2UI &a, const SFVEC2UI &b)
Class is responsible for providing colors for traces on simulation plot.
wxDEFINE_EVENT(EVT_SIM_CURSOR_UPDATE, wxCommandEvent)
static void getSISuffix(double x, const wxString &unit, int &power, wxString &suffix)
static int countDecimalDigits(double x, int maxDigits)
static bool smithView(mpWindow &aWindow, SMITH_VIEW &aView)
static wxString formatFloat(double x, int nDigits)
SIM_TRACE_TYPE
Definition sim_types.h:49
@ SPT_AC_PHASE
Definition sim_types.h:53
@ SPT_AC_GAIN
Definition sim_types.h:54
@ SPT_VOLTAGE
Definition sim_types.h:51
@ SPT_POWER
Definition sim_types.h:55
@ SPT_CURRENT
Definition sim_types.h:52
@ SPT_SP_SMITH
Definition sim_types.h:57
double MagnitudeToDb(double aMagnitude)
Convert a linear magnitude to decibels.
Definition sim_types.h:82
@ ST_SP
Definition sim_types.h:42
@ ST_TRAN
Definition sim_types.h:41
@ ST_NOISE
Definition sim_types.h:36
@ ST_AC
Definition sim_types.h:33
@ ST_DC
Definition sim_types.h:34
@ ST_FFT
Definition sim_types.h:43
static SIM_MOUSE_WHEEL_ACTION_SET GetMouseDefaults()
Contains preferences pertaining to the simulator.
SIM_MOUSE_WHEEL_ACTION_SET mouse_wheel_actions
< Smith chart placement plus pan/zoom, maps a gamma point to a screen pixel and back.
Definition smith_math.h:32
wxRealPoint pan
Definition smith_math.h:36
double zoom
Definition smith_math.h:35
wxRealPoint ToGamma(const wxPoint &aPt) const
Definition smith_math.h:53
wxPoint center
Definition smith_math.h:33
wxPoint ToScreen(double aRe, double aIm) const
Definition smith_math.h:39
double radius
Definition smith_math.h:34
wxRect plotRect
Definition smith_math.h:37
VECTOR2I center
int radius
VECTOR2I end
int delta
#define M_PI