This commit is contained in:
GukSang.Jin
2026-05-09 18:32:35 +08:00
parent 52795a3b34
commit f3d3289d51
3 changed files with 336 additions and 93 deletions

View File

@@ -797,6 +797,8 @@
LegendPosition="Hidden"
ZoomMode="None"
TooltipPosition="Hidden"
AnimationsSpeed="0:0:0"
EasingFunction="{x:Null}"
DrawMarginFrame="{x:Null}" />
</Border>
</Border>

View File

@@ -115,8 +115,7 @@ public sealed class RunExportService
{
return sample.SampleIndex > 0
&& IsFinite(sample.DisplacementMm)
&& IsFinite(sample.ForceN)
&& IsFinite(sample.SpeedMmPerMin);
&& IsFinite(sample.ForceN);
}
private static bool IsFinite(double value)
@@ -305,7 +304,10 @@ public sealed class RunExportService
worksheet.Cell(rowIndex, 16).Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss.000";
worksheet.Cell(rowIndex, 17).Value = sample.DisplacementMm;
worksheet.Cell(rowIndex, 18).Value = sample.ForceN;
worksheet.Cell(rowIndex, 19).Value = sample.SpeedMmPerMin;
if (IsFinite(sample.SpeedMmPerMin))
{
worksheet.Cell(rowIndex, 19).Value = sample.SpeedMmPerMin;
}
worksheet.Cell(rowIndex, 4).Style.Fill.BackgroundColor = ToXlColor(item.CurveColorHex);
worksheet.Cell(rowIndex, 4).Style.Font.FontColor = XLColor.White;
@@ -458,15 +460,13 @@ public sealed class RunExportService
var charts = new List<ChartDefinition>();
var perRunStartRow = Math.Max(10, runs.Count + 8);
const int chartHeight = 11;
const int chartWidth = 9;
const int chartWidth = 19;
const int rowGap = 2;
for (var index = 0; index < perRunSeries.Count; index++)
{
var chartRowGroup = index / 2;
var chartColumnGroup = index % 2;
var fromColumn = chartColumnGroup == 0 ? 0 : 10;
var fromRow = perRunStartRow + chartRowGroup * (chartHeight + rowGap);
var fromColumn = 0;
var fromRow = perRunStartRow + index * (chartHeight + rowGap);
var toColumn = fromColumn + chartWidth;
var toRow = fromRow + chartHeight;
var runSeries = perRunSeries[index];
@@ -492,7 +492,7 @@ public sealed class RunExportService
private static int CalculateChartSheetLastRow(IReadOnlyList<ExportCurveData> runs)
{
var chartCount = runs.Count(IsChartableRun);
return Math.Max(24, runs.Count + 22 + ((chartCount + 1) / 2) * 13);
return Math.Max(24, runs.Count + 22 + chartCount * 13);
}
private static void ApplyDefaultWorksheetStyle(IXLWorksheet worksheet)

View File

@@ -130,8 +130,11 @@ public sealed class MainViewModel : ObservableObject, IDisposable
private double _runningPeakForceN;
private double _kineticForceSum;
private int _kineticSampleCount;
private double _runStartHorizontalPositionMm = double.NaN;
private double _lastRealtimeChartDisplacementMm = double.NaN;
private DateTime _lastRealtimeChartRefreshAt = DateTime.MinValue;
private double _lastForceXAxisMaxLimit;
private double _lastForceYAxisMaxLimit;
public MainViewModel()
{
@@ -179,10 +182,6 @@ public sealed class MainViewModel : ObservableObject, IDisposable
var seriesStroke = new SolidColorPaint(new SKColor(0, 105, 180), 3);
var seriesGeometryFill = new SolidColorPaint(new SKColor(0, 131, 154));
var axisLabelPaint = new SolidColorPaint(new SKColor(74, 92, 108));
var axisNamePaint = new SolidColorPaint(new SKColor(33, 49, 61));
var separatorPaint = new SolidColorPaint(new SKColor(209, 219, 227), 1);
var subsectionPaint = new SolidColorPaint(new SKColor(229, 236, 242), 1);
var peakStroke = new SolidColorPaint(new SKColor(198, 107, 88), 1.5f);
var averageStroke = new SolidColorPaint(new SKColor(196, 150, 69), 1.5f);
var currentPointFill = new SolidColorPaint(new SKColor(0, 105, 180));
@@ -230,39 +229,10 @@ public sealed class MainViewModel : ObservableObject, IDisposable
}
];
ForceXAxes =
[
new Axis
{
Name = "位移 / mm",
NameTextSize = 12,
TextSize = 11,
MinLimit = 0,
MinStep = 25,
LabelsPaint = axisLabelPaint,
NamePaint = axisNamePaint,
SeparatorsPaint = separatorPaint,
SubseparatorsPaint = subsectionPaint,
Padding = new Padding(8, 0, 0, 0)
}
];
ForceYAxes =
[
new Axis
{
Name = "力值 / N",
NameTextSize = 12,
TextSize = 11,
MinLimit = 0,
MinStep = 0.1,
LabelsPaint = axisLabelPaint,
NamePaint = axisNamePaint,
SeparatorsPaint = separatorPaint,
SubseparatorsPaint = subsectionPaint,
Padding = new Padding(0, 0, 8, 0)
}
];
_lastForceXAxisMaxLimit = 1;
_lastForceYAxisMaxLimit = 0.5;
ForceXAxes = CreateForceXAxes(_lastForceXAxisMaxLimit);
ForceYAxes = CreateForceYAxes(_lastForceYAxisMaxLimit);
_kineticBand = new RectangularSection
{
@@ -841,6 +811,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
InterlockMessage = "试验已停止,可重新装样或调整参数。";
AddWarningEvent("试验被人工停止。");
RaiseStatusProperties();
RaiseCommandStates();
}
private async void Rise()
@@ -900,6 +871,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
StateBrush = BrushFromHex("#6C8E78");
InterlockMessage = "设备已复位,可重新开始试验。";
RaiseStatusProperties();
RaiseCommandStates();
}
private void CompleteCalibration()
@@ -938,37 +910,15 @@ public sealed class MainViewModel : ObservableObject, IDisposable
return;
}
_currentRunSamples.Add(new RawSampleRecord
if (!AppendRunningSample(frame, out var isCompleted))
{
RunId = _activeRunId,
SampleIndex = _currentRunSamples.Count + 1,
CapturedAt = DateTime.Now,
DisplacementMm = frame.DisplacementMm,
ForceN = CurrentForceN,
SpeedMmPerMin = frame.SpeedMmPerMin
});
AddRealtimeChartSample(frame.DisplacementMm, CurrentForceN);
UpdateCurrentPoint(frame.DisplacementMm, CurrentForceN);
UpdatePreviewResult(frame.DisplacementMm, CurrentForceN);
StaticCoefficient1 = frame.StaticCoefficient1;
KineticCoefficient1 = frame.KineticCoefficient1;
StandardDeviation1 = frame.StandardDeviation1;
StaticCoefficient2 = frame.StaticCoefficient2;
KineticCoefficient2 = frame.KineticCoefficient2;
StandardDeviation2 = frame.StandardDeviation2;
var activeReplicateCount = GetActiveReplicateCount();
CurrentStaticCoefficient = ResolveRepresentativeValue(activeReplicateCount, StaticCoefficient1, StaticCoefficient2);
CurrentKineticCoefficient = ResolveRepresentativeValue(activeReplicateCount, KineticCoefficient1, KineticCoefficient2);
TrialProgressPercent = Math.Min(100, frame.DisplacementMm / Math.Max(Recipe.TravelMm, 1) * 100);
if (frame.IsCompleted)
{
RefreshRealtimeChartPresentation(force: true);
FinalizeRun();
RaiseStatusProperties();
return;
}
else
if (isCompleted)
{
RefreshRealtimeChartPresentation(force: false);
FinalizeRun();
}
RaiseStatusProperties();
@@ -993,7 +943,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
private void UpdateLiveProcessSnapshot(ProcessFrame frame)
{
CurrentForceN = Math.Max(frame.ForceN, 0.001);
CurrentDisplacementMm = frame.DisplacementMm;
CurrentDisplacementMm = frame.HorizontalPosition;
CurrentSpeedMmPerMin = frame.SpeedMmPerMin;
CurrentLiftSpeed = frame.LiftSpeed;
CurrentLiftDisplacement = frame.LiftDisplacement;
@@ -1063,6 +1013,76 @@ public sealed class MainViewModel : ObservableObject, IDisposable
_forceSamples[^1] = sample;
}
private bool AppendRunningSample(ProcessFrame frame, out bool isCompleted)
{
isCompleted = false;
if (!TryResolveRunningPosition(frame, out var horizontalPositionMm, out var travelDeltaMm))
{
AddWarningEvent("实时采样包含无效水平位置,已跳过本次曲线点。");
return false;
}
var forceN = CurrentForceN;
if (!IsFinite(forceN))
{
AddWarningEvent("实时采样包含无效力值,已跳过本次曲线点。");
return false;
}
CurrentDisplacementMm = horizontalPositionMm;
var isFirstChartSample = _forceSamples.Count == 0;
_currentRunSamples.Add(new RawSampleRecord
{
RunId = _activeRunId,
SampleIndex = _currentRunSamples.Count + 1,
CapturedAt = DateTime.Now,
DisplacementMm = horizontalPositionMm,
ForceN = forceN,
SpeedMmPerMin = frame.SpeedMmPerMin
});
AddRealtimeChartSample(horizontalPositionMm, forceN);
UpdateCurrentPoint(horizontalPositionMm, forceN);
UpdatePreviewResult(travelDeltaMm, forceN);
StaticCoefficient1 = frame.StaticCoefficient1;
KineticCoefficient1 = frame.KineticCoefficient1;
StandardDeviation1 = frame.StandardDeviation1;
StaticCoefficient2 = frame.StaticCoefficient2;
KineticCoefficient2 = frame.KineticCoefficient2;
StandardDeviation2 = frame.StandardDeviation2;
var activeReplicateCount = GetActiveReplicateCount();
CurrentStaticCoefficient = ResolveRepresentativeValue(activeReplicateCount, StaticCoefficient1, StaticCoefficient2);
CurrentKineticCoefficient = ResolveRepresentativeValue(activeReplicateCount, KineticCoefficient1, KineticCoefficient2);
TrialProgressPercent = Math.Min(100, travelDeltaMm / Math.Max(Recipe.TravelMm, 1) * 100);
isCompleted = travelDeltaMm >= GetActiveTravelMm();
RefreshRealtimeChartPresentation(force: isFirstChartSample || isCompleted);
if (isFirstChartSample)
{
_exportReportCommand.RaiseCanExecuteChanged();
}
return true;
}
private bool TryResolveRunningPosition(ProcessFrame frame, out double horizontalPositionMm, out double travelDeltaMm)
{
horizontalPositionMm = 0;
travelDeltaMm = 0;
if (!IsFinite(frame.HorizontalPosition))
{
return false;
}
horizontalPositionMm = frame.HorizontalPosition;
if (!IsFinite(_runStartHorizontalPositionMm))
{
_runStartHorizontalPositionMm = horizontalPositionMm;
}
travelDeltaMm = Math.Abs(horizontalPositionMm - _runStartHorizontalPositionMm);
return IsFinite(horizontalPositionMm) && IsFinite(travelDeltaMm);
}
private void UpdatePreviewResult(double displacementMm, double forceN)
{
_runningPeakForceN = Math.Max(_runningPeakForceN, forceN);
@@ -1116,12 +1136,155 @@ public sealed class MainViewModel : ObservableObject, IDisposable
_lastRealtimeChartRefreshAt = now;
UpdateReferenceLines();
ForceXAxes[0].MaxLimit = Math.Max(GetActiveTravelMm(), CurrentDisplacementMm + 5);
ForceYAxes[0].MaxLimit = Math.Max(CurrentPeakForceN * 1.15, 0.5);
_kineticBand.Yj = Math.Max(CurrentPeakForceN * 1.15, 0.5);
var visibleForceN = Math.Max(CurrentPeakForceN, CurrentForceN);
var yMax = Math.Max(visibleForceN * 1.15, 0.5);
var xMax = CalculateRealtimeXAxisMaxLimit();
UpdateAxisLimits(xMax, yMax);
if (UpdateKineticBand(yMax))
{
OnPropertyChanged(nameof(ForceSections));
}
}
private double CalculateRealtimeXAxisMaxLimit()
{
var sampleMax = _forceSamples.Count == 0
? 0
: _forceSamples.Max(sample => sample.X ?? 0);
var currentMax = Math.Max(CurrentDisplacementMm, CurrentHorizontalPosition);
var visibleMax = Math.Max(sampleMax, currentMax);
return Math.Max(visibleMax + 5, 1);
}
private void UpdateAxisLimits(double xMax, double yMax)
{
var changed = false;
if (ShouldUpdateAxisLimit(_lastForceXAxisMaxLimit, xMax))
{
_lastForceXAxisMaxLimit = xMax;
ForceXAxes[0].MaxLimit = xMax;
changed = true;
}
if (ShouldUpdateAxisLimit(_lastForceYAxisMaxLimit, yMax))
{
_lastForceYAxisMaxLimit = yMax;
ForceYAxes[0].MaxLimit = yMax;
changed = true;
}
if (changed)
{
OnPropertyChanged(nameof(ForceXAxes));
OnPropertyChanged(nameof(ForceYAxes));
}
}
private void SetAxisLimits(double xMax, double yMax)
{
_lastForceXAxisMaxLimit = xMax;
_lastForceYAxisMaxLimit = yMax;
ForceXAxes[0].MaxLimit = xMax;
ForceYAxes[0].MaxLimit = yMax;
OnPropertyChanged(nameof(ForceXAxes));
OnPropertyChanged(nameof(ForceYAxes));
OnPropertyChanged(nameof(ForceSections));
}
private static bool ShouldUpdateAxisLimit(double currentLimit, double nextLimit)
{
if (!IsFinite(currentLimit) || !IsFinite(nextLimit))
{
return false;
}
return nextLimit > currentLimit || currentLimit - nextLimit > Math.Max(2, currentLimit * 0.1);
}
private bool UpdateKineticBand(double yMax)
{
var travelMm = GetActiveTravelMm();
double xi;
double xj;
if (IsFinite(_runStartHorizontalPositionMm))
{
var direction = CurrentHorizontalPosition >= _runStartHorizontalPositionMm ? 1 : -1;
var kineticStart = _runStartHorizontalPositionMm + direction * travelMm * 0.35;
var kineticEnd = _runStartHorizontalPositionMm + direction * travelMm;
xi = Math.Min(kineticStart, kineticEnd);
xj = Math.Max(kineticStart, kineticEnd);
}
else
{
xi = travelMm * 0.35;
xj = travelMm;
}
var changed = HasMeaningfulChange(_kineticBand.Xi ?? double.NaN, xi)
|| HasMeaningfulChange(_kineticBand.Xj ?? double.NaN, xj)
|| HasMeaningfulChange(_kineticBand.Yj ?? double.NaN, yMax)
|| HasMeaningfulChange(_kineticBand.Yi ?? double.NaN, 0);
if (!changed)
{
return false;
}
_kineticBand.Xi = xi;
_kineticBand.Xj = xj;
_kineticBand.Yi = 0;
_kineticBand.Yj = yMax;
return true;
}
private static bool HasMeaningfulChange(double current, double next)
{
if (!IsFinite(current) || !IsFinite(next))
{
return true;
}
return Math.Abs(current - next) > 0.001;
}
private static Axis[] CreateForceXAxes(double maxLimit)
{
return
[
new Axis
{
Name = "位移 / mm",
NameTextSize = 12,
TextSize = 11,
MinLimit = 0,
MaxLimit = Math.Max(maxLimit, 1),
MinStep = 25,
LabelsPaint = new SolidColorPaint(new SKColor(74, 92, 108)),
NamePaint = new SolidColorPaint(new SKColor(33, 49, 61)),
SeparatorsPaint = new SolidColorPaint(new SKColor(209, 219, 227), 1),
SubseparatorsPaint = new SolidColorPaint(new SKColor(229, 236, 242), 1),
Padding = new Padding(8, 0, 0, 0)
}
];
}
private static Axis[] CreateForceYAxes(double maxLimit)
{
return
[
new Axis
{
Name = "力值 / N",
NameTextSize = 12,
TextSize = 11,
MinLimit = 0,
MaxLimit = Math.Max(maxLimit, 0.5),
MinStep = 0.1,
LabelsPaint = new SolidColorPaint(new SKColor(74, 92, 108)),
NamePaint = new SolidColorPaint(new SKColor(33, 49, 61)),
SeparatorsPaint = new SolidColorPaint(new SKColor(209, 219, 227), 1),
SubseparatorsPaint = new SolidColorPaint(new SKColor(229, 236, 242), 1),
Padding = new Padding(0, 0, 8, 0)
}
];
}
private void UpdateReferenceLine(ObservableCollection<ObservablePoint> target, double y)
@@ -1132,7 +1295,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
return;
}
var xEnd = Math.Max(GetDisplayRecipeSnapshot().TravelMm, CurrentDisplacementMm);
var xEnd = Math.Max(_lastForceXAxisMaxLimit, CurrentDisplacementMm);
if (target.Count == 2)
{
target[0] = new ObservablePoint(0, y);
@@ -1150,6 +1313,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
_runningPeakForceN = 0;
_kineticForceSum = 0;
_kineticSampleCount = 0;
_runStartHorizontalPositionMm = double.NaN;
_lastRealtimeChartDisplacementMm = double.NaN;
_lastRealtimeChartRefreshAt = DateTime.MinValue;
}
@@ -1343,7 +1507,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
private bool CanExportHistoricalReport()
{
return RunHistory.Count > 0;
return RunHistory.Count > 0 || HasActiveExportSamples();
}
private bool CanClearHistoryStable()
@@ -1441,14 +1605,16 @@ public sealed class MainViewModel : ObservableObject, IDisposable
TrialProgressPercent = Math.Min(100, CurrentDisplacementMm / Math.Max(data.Recipe.TravelMm, 1) * 100);
UpdateReferenceLines();
ForceXAxes[0].MaxLimit = Math.Max(data.Recipe.TravelMm, CurrentDisplacementMm + 5);
ForceYAxes[0].MaxLimit = Math.Max(CurrentPeakForceN * 1.15, 0.5);
_kineticBand.Xi = data.Recipe.TravelMm * 0.35;
_kineticBand.Xj = data.Recipe.TravelMm;
var historyMaxX = data.Samples.Count == 0
? CurrentDisplacementMm
: data.Samples.Max(sample => sample.DisplacementMm);
var yMax = Math.Max(CurrentPeakForceN * 1.15, 0.5);
SetAxisLimits(Math.Max(historyMaxX + 5, 1), yMax);
var historyStartX = data.Samples.Count == 0 ? 0 : data.Samples.Min(sample => sample.DisplacementMm);
_kineticBand.Xi = historyStartX + data.Recipe.TravelMm * 0.35;
_kineticBand.Xj = historyStartX + data.Recipe.TravelMm;
_kineticBand.Yi = 0;
_kineticBand.Yj = Math.Max(CurrentPeakForceN * 1.15, 0.5);
OnPropertyChanged(nameof(ForceXAxes));
OnPropertyChanged(nameof(ForceYAxes));
_kineticBand.Yj = yMax;
OnPropertyChanged(nameof(ForceSections));
RaiseStatusProperties();
OnPropertyChanged(nameof(AnalysisModeSummary));
@@ -1524,15 +1690,12 @@ public sealed class MainViewModel : ObservableObject, IDisposable
CurrentKineticCoefficient = 0;
TrialProgressPercent = 0;
ForceXAxes[0].MaxLimit = Math.Max(Recipe.TravelMm, 50);
ForceYAxes[0].MaxLimit = 0.5;
SetAxisLimits(1, 0.5);
_kineticBand.Xi = Recipe.TravelMm * 0.35;
_kineticBand.Xj = Recipe.TravelMm;
_kineticBand.Yi = 0;
_kineticBand.Yj = 0.5;
OnPropertyChanged(nameof(ForceXAxes));
OnPropertyChanged(nameof(ForceYAxes));
OnPropertyChanged(nameof(ForceSections));
}
@@ -1541,6 +1704,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
_activeRunId = Guid.Empty;
_activeRecipeSnapshot = null;
_activeRunStartedByPlc = false;
_runStartHorizontalPositionMm = double.NaN;
_currentRunSamples.Clear();
}
@@ -1743,6 +1907,13 @@ public sealed class MainViewModel : ObservableObject, IDisposable
}
}
var activeRunData = BuildActiveRunExportData();
if (activeRunData is not null)
{
historyData.Add(activeRunData);
hasExportableSamples = true;
}
exportData = historyData;
if (!hasExportableSamples)
{
@@ -1781,7 +1952,9 @@ public sealed class MainViewModel : ObservableObject, IDisposable
}
var workbookPath = _runExportService.ExportHistoricalComparisonReport(exportData, saveFileDialog.FileName);
foreach (var item in exportData.Where(data => data.Samples.Any(RunExportService.IsValidSample)))
foreach (var item in exportData.Where(data =>
data.Samples.Any(RunExportService.IsValidSample)
&& RunHistory.Any(run => run.RunId == data.Run.RunId)))
{
_dataRepository.UpdateExportPaths(item.Run.RunId, null, workbookPath);
item.Run.ReportExportPath = workbookPath;
@@ -1797,6 +1970,74 @@ public sealed class MainViewModel : ObservableObject, IDisposable
}
}
private bool HasActiveExportSamples()
{
return _activeRunId != Guid.Empty
&& _activeRecipeSnapshot is not null
&& _currentRunSamples.Any(RunExportService.IsValidSample);
}
private PersistedRunData? BuildActiveRunExportData()
{
if (!HasActiveExportSamples())
{
return null;
}
var activeRecipeSnapshot = _activeRecipeSnapshot;
if (activeRecipeSnapshot is null)
{
return null;
}
var activeSamples = _currentRunSamples
.Where(RunExportService.IsValidSample)
.Select(sample => new RawSampleRecord
{
RunId = sample.RunId,
SampleIndex = sample.SampleIndex,
CapturedAt = sample.CapturedAt,
DisplacementMm = sample.DisplacementMm,
ForceN = sample.ForceN,
SpeedMmPerMin = sample.SpeedMmPerMin
})
.ToArray();
if (activeSamples.Length == 0)
{
return null;
}
var activeRun = new RunRecord
{
RunId = _activeRunId,
RunIndex = NextRunIndex,
CompletedAt = DateTime.Now,
BatchNumber = activeRecipeSnapshot.BatchNumber,
TestMode = activeRecipeSnapshot.TestMode,
StaticCoefficient = CurrentStaticCoefficient,
StaticCoefficient1 = StaticCoefficient1,
StaticCoefficient2 = StaticCoefficient2,
KineticCoefficient = CurrentKineticCoefficient,
KineticCoefficient1 = KineticCoefficient1,
KineticCoefficient2 = KineticCoefficient2,
StandardDeviation = ResolveRepresentativeValue(GetActiveReplicateCount(), StandardDeviation1, StandardDeviation2),
StandardDeviation1 = StandardDeviation1,
StandardDeviation2 = StandardDeviation2,
PeakForceN = CurrentPeakForceN,
AverageForceN = CurrentAverageForceN,
Judgement = "采样中",
SampleCount = activeSamples.Length
};
return new PersistedRunData
{
Run = activeRun,
Recipe = activeRecipeSnapshot,
Samples = activeSamples
};
}
private void ClearHistoryData()
{
var historyCount = RunHistory.Count;