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