Skip to content

Commit ef3db1e

Browse files
Merge pull request #25 from RuntimeRascal/copilot/add-circle-tool
Add Circle/Ellipse drawing tool with bounding box approach and Shift modifier
2 parents 6f25a39 + e7877ff commit ef3db1e

File tree

16 files changed

+622
-20
lines changed

16 files changed

+622
-20
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ 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

88

9+
## v1.0.9
10+
11+
### Added
12+
- **Circle/Ellipse Tool** - Draw circles and ellipses by defining a bounding box
13+
- Press `C` to activate Circle tool
14+
- Uses bounding box approach: first click sets one corner, second click defines the opposite corner
15+
- **Hold Shift** while drawing to create perfect circles (equal width and height)
16+
- Live preview updates as you move the mouse
17+
- Respects current color and thickness settings
18+
- Custom circle cursor with corner markers
19+
- Supports right-click color cycling and mouse wheel thickness adjustment
20+
- Works seamlessly with Eraser tool
21+
- Consistent with Rectangle tool behavior for intuitive shape creation
22+
923
## v1.0.8
1024

1125
### Added

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.8</Version>
3+
<Version Condition="'$(Version)' == ''">1.0.9</Version>
44
<OutputName>GhostDrawSetup-$(Version)</OutputName>
55
<OutputType>Package</OutputType>
66
<Platform>x64</Platform>

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
## ✨ Features
2424

2525
### 🎨 **Drawing Tools**
26-
- **Multiple Drawing Tools** - Switch between Pen (freehand), Line (straight lines), Rectangle (boxes), and Eraser tools with keyboard shortcuts
26+
- **Multiple Drawing Tools** - Switch between Pen (freehand), Line (straight lines), Rectangle (boxes), Circle/Ellipse, and Eraser tools with keyboard shortcuts
27+
- **Perfect Shapes** - Hold Shift while using Circle tool to draw perfect circles
2728
- **Customizable Color Palette** - Create your own color collection and cycle through them while drawing
2829
- **Variable Brush Thickness** - Adjust brush size from 1-100px with configurable min/max ranges
2930
- **Smooth Drawing** - High-performance rendering for fluid strokes

Src/GhostDraw/App.xaml.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ protected override void OnStartup(StartupEventArgs e)
7878
_keyboardHook.LineToolPressed += OnLineToolPressed;
7979
_keyboardHook.EraserToolPressed += OnEraserToolPressed;
8080
_keyboardHook.RectangleToolPressed += OnRectangleToolPressed;
81+
_keyboardHook.CircleToolPressed += OnCircleToolPressed;
8182
_keyboardHook.HelpPressed += OnHelpPressed;
8283
_keyboardHook.ScreenshotFullPressed += OnScreenshotFullPressed;
8384
_keyboardHook.Start();
@@ -325,6 +326,23 @@ private void OnRectangleToolPressed(object? sender, EventArgs e)
325326
}
326327
}
327328

329+
private void OnCircleToolPressed(object? sender, EventArgs e)
330+
{
331+
try
332+
{
333+
// Only switch to circle tool if drawing mode is active
334+
if (_drawingManager?.IsDrawingMode == true)
335+
{
336+
_logger?.LogInformation("C pressed - selecting circle tool");
337+
_drawingManager?.SetCircleTool();
338+
}
339+
}
340+
catch (Exception ex)
341+
{
342+
_exceptionHandler?.HandleException(ex, "Circle tool handler");
343+
}
344+
}
345+
328346
private void OnHelpPressed(object? sender, EventArgs e)
329347
{
330348
try

Src/GhostDraw/Core/DrawTool.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,10 @@ public enum DrawTool
2626
/// <summary>
2727
/// Rectangle tool - click two points to draw a rectangle
2828
/// </summary>
29-
Rectangle
29+
Rectangle,
30+
31+
/// <summary>
32+
/// Circle tool - click two points to draw a circle/ellipse
33+
/// </summary>
34+
Circle
3035
}

Src/GhostDraw/Core/GlobalKeyboardHook.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class GlobalKeyboardHook : IDisposable
1717
private const int VK_P = 0x50; // 80 - 'P' key for pen tool
1818
private const int VK_E = 0x45; // 69 - 'E' key for eraser tool
1919
private const int VK_U = 0x55; // 85 - 'U' key for rectangle tool
20+
private const int VK_C = 0x43; // 67 - 'C' key for circle tool
2021
private const int VK_F1 = 0x70; // 112 - 'F1' key for help
2122
private const int VK_S = 0x53; // 83 - 'S' key for screenshot (Ctrl+S only)
2223
private const int VK_LCONTROL = 0xA2; // 162 - Left Control key
@@ -37,6 +38,7 @@ public class GlobalKeyboardHook : IDisposable
3738
public event EventHandler? LineToolPressed;
3839
public event EventHandler? EraserToolPressed;
3940
public event EventHandler? RectangleToolPressed;
41+
public event EventHandler? CircleToolPressed;
4042
public event EventHandler? HelpPressed;
4143
public event EventHandler? ScreenshotFullPressed;
4244

@@ -247,6 +249,13 @@ private nint HookCallback(int nCode, nint wParam, nint lParam)
247249
RectangleToolPressed?.Invoke(this, EventArgs.Empty);
248250
}
249251

252+
// Check for C key press (circle tool)
253+
if (vkCode == VK_C && isKeyDown)
254+
{
255+
_logger.LogDebug("C key pressed - circle tool request");
256+
CircleToolPressed?.Invoke(this, EventArgs.Empty);
257+
}
258+
250259
// Check for F1 key press (help)
251260
if (vkCode == VK_F1 && isKeyDown)
252261
{

Src/GhostDraw/Core/ServiceConfiguration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public static ServiceProvider ConfigureServices()
6464
services.AddSingleton<GhostDraw.Tools.LineTool>();
6565
services.AddSingleton<GhostDraw.Tools.EraserTool>();
6666
services.AddSingleton<GhostDraw.Tools.RectangleTool>();
67+
services.AddSingleton<GhostDraw.Tools.CircleTool>();
6768

6869
services.AddSingleton<OverlayWindow>();
6970
services.AddSingleton<GlobalKeyboardHook>();

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.8</Version>
11+
<Version>1.0.9</Version>
1212
<EnableWindowsTargeting>true</EnableWindowsTargeting>
1313
</PropertyGroup>
1414

Src/GhostDraw/Helpers/CursorHelper.cs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,108 @@ public WpfCursor CreateLineCursor(string colorHex)
480480
}
481481
}
482482

483+
/// <summary>
484+
/// Creates a cursor for the Circle tool with color indicator
485+
/// </summary>
486+
/// <param name="colorHex">Hex color for the circle (e.g., "#FF0000")</param>
487+
/// <returns>Custom cursor</returns>
488+
public WpfCursor CreateCircleCursor(string colorHex)
489+
{
490+
lock (_cursorLock)
491+
{
492+
if (_disposed)
493+
{
494+
_logger.LogWarning("CreateCircleCursor called on disposed CursorHelper");
495+
return WpfCursors.Cross;
496+
}
497+
498+
try
499+
{
500+
_logger.LogDebug("Creating circle cursor with color {Color}", colorHex);
501+
502+
// Destroy previous cursor handle to prevent leaks
503+
if (_currentCursorHandle != nint.Zero)
504+
{
505+
try
506+
{
507+
DestroyCursor(_currentCursorHandle);
508+
_logger.LogDebug("Destroyed previous cursor handle");
509+
}
510+
catch (Exception ex)
511+
{
512+
_logger.LogError(ex, "Failed to destroy previous cursor handle");
513+
}
514+
_currentCursorHandle = nint.Zero;
515+
}
516+
517+
// Create a bitmap for the cursor (32x32 pixels)
518+
int size = 32;
519+
using (Bitmap bitmap = new Bitmap(size, size))
520+
using (Graphics g = Graphics.FromImage(bitmap))
521+
{
522+
g.SmoothingMode = SmoothingMode.AntiAlias;
523+
g.Clear(Color.Transparent);
524+
525+
// Parse the circle color
526+
Color circleColor = ColorTranslator.FromHtml(colorHex);
527+
528+
// Draw a small circle preview
529+
int circleRadius = 6;
530+
int circleLeft = 6;
531+
int circleTop = (size - circleRadius * 2) / 2;
532+
533+
// Draw circle outline with the active color
534+
using (Pen circlePen = new Pen(circleColor, 2))
535+
{
536+
g.DrawEllipse(circlePen, circleLeft, circleTop, circleRadius * 2, circleRadius * 2);
537+
}
538+
539+
// Draw corner markers (small filled squares at key points - top-left, top-right, bottom-left, bottom-right)
540+
int cornerSize = 3;
541+
using (SolidBrush cornerBrush = new SolidBrush(Color.White))
542+
{
543+
// Top-left (this is the hotspot - represents bounding box corner)
544+
g.FillRectangle(cornerBrush, circleLeft - 1, circleTop - 1, cornerSize, cornerSize);
545+
// Top-right
546+
g.FillRectangle(cornerBrush, circleLeft + circleRadius * 2 - 1, circleTop - 1, cornerSize, cornerSize);
547+
// Bottom-left
548+
g.FillRectangle(cornerBrush, circleLeft - 1, circleTop + circleRadius * 2 - 1, cornerSize, cornerSize);
549+
// Bottom-right
550+
g.FillRectangle(cornerBrush, circleLeft + circleRadius * 2 - 1, circleTop + circleRadius * 2 - 1, cornerSize, cornerSize);
551+
}
552+
553+
// Draw black outlines for corner markers
554+
using (Pen cornerOutline = new Pen(Color.Black, 1))
555+
{
556+
g.DrawRectangle(cornerOutline, circleLeft - 1, circleTop - 1, cornerSize, cornerSize);
557+
g.DrawRectangle(cornerOutline, circleLeft + circleRadius * 2 - 1, circleTop - 1, cornerSize, cornerSize);
558+
g.DrawRectangle(cornerOutline, circleLeft - 1, circleTop + circleRadius * 2 - 1, cornerSize, cornerSize);
559+
g.DrawRectangle(cornerOutline, circleLeft + circleRadius * 2 - 1, circleTop + circleRadius * 2 - 1, cornerSize, cornerSize);
560+
}
561+
562+
// Convert bitmap to cursor with hotspot at top-left corner marker
563+
nint hCursor = CreateCursorFromBitmap(bitmap, circleLeft, circleTop);
564+
565+
if (hCursor != nint.Zero)
566+
{
567+
_currentCursorHandle = hCursor;
568+
_logger.LogDebug("Successfully created circle cursor (handle: {Handle})", hCursor);
569+
570+
return System.Windows.Interop.CursorInteropHelper.Create(new SafeCursorHandle(hCursor));
571+
}
572+
}
573+
574+
_logger.LogWarning("Failed to create circle cursor, returning default");
575+
return WpfCursors.Cross;
576+
}
577+
catch (Exception ex)
578+
{
579+
_logger.LogError(ex, "Error creating circle cursor, using default");
580+
return WpfCursors.Cross;
581+
}
582+
}
583+
}
584+
483585
private nint CreateCursorFromBitmap(Bitmap bitmap, int hotspotX, int hotspotY)
484586
{
485587
nint hIcon = nint.Zero;

Src/GhostDraw/Managers/DrawingManager.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,31 @@ public void SetRectangleTool()
322322
}
323323
}
324324

325+
/// <summary>
326+
/// Sets the active tool to Circle
327+
/// </summary>
328+
public void SetCircleTool()
329+
{
330+
try
331+
{
332+
if (_overlayWindow.IsVisible)
333+
{
334+
_appSettings.SetActiveTool(DrawTool.Circle);
335+
_overlayWindow.OnToolChanged(DrawTool.Circle);
336+
_logger.LogInformation("Tool set to Circle");
337+
}
338+
else
339+
{
340+
_logger.LogDebug("SetCircleTool ignored - overlay not visible");
341+
}
342+
}
343+
catch (Exception ex)
344+
{
345+
_logger.LogError(ex, "Failed to set circle tool");
346+
// Don't re-throw - not critical
347+
}
348+
}
349+
325350
/// <summary>
326351
/// Shows the help popup with keyboard shortcuts
327352
/// </summary>

0 commit comments

Comments
 (0)