Skip to content

Commit 9fe9361

Browse files
Merge pull request #19 from RuntimeRascal/copilot/add-eraser-tool-functionality
Add eraser tool with E/P key toggle and refactor to tool-based architecture
2 parents 76d67f1 + 2093bd0 commit 9fe9361

File tree

18 files changed

+989
-208
lines changed

18 files changed

+989
-208
lines changed

CHANGELOG.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to GhostDraw will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## v1.0.6
9+
10+
### Added
11+
- **Eraser Tool** - Remove drawing objects underneath the cursor
12+
- Press `E` to activate Eraser tool
13+
- Click and drag to erase drawings interactively
14+
- Eraser size adjusts with mouse wheel (same as brush thickness)
15+
- Intelligent intersection detection for precise erasing
16+
- Works with both polylines (pen strokes) and lines (straight line tool)
17+
- Custom eraser cursor with visual feedback
18+
- **Improved Code Quality**
19+
- Enhanced tool interface consistency
20+
21+
### Fixed
22+
- LineTool's `OnDeactivated` method now properly resets state without calling non-existent method
23+
824
## v1.0.5
925

1026
### Added
@@ -115,4 +131,3 @@ When adding new features or fixes:
115131

116132
### Fixed
117133
- Issue description and what was fixed
118-
```

Installer/GhostDraw.Installer.wixproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="WixToolset.Sdk/4.0.5">
22
<PropertyGroup>
3-
<Version Condition="'$(Version)' == ''">1.0.5</Version>
3+
<Version Condition="'$(Version)' == ''">1.0.6</Version>
44
<OutputName>GhostDrawSetup-$(Version)</OutputName>
55
<OutputType>Package</OutputType>
66
<Platform>x64</Platform>

Src/GhostDraw/App.xaml.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ protected override void OnStartup(StartupEventArgs e)
7676
_keyboardHook.ClearCanvasPressed += OnClearCanvasPressed;
7777
_keyboardHook.PenToolPressed += OnPenToolPressed;
7878
_keyboardHook.LineToolPressed += OnLineToolPressed;
79+
_keyboardHook.EraserToolPressed += OnEraserToolPressed;
7980
_keyboardHook.HelpPressed += OnHelpPressed;
8081
_keyboardHook.Start();
8182

@@ -288,6 +289,23 @@ private void OnLineToolPressed(object? sender, EventArgs e)
288289
}
289290
}
290291

292+
private void OnEraserToolPressed(object? sender, EventArgs e)
293+
{
294+
try
295+
{
296+
// Only switch to eraser tool if drawing mode is active
297+
if (_drawingManager?.IsDrawingMode == true)
298+
{
299+
_logger?.LogInformation("E pressed - selecting eraser tool");
300+
_drawingManager?.SetEraserTool();
301+
}
302+
}
303+
catch (Exception ex)
304+
{
305+
_exceptionHandler?.HandleException(ex, "Eraser tool handler");
306+
}
307+
}
308+
291309
private void OnHelpPressed(object? sender, EventArgs e)
292310
{
293311
try

Src/GhostDraw/Core/DrawTool.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,10 @@ public enum DrawTool
1616
/// <summary>
1717
/// Straight line tool - click two points to draw a line
1818
/// </summary>
19-
Line
19+
Line,
20+
21+
/// <summary>
22+
/// Eraser tool - removes drawing objects underneath the cursor
23+
/// </summary>
24+
Eraser
2025
}

Src/GhostDraw/Core/GlobalKeyboardHook.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class GlobalKeyboardHook : IDisposable
1515
private const int VK_R = 0x52; // 82 - 'R' key for clear canvas
1616
private const int VK_L = 0x4C; // 76 - 'L' key for line tool
1717
private const int VK_P = 0x50; // 80 - 'P' key for pen tool
18+
private const int VK_E = 0x45; // 69 - 'E' key for eraser tool
1819
private const int VK_F1 = 0x70; // 112 - 'F1' key for help
1920

2021
private readonly ILogger<GlobalKeyboardHook> _logger;
@@ -30,6 +31,7 @@ public class GlobalKeyboardHook : IDisposable
3031
public event EventHandler? ClearCanvasPressed;
3132
public event EventHandler? PenToolPressed;
3233
public event EventHandler? LineToolPressed;
34+
public event EventHandler? EraserToolPressed;
3335
public event EventHandler? HelpPressed;
3436

3537
// NEW: Raw key events for recorder
@@ -210,6 +212,13 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
210212
PenToolPressed?.Invoke(this, EventArgs.Empty);
211213
}
212214

215+
// Check for E key press (eraser tool)
216+
if (vkCode == VK_E && isKeyDown)
217+
{
218+
_logger.LogDebug("E key pressed - eraser tool request");
219+
EraserToolPressed?.Invoke(this, EventArgs.Empty);
220+
}
221+
213222
// Check for F1 key press (help)
214223
if (vkCode == VK_F1 && isKeyDown)
215224
{

Src/GhostDraw/Core/ServiceConfiguration.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ public static ServiceProvider ConfigureServices()
5757
// Register application services (order matters for dependencies)
5858
services.AddSingleton<AppSettingsService>();
5959
services.AddSingleton<CursorHelper>();
60+
61+
// Register drawing tools
62+
services.AddSingleton<GhostDraw.Tools.PenTool>();
63+
services.AddSingleton<GhostDraw.Tools.LineTool>();
64+
services.AddSingleton<GhostDraw.Tools.EraserTool>();
65+
6066
services.AddSingleton<OverlayWindow>();
6167
services.AddSingleton<GlobalKeyboardHook>();
6268
services.AddSingleton<DrawingManager>();

Src/GhostDraw/GhostDraw.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<UseWPF>true</UseWPF>
99
<UseWindowsForms>true</UseWindowsForms>
1010
<ApplicationIcon>Assets\favicon.ico</ApplicationIcon>
11-
<Version>1.0.5</Version>
11+
<Version>1.0.6</Version>
1212
</PropertyGroup>
1313

1414
<ItemGroup>

Src/GhostDraw/Helpers/CursorHelper.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,118 @@ public WpfCursor CreateColoredPencilCursor(string tipColorHex)
147147
}
148148
}
149149

150+
/// <summary>
151+
/// Creates an eraser cursor
152+
/// </summary>
153+
/// <returns>Custom eraser cursor</returns>
154+
public WpfCursor CreateEraserCursor()
155+
{
156+
lock (_cursorLock)
157+
{
158+
if (_disposed)
159+
{
160+
_logger.LogWarning("CreateEraserCursor called on disposed CursorHelper");
161+
return WpfCursors.Cross;
162+
}
163+
164+
try
165+
{
166+
_logger.LogDebug("Creating eraser cursor");
167+
168+
// Destroy previous cursor handle to prevent leaks
169+
if (_currentCursorHandle != nint.Zero)
170+
{
171+
try
172+
{
173+
DestroyCursor(_currentCursorHandle);
174+
_logger.LogDebug("Destroyed previous cursor handle");
175+
}
176+
catch (Exception ex)
177+
{
178+
_logger.LogError(ex, "Failed to destroy previous cursor handle");
179+
}
180+
_currentCursorHandle = nint.Zero;
181+
}
182+
183+
// Create a bitmap for the cursor (32x32 pixels)
184+
int size = 32;
185+
using (Bitmap bitmap = new Bitmap(size, size))
186+
using (Graphics g = Graphics.FromImage(bitmap))
187+
{
188+
g.SmoothingMode = SmoothingMode.AntiAlias;
189+
g.Clear(Color.Transparent);
190+
191+
// Draw eraser shape (rectangle with slight perspective)
192+
int eraserWidth = 16;
193+
int eraserHeight = 12;
194+
int eraserLeft = (size - eraserWidth) / 2;
195+
int eraserTop = (size - eraserHeight) / 2 - 2;
196+
197+
// Draw eraser body (pink/beige color like a traditional eraser)
198+
using (LinearGradientBrush eraserBrush = new LinearGradientBrush(
199+
new Rectangle(eraserLeft, eraserTop, eraserWidth, eraserHeight),
200+
Color.FromArgb(255, 220, 220),
201+
Color.FromArgb(255, 180, 180),
202+
45f))
203+
{
204+
g.FillRectangle(eraserBrush, eraserLeft, eraserTop, eraserWidth, eraserHeight);
205+
}
206+
207+
// Draw eraser outline
208+
using (Pen outlinePen = new Pen(Color.Black, 1.5f))
209+
{
210+
g.DrawRectangle(outlinePen, eraserLeft, eraserTop, eraserWidth, eraserHeight);
211+
}
212+
213+
// Draw diagonal lines to give texture
214+
using (Pen texturePen = new Pen(Color.FromArgb(100, 200, 150, 150), 1))
215+
{
216+
for (int i = 0; i < 3; i++)
217+
{
218+
int offset = i * 5;
219+
g.DrawLine(texturePen,
220+
eraserLeft + offset, eraserTop,
221+
eraserLeft + offset + 6, eraserTop + eraserHeight);
222+
}
223+
}
224+
225+
// Add highlight for depth
226+
using (Pen highlight = new Pen(Color.White, 1))
227+
{
228+
g.DrawLine(highlight, eraserLeft + 2, eraserTop + 2, eraserLeft + eraserWidth - 4, eraserTop + 2);
229+
}
230+
231+
// Draw small "eraser particles" below to indicate erasing action
232+
using (SolidBrush particleBrush = new SolidBrush(Color.FromArgb(150, 180, 180, 180)))
233+
{
234+
g.FillEllipse(particleBrush, eraserLeft + 3, eraserTop + eraserHeight + 2, 2, 2);
235+
g.FillEllipse(particleBrush, eraserLeft + 8, eraserTop + eraserHeight + 4, 2, 2);
236+
g.FillEllipse(particleBrush, eraserLeft + 12, eraserTop + eraserHeight + 3, 2, 2);
237+
}
238+
239+
// Convert bitmap to cursor with hotspot at center
240+
nint hCursor = CreateCursorFromBitmap(bitmap, size / 2, size / 2);
241+
242+
if (hCursor != nint.Zero)
243+
{
244+
_currentCursorHandle = hCursor;
245+
_logger.LogDebug("Successfully created eraser cursor (handle: {Handle})", hCursor);
246+
247+
return System.Windows.Interop.CursorInteropHelper.Create(new SafeCursorHandle(hCursor));
248+
}
249+
}
250+
251+
_logger.LogWarning("Failed to create eraser cursor, returning default");
252+
return WpfCursors.Cross;
253+
}
254+
catch (Exception ex)
255+
{
256+
_logger.LogError(ex, "Error creating eraser cursor, using default");
257+
return WpfCursors.Cross;
258+
}
259+
}
260+
}
261+
150262
/// <summary>
151263
/// Creates a crosshair cursor with color indicator for the Line tool
152264
/// </summary>

Src/GhostDraw/Managers/DrawingManager.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,31 @@ public void SetLineTool()
240240
}
241241
}
242242

243+
/// <summary>
244+
/// Sets the active tool to Eraser
245+
/// </summary>
246+
public void SetEraserTool()
247+
{
248+
try
249+
{
250+
if (_overlayWindow.IsVisible)
251+
{
252+
_appSettings.SetActiveTool(DrawTool.Eraser);
253+
_overlayWindow.OnToolChanged(DrawTool.Eraser);
254+
_logger.LogInformation("Tool set to Eraser");
255+
}
256+
else
257+
{
258+
_logger.LogDebug("SetEraserTool ignored - overlay not visible");
259+
}
260+
}
261+
catch (Exception ex)
262+
{
263+
_logger.LogError(ex, "Failed to set eraser tool");
264+
// Don't re-throw - not critical
265+
}
266+
}
267+
243268
/// <summary>
244269
/// Shows the help popup with keyboard shortcuts
245270
/// </summary>

0 commit comments

Comments
 (0)