更新2026
This commit is contained in:
@@ -7,8 +7,27 @@ using LiveChartsCore.Defaults;
|
||||
|
||||
namespace COFTester.Controls;
|
||||
|
||||
public enum RealtimeForceChartPart
|
||||
{
|
||||
Full,
|
||||
LeftAxis,
|
||||
Plot,
|
||||
RightAxis
|
||||
}
|
||||
|
||||
public sealed class RealtimeForceChart : FrameworkElement
|
||||
{
|
||||
private const double PlotTop = 36;
|
||||
private const double PlotBottomMargin = 68;
|
||||
private const double FullChartLeftMargin = 78;
|
||||
private const double FullChartRightMargin = 142;
|
||||
|
||||
public static readonly DependencyProperty ChartPartProperty = DependencyProperty.Register(
|
||||
nameof(ChartPart),
|
||||
typeof(RealtimeForceChartPart),
|
||||
typeof(RealtimeForceChart),
|
||||
new FrameworkPropertyMetadata(RealtimeForceChartPart.Full, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
public static readonly DependencyProperty SamplesProperty = DependencyProperty.Register(
|
||||
nameof(Samples),
|
||||
typeof(IEnumerable),
|
||||
@@ -72,6 +91,12 @@ public sealed class RealtimeForceChart : FrameworkElement
|
||||
private readonly Brush _pointBrush = new SolidColorBrush(Color.FromRgb(18, 22, 25));
|
||||
private readonly Pen _pointPen = new(new SolidColorBrush(Color.FromRgb(247, 249, 251)), 1.8);
|
||||
|
||||
public RealtimeForceChartPart ChartPart
|
||||
{
|
||||
get => (RealtimeForceChartPart)GetValue(ChartPartProperty);
|
||||
set => SetValue(ChartPartProperty, value);
|
||||
}
|
||||
|
||||
public IEnumerable? Samples
|
||||
{
|
||||
get => (IEnumerable?)GetValue(SamplesProperty);
|
||||
@@ -126,28 +151,80 @@ public sealed class RealtimeForceChart : FrameworkElement
|
||||
|
||||
var width = ActualWidth;
|
||||
var height = ActualHeight;
|
||||
if (width < 120 || height < 90)
|
||||
var minimumWidth = ChartPart == RealtimeForceChartPart.Plot ? 1 : 120;
|
||||
if (width < minimumWidth || height < 90)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var plot = new Rect(78, 36, Math.Max(1, width - 220), Math.Max(1, height - 104));
|
||||
var samples = ReadPoints(Samples);
|
||||
var currentPoint = ReadPoints(CurrentPointSamples).LastOrDefault();
|
||||
var xMax = ResolveAxisMax(XMax, TravelMm, samples.Select(point => point.X));
|
||||
var yMax = ResolveAxisMax(YMax, 0.5, samples.Select(point => point.Y));
|
||||
|
||||
drawingContext.DrawRectangle(_chartBackgroundBrush, _chartBorderPen, new Rect(0.5, 0.5, Math.Max(0, width - 1), Math.Max(0, height - 1)));
|
||||
DrawGridAndAxes(drawingContext, plot, xMax, yMax);
|
||||
|
||||
switch (ChartPart)
|
||||
{
|
||||
case RealtimeForceChartPart.LeftAxis:
|
||||
DrawLeftAxis(drawingContext, width, height, yMax);
|
||||
break;
|
||||
case RealtimeForceChartPart.Plot:
|
||||
DrawPlot(drawingContext, width, height, xMax, yMax, samples);
|
||||
break;
|
||||
case RealtimeForceChartPart.RightAxis:
|
||||
DrawRightAxis(drawingContext, width, height, yMax);
|
||||
break;
|
||||
default:
|
||||
DrawFullChart(drawingContext, width, height, xMax, yMax, samples);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFullChart(DrawingContext drawingContext, double width, double height, double xMax, double yMax, IReadOnlyList<ChartPoint> samples)
|
||||
{
|
||||
var plot = new Rect(
|
||||
FullChartLeftMargin,
|
||||
PlotTop,
|
||||
Math.Max(1, width - FullChartLeftMargin - FullChartRightMargin),
|
||||
Math.Max(1, height - PlotTop - PlotBottomMargin));
|
||||
var currentPoint = ReadPoints(CurrentPointSamples).LastOrDefault();
|
||||
|
||||
DrawPlotGridAndXAxis(drawingContext, plot, xMax, yMax);
|
||||
DrawYAxisLabels(drawingContext, plot, yMax, leftAxis: true, labelX: plot.Left - 10);
|
||||
DrawYAxisLabels(drawingContext, plot, yMax, leftAxis: false, labelX: plot.Right + 8);
|
||||
DrawReferenceLine(drawingContext, plot, xMax, yMax, ReadLineValue(PeakLineSamples), _peakPen);
|
||||
DrawReferenceLine(drawingContext, plot, xMax, yMax, ReadLineValue(AverageLineSamples), _averagePen);
|
||||
DrawCurve(drawingContext, plot, xMax, yMax, samples);
|
||||
DrawCurrentPoint(drawingContext, plot, xMax, yMax, currentPoint);
|
||||
DrawChartTitles(drawingContext, plot);
|
||||
}
|
||||
|
||||
if (currentPoint is not null)
|
||||
{
|
||||
var point = ToScreen(currentPoint, plot, xMax, yMax);
|
||||
drawingContext.DrawEllipse(_pointBrush, _pointPen, point, 5.5, 5.5);
|
||||
}
|
||||
private void DrawPlot(DrawingContext drawingContext, double width, double height, double xMax, double yMax, IReadOnlyList<ChartPoint> samples)
|
||||
{
|
||||
var plot = CreateSplitPlotRect(width, height);
|
||||
var currentPoint = ReadPoints(CurrentPointSamples).LastOrDefault();
|
||||
|
||||
DrawPlotGridAndXAxis(drawingContext, plot, xMax, yMax);
|
||||
DrawReferenceLine(drawingContext, plot, xMax, yMax, ReadLineValue(PeakLineSamples), _peakPen);
|
||||
DrawReferenceLine(drawingContext, plot, xMax, yMax, ReadLineValue(AverageLineSamples), _averagePen);
|
||||
DrawCurve(drawingContext, plot, xMax, yMax, samples);
|
||||
DrawCurrentPoint(drawingContext, plot, xMax, yMax, currentPoint);
|
||||
}
|
||||
|
||||
private void DrawLeftAxis(DrawingContext drawingContext, double width, double height, double yMax)
|
||||
{
|
||||
var plot = CreateAxisPlotRect(width, height);
|
||||
DrawYAxisLabels(drawingContext, plot, yMax, leftAxis: true, labelX: width - 10);
|
||||
DrawText(drawingContext, "Friction force [N]", Math.Max(8, width - 116), plot.Top - 25, 12, _titleBrush);
|
||||
drawingContext.DrawLine(_axisPen, new Point(width - 0.5, plot.Top), new Point(width - 0.5, plot.Bottom));
|
||||
}
|
||||
|
||||
private void DrawRightAxis(DrawingContext drawingContext, double width, double height, double yMax)
|
||||
{
|
||||
var plot = CreateAxisPlotRect(width, height);
|
||||
DrawYAxisLabels(drawingContext, plot, yMax, leftAxis: false, labelX: 10);
|
||||
DrawText(drawingContext, "Coefficient of friction", 10, plot.Top - 25, 11, _titleBrush);
|
||||
drawingContext.DrawLine(_axisPen, new Point(0.5, plot.Top), new Point(0.5, plot.Bottom));
|
||||
}
|
||||
|
||||
private static void OnCollectionSourceChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
|
||||
@@ -171,7 +248,7 @@ public sealed class RealtimeForceChart : FrameworkElement
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
private void DrawGridAndAxes(DrawingContext drawingContext, Rect plot, double xMax, double yMax)
|
||||
private void DrawPlotGridAndXAxis(DrawingContext drawingContext, Rect plot, double xMax, double yMax)
|
||||
{
|
||||
drawingContext.DrawRectangle(null, _axisPen, plot);
|
||||
|
||||
@@ -203,7 +280,6 @@ public sealed class RealtimeForceChart : FrameworkElement
|
||||
|
||||
const int yDivisions = 10;
|
||||
const int yMinorDivisions = 3;
|
||||
var normalForceN = ResolveNormalForceN();
|
||||
for (var index = 0; index <= yDivisions; index++)
|
||||
{
|
||||
var y = plot.Bottom - plot.Height * index / yDivisions;
|
||||
@@ -216,30 +292,78 @@ public sealed class RealtimeForceChart : FrameworkElement
|
||||
drawingContext.DrawLine(_minorGridPen, new Point(plot.Left, minorY), new Point(plot.Right, minorY));
|
||||
}
|
||||
}
|
||||
|
||||
DrawRightAlignedText(
|
||||
drawingContext,
|
||||
FormatForceNewtonLabel(yMax * index / yDivisions),
|
||||
plot.Left - 10,
|
||||
y - 8,
|
||||
11,
|
||||
_labelBrush,
|
||||
4);
|
||||
DrawText(
|
||||
drawingContext,
|
||||
FormatCoefficientLabel(yMax * index / yDivisions, normalForceN),
|
||||
plot.Right + 8,
|
||||
y - 8,
|
||||
11,
|
||||
_labelBrush);
|
||||
}
|
||||
|
||||
var xAxisTitleY = Math.Min(plot.Bottom + 30, ActualHeight - 18);
|
||||
DrawText(drawingContext, "Time [sec]", plot.Right - 58, xAxisTitleY, 12, _titleBrush);
|
||||
}
|
||||
|
||||
private void DrawYAxisLabels(DrawingContext drawingContext, Rect plot, double yMax, bool leftAxis, double labelX)
|
||||
{
|
||||
const int yDivisions = 10;
|
||||
var normalForceN = ResolveNormalForceN();
|
||||
for (var index = 0; index <= yDivisions; index++)
|
||||
{
|
||||
var y = plot.Bottom - plot.Height * index / yDivisions;
|
||||
var value = yMax * index / yDivisions;
|
||||
if (leftAxis)
|
||||
{
|
||||
DrawRightAlignedText(
|
||||
drawingContext,
|
||||
FormatForceNewtonLabel(value),
|
||||
labelX,
|
||||
y - 8,
|
||||
11,
|
||||
_labelBrush,
|
||||
4);
|
||||
continue;
|
||||
}
|
||||
|
||||
DrawText(
|
||||
drawingContext,
|
||||
FormatCoefficientLabel(value, normalForceN),
|
||||
labelX,
|
||||
y - 8,
|
||||
11,
|
||||
_labelBrush);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawChartTitles(DrawingContext drawingContext, Rect plot)
|
||||
{
|
||||
DrawText(drawingContext, "Friction force [N]", plot.Left, plot.Top - 25, 12, _titleBrush);
|
||||
DrawText(drawingContext, "Coefficient of friction", plot.Right - 8, plot.Top - 25, 11, _titleBrush);
|
||||
}
|
||||
|
||||
private void DrawCurrentPoint(DrawingContext drawingContext, Rect plot, double xMax, double yMax, ChartPoint? currentPoint)
|
||||
{
|
||||
if (currentPoint is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var point = ToScreen(currentPoint, plot, xMax, yMax);
|
||||
drawingContext.DrawEllipse(_pointBrush, _pointPen, point, 5.5, 5.5);
|
||||
}
|
||||
|
||||
private static Rect CreateSplitPlotRect(double width, double height)
|
||||
{
|
||||
return new Rect(
|
||||
0,
|
||||
PlotTop,
|
||||
Math.Max(1, width),
|
||||
Math.Max(1, height - PlotTop - PlotBottomMargin));
|
||||
}
|
||||
|
||||
private static Rect CreateAxisPlotRect(double width, double height)
|
||||
{
|
||||
return new Rect(
|
||||
0,
|
||||
PlotTop,
|
||||
Math.Max(1, width),
|
||||
Math.Max(1, height - PlotTop - PlotBottomMargin));
|
||||
}
|
||||
|
||||
private void DrawReferenceLine(DrawingContext drawingContext, Rect plot, double xMax, double yMax, double? value, Pen pen)
|
||||
{
|
||||
if (value is null || value <= 0 || !IsFinite(value.Value))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -58,8 +59,11 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
||||
private static readonly TimeSpan RealtimeChartRefreshInterval = TimeSpan.FromMilliseconds(250);
|
||||
private const double RealtimeChartTimeStepSeconds = 0.05;
|
||||
private const double RealtimeChartXAxisPaddingSeconds = 1;
|
||||
private const double RealtimeChartMinimumCanvasWidth = 900;
|
||||
private const double RealtimeChartMinimumCanvasWidth = 1;
|
||||
private const double RealtimeChartPixelsPerSecond = 18;
|
||||
private const double RealtimeChartMinimumZoomScale = 0.000001;
|
||||
private const double RealtimeChartMaximumZoomScale = 4;
|
||||
private const double RealtimeChartZoomStep = 1.25;
|
||||
private const double MinimumEmptyChartXAxisMaxLimit = 1;
|
||||
private const double EmptyChartYAxisMaxLimit = 0.5;
|
||||
private const double EmptyChartPointForceN = 0.001;
|
||||
@@ -83,6 +87,9 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
||||
private readonly RelayCommand _completeCalibrationCommand;
|
||||
private readonly RelayCommand _exportReportCommand;
|
||||
private readonly RelayCommand _clearHistoryCommand;
|
||||
private readonly RelayCommand _realtimeChartZoomInCommand;
|
||||
private readonly RelayCommand _realtimeChartZoomOutCommand;
|
||||
private readonly RelayCommand _realtimeChartResetZoomCommand;
|
||||
private readonly TestDataRepository _dataRepository;
|
||||
private readonly RunExportService _runExportService;
|
||||
private readonly ObservableCollection<ObservablePoint> _forceSamples = [];
|
||||
@@ -155,6 +162,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
||||
private int _nextReciprocatingRecordIndex = 1;
|
||||
private double _lastForceXAxisMaxLimit;
|
||||
private double _lastForceYAxisMaxLimit;
|
||||
private double _realtimeChartZoomScale = 1;
|
||||
|
||||
public MainViewModel()
|
||||
{
|
||||
@@ -189,6 +197,9 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
||||
_completeCalibrationCommand = new RelayCommand(CompleteCalibration, () => SelectedCalibrationItem is not null);
|
||||
_exportReportCommand = new RelayCommand(ExportHistoricalComparisonReport, CanExportHistoricalReport);
|
||||
_clearHistoryCommand = new RelayCommand(ClearHistoryData, CanClearHistoryStable);
|
||||
_realtimeChartZoomInCommand = new RelayCommand(ZoomInRealtimeChart, () => _realtimeChartZoomScale < RealtimeChartMaximumZoomScale);
|
||||
_realtimeChartZoomOutCommand = new RelayCommand(ZoomOutRealtimeChart);
|
||||
_realtimeChartResetZoomCommand = new RelayCommand(ResetRealtimeChartZoom, () => Math.Abs(_realtimeChartZoomScale - 1) > 0.001);
|
||||
|
||||
_timer = new DispatcherTimer
|
||||
{
|
||||
@@ -312,10 +323,14 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
||||
|
||||
public double RealtimeChartXMax => _lastForceXAxisMaxLimit;
|
||||
|
||||
public double RealtimeChartCanvasWidth => Math.Max(RealtimeChartMinimumCanvasWidth, _lastForceXAxisMaxLimit * RealtimeChartPixelsPerSecond);
|
||||
public double RealtimeChartCanvasWidth => Math.Max(
|
||||
RealtimeChartMinimumCanvasWidth,
|
||||
_lastForceXAxisMaxLimit * RealtimeChartPixelsPerSecond * _realtimeChartZoomScale);
|
||||
|
||||
public double RealtimeChartYMax => _lastForceYAxisMaxLimit;
|
||||
|
||||
public string RealtimeChartZoomText => FormatRealtimeChartZoomText(_realtimeChartZoomScale);
|
||||
|
||||
public RelayCommand StartCommand => _startCommand;
|
||||
|
||||
public RelayCommand PauseCommand => _pauseCommand;
|
||||
@@ -342,6 +357,26 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
||||
|
||||
public RelayCommand ClearHistoryCommand => _clearHistoryCommand;
|
||||
|
||||
public RelayCommand RealtimeChartZoomInCommand => _realtimeChartZoomInCommand;
|
||||
|
||||
public RelayCommand RealtimeChartZoomOutCommand => _realtimeChartZoomOutCommand;
|
||||
|
||||
public RelayCommand RealtimeChartResetZoomCommand => _realtimeChartResetZoomCommand;
|
||||
|
||||
public void ZoomRealtimeChartFromWheel(int wheelDelta)
|
||||
{
|
||||
if (wheelDelta > 0)
|
||||
{
|
||||
ZoomInRealtimeChart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (wheelDelta < 0)
|
||||
{
|
||||
ZoomOutRealtimeChart();
|
||||
}
|
||||
}
|
||||
|
||||
public void BeginTableMotion(string direction)
|
||||
{
|
||||
if (!TryGetTableMotionCoil(direction, out var actionName, out var coilAddress))
|
||||
@@ -1463,6 +1498,49 @@ public sealed class MainViewModel : ObservableObject, IDisposable
|
||||
];
|
||||
}
|
||||
|
||||
private void ZoomInRealtimeChart()
|
||||
{
|
||||
SetRealtimeChartZoom(_realtimeChartZoomScale * RealtimeChartZoomStep);
|
||||
}
|
||||
|
||||
private void ZoomOutRealtimeChart()
|
||||
{
|
||||
SetRealtimeChartZoom(_realtimeChartZoomScale / RealtimeChartZoomStep);
|
||||
}
|
||||
|
||||
private void ResetRealtimeChartZoom()
|
||||
{
|
||||
SetRealtimeChartZoom(1);
|
||||
}
|
||||
|
||||
private void SetRealtimeChartZoom(double zoomScale)
|
||||
{
|
||||
var nextZoomScale = Math.Clamp(zoomScale, RealtimeChartMinimumZoomScale, RealtimeChartMaximumZoomScale);
|
||||
var tolerance = Math.Max(0.000000000001, Math.Abs(_realtimeChartZoomScale) * 0.000000001);
|
||||
if (Math.Abs(nextZoomScale - _realtimeChartZoomScale) <= tolerance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_realtimeChartZoomScale = nextZoomScale;
|
||||
OnPropertyChanged(nameof(RealtimeChartCanvasWidth));
|
||||
OnPropertyChanged(nameof(RealtimeChartZoomText));
|
||||
_realtimeChartZoomInCommand.RaiseCanExecuteChanged();
|
||||
_realtimeChartZoomOutCommand.RaiseCanExecuteChanged();
|
||||
_realtimeChartResetZoomCommand.RaiseCanExecuteChanged();
|
||||
NotifyRealtimeChartChanged();
|
||||
}
|
||||
|
||||
private static string FormatRealtimeChartZoomText(double zoomScale)
|
||||
{
|
||||
return zoomScale switch
|
||||
{
|
||||
>= 0.1 => zoomScale.ToString("P0", CultureInfo.CurrentCulture),
|
||||
>= 0.01 => zoomScale.ToString("P1", CultureInfo.CurrentCulture),
|
||||
_ => zoomScale.ToString("P2", CultureInfo.CurrentCulture)
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateReferenceLine(ObservableCollection<ObservablePoint> target, double y)
|
||||
{
|
||||
if (y <= 0)
|
||||
|
||||
Reference in New Issue
Block a user