Feature/color picker pixel ruler - #95
Conversation
Color Picker: hover loupe shows 11x11 magnified region with hex label; click samples the pixel from the background capture, sets ActiveColor, copies hex to clipboard, shows toast, and reverts to previous tool. Pixel Ruler: drag to draw a line with adaptive tick marks and a live pixel-distance label; committed as a single undoable canvas element. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Incremented version.json from 5.5 to 5.6 to prepare for the next release. No other changes were made.
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR adds two new annotation-time utilities to the overlay workflow: a color picker with a magnified loupe/hex copy flow, and a pixel ruler annotation for measuring on-screen distances. It extends the existing annotation infrastructure so these tools fit into the same renderer, interaction controller, and toolbar system used by the rest of Pointframe.
Changes:
- Add
ColorPickerandPixelRulertools to the overlay annotation toolbar and tool model. - Extend the renderer/controller pipeline to support pixel sampling, loupe cropping, and ruler rendering.
- Bump the application version to
5.6.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
version.json |
Bumps the app version for this feature release. |
Pointframe/ViewModels/OverlayViewModel.cs |
Removes duplicate DPI properties so overlay state relies on the shared annotation model. |
Pointframe/ViewModels/AnnotationViewModel.cs |
Adds shared DPI fields, previous-tool tracking, and pixel-ruler shape parameter creation. |
Pointframe/Services/Handlers/PixelRulerShapeHandler.cs |
Introduces the new ruler drawing handler with ticks, endpoints, and label rendering. |
Pointframe/Services/Handlers/ColorPickerShapeHandler.cs |
Adds a no-op handler placeholder for the color picker tool. |
Pointframe/Services/AnnotationCanvasRenderer.cs |
Adds background format conversion, pixel sampling/loupe cropping helpers, and registers new tool handlers. |
Pointframe/Services/AnnotationCanvasInteractionController.cs |
Adds color-picker click handling and loupe-position updates during pointer movement. |
Pointframe/OverlayWindow.xaml.cs |
Wires overlay callbacks for color picking, loupe updates, pointer movement, and Escape behavior. |
Pointframe/OverlayWindow.xaml |
Adds toolbar buttons for the new tools and the color-picker loupe UI. |
Pointframe/OverlayWindow.ColorPicker.cs |
Implements loupe positioning/update logic and toolbar resync helper methods. |
Pointframe/Models/ShapeParameters.cs |
Defines the new PixelRulerShapeParameters record. |
Pointframe/AnnotationTool.cs |
Extends the annotation tool enum with ColorPicker and PixelRuler. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _onColorPicked?.Invoke(color ?? Colors.Transparent, point); | ||
| _viewModel.RevertToPreviousTool(); |
| var px = Math.Clamp((int)Math.Round(dipPoint.X * _dpiX), 0, _backgroundCapture.PixelWidth - 1); | ||
| var py = Math.Clamp((int)Math.Round(dipPoint.Y * _dpiY), 0, _backgroundCapture.PixelHeight - 1); |
| var x = Math.Max(0, cx - halfPixels); | ||
| var y = Math.Max(0, cy - halfPixels); | ||
| var w = Math.Min(halfPixels * 2 + 1, _backgroundCapture.PixelWidth - x); | ||
| var h = Math.Min(halfPixels * 2 + 1, _backgroundCapture.PixelHeight - y); | ||
| if (w <= 0 || h <= 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
| <RadioButton Style="{StaticResource ToolBtn}" | ||
| GroupName="AnnotTool" | ||
| Tag="ColorPicker" Click="Tool_Click" ToolTip="Color Picker" | ||
| AutomationProperties.AutomationId="OverlayWindow.Tool.ColorPicker"> |
| <RadioButton Style="{StaticResource ToolBtn}" | ||
| GroupName="AnnotTool" | ||
| Tag="PixelRuler" Click="Tool_Click" ToolTip="Pixel Ruler" | ||
| AutomationProperties.AutomationId="OverlayWindow.Tool.PixelRuler"> |
| return; | ||
| } | ||
|
|
||
| _container.Children.Clear(); |
| AnnotationTool.PixelRuler => new PixelRulerShapeParameters( | ||
| P1: DragStart, | ||
| P2: DragCurrent, | ||
| Color: color, | ||
| Thickness: thick, | ||
| DpiX: DpiX, | ||
| DpiY: DpiY), |
| // Position loupe near cursor, flip if near an edge | ||
| const double Offset = 18; | ||
| const double LoupeW = 120; | ||
| const double LoupeH = 140; | ||
|
|
||
| var lx = dipPoint.Value.X + Offset; | ||
| var ly = dipPoint.Value.Y - LoupeH - Offset; | ||
| if (ly < 0) | ||
| { | ||
| ly = dipPoint.Value.Y + Offset; | ||
| } | ||
|
|
||
| if (lx + LoupeW > ActualWidth) | ||
| { | ||
| lx = dipPoint.Value.X - LoupeW - Offset; |
| _vm.RevertToPreviousTool(); | ||
| SyncToolbarToSelectedTool(); | ||
| UpdateLoupe(null); | ||
| AnnotationCanvas.Cursor = Cursors.Cross; |
| public Color? SamplePixelColor(Point dipPoint) | ||
| { | ||
| if (_backgroundCapture is null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var px = Math.Clamp((int)Math.Round(dipPoint.X * _dpiX), 0, _backgroundCapture.PixelWidth - 1); | ||
| var py = Math.Clamp((int)Math.Round(dipPoint.Y * _dpiY), 0, _backgroundCapture.PixelHeight - 1); | ||
| var bytes = new byte[4]; | ||
| _backgroundCapture.CopyPixels(new Int32Rect(px, py, 1, 1), bytes, 4, 0); | ||
| return Color.FromRgb(bytes[2], bytes[1], bytes[0]); // BGRA → RGB | ||
| } | ||
|
|
||
| public BitmapSource? CropLoupeRegion(Point dipCenter, int halfPixels) | ||
| { | ||
| if (_backgroundCapture is null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var cx = (int)Math.Round(dipCenter.X * _dpiX); | ||
| var cy = (int)Math.Round(dipCenter.Y * _dpiY); | ||
| var x = Math.Max(0, cx - halfPixels); | ||
| var y = Math.Max(0, cy - halfPixels); | ||
| var w = Math.Min(halfPixels * 2 + 1, _backgroundCapture.PixelWidth - x); | ||
| var h = Math.Min(halfPixels * 2 + 1, _backgroundCapture.PixelHeight - y); | ||
| if (w <= 0 || h <= 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var crop = new CroppedBitmap(_backgroundCapture, new Int32Rect(x, y, w, h)); | ||
| crop.Freeze(); | ||
| return crop; | ||
| } |
No description provided.