更新2026

This commit is contained in:
GukSang.Jin
2026-05-15 09:33:09 +08:00
parent de4ed64198
commit 35340a8247
2 changed files with 231 additions and 29 deletions

View File

@@ -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))

View File

@@ -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)