From 9e1149d6b1fd0d954c87524a04c4133b2de33919 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 03:43:52 +0000 Subject: [PATCH 01/11] Initial plan From b84034694a8346002ba66480e89c98c80ce3d3b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 03:52:12 +0000 Subject: [PATCH 02/11] Implement Phase 4.0: sketch-style curve editing with ViewModel logic, view integration, and tests Co-authored-by: jordanrobot <119146+jordanrobot@users.noreply.github.com> --- .../Models/UserPreferences.cs | 10 +- .../ViewModels/ChartViewModel.cs | 192 +++++++++++++++ .../Views/ChartView.axaml.cs | 95 +++++++- .../ViewModels/ChartViewModelTests.cs | 226 ++++++++++++++++++ 4 files changed, 521 insertions(+), 2 deletions(-) diff --git a/src/MotorEditor.Avalonia/Models/UserPreferences.cs b/src/MotorEditor.Avalonia/Models/UserPreferences.cs index 6c0fc04..46fb979 100644 --- a/src/MotorEditor.Avalonia/Models/UserPreferences.cs +++ b/src/MotorEditor.Avalonia/Models/UserPreferences.cs @@ -43,6 +43,13 @@ public sealed class UserPreferences /// public bool ShowVoltageMaxSpeedLine { get; set; } = true; + /// + /// Torque snap increment used during sketch editing. The torque value + /// recorded when the user sketches on the chart is rounded to the + /// nearest multiple of this value. Defaults to 0.2. + /// + public decimal TorqueSnapIncrement { get; set; } = 0.2m; + /// /// Creates a copy of the current preferences. /// @@ -54,7 +61,8 @@ public UserPreferences Clone() Theme = Theme, CurveColors = new List(CurveColors), ShowMotorRatedSpeedLine = ShowMotorRatedSpeedLine, - ShowVoltageMaxSpeedLine = ShowVoltageMaxSpeedLine + ShowVoltageMaxSpeedLine = ShowVoltageMaxSpeedLine, + TorqueSnapIncrement = TorqueSnapIncrement }; } } diff --git a/src/MotorEditor.Avalonia/ViewModels/ChartViewModel.cs b/src/MotorEditor.Avalonia/ViewModels/ChartViewModel.cs index 6afc511..5fdc5a6 100644 --- a/src/MotorEditor.Avalonia/ViewModels/ChartViewModel.cs +++ b/src/MotorEditor.Avalonia/ViewModels/ChartViewModel.cs @@ -98,6 +98,8 @@ public partial class ChartViewModel : ViewModelBase private string? _underlayImagePath; private double _underlayAnchorX; private double _underlayAnchorY; + private string? _sketchEditSeriesName; + private decimal _torqueSnapIncrement = 0.2m; [ObservableProperty] private ObservableCollection _series = []; @@ -1693,4 +1695,194 @@ private double RoundToNiceValue(double value, bool roundUp, bool isPowerValue = return result; } + + /// + /// Gets the name of the curve series currently in sketch-edit mode, + /// or null if no series is being sketch-edited. + /// Only one series can be in sketch-edit mode at a time. + /// + public string? SketchEditSeriesName + { + get => _sketchEditSeriesName; + private set + { + if (string.Equals(_sketchEditSeriesName, value, StringComparison.Ordinal)) + { + return; + } + + _sketchEditSeriesName = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsSketchEditActive)); + } + } + + /// + /// Gets a value indicating whether sketch-edit mode is currently active + /// for any curve series. + /// + public bool IsSketchEditActive => !string.IsNullOrEmpty(_sketchEditSeriesName); + + /// + /// Gets or sets the torque snap increment used during sketch editing. + /// The torque value is rounded to the nearest multiple of this value. + /// Must be greater than zero. Defaults to 0.2. + /// + public decimal TorqueSnapIncrement + { + get => _torqueSnapIncrement; + set + { + if (value <= 0) + { + return; + } + + if (_torqueSnapIncrement == value) + { + return; + } + + _torqueSnapIncrement = value; + OnPropertyChanged(); + } + } + + /// + /// Activates sketch-edit mode for the specified curve series. + /// Any previously active sketch-edit series is deactivated first. + /// + /// The name of the curve series to edit. + /// Must match a series in the current voltage configuration. + public void SetSketchEditSeries(string seriesName) + { + if (string.IsNullOrWhiteSpace(seriesName)) + { + return; + } + + if (_currentVoltage is null) + { + return; + } + + var curve = _currentVoltage.Curves.FirstOrDefault(c => c.Name == seriesName); + if (curve is null) + { + return; + } + + SketchEditSeriesName = seriesName; + } + + /// + /// Deactivates sketch-edit mode. + /// + public void ClearSketchEditSeries() + { + SketchEditSeriesName = null; + } + + /// + /// Applies a sketch point to the active sketch-edit curve series. + /// The value is snapped to the nearest + /// speed (RPM) data point in the series, and + /// is rounded to the nearest . + /// + /// The X coordinate in chart data space (RPM). + /// The Y coordinate in chart data space (torque). + /// true if a data point was modified; otherwise false. + public bool ApplySketchPoint(double chartX, double chartY) + { + if (!IsSketchEditActive || _currentVoltage is null) + { + return false; + } + + var curve = _currentVoltage.Curves.FirstOrDefault(c => c.Name == _sketchEditSeriesName); + if (curve is null || curve.Data.Count == 0) + { + return false; + } + + // Snap X to the nearest speed data point. + var nearestIndex = FindNearestSpeedIndex(curve, chartX); + if (nearestIndex < 0) + { + return false; + } + + // Snap Y to the nearest torque increment. + var snappedTorque = SnapTorque((decimal)chartY, _torqueSnapIncrement); + + var point = curve.Data[nearestIndex]; + + // No change needed if the torque is already at the snapped value. + if (point.Torque == snappedTorque) + { + return false; + } + + if (_undoStack is not null) + { + var command = new EditPointCommand(curve, nearestIndex, point.Rpm, snappedTorque); + _undoStack.PushAndExecute(command); + } + else + { + point.Torque = snappedTorque; + } + + // Update the cached observable point so the chart line updates + // without a full rebuild. + if (_seriesDataCache.TryGetValue(curve.Name, out var cachedPoints) + && nearestIndex < cachedPoints.Count) + { + cachedPoints[nearestIndex].Y = (double)snappedTorque; + } + + DataChanged?.Invoke(this, EventArgs.Empty); + return true; + } + + /// + /// Finds the index of the data point in + /// whose RPM is closest to . + /// + internal static int FindNearestSpeedIndex(Curve curve, double rpm) + { + if (curve.Data.Count == 0) + { + return -1; + } + + var bestIndex = 0; + var bestDist = Math.Abs((double)curve.Data[0].Rpm - rpm); + + for (var i = 1; i < curve.Data.Count; i++) + { + var dist = Math.Abs((double)curve.Data[i].Rpm - rpm); + if (dist < bestDist) + { + bestDist = dist; + bestIndex = i; + } + } + + return bestIndex; + } + + /// + /// Rounds to the nearest multiple of + /// . + /// + internal static decimal SnapTorque(decimal torque, decimal increment) + { + if (increment <= 0) + { + return torque; + } + + return Math.Round(torque / increment, MidpointRounding.AwayFromZero) * increment; + } } diff --git a/src/MotorEditor.Avalonia/Views/ChartView.axaml.cs b/src/MotorEditor.Avalonia/Views/ChartView.axaml.cs index 360c989..8fad983 100644 --- a/src/MotorEditor.Avalonia/Views/ChartView.axaml.cs +++ b/src/MotorEditor.Avalonia/Views/ChartView.axaml.cs @@ -23,6 +23,7 @@ public partial class ChartView : UserControl { private ChartViewModel? _chartViewModel; private bool _isUnderlayDragging; + private bool _isSketchDragging; private Point _dragStart; private double _startOffsetX; private double _startOffsetY; @@ -86,6 +87,20 @@ private void OnChartPointerPressed(object? sender, PointerPressedEventArgs e) return; } + // When sketch-edit mode is active, apply the sketch point + // and begin tracking the drag. + if (vm.IsSketchEditActive) + { + if (TryApplySketchAtPixel(vm, e.GetPosition(TorqueChart))) + { + _isSketchDragging = true; + e.Pointer.Capture(TorqueChart); + e.Handled = true; + } + + return; + } + // Use the underlying chart to find the nearest point under the // cursor within a reasonable radius. var chart = TorqueChart.CoreChart; @@ -143,7 +158,20 @@ private void OnChartPointerPressed(object? sender, PointerPressedEventArgs e) private void OnChartPointerMoved(object? sender, PointerEventArgs e) { - if (!_isUnderlayDragging || _chartViewModel is null) + if (_chartViewModel is null) + { + return; + } + + // Continue sketch-edit drag: apply sketch point as the mouse moves. + if (_isSketchDragging && _chartViewModel.IsSketchEditActive) + { + TryApplySketchAtPixel(_chartViewModel, e.GetPosition(TorqueChart)); + e.Handled = true; + return; + } + + if (!_isUnderlayDragging) { return; } @@ -175,6 +203,14 @@ private void OnChartPointerMoved(object? sender, PointerEventArgs e) private void OnChartPointerReleased(object? sender, PointerEventArgs e) { + if (_isSketchDragging) + { + _isSketchDragging = false; + e.Pointer.Capture(null); + e.Handled = true; + return; + } + if (!_isUnderlayDragging) { return; @@ -188,6 +224,7 @@ private void OnChartPointerReleased(object? sender, PointerEventArgs e) private void OnChartPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) { _isUnderlayDragging = false; + _isSketchDragging = false; } private void OnChartViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -245,6 +282,62 @@ _chartViewModel.UnderlayImage is null || return true; } + /// + /// Converts a pixel position on the chart to data-space coordinates + /// and forwards it to the view model's sketch-edit logic. + /// + private bool TryApplySketchAtPixel(ChartViewModel vm, Point pixelPosition) + { + var chart = TorqueChart.CoreChart; + if (chart is null) + { + return false; + } + + var drawLocation = chart.DrawMarginLocation; + var drawSize = chart.DrawMarginSize; + if (drawSize.Width <= 0 || drawSize.Height <= 0) + { + return false; + } + + // Only apply when the pointer is within the draw margin area. + if (pixelPosition.X < drawLocation.X || + pixelPosition.X > drawLocation.X + drawSize.Width || + pixelPosition.Y < drawLocation.Y || + pixelPosition.Y > drawLocation.Y + drawSize.Height) + { + return false; + } + + // Use the primary X and Y axes to convert pixel to data coordinates. + var xAxes = vm.XAxes; + var yAxes = vm.YAxes; + if (xAxes.Length == 0 || yAxes.Length == 0) + { + return false; + } + + var xAxis = xAxes[0]; + var yAxis = yAxes[0]; + + var xMin = xAxis.MinLimit ?? 0; + var xMax = xAxis.MaxLimit ?? 1; + var yMin = yAxis.MinLimit ?? 0; + var yMax = yAxis.MaxLimit ?? 1; + + if (xMax - xMin <= 0 || yMax - yMin <= 0) + { + return false; + } + + // Linear interpolation from pixel space to data space. + var chartX = xMin + (pixelPosition.X - drawLocation.X) / drawSize.Width * (xMax - xMin); + var chartY = yMax - (pixelPosition.Y - drawLocation.Y) / drawSize.Height * (yMax - yMin); + + return vm.ApplySketchPoint(chartX, chartY); + } + private void QueueUnderlayLayout() { if (_underlayLayoutQueued) diff --git a/tests/CurveEditor.Tests/ViewModels/ChartViewModelTests.cs b/tests/CurveEditor.Tests/ViewModels/ChartViewModelTests.cs index 60ebe8f..d5a41a2 100644 --- a/tests/CurveEditor.Tests/ViewModels/ChartViewModelTests.cs +++ b/tests/CurveEditor.Tests/ViewModels/ChartViewModelTests.cs @@ -996,4 +996,230 @@ public void TorqueAxisLabels_UseWholeNumbersForLargeValues(string torqueUnit) Assert.DoesNotContain(".", label100); Assert.DoesNotContain(".", label50); } + + // --- Sketch-edit tests --- + + [Fact] + public void SetSketchEditSeries_WithValidName_ActivatesSketchEdit() + { + var viewModel = new ChartViewModel(); + viewModel.CurrentVoltage = CreateTestVoltage(); + + viewModel.SetSketchEditSeries("Peak"); + + Assert.True(viewModel.IsSketchEditActive); + Assert.Equal("Peak", viewModel.SketchEditSeriesName); + } + + [Fact] + public void SetSketchEditSeries_WithInvalidName_DoesNotActivate() + { + var viewModel = new ChartViewModel(); + viewModel.CurrentVoltage = CreateTestVoltage(); + + viewModel.SetSketchEditSeries("NonExistent"); + + Assert.False(viewModel.IsSketchEditActive); + Assert.Null(viewModel.SketchEditSeriesName); + } + + [Fact] + public void SetSketchEditSeries_WithNullVoltage_DoesNotActivate() + { + var viewModel = new ChartViewModel(); + + viewModel.SetSketchEditSeries("Peak"); + + Assert.False(viewModel.IsSketchEditActive); + } + + [Fact] + public void ClearSketchEditSeries_DeactivatesSketchEdit() + { + var viewModel = new ChartViewModel(); + viewModel.CurrentVoltage = CreateTestVoltage(); + viewModel.SetSketchEditSeries("Peak"); + + viewModel.ClearSketchEditSeries(); + + Assert.False(viewModel.IsSketchEditActive); + Assert.Null(viewModel.SketchEditSeriesName); + } + + [Fact] + public void SetSketchEditSeries_ReplacesExisting() + { + var viewModel = new ChartViewModel(); + viewModel.CurrentVoltage = CreateTestVoltage(); + viewModel.SetSketchEditSeries("Peak"); + + viewModel.SetSketchEditSeries("Continuous"); + + Assert.Equal("Continuous", viewModel.SketchEditSeriesName); + } + + [Theory] + [InlineData(10.0, 0.2, 10.0)] + [InlineData(10.09, 0.2, 10.0)] + [InlineData(10.15, 0.2, 10.2)] + [InlineData(10.3, 0.2, 10.4)] + [InlineData(10.5, 0.5, 10.5)] + [InlineData(10.6, 0.5, 10.5)] + [InlineData(10.75, 0.5, 11.0)] + [InlineData(10.0, 1.0, 10.0)] + [InlineData(10.4, 1.0, 10.0)] + [InlineData(10.5, 1.0, 11.0)] + public void SnapTorque_RoundsToNearestIncrement(double torque, double increment, double expected) + { + var result = ChartViewModel.SnapTorque((decimal)torque, (decimal)increment); + + Assert.Equal((decimal)expected, result); + } + + [Fact] + public void SnapTorque_WithZeroIncrement_ReturnsTorqueUnchanged() + { + var result = ChartViewModel.SnapTorque(10.3m, 0m); + + Assert.Equal(10.3m, result); + } + + [Fact] + public void FindNearestSpeedIndex_ReturnsClosestIndex() + { + var curve = new Curve("Test"); + curve.InitializeData(1000, 50); + // Data points at 0%, 1%, 2%… of 1000 RPM → 0, 10, 20, 30… + + var index = ChartViewModel.FindNearestSpeedIndex(curve, 25); + + // Closest data point to 25 RPM is at index 3 (30 RPM) or index 2 (20 RPM). + // 25 is equidistant between 20 and 30, but the algorithm picks the first found with lower distance. + // At 20 RPM dist=5, at 30 RPM dist=5. Since loop iterates forward, the first match (index 2) stays. + Assert.Equal(2, index); // 20 RPM (closest or tied) + } + + [Fact] + public void FindNearestSpeedIndex_WithEmptyCurve_ReturnsNegativeOne() + { + var curve = new Curve("Empty"); + + var index = ChartViewModel.FindNearestSpeedIndex(curve, 100); + + Assert.Equal(-1, index); + } + + [Fact] + public void ApplySketchPoint_WhenNotActive_ReturnsFalse() + { + var viewModel = new ChartViewModel(); + viewModel.CurrentVoltage = CreateTestVoltage(); + + var result = viewModel.ApplySketchPoint(100, 50); + + Assert.False(result); + } + + [Fact] + public void ApplySketchPoint_WhenActive_UpdatesTorqueAndReturnsTrue() + { + var viewModel = new ChartViewModel(); + var voltage = CreateTestVoltage(); + viewModel.CurrentVoltage = voltage; + viewModel.SetSketchEditSeries("Peak"); + + // Pick an RPM that is close to a known data point. + var peakCurve = voltage.Curves.First(c => c.Name == "Peak"); + var targetRpm = (double)peakCurve.Data[5].Rpm; // 5% of 5000 = 250 RPM + var newTorque = 42.3; // Should snap to 42.4 (nearest 0.2) + + var result = viewModel.ApplySketchPoint(targetRpm, newTorque); + + Assert.True(result); + Assert.Equal(42.4m, peakCurve.Data[5].Torque); + } + + [Fact] + public void ApplySketchPoint_WithCustomSnapIncrement_SnapsCorrectly() + { + var viewModel = new ChartViewModel(); + var voltage = CreateTestVoltage(); + viewModel.CurrentVoltage = voltage; + viewModel.TorqueSnapIncrement = 0.5m; + viewModel.SetSketchEditSeries("Peak"); + + var peakCurve = voltage.Curves.First(c => c.Name == "Peak"); + var targetRpm = (double)peakCurve.Data[10].Rpm; + var newTorque = 42.3; + + viewModel.ApplySketchPoint(targetRpm, newTorque); + + Assert.Equal(42.5m, peakCurve.Data[10].Torque); + } + + [Fact] + public void ApplySketchPoint_RaisesDataChanged() + { + var viewModel = new ChartViewModel(); + var voltage = CreateTestVoltage(); + viewModel.CurrentVoltage = voltage; + viewModel.SetSketchEditSeries("Peak"); + var eventRaised = false; + viewModel.DataChanged += (_, _) => eventRaised = true; + + var peakCurve = voltage.Curves.First(c => c.Name == "Peak"); + var targetRpm = (double)peakCurve.Data[5].Rpm; + viewModel.ApplySketchPoint(targetRpm, 42.3); + + Assert.True(eventRaised); + } + + [Fact] + public void ApplySketchPoint_WhenTorqueAlreadyAtSnappedValue_ReturnsFalse() + { + var viewModel = new ChartViewModel(); + var voltage = CreateTestVoltage(); + viewModel.CurrentVoltage = voltage; + viewModel.SetSketchEditSeries("Peak"); + + var peakCurve = voltage.Curves.First(c => c.Name == "Peak"); + // Set the target torque to the current snapped value. + var currentTorque = peakCurve.Data[5].Torque; + var targetRpm = (double)peakCurve.Data[5].Rpm; + + var result = viewModel.ApplySketchPoint(targetRpm, (double)currentTorque); + + Assert.False(result); + } + + [Fact] + public void TorqueSnapIncrement_DefaultsToPointTwo() + { + var viewModel = new ChartViewModel(); + + Assert.Equal(0.2m, viewModel.TorqueSnapIncrement); + } + + [Fact] + public void TorqueSnapIncrement_RejectsZeroOrNegative() + { + var viewModel = new ChartViewModel(); + + viewModel.TorqueSnapIncrement = 0m; + Assert.Equal(0.2m, viewModel.TorqueSnapIncrement); + + viewModel.TorqueSnapIncrement = -1m; + Assert.Equal(0.2m, viewModel.TorqueSnapIncrement); + } + + [Fact] + public void SetSketchEditSeries_WithWhitespaceName_DoesNotActivate() + { + var viewModel = new ChartViewModel(); + viewModel.CurrentVoltage = CreateTestVoltage(); + + viewModel.SetSketchEditSeries(" "); + + Assert.False(viewModel.IsSketchEditActive); + } } From 2ca1f2971575e8107a287eed2d53f0ed0960a6f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 03:55:31 +0000 Subject: [PATCH 03/11] Add tracking files and mark Phase 4.0 sketch editing tasks complete in plan Co-authored-by: jordanrobot <119146+jordanrobot@users.noreply.github.com> --- .../20260219-sketch-style-editing-changes.md | 56 +++++++++++++++++++ .github/planning/phase-4-plan.md | 12 ++-- 2 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 .copilot-tracking/changes/20260219-sketch-style-editing-changes.md diff --git a/.copilot-tracking/changes/20260219-sketch-style-editing-changes.md b/.copilot-tracking/changes/20260219-sketch-style-editing-changes.md new file mode 100644 index 0000000..e1d4e54 --- /dev/null +++ b/.copilot-tracking/changes/20260219-sketch-style-editing-changes.md @@ -0,0 +1,56 @@ + +# Release Changes: Phase 4.0 Sketch-Style Curve Editing + +**Related Plan**: .github/planning/phase-4-plan.md (Section 0) +**Implementation Date**: 2026-02-19 + +## Summary + +Implements sketch-style curve editing on the chart, allowing users to click and drag to draw torque values directly onto a curve series. The mouse position is snapped to the nearest speed data point (X axis) and rounded to a configurable torque increment (Y axis, default 0.2). + +## Changes + +### Added + +- src/MotorEditor.Avalonia/ViewModels/ChartViewModel.cs - Added `SketchEditSeriesName`, `IsSketchEditActive`, `TorqueSnapIncrement` properties and `SetSketchEditSeries`, `ClearSketchEditSeries`, `ApplySketchPoint`, `FindNearestSpeedIndex`, `SnapTorque` methods for sketch-edit mode +- src/MotorEditor.Avalonia/Views/ChartView.axaml.cs - Added `_isSketchDragging` state, `TryApplySketchAtPixel` helper, and sketch-edit handling in pointer event handlers +- tests/CurveEditor.Tests/ViewModels/ChartViewModelTests.cs - Added 26 unit tests covering sketch-edit activation/deactivation, torque snapping, nearest speed index, and ApplySketchPoint behavior + +### Modified + +- src/MotorEditor.Avalonia/Models/UserPreferences.cs - Added `TorqueSnapIncrement` property (default 0.2) with clone support +- src/MotorEditor.Avalonia/Views/ChartView.axaml.cs - Extended pointer pressed/moved/released/capture-lost handlers to support sketch-edit drag interactions + +### Removed + +(none) + +## Release Summary + +**Total Files Affected**: 4 + +### Files Created (0) + +(none) + +### Files Modified (4) + +- src/MotorEditor.Avalonia/Models/UserPreferences.cs - Added TorqueSnapIncrement preference +- src/MotorEditor.Avalonia/ViewModels/ChartViewModel.cs - Added sketch-edit state management and point application logic +- src/MotorEditor.Avalonia/Views/ChartView.axaml.cs - Added sketch-edit mouse interaction handling +- tests/CurveEditor.Tests/ViewModels/ChartViewModelTests.cs - Added 26 unit tests for sketch-edit functionality + +### Files Removed (0) + +(none) + +### Dependencies & Infrastructure + +- **New Dependencies**: None +- **Updated Dependencies**: None +- **Infrastructure Changes**: None +- **Configuration Updates**: None + +### Deployment Notes + +No special deployment considerations. The sketch-edit feature is purely additive and does not change existing behavior. diff --git a/.github/planning/phase-4-plan.md b/.github/planning/phase-4-plan.md index 9f2a98a..3dfabec 100644 --- a/.github/planning/phase-4-plan.md +++ b/.github/planning/phase-4-plan.md @@ -3,12 +3,12 @@ ### 0. Sketch-Style Curve Editing - **Goal**: Enable simple, straightforward editing of a specified torque curve by clicking and dragging the mouse in the graph area. - **Steps**: - - [ ] Enable "Sketch edit" mode for a curve series. Only one curve series can be in sketch edit mode at a time. - - [ ] Implement a manner to track mouse position over the chart area in near-real time, in terms of x (speed), and y (torque). - - [ ] The speed component of the mouse position shall be rounded to the nearest speed data point in the chart field. - - [ ] The torque component of the mouse position shall be rounded to the nearest 0.2; this value shall be set-able in preferences. - - [ ] When the mouse is clicked while in the graph area, the curve with an active sketch-edit mode, will record the position of the mouse in the curve data, at the position of the x (speed) value in the dataset. E.g. if the mouse is nearest to 5% speed, and is nearest to 10.2 Nm and the user clicks, the curve series data should update such that at 5% speed that curve torque value is equal to 10.2 Nm. - - [ ] The user shall be able to click and drag the mouse over the graph, with the positions of the active sketch-edit curve being written to the curve data series as the mouse moves. Hence, the user could "sketch" over an image motor curve, cleaning up slight data errors very easily. + - [X] Enable "Sketch edit" mode for a curve series. Only one curve series can be in sketch edit mode at a time. + - [X] Implement a manner to track mouse position over the chart area in near-real time, in terms of x (speed), and y (torque). + - [X] The speed component of the mouse position shall be rounded to the nearest speed data point in the chart field. + - [X] The torque component of the mouse position shall be rounded to the nearest 0.2; this value shall be set-able in preferences. + - [X] When the mouse is clicked while in the graph area, the curve with an active sketch-edit mode, will record the position of the mouse in the curve data, at the position of the x (speed) value in the dataset. E.g. if the mouse is nearest to 5% speed, and is nearest to 10.2 Nm and the user clicks, the curve series data should update such that at 5% speed that curve torque value is equal to 10.2 Nm. + - [X] The user shall be able to click and drag the mouse over the graph, with the positions of the active sketch-edit curve being written to the curve data series as the mouse moves. Hence, the user could "sketch" over an image motor curve, cleaning up slight data errors very easily. ### 1. EQ-Style Curve Editing - **Goal**: Enable rich, EQ-style editing of torque curves directly on the chart while keeping the data grid and underlying models in sync. From 7c23960aa798fdcd26705209ef1e9427c00ce007 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 04:16:30 +0000 Subject: [PATCH 04/11] Add sketch-edit toggle button in series header and chart mode indicator Co-authored-by: jordanrobot <119146+jordanrobot@users.noreply.github.com> --- .../20260219-sketch-style-editing-changes.md | 10 ++- .../Views/ChartView.axaml | 16 +++++ .../Views/CurveDataPanel.axaml | 7 ++ .../Views/CurveDataPanel.axaml.cs | 69 +++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/.copilot-tracking/changes/20260219-sketch-style-editing-changes.md b/.copilot-tracking/changes/20260219-sketch-style-editing-changes.md index e1d4e54..b9a7b4d 100644 --- a/.copilot-tracking/changes/20260219-sketch-style-editing-changes.md +++ b/.copilot-tracking/changes/20260219-sketch-style-editing-changes.md @@ -19,7 +19,10 @@ Implements sketch-style curve editing on the chart, allowing users to click and ### Modified - src/MotorEditor.Avalonia/Models/UserPreferences.cs - Added `TorqueSnapIncrement` property (default 0.2) with clone support +- src/MotorEditor.Avalonia/Views/ChartView.axaml - Added sketch-edit mode indicator overlay showing active series name - src/MotorEditor.Avalonia/Views/ChartView.axaml.cs - Extended pointer pressed/moved/released/capture-lost handlers to support sketch-edit drag interactions +- src/MotorEditor.Avalonia/Views/CurveDataPanel.axaml - Added per-series ✏ sketch-edit toggle button in the column header +- src/MotorEditor.Avalonia/Views/CurveDataPanel.axaml.cs - Added `OnSketchEditToggleClick` and `RefreshSketchEditToggles` handlers for sketch-edit toggle ### Removed @@ -27,17 +30,20 @@ Implements sketch-style curve editing on the chart, allowing users to click and ## Release Summary -**Total Files Affected**: 4 +**Total Files Affected**: 6 ### Files Created (0) (none) -### Files Modified (4) +### Files Modified (6) - src/MotorEditor.Avalonia/Models/UserPreferences.cs - Added TorqueSnapIncrement preference - src/MotorEditor.Avalonia/ViewModels/ChartViewModel.cs - Added sketch-edit state management and point application logic +- src/MotorEditor.Avalonia/Views/ChartView.axaml - Added sketch-edit mode indicator overlay - src/MotorEditor.Avalonia/Views/ChartView.axaml.cs - Added sketch-edit mouse interaction handling +- src/MotorEditor.Avalonia/Views/CurveDataPanel.axaml - Added per-series sketch-edit toggle button in column header +- src/MotorEditor.Avalonia/Views/CurveDataPanel.axaml.cs - Added sketch-edit toggle click handler and sibling refresh logic - tests/CurveEditor.Tests/ViewModels/ChartViewModelTests.cs - Added 26 unit tests for sketch-edit functionality ### Files Removed (0) diff --git a/src/MotorEditor.Avalonia/Views/ChartView.axaml b/src/MotorEditor.Avalonia/Views/ChartView.axaml index a2fab72..6761269 100644 --- a/src/MotorEditor.Avalonia/Views/ChartView.axaml +++ b/src/MotorEditor.Avalonia/Views/ChartView.axaml @@ -74,6 +74,22 @@ VerticalAlignment="Top" Margin="0,20,0,0" IsHitTestVisible="False"/> + + + + + + + + + diff --git a/src/MotorEditor.Avalonia/Views/CurveDataPanel.axaml b/src/MotorEditor.Avalonia/Views/CurveDataPanel.axaml index 8c2134a..d52d20a 100644 --- a/src/MotorEditor.Avalonia/Views/CurveDataPanel.axaml +++ b/src/MotorEditor.Avalonia/Views/CurveDataPanel.axaml @@ -143,6 +143,13 @@ + + + +