更新扭矩转速曲线
This commit is contained in:
@@ -768,7 +768,8 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<local:TorqueTrendControl Samples="{Binding TorqueSamples}"
|
||||
<local:TorqueTrendControl x:Name="RealtimeTorqueTrend"
|
||||
Samples="{Binding TorqueSamples}"
|
||||
MinHeight="210" />
|
||||
<TextBlock Grid.Row="1"
|
||||
Text="{Binding TorqueCurveStatusText}"
|
||||
@@ -784,6 +785,43 @@
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="#52616F"
|
||||
Margin="0,4,0,0" />
|
||||
<StackPanel Grid.Row="0"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,8,8,0">
|
||||
<Button Content="放大"
|
||||
Click="TorqueTrendZoomIn_Click"
|
||||
FontSize="12"
|
||||
Padding="9,3"
|
||||
MinHeight="28"
|
||||
Margin="0,0,5,0"
|
||||
Background="#2563EB"
|
||||
BorderBrush="#1D4ED8" />
|
||||
<Button Content="缩小"
|
||||
Click="TorqueTrendZoomOut_Click"
|
||||
FontSize="12"
|
||||
Padding="9,3"
|
||||
MinHeight="28"
|
||||
Margin="0,0,5,0"
|
||||
Background="#2563EB"
|
||||
BorderBrush="#1D4ED8" />
|
||||
<Button Content="复位视图"
|
||||
Click="TorqueTrendReset_Click"
|
||||
FontSize="12"
|
||||
Padding="9,3"
|
||||
MinHeight="28"
|
||||
Margin="0,0,5,0"
|
||||
Background="#64748B"
|
||||
BorderBrush="#475569" />
|
||||
<Button Content="全屏"
|
||||
Click="TorqueTrendFullscreen_Click"
|
||||
FontSize="12"
|
||||
Padding="9,3"
|
||||
MinHeight="28"
|
||||
Background="#0F766E"
|
||||
BorderBrush="#0F5F59" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -1120,7 +1158,7 @@
|
||||
<ColumnDefinition Width="90" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.ColumnSpan="3" Text="轴向移动量参数(2号轴)" Style="{StaticResource MetricTitle}" HorizontalAlignment="Left" />
|
||||
<TextBlock Grid.ColumnSpan="3" Text="轴向移动量参数" Style="{StaticResource MetricTitle}" HorizontalAlignment="Left" />
|
||||
|
||||
<TextBlock Grid.Row="1" Text="位移极限" Style="{StaticResource FormLabel}" Margin="0,16,0,0" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" x:Name="AxialDisplacementLimitInput" Text="{Binding AxialDisplacementLimitInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="轴向位移极限" Margin="0,16,10,0" />
|
||||
@@ -1171,7 +1209,7 @@
|
||||
<ColumnDefinition Width="90" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.ColumnSpan="3" Text="转速/扭矩参数(1号轴)" Style="{StaticResource MetricTitle}" HorizontalAlignment="Left" />
|
||||
<TextBlock Grid.ColumnSpan="3" Text="转速/扭矩参数" Style="{StaticResource MetricTitle}" HorizontalAlignment="Left" />
|
||||
|
||||
<TextBlock Grid.Row="1" Text="位移极限" Style="{StaticResource FormLabel}" Margin="0,16,0,0" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" x:Name="SpeedTorqueDisplacementLimitInput" Text="{Binding SpeedTorqueDisplacementLimitInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="转速/扭矩位移极限" Margin="0,16,10,0" />
|
||||
|
||||
@@ -239,4 +239,34 @@ public partial class MainWindow : Window
|
||||
return button.Tag is string tag
|
||||
&& Enum.TryParse(tag, ignoreCase: true, out target);
|
||||
}
|
||||
|
||||
private void TorqueTrendZoomIn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RealtimeTorqueTrend.ZoomIn();
|
||||
}
|
||||
|
||||
private void TorqueTrendZoomOut_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RealtimeTorqueTrend.ZoomOut();
|
||||
}
|
||||
|
||||
private void TorqueTrendReset_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RealtimeTorqueTrend.ResetView();
|
||||
}
|
||||
|
||||
private void TorqueTrendFullscreen_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not MainWindowViewModel viewModel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var window = new TorqueTrendWindow(viewModel.TorqueSamples, RealtimeTorqueTrend.GetViewState())
|
||||
{
|
||||
Owner = this
|
||||
};
|
||||
window.ShowDialog();
|
||||
RealtimeTorqueTrend.ApplyViewState(window.ViewState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1241,9 +1241,9 @@ public sealed class MainWindowViewModel : ObservableObject
|
||||
private void UpdateParameterSummaries()
|
||||
{
|
||||
AxialConfigSummaryText =
|
||||
$"2号轴:位移极限 {FormatDisplacement(_parameterConfig.AxialDisplacementLimit)} mm;速度 {FormatSpeedSetting(_parameterConfig.AxialSpeed)} mm/min;手动位移 {FormatDisplacement(_parameterConfig.AxialManualDisplacement)} mm;{GetActiveAxialForceSetpointName()} {FormatForce(GetActiveAxialForceSetpoint())} N";
|
||||
$"位移极限 {FormatDisplacement(_parameterConfig.AxialDisplacementLimit)} mm;速度 {FormatSpeedSetting(_parameterConfig.AxialSpeed)} mm/min;手动位移 {FormatDisplacement(_parameterConfig.AxialManualDisplacement)} mm;{GetActiveAxialForceSetpointName()} {FormatForce(GetActiveAxialForceSetpoint())} N";
|
||||
SpeedTorqueConfigSummaryText =
|
||||
$"1号轴:位移极限 {FormatDisplacement(_parameterConfig.SpeedTorqueDisplacementLimit)} mm;速度 {FormatSpeedSetting(_parameterConfig.SpeedTorqueSpeed)} mm/min;手动位移 {FormatDisplacement(_parameterConfig.SpeedTorqueManualDisplacement)} mm;低速停止 {FormatSpeed(_parameterConfig.SpeedStopThreshold)} r/min;负载转速 {FormatSpeedSetting(_parameterConfig.LoadSpeedSetting)} r/min";
|
||||
$"位移极限 {FormatDisplacement(_parameterConfig.SpeedTorqueDisplacementLimit)} mm;速度 {FormatSpeedSetting(_parameterConfig.SpeedTorqueSpeed)} mm/min;手动位移 {FormatDisplacement(_parameterConfig.SpeedTorqueManualDisplacement)} mm;低速停止 {FormatSpeed(_parameterConfig.SpeedStopThreshold)} r/min;负载转速 {FormatSpeedSetting(_parameterConfig.LoadSpeedSetting)} r/min";
|
||||
}
|
||||
|
||||
private void Export()
|
||||
|
||||
@@ -2,14 +2,40 @@ using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace DentistryHandpieces;
|
||||
|
||||
public readonly record struct TorqueTrendViewState(
|
||||
bool IsManual,
|
||||
double MinTorque,
|
||||
double MaxTorque,
|
||||
double MinSpeed,
|
||||
double MaxSpeed);
|
||||
|
||||
public sealed class TorqueTrendControl : FrameworkElement
|
||||
{
|
||||
private const string TorqueUnit = "mN.m";
|
||||
private const string SpeedUnit = "r/min";
|
||||
private const double SensorMinTorque = 0;
|
||||
private const double SensorMaxTorque = 100;
|
||||
private const double MinimumTorqueRange = 0.1;
|
||||
private const double MinimumSpeedRange = 1;
|
||||
|
||||
private bool _isManualView;
|
||||
private double _viewMinTorque = SensorMinTorque;
|
||||
private double _viewMaxTorque = SensorMaxTorque;
|
||||
private double _viewMinSpeed;
|
||||
private double _viewMaxSpeed = 1;
|
||||
private Point? _lastDragPoint;
|
||||
|
||||
public TorqueTrendControl()
|
||||
{
|
||||
Focusable = true;
|
||||
IsManipulationEnabled = true;
|
||||
Cursor = Cursors.Cross;
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SamplesProperty =
|
||||
DependencyProperty.Register(
|
||||
@@ -24,6 +50,51 @@ public sealed class TorqueTrendControl : FrameworkElement
|
||||
set => SetValue(SamplesProperty, value);
|
||||
}
|
||||
|
||||
public TorqueTrendViewState GetViewState()
|
||||
{
|
||||
return new TorqueTrendViewState(
|
||||
_isManualView,
|
||||
_viewMinTorque,
|
||||
_viewMaxTorque,
|
||||
_viewMinSpeed,
|
||||
_viewMaxSpeed);
|
||||
}
|
||||
|
||||
public void ApplyViewState(TorqueTrendViewState state)
|
||||
{
|
||||
if (!state.IsManual)
|
||||
{
|
||||
ResetView();
|
||||
return;
|
||||
}
|
||||
|
||||
_isManualView = true;
|
||||
_viewMinTorque = state.MinTorque;
|
||||
_viewMaxTorque = state.MaxTorque;
|
||||
_viewMinSpeed = state.MinSpeed;
|
||||
_viewMaxSpeed = state.MaxSpeed;
|
||||
ClampView();
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
public void ZoomIn()
|
||||
{
|
||||
Zoom(0.72, new Point(ActualWidth / 2, ActualHeight / 2));
|
||||
}
|
||||
|
||||
public void ZoomOut()
|
||||
{
|
||||
Zoom(1.38, new Point(ActualWidth / 2, ActualHeight / 2));
|
||||
}
|
||||
|
||||
public void ResetView()
|
||||
{
|
||||
_isManualView = false;
|
||||
_viewMinTorque = SensorMinTorque;
|
||||
_viewMaxTorque = SensorMaxTorque;
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
double width = double.IsInfinity(availableSize.Width) ? 360 : availableSize.Width;
|
||||
@@ -47,107 +118,107 @@ public sealed class TorqueTrendControl : FrameworkElement
|
||||
return;
|
||||
}
|
||||
|
||||
Rect plot = new(46, 14, ActualWidth - 64, ActualHeight - 38);
|
||||
var gridPen = new Pen(new SolidColorBrush(Color.FromRgb(219, 229, 238)), 1);
|
||||
for (int i = 0; i <= 4; i++)
|
||||
{
|
||||
double y = plot.Top + plot.Height * i / 4;
|
||||
drawingContext.DrawLine(gridPen, new Point(plot.Left, y), new Point(plot.Right, y));
|
||||
}
|
||||
|
||||
for (int i = 0; i <= 5; i++)
|
||||
{
|
||||
double x = plot.Left + plot.Width * i / 5;
|
||||
drawingContext.DrawLine(gridPen, new Point(x, plot.Top), new Point(x, plot.Bottom));
|
||||
}
|
||||
Rect plot = GetPlotRect();
|
||||
DrawGrid(drawingContext, plot);
|
||||
|
||||
List<TorqueSamplePayload> samples = ReadSamples();
|
||||
if (samples.Count == 0)
|
||||
{
|
||||
DrawText(drawingContext, "等待转速/扭矩数据", 15, Color.FromRgb(82, 97, 111), new Point(plot.Left + 8, plot.Top + 8));
|
||||
DrawText(drawingContext, $"转速 ({SpeedUnit})", 12, Color.FromRgb(82, 97, 111), new Point(6, plot.Top));
|
||||
DrawText(drawingContext, $"扭矩 ({TorqueUnit})", 12, Color.FromRgb(82, 97, 111), new Point(plot.Right - 82, plot.Bottom + 5));
|
||||
DrawAxisTitles(drawingContext, plot);
|
||||
return;
|
||||
}
|
||||
|
||||
List<double> torqueValues = samples.Select(sample => sample.TorqueMilliNewtonMeters).ToList();
|
||||
double minTorque = torqueValues.Min();
|
||||
double maxTorque = torqueValues.Max();
|
||||
double torqueRange = maxTorque - minTorque;
|
||||
if (torqueRange < 0.01)
|
||||
{
|
||||
torqueRange = 1;
|
||||
minTorque -= 0.5;
|
||||
maxTorque += 0.5;
|
||||
}
|
||||
else
|
||||
{
|
||||
double padding = torqueRange * 0.12;
|
||||
minTorque -= padding;
|
||||
maxTorque += padding;
|
||||
torqueRange = maxTorque - minTorque;
|
||||
}
|
||||
(double minTorque, double maxTorque, double minSpeed, double maxSpeed) = GetVisibleRanges(samples);
|
||||
DrawAxisLabels(drawingContext, plot, minTorque, maxTorque, minSpeed, maxSpeed);
|
||||
|
||||
List<double> speedValues = samples.Select(sample => sample.SpeedRpm).ToList();
|
||||
double minSpeed = speedValues.Min();
|
||||
double maxSpeed = speedValues.Max();
|
||||
double speedRange = maxSpeed - minSpeed;
|
||||
if (speedRange < 1)
|
||||
{
|
||||
double padding = Math.Max(Math.Abs(minSpeed) * 0.05, 1);
|
||||
minSpeed -= padding;
|
||||
maxSpeed += padding;
|
||||
speedRange = maxSpeed - minSpeed;
|
||||
}
|
||||
else
|
||||
{
|
||||
double padding = speedRange * 0.05;
|
||||
minSpeed -= padding;
|
||||
maxSpeed += padding;
|
||||
speedRange = maxSpeed - minSpeed;
|
||||
}
|
||||
var points = samples
|
||||
.Select(sample => ToPlotPoint(sample, plot, minTorque, maxTorque, minSpeed, maxSpeed))
|
||||
.ToList();
|
||||
|
||||
DrawText(drawingContext, maxSpeed.ToString("0", CultureInfo.InvariantCulture), 12, Color.FromRgb(82, 97, 111), new Point(8, plot.Top - 2));
|
||||
DrawText(drawingContext, minSpeed.ToString("0", CultureInfo.InvariantCulture), 12, Color.FromRgb(82, 97, 111), new Point(8, plot.Bottom - 14));
|
||||
DrawText(drawingContext, minTorque.ToString("0.##", CultureInfo.InvariantCulture), 12, Color.FromRgb(82, 97, 111), new Point(plot.Left, plot.Bottom + 5));
|
||||
DrawText(drawingContext, maxTorque.ToString("0.##", CultureInfo.InvariantCulture), 12, Color.FromRgb(82, 97, 111), new Point(plot.Right - 38, plot.Bottom + 5));
|
||||
DrawText(drawingContext, $"转速 ({SpeedUnit})", 11, Color.FromRgb(82, 97, 111), new Point(plot.Left + 4, plot.Top + 3));
|
||||
DrawText(drawingContext, $"扭矩 ({TorqueUnit})", 11, Color.FromRgb(82, 97, 111), new Point(plot.Right - 82, plot.Bottom - 18));
|
||||
|
||||
var geometry = new StreamGeometry();
|
||||
using (StreamGeometryContext context = geometry.Open())
|
||||
{
|
||||
for (int i = 0; i < samples.Count; i++)
|
||||
{
|
||||
double x = plot.Left + plot.Width * (samples[i].TorqueMilliNewtonMeters - minTorque) / torqueRange;
|
||||
double y = plot.Bottom - (samples[i].SpeedRpm - minSpeed) / speedRange * plot.Height;
|
||||
Point point = new(x, y);
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
context.BeginFigure(point, false, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.LineTo(point, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
geometry.Freeze();
|
||||
var linePen = new Pen(new SolidColorBrush(Color.FromRgb(29, 78, 216)), 2.4);
|
||||
drawingContext.DrawGeometry(null, linePen, geometry);
|
||||
drawingContext.PushClip(new RectangleGeometry(plot));
|
||||
DrawTrajectory(drawingContext, points);
|
||||
DrawSamplePoints(drawingContext, points);
|
||||
DrawCurrentPoint(drawingContext, points[^1]);
|
||||
drawingContext.Pop();
|
||||
|
||||
TorqueSamplePayload current = samples[^1];
|
||||
double currentY = plot.Bottom - (current.SpeedRpm - minSpeed) / speedRange * plot.Height;
|
||||
var currentPen = new Pen(new SolidColorBrush(Color.FromRgb(15, 118, 110)), 1.2);
|
||||
drawingContext.DrawLine(currentPen, new Point(plot.Left, currentY), new Point(plot.Right, currentY));
|
||||
DrawText(
|
||||
drawingContext,
|
||||
$"当前 {current.SpeedRpm:0} {SpeedUnit} / {current.TorqueMilliNewtonMeters:0.##} {TorqueUnit}",
|
||||
12,
|
||||
Color.FromRgb(15, 118, 110),
|
||||
new Point(plot.Left + 6, Math.Max(plot.Top + 18, currentY - 20)));
|
||||
new Point(plot.Left + 6, plot.Top + 5));
|
||||
}
|
||||
|
||||
protected override void OnMouseWheel(MouseWheelEventArgs e)
|
||||
{
|
||||
base.OnMouseWheel(e);
|
||||
Zoom(e.Delta > 0 ? 0.82 : 1.22, e.GetPosition(this));
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
|
||||
{
|
||||
base.OnMouseLeftButtonDown(e);
|
||||
Focus();
|
||||
_lastDragPoint = e.GetPosition(this);
|
||||
CaptureMouse();
|
||||
Cursor = Cursors.Hand;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override void OnMouseMove(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseMove(e);
|
||||
if (_lastDragPoint is not Point previous || e.LeftButton != MouseButtonState.Pressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Point current = e.GetPosition(this);
|
||||
Pan(current.X - previous.X, current.Y - previous.Y);
|
||||
_lastDragPoint = current;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
|
||||
{
|
||||
base.OnMouseLeftButtonUp(e);
|
||||
EndMouseDrag();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override void OnLostMouseCapture(MouseEventArgs e)
|
||||
{
|
||||
base.OnLostMouseCapture(e);
|
||||
EndMouseDrag();
|
||||
}
|
||||
|
||||
protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
|
||||
{
|
||||
base.OnManipulationStarting(e);
|
||||
e.ManipulationContainer = this;
|
||||
e.Mode = ManipulationModes.Scale | ManipulationModes.Translate;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
|
||||
{
|
||||
base.OnManipulationDelta(e);
|
||||
Vector translation = e.DeltaManipulation.Translation;
|
||||
if (Math.Abs(translation.X) > 0.01 || Math.Abs(translation.Y) > 0.01)
|
||||
{
|
||||
Pan(translation.X, translation.Y);
|
||||
}
|
||||
|
||||
double scale = Math.Sqrt(e.DeltaManipulation.Scale.X * e.DeltaManipulation.Scale.Y);
|
||||
if (double.IsFinite(scale) && Math.Abs(scale - 1) > 0.005)
|
||||
{
|
||||
Zoom(1 / scale, e.ManipulationOrigin);
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void OnSamplesChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
|
||||
@@ -171,6 +242,210 @@ public sealed class TorqueTrendControl : FrameworkElement
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
private Rect GetPlotRect()
|
||||
{
|
||||
return new Rect(52, 14, Math.Max(1, ActualWidth - 70), Math.Max(1, ActualHeight - 42));
|
||||
}
|
||||
|
||||
private static void DrawGrid(DrawingContext drawingContext, Rect plot)
|
||||
{
|
||||
var gridPen = new Pen(new SolidColorBrush(Color.FromRgb(219, 229, 238)), 1);
|
||||
for (int i = 0; i <= 4; i++)
|
||||
{
|
||||
double y = plot.Top + plot.Height * i / 4;
|
||||
drawingContext.DrawLine(gridPen, new Point(plot.Left, y), new Point(plot.Right, y));
|
||||
}
|
||||
|
||||
for (int i = 0; i <= 5; i++)
|
||||
{
|
||||
double x = plot.Left + plot.Width * i / 5;
|
||||
drawingContext.DrawLine(gridPen, new Point(x, plot.Top), new Point(x, plot.Bottom));
|
||||
}
|
||||
}
|
||||
|
||||
private (double MinTorque, double MaxTorque, double MinSpeed, double MaxSpeed) GetVisibleRanges(List<TorqueSamplePayload> samples)
|
||||
{
|
||||
if (!_isManualView)
|
||||
{
|
||||
(_viewMinSpeed, _viewMaxSpeed) = GetAutoSpeedRange(samples);
|
||||
}
|
||||
|
||||
return (_viewMinTorque, _viewMaxTorque, _viewMinSpeed, _viewMaxSpeed);
|
||||
}
|
||||
|
||||
private static (double MinSpeed, double MaxSpeed) GetAutoSpeedRange(List<TorqueSamplePayload> samples)
|
||||
{
|
||||
double minSpeed = samples.Min(sample => sample.SpeedRpm);
|
||||
double maxSpeed = samples.Max(sample => sample.SpeedRpm);
|
||||
double range = maxSpeed - minSpeed;
|
||||
double padding = range < MinimumSpeedRange
|
||||
? Math.Max(Math.Abs(minSpeed) * 0.05, MinimumSpeedRange)
|
||||
: range * 0.08;
|
||||
return (minSpeed - padding, maxSpeed + padding);
|
||||
}
|
||||
|
||||
private static Point ToPlotPoint(
|
||||
TorqueSamplePayload sample,
|
||||
Rect plot,
|
||||
double minTorque,
|
||||
double maxTorque,
|
||||
double minSpeed,
|
||||
double maxSpeed)
|
||||
{
|
||||
double x = plot.Left + plot.Width * (sample.TorqueMilliNewtonMeters - minTorque) / (maxTorque - minTorque);
|
||||
double y = plot.Bottom - (sample.SpeedRpm - minSpeed) / (maxSpeed - minSpeed) * plot.Height;
|
||||
return new Point(x, y);
|
||||
}
|
||||
|
||||
private static void DrawTrajectory(DrawingContext drawingContext, IReadOnlyList<Point> points)
|
||||
{
|
||||
var geometry = new StreamGeometry();
|
||||
using (StreamGeometryContext context = geometry.Open())
|
||||
{
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
context.BeginFigure(points[i], false, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.LineTo(points[i], true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
geometry.Freeze();
|
||||
var lineBrush = new SolidColorBrush(Color.FromArgb(85, 29, 78, 216));
|
||||
drawingContext.DrawGeometry(null, new Pen(lineBrush, 1.2), geometry);
|
||||
}
|
||||
|
||||
private static void DrawSamplePoints(DrawingContext drawingContext, IReadOnlyList<Point> points)
|
||||
{
|
||||
var pointBrush = new SolidColorBrush(Color.FromArgb(175, 29, 78, 216));
|
||||
Point? lastDrawn = null;
|
||||
foreach (Point point in points)
|
||||
{
|
||||
if (lastDrawn is Point previous
|
||||
&& Math.Abs(point.X - previous.X) < 1.5
|
||||
&& Math.Abs(point.Y - previous.Y) < 1.5)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
drawingContext.DrawEllipse(pointBrush, null, point, 2.2, 2.2);
|
||||
lastDrawn = point;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawCurrentPoint(DrawingContext drawingContext, Point point)
|
||||
{
|
||||
var brush = new SolidColorBrush(Color.FromRgb(15, 118, 110));
|
||||
drawingContext.DrawEllipse(brush, new Pen(Brushes.White, 1.5), point, 5, 5);
|
||||
}
|
||||
|
||||
private void DrawAxisLabels(
|
||||
DrawingContext drawingContext,
|
||||
Rect plot,
|
||||
double minTorque,
|
||||
double maxTorque,
|
||||
double minSpeed,
|
||||
double maxSpeed)
|
||||
{
|
||||
DrawText(drawingContext, maxSpeed.ToString("0", CultureInfo.InvariantCulture), 11, Color.FromRgb(82, 97, 111), new Point(5, plot.Top - 2));
|
||||
DrawText(drawingContext, minSpeed.ToString("0", CultureInfo.InvariantCulture), 11, Color.FromRgb(82, 97, 111), new Point(5, plot.Bottom - 14));
|
||||
DrawText(drawingContext, minTorque.ToString("0.##", CultureInfo.InvariantCulture), 11, Color.FromRgb(82, 97, 111), new Point(plot.Left, plot.Bottom + 5));
|
||||
DrawText(drawingContext, maxTorque.ToString("0.##", CultureInfo.InvariantCulture), 11, Color.FromRgb(82, 97, 111), new Point(plot.Right - 34, plot.Bottom + 5));
|
||||
DrawAxisTitles(drawingContext, plot);
|
||||
}
|
||||
|
||||
private void DrawAxisTitles(DrawingContext drawingContext, Rect plot)
|
||||
{
|
||||
DrawText(drawingContext, $"转速 ({SpeedUnit})", 11, Color.FromRgb(82, 97, 111), new Point(plot.Left + 4, plot.Top + 22));
|
||||
DrawText(drawingContext, $"扭矩 ({TorqueUnit})", 11, Color.FromRgb(82, 97, 111), new Point(plot.Right - 82, plot.Bottom - 18));
|
||||
}
|
||||
|
||||
private void Zoom(double factor, Point origin)
|
||||
{
|
||||
Rect plot = GetPlotRect();
|
||||
if (plot.Width <= 1 || plot.Height <= 1 || !double.IsFinite(factor) || factor <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureManualView();
|
||||
double originX = Math.Clamp((origin.X - plot.Left) / plot.Width, 0, 1);
|
||||
double originY = Math.Clamp((plot.Bottom - origin.Y) / plot.Height, 0, 1);
|
||||
double torqueAtOrigin = _viewMinTorque + (_viewMaxTorque - _viewMinTorque) * originX;
|
||||
double speedAtOrigin = _viewMinSpeed + (_viewMaxSpeed - _viewMinSpeed) * originY;
|
||||
double torqueRange = Math.Max(MinimumTorqueRange, (_viewMaxTorque - _viewMinTorque) * factor);
|
||||
double speedRange = Math.Max(MinimumSpeedRange, (_viewMaxSpeed - _viewMinSpeed) * factor);
|
||||
|
||||
_viewMinTorque = torqueAtOrigin - torqueRange * originX;
|
||||
_viewMaxTorque = _viewMinTorque + torqueRange;
|
||||
_viewMinSpeed = speedAtOrigin - speedRange * originY;
|
||||
_viewMaxSpeed = _viewMinSpeed + speedRange;
|
||||
ClampView();
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
private void Pan(double deltaX, double deltaY)
|
||||
{
|
||||
Rect plot = GetPlotRect();
|
||||
if (plot.Width <= 1 || plot.Height <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureManualView();
|
||||
double torqueShift = -deltaX / plot.Width * (_viewMaxTorque - _viewMinTorque);
|
||||
double speedShift = deltaY / plot.Height * (_viewMaxSpeed - _viewMinSpeed);
|
||||
_viewMinTorque += torqueShift;
|
||||
_viewMaxTorque += torqueShift;
|
||||
_viewMinSpeed += speedShift;
|
||||
_viewMaxSpeed += speedShift;
|
||||
ClampView();
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
private void EnsureManualView()
|
||||
{
|
||||
if (_isManualView)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<TorqueSamplePayload> samples = ReadSamples();
|
||||
(_viewMinSpeed, _viewMaxSpeed) = samples.Count == 0 ? (0, 1) : GetAutoSpeedRange(samples);
|
||||
_isManualView = true;
|
||||
}
|
||||
|
||||
private void ClampView()
|
||||
{
|
||||
double torqueRange = Math.Clamp(_viewMaxTorque - _viewMinTorque, MinimumTorqueRange, SensorMaxTorque - SensorMinTorque);
|
||||
_viewMinTorque = Math.Clamp(_viewMinTorque, SensorMinTorque, SensorMaxTorque - torqueRange);
|
||||
_viewMaxTorque = _viewMinTorque + torqueRange;
|
||||
|
||||
if (!double.IsFinite(_viewMinSpeed)
|
||||
|| !double.IsFinite(_viewMaxSpeed)
|
||||
|| _viewMaxSpeed - _viewMinSpeed < MinimumSpeedRange)
|
||||
{
|
||||
_viewMinSpeed = 0;
|
||||
_viewMaxSpeed = MinimumSpeedRange;
|
||||
}
|
||||
}
|
||||
|
||||
private void EndMouseDrag()
|
||||
{
|
||||
_lastDragPoint = null;
|
||||
if (IsMouseCaptured)
|
||||
{
|
||||
ReleaseMouseCapture();
|
||||
}
|
||||
|
||||
Cursor = Cursors.Cross;
|
||||
}
|
||||
|
||||
private List<TorqueSamplePayload> ReadSamples()
|
||||
{
|
||||
if (Samples is null)
|
||||
@@ -182,16 +457,13 @@ public sealed class TorqueTrendControl : FrameworkElement
|
||||
foreach (object? sample in Samples)
|
||||
{
|
||||
if (sample is TorqueSamplePayload torqueSample
|
||||
&& !double.IsNaN(torqueSample.ElapsedSeconds)
|
||||
&& !double.IsInfinity(torqueSample.ElapsedSeconds)
|
||||
&& !double.IsNaN(torqueSample.SpeedRpm)
|
||||
&& !double.IsInfinity(torqueSample.SpeedRpm)
|
||||
&& !double.IsNaN(torqueSample.TorqueMilliNewtonMeters)
|
||||
&& !double.IsInfinity(torqueSample.TorqueMilliNewtonMeters))
|
||||
&& double.IsFinite(torqueSample.ElapsedSeconds)
|
||||
&& double.IsFinite(torqueSample.SpeedRpm)
|
||||
&& double.IsFinite(torqueSample.TorqueMilliNewtonMeters))
|
||||
{
|
||||
values.Add(torqueSample);
|
||||
}
|
||||
else if (sample is double value && !double.IsNaN(value) && !double.IsInfinity(value))
|
||||
else if (sample is double value && double.IsFinite(value))
|
||||
{
|
||||
values.Add(new TorqueSamplePayload { ElapsedSeconds = values.Count, SpeedRpm = values.Count, TorqueMilliNewtonMeters = value });
|
||||
}
|
||||
|
||||
76
DentistryHandpieces/TorqueTrendWindow.cs
Normal file
76
DentistryHandpieces/TorqueTrendWindow.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Collections;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace DentistryHandpieces;
|
||||
|
||||
public sealed class TorqueTrendWindow : Window
|
||||
{
|
||||
private readonly TorqueTrendControl _trendControl;
|
||||
|
||||
public TorqueTrendWindow(IEnumerable samples, TorqueTrendViewState initialViewState)
|
||||
{
|
||||
Title = "转速/扭矩实时曲线";
|
||||
WindowState = WindowState.Maximized;
|
||||
WindowStyle = WindowStyle.None;
|
||||
Background = new SolidColorBrush(Color.FromRgb(241, 245, 249));
|
||||
KeyDown += TorqueTrendWindow_KeyDown;
|
||||
|
||||
_trendControl = new TorqueTrendControl
|
||||
{
|
||||
Samples = samples,
|
||||
Margin = new Thickness(18)
|
||||
};
|
||||
_trendControl.ApplyViewState(initialViewState);
|
||||
|
||||
var toolbar = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(18, 12, 18, 0)
|
||||
};
|
||||
toolbar.Children.Add(CreateButton("放大", (_, _) => _trendControl.ZoomIn()));
|
||||
toolbar.Children.Add(CreateButton("缩小", (_, _) => _trendControl.ZoomOut()));
|
||||
toolbar.Children.Add(CreateButton("复位视图", (_, _) => _trendControl.ResetView()));
|
||||
toolbar.Children.Add(CreateButton("退出全屏", (_, _) => Close()));
|
||||
|
||||
var layout = new Grid();
|
||||
layout.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
layout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||
Grid.SetRow(toolbar, 0);
|
||||
Grid.SetRow(_trendControl, 1);
|
||||
layout.Children.Add(toolbar);
|
||||
layout.Children.Add(_trendControl);
|
||||
Content = layout;
|
||||
}
|
||||
|
||||
public TorqueTrendViewState ViewState => _trendControl.GetViewState();
|
||||
|
||||
private static Button CreateButton(string text, RoutedEventHandler click)
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
Content = text,
|
||||
MinWidth = 100,
|
||||
MinHeight = 40,
|
||||
FontSize = 15,
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
Padding = new Thickness(14, 5, 14, 5),
|
||||
Background = new SolidColorBrush(Color.FromRgb(37, 99, 235)),
|
||||
Foreground = Brushes.White,
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(29, 78, 216))
|
||||
};
|
||||
button.Click += click;
|
||||
return button;
|
||||
}
|
||||
|
||||
private void TorqueTrendWindow_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user