Skip to content

Commit 95f96c4

Browse files
aksOpsclaude
andcommitted
feat: zoom-center-on-cursor, deep-clone helper, UI polish
Core helpers + their tests (all Linux-verifiable) * Get-ZoomCenteredOffset — pure math for Ctrl+MouseWheel zoom that preserves the content point under the cursor across a scale change, clamped to [0, Content-Viewport]. Previously the wheel handler only changed scale and left the scroll offset alone, so the README claim "Zoom centered on cursor" wasn't actually implemented. Wire it into the handler. * Copy-AnnotationList — pull the three identical deep-clone loops out of Snapshot-State / Do-Undo / Do-Redo into a single pure helper; tests cover null/empty input, single-annotation mutation isolation, mixed-type round-trip. * Headless tests: 72 -> 84 (+6 zoom math + 6 annotation clone). XAML polish (parse-verified, WPF eye-test still recommended) * Bump TextSecondaryBrush 0xAA -> 0xDD and About window #88FFFFFF / #BBBBBB -> #BBFFFFFF / #DDDDDD for WCAG AA. * Tiny center dot in the magnifier loupe for pixel-precise aim. * <ResizeGrip> at preview bottom-right — makes the edge-drag affordance discoverable on chromeless windows. * MinWidth/MinHeight on the About window so narrow DPI setups don't clip the caption buttons. * Copy button now flashes "Copied!" for 1.2s after clipboard set. * Active color swatch: 30x30 with a 3px accent ring (was 26x26 with a 2px white ring) — the selection is now obvious. CI hygiene * FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true at workflow level clears the actions/checkout@v4 Node 20 deprecation notice ahead of the 2026-09 removal. All 4 embedded XAML blocks still parse cleanly via [xml] and AST parser. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c7af4e7 commit 95f96c4

3 files changed

Lines changed: 223 additions & 54 deletions

File tree

.github/workflows/test.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ on:
77
branches: [main]
88
workflow_dispatch:
99

10+
# Opt in to Node 24 runtime for actions/* (Node 20 is deprecated;
11+
# forced default in June 2026, removed in September 2026).
12+
env:
13+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
14+
1015
jobs:
1116
headless:
1217
name: Pure-logic tests (pwsh 7.5+)

SnipIT.ps1

Lines changed: 106 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,54 @@ function Get-ClampedAnnotationRect {
183183
[pscustomobject]@{ X = $nx; Y = $ny; Width = $nw; Height = $nh }
184184
}
185185

186+
function Get-ZoomCenteredOffset {
187+
# Compute the scroll offset that keeps the content point currently under
188+
# ($CursorX, $CursorY) anchored after a scale change (Ctrl+MouseWheel zoom).
189+
# Content coords = viewport-offset + viewport-position; if the same image
190+
# pixel should land on the same viewport pixel after scaling, the offset
191+
# must shift by (OldOffset + Cursor) * NewScale/OldScale - Cursor.
192+
# Result is clamped to [0, Content - Viewport].
193+
param(
194+
[Parameter(Mandatory)] [double]$CursorX,
195+
[Parameter(Mandatory)] [double]$CursorY,
196+
[Parameter(Mandatory)] [double]$OldScrollX,
197+
[Parameter(Mandatory)] [double]$OldScrollY,
198+
[Parameter(Mandatory)] [double]$OldScale,
199+
[Parameter(Mandatory)] [double]$NewScale,
200+
[Parameter(Mandatory)] [double]$ContentWidth,
201+
[Parameter(Mandatory)] [double]$ContentHeight,
202+
[Parameter(Mandatory)] [double]$ViewportWidth,
203+
[Parameter(Mandatory)] [double]$ViewportHeight
204+
)
205+
if ($OldScale -le 0) { $OldScale = 1.0 }
206+
$ratio = $NewScale / $OldScale
207+
$newX = ($OldScrollX + $CursorX) * $ratio - $CursorX
208+
$newY = ($OldScrollY + $CursorY) * $ratio - $CursorY
209+
$maxX = [math]::Max(0.0, $ContentWidth - $ViewportWidth)
210+
$maxY = [math]::Max(0.0, $ContentHeight - $ViewportHeight)
211+
$newX = [math]::Max(0.0, [math]::Min($maxX, $newX))
212+
$newY = [math]::Max(0.0, [math]::Min($maxY, $newY))
213+
[pscustomobject]@{ X = $newX; Y = $newY }
214+
}
215+
216+
function Copy-AnnotationList {
217+
# Deep-clone an enumerable of annotation pscustomobjects into a new
218+
# ArrayList. ArrayList stores references, so Snapshot-State / Do-Undo /
219+
# Do-Redo need fresh copies to avoid letting undo entries mutate each
220+
# other when the live Annotations list is edited.
221+
param([AllowNull()][AllowEmptyCollection()] $Annotations)
222+
$copy = New-Object System.Collections.ArrayList
223+
if ($null -eq $Annotations) { return ,$copy }
224+
foreach ($a in $Annotations) {
225+
[void]$copy.Add([pscustomobject]@{
226+
Type=$a.Type; Color=$a.Color
227+
X=$a.X; Y=$a.Y; W=$a.W; H=$a.H
228+
Text=$a.Text; FontSize=$a.FontSize
229+
})
230+
}
231+
return ,$copy
232+
}
233+
186234
function Get-TrimmedRecent {
187235
# Keep only the top N items (for capping unbounded undo/redo stacks).
188236
# $Items is expected in most-recent-first order, matching [Stack].ToArray().
@@ -545,7 +593,7 @@ function Show-AboutWindow {
545593
[xml]$xaml = @"
546594
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
547595
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
548-
Title="About SnipIT" Width="420" Height="300"
596+
Title="About SnipIT" Width="420" Height="300" MinWidth="360" MinHeight="280"
549597
WindowStartupLocation="CenterScreen"
550598
WindowStyle="None" AllowsTransparency="True" ResizeMode="NoResize"
551599
Background="#FF1B1B1B">
@@ -565,7 +613,7 @@ function Show-AboutWindow {
565613
</Border>
566614
<StackPanel Grid.Row="1" Margin="24,18,24,0">
567615
<TextBlock Text="Professional snipping tool" FontSize="13" Foreground="#CCFFFFFF" Margin="0,0,0,14"/>
568-
<TextBlock Text="PowerShell 7.5+ on .NET 9" FontSize="11" Foreground="#88FFFFFF" Margin="0,0,0,16"/>
616+
<TextBlock Text="PowerShell 7.5+ on .NET 9" FontSize="11" Foreground="#BBFFFFFF" Margin="0,0,0,16"/>
569617
<TextBlock Text="Hotkeys" FontSize="12" FontWeight="SemiBold" Foreground="White" Margin="0,0,0,6"/>
570618
<Grid>
571619
<Grid.ColumnDefinitions>
@@ -576,11 +624,11 @@ function Show-AboutWindow {
576624
<RowDefinition/><RowDefinition/><RowDefinition/>
577625
</Grid.RowDefinitions>
578626
<TextBlock Grid.Row="0" Grid.Column="0" Text="Ctrl + Shift + S" FontFamily="Consolas" Foreground="#DDDDDD"/>
579-
<TextBlock Grid.Row="0" Grid.Column="1" Text="Smart capture" Foreground="#BBBBBB"/>
627+
<TextBlock Grid.Row="0" Grid.Column="1" Text="Smart capture" Foreground="#DDDDDD"/>
580628
<TextBlock Grid.Row="1" Grid.Column="0" Text="Ctrl + Shift + F" FontFamily="Consolas" Foreground="#DDDDDD"/>
581-
<TextBlock Grid.Row="1" Grid.Column="1" Text="Full screen" Foreground="#BBBBBB"/>
629+
<TextBlock Grid.Row="1" Grid.Column="1" Text="Full screen" Foreground="#DDDDDD"/>
582630
<TextBlock Grid.Row="2" Grid.Column="0" Text="Ctrl + Shift + W" FontFamily="Consolas" Foreground="#DDDDDD"/>
583-
<TextBlock Grid.Row="2" Grid.Column="1" Text="Active window" Foreground="#BBBBBB"/>
631+
<TextBlock Grid.Row="2" Grid.Column="1" Text="Active window" Foreground="#DDDDDD"/>
584632
</Grid>
585633
</StackPanel>
586634
<Border Grid.Row="2" Padding="20,12" Background="#22000000">
@@ -645,6 +693,9 @@ function Show-SmartOverlay {
645693
RenderOptions.BitmapScalingMode="NearestNeighbor"/>
646694
<Line X1="72" Y1="0" X2="72" Y2="144" Stroke="#990078D4" StrokeThickness="1"/>
647695
<Line X1="0" Y1="72" X2="144" Y2="72" Stroke="#990078D4" StrokeThickness="1"/>
696+
<!-- Tiny center dot — marks the exact pixel under the cursor -->
697+
<Rectangle Width="4" Height="4" Fill="#FFFF4081"
698+
HorizontalAlignment="Center" VerticalAlignment="Center"/>
648699
</Grid>
649700
</Border>
650701
<TextBlock x:Name="LoupeText" Foreground="White" FontFamily="Consolas"
@@ -842,7 +893,7 @@ function Show-PreviewWindow {
842893
<SolidColorBrush x:Key="AccentBrush" Color="#FF0078D4"/>
843894
<SolidColorBrush x:Key="AccentHoverBrush" Color="#330078D4"/>
844895
<SolidColorBrush x:Key="PanelOverlayBrush" Color="#22000000"/>
845-
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#AAFFFFFF"/>
896+
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#DDFFFFFF"/>
846897
<SolidColorBrush x:Key="BorderLightBrush" Color="#33FFFFFF"/>
847898
848899
<!-- Active-tool visual state for the 4 annotation tools + Pin.
@@ -985,6 +1036,11 @@ function Show-PreviewWindow {
9851036
</Button>
9861037
</StackPanel>
9871038
</Border>
1039+
1040+
<!-- Resize grip affordance. Chromeless windows still accept edge-drag,
1041+
but this makes the corner discoverable. -->
1042+
<ResizeGrip Grid.Row="3" HorizontalAlignment="Right" VerticalAlignment="Bottom"
1043+
Width="14" Height="14" Opacity="0.55" IsTabStop="False"/>
9881044
</Grid>
9891045
</Window>
9901046
"@
@@ -1151,63 +1207,30 @@ function Show-PreviewWindow {
11511207
}
11521208

11531209
function script:Snapshot-State {
1154-
# Deep-copy current annotations into undo stack, clear redo
1155-
$copy = New-Object System.Collections.ArrayList
1156-
foreach ($a in $state.Annotations) {
1157-
[void]$copy.Add([pscustomobject]@{
1158-
Type=$a.Type; Color=$a.Color
1159-
X=$a.X; Y=$a.Y; W=$a.W; H=$a.H
1160-
Text=$a.Text; FontSize=$a.FontSize
1161-
})
1162-
}
1163-
$state.UndoStack.Push($copy)
1210+
$state.UndoStack.Push( (Copy-AnnotationList $state.Annotations) )
11641211
$state.RedoStack.Clear()
11651212
Trim-SnipStack $state.UndoStack
11661213
}
11671214

11681215
function script:Restore-State {
11691216
param($snapshot)
11701217
$state.Annotations.Clear()
1171-
foreach ($a in $snapshot) {
1172-
[void]$state.Annotations.Add([pscustomobject]@{
1173-
Type=$a.Type; Color=$a.Color
1174-
X=$a.X; Y=$a.Y; W=$a.W; H=$a.H
1175-
Text=$a.Text; FontSize=$a.FontSize
1176-
})
1177-
}
1218+
foreach ($a in (Copy-AnnotationList $snapshot)) { [void]$state.Annotations.Add($a) }
11781219
Render-Annotations
11791220
}
11801221

11811222
function script:Do-Undo {
11821223
if ($state.UndoStack.Count -eq 0) { return }
1183-
$current = New-Object System.Collections.ArrayList
1184-
foreach ($a in $state.Annotations) {
1185-
[void]$current.Add([pscustomobject]@{
1186-
Type=$a.Type; Color=$a.Color
1187-
X=$a.X; Y=$a.Y; W=$a.W; H=$a.H
1188-
Text=$a.Text; FontSize=$a.FontSize
1189-
})
1190-
}
1191-
$state.RedoStack.Push($current)
1224+
$state.RedoStack.Push( (Copy-AnnotationList $state.Annotations) )
11921225
Trim-SnipStack $state.RedoStack
1193-
$prev = $state.UndoStack.Pop()
1194-
Restore-State $prev
1226+
Restore-State $state.UndoStack.Pop()
11951227
}
11961228

11971229
function script:Do-Redo {
11981230
if ($state.RedoStack.Count -eq 0) { return }
1199-
$current = New-Object System.Collections.ArrayList
1200-
foreach ($a in $state.Annotations) {
1201-
[void]$current.Add([pscustomobject]@{
1202-
Type=$a.Type; Color=$a.Color
1203-
X=$a.X; Y=$a.Y; W=$a.W; H=$a.H
1204-
Text=$a.Text; FontSize=$a.FontSize
1205-
})
1206-
}
1207-
$state.UndoStack.Push($current)
1231+
$state.UndoStack.Push( (Copy-AnnotationList $state.Annotations) )
12081232
Trim-SnipStack $state.UndoStack
1209-
$next = $state.RedoStack.Pop()
1210-
Restore-State $next
1233+
Restore-State $state.RedoStack.Pop()
12111234
}
12121235

12131236
# Named color picker. Tests and the real swatch click handler both call
@@ -1237,18 +1260,24 @@ function Show-PreviewWindow {
12371260
$colorBar.Children.Clear()
12381261
foreach ($name in $palette.Keys) {
12391262
$rgb = $palette[$name]
1263+
$isActive = ($state.ActiveColor -eq $name)
12401264
$sw = New-Object System.Windows.Controls.Border
1241-
$sw.Width = 26; $sw.Height = 26
1265+
# Slightly larger + accent-coloured ring when active — makes the
1266+
# selected swatch unambiguous at a glance.
1267+
if ($isActive) { $sw.Width = 30; $sw.Height = 30 }
1268+
else { $sw.Width = 26; $sw.Height = 26 }
12421269
$sw.Margin = New-Object System.Windows.Thickness 3, 0, 3, 0
12431270
$sw.CornerRadius = New-Object System.Windows.CornerRadius 0
12441271
$sw.Background = New-Object System.Windows.Media.SolidColorBrush(
12451272
(To-WpfColor 255 $rgb.R $rgb.G $rgb.B))
1246-
$sw.BorderBrush = New-Object System.Windows.Media.SolidColorBrush(
1247-
([System.Windows.Media.Colors]::White))
1248-
$sw.BorderThickness = if ($state.ActiveColor -eq $name) {
1249-
New-Object System.Windows.Thickness 2
1273+
if ($isActive) {
1274+
$sw.BorderBrush = New-Object System.Windows.Media.SolidColorBrush(
1275+
([System.Windows.Media.Color]::FromArgb(255, 0x00, 0x78, 0xD4)))
1276+
$sw.BorderThickness = New-Object System.Windows.Thickness 3
12501277
} else {
1251-
New-Object System.Windows.Thickness 0
1278+
$sw.BorderBrush = New-Object System.Windows.Media.SolidColorBrush(
1279+
([System.Windows.Media.Colors]::White))
1280+
$sw.BorderThickness = New-Object System.Windows.Thickness 0
12521281
}
12531282
$sw.Cursor = [System.Windows.Input.Cursors]::Hand
12541283
# Non-focusable so clicking a swatch while a text box is open
@@ -1719,8 +1748,23 @@ function Show-PreviewWindow {
17191748

17201749
$win.Add_PreviewMouseWheel({
17211750
if (([System.Windows.Input.Keyboard]::Modifiers -band [System.Windows.Input.ModifierKeys]::Control) -ne 0) {
1722-
$factor = if ($_.Delta -gt 0) { 1.25 } else { 1 / 1.25 }
1751+
$factor = if ($_.Delta -gt 0) { 1.25 } else { 1 / 1.25 }
1752+
$oldScale = $layoutScale.ScaleX
1753+
$cursor = $_.GetPosition($scroller)
1754+
$oldSx = $scroller.HorizontalOffset
1755+
$oldSy = $scroller.VerticalOffset
17231756
& $zoomBy $factor
1757+
$newScale = $layoutScale.ScaleX
1758+
$offset = Get-ZoomCenteredOffset `
1759+
-CursorX $cursor.X -CursorY $cursor.Y `
1760+
-OldScrollX $oldSx -OldScrollY $oldSy `
1761+
-OldScale $oldScale -NewScale $newScale `
1762+
-ContentWidth ($Bitmap.Width * $newScale) `
1763+
-ContentHeight ($Bitmap.Height * $newScale) `
1764+
-ViewportWidth $scroller.ViewportWidth `
1765+
-ViewportHeight $scroller.ViewportHeight
1766+
$scroller.ScrollToHorizontalOffset($offset.X)
1767+
$scroller.ScrollToVerticalOffset( $offset.Y)
17241768
$_.Handled = $true
17251769
}
17261770
}.GetNewClosure())
@@ -1793,12 +1837,20 @@ function Show-PreviewWindow {
17931837
return $flat
17941838
}
17951839

1796-
$win.FindName('CopyBtn').Add_Click({
1840+
$copyBtn = $win.FindName('CopyBtn')
1841+
$copyBtn.Add_Click({
17971842
$flat = Get-FlattenedBitmap
17981843
$clipSrc = Convert-BitmapToBitmapSource $flat
17991844
[System.Windows.Clipboard]::SetImage($clipSrc)
18001845
if ($flat -ne $Bitmap) { $flat.Dispose() }
1801-
})
1846+
# Brief confirmation — second StackPanel child is the "Copy" label.
1847+
$lbl = $copyBtn.Content.Children[1]
1848+
$lbl.Text = 'Copied!'
1849+
$timer = New-Object System.Windows.Threading.DispatcherTimer
1850+
$timer.Interval = [timespan]::FromMilliseconds(1200)
1851+
$timer.Add_Tick({ $lbl.Text = 'Copy'; $timer.Stop() }.GetNewClosure())
1852+
$timer.Start()
1853+
}.GetNewClosure())
18021854
$win.FindName('SaveBtn').Add_Click({
18031855
$flat = Get-FlattenedBitmap
18041856
Save-CaptureToFile -Bitmap $flat | Out-Null

Test-SnipIT.ps1

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,118 @@ It 'is case-insensitive for extension recognition' {
209209
ShouldBe (Resolve-SaveImagePath -Path '/tmp/a.PNG' -FilterFormat 'Jpeg') '/tmp/a.PNG'
210210
}
211211

212+
Describe 'Get-ZoomCenteredOffset'
213+
It 'keeps the same content point under the cursor at center of viewport' {
214+
# Cursor at (400, 300) in viewport. Old offset 0, old scale 1, new scale 2.
215+
# Content point under cursor before zoom: (400, 300). After 2x it's at (800, 600).
216+
# New offset must shift so content (800, 600) maps back to viewport (400, 300).
217+
$o = Get-ZoomCenteredOffset -CursorX 400 -CursorY 300 `
218+
-OldScrollX 0 -OldScrollY 0 -OldScale 1 -NewScale 2 `
219+
-ContentWidth 1920 -ContentHeight 1080 `
220+
-ViewportWidth 800 -ViewportHeight 600
221+
ShouldBe $o.X 400; ShouldBe $o.Y 300
222+
}
223+
It 'zooming in near the right edge clamps to the content boundary' {
224+
# Cursor near right edge; after 2x zoom, the computed offset would exceed content-viewport.
225+
$o = Get-ZoomCenteredOffset -CursorX 790 -CursorY 590 `
226+
-OldScrollX 1000 -OldScrollY 400 -OldScale 1 -NewScale 2 `
227+
-ContentWidth 2000 -ContentHeight 800 `
228+
-ViewportWidth 800 -ViewportHeight 600
229+
ShouldBe $o.X 1200 # max = ContentW - ViewportW = 2000 - 800
230+
ShouldBe $o.Y 200 # max = ContentH - ViewportH = 800 - 600
231+
}
232+
It 'zooming out past the content fit clamps to zero' {
233+
# Zoom from 2x down to 0.5x in a small image — no room to scroll.
234+
$o = Get-ZoomCenteredOffset -CursorX 100 -CursorY 100 `
235+
-OldScrollX 50 -OldScrollY 50 -OldScale 2 -NewScale 0.5 `
236+
-ContentWidth 200 -ContentHeight 200 `
237+
-ViewportWidth 400 -ViewportHeight 400
238+
ShouldBe $o.X 0; ShouldBe $o.Y 0
239+
}
240+
It 'handles a zero OldScale gracefully (treats it as 1)' {
241+
# Degenerate input; earlier zoom was a no-op. Should not divide by zero.
242+
$o = Get-ZoomCenteredOffset -CursorX 50 -CursorY 50 `
243+
-OldScrollX 0 -OldScrollY 0 -OldScale 0 -NewScale 2 `
244+
-ContentWidth 1000 -ContentHeight 1000 `
245+
-ViewportWidth 400 -ViewportHeight 400
246+
ShouldBe $o.X 50; ShouldBe $o.Y 50
247+
}
248+
It 'a no-op scale change leaves the offset untouched' {
249+
$o = Get-ZoomCenteredOffset -CursorX 123 -CursorY 456 `
250+
-OldScrollX 77 -OldScrollY 88 -OldScale 1.5 -NewScale 1.5 `
251+
-ContentWidth 2000 -ContentHeight 1500 `
252+
-ViewportWidth 800 -ViewportHeight 600
253+
ShouldBe $o.X 77; ShouldBe $o.Y 88
254+
}
255+
It 'matrix: 0.5x / 1x / 2x / 5x at viewport center produces sensible offsets' {
256+
$row = 300; $col = 400
257+
foreach ($s in 0.5, 1.0, 2.0, 5.0) {
258+
$o = Get-ZoomCenteredOffset -CursorX $col -CursorY $row `
259+
-OldScrollX 0 -OldScrollY 0 -OldScale 1 -NewScale $s `
260+
-ContentWidth (4000) -ContentHeight (3000) `
261+
-ViewportWidth 800 -ViewportHeight 600
262+
# Expected: cursor*(s-1); clamped to [0, 4000-800] and [0, 3000-600]
263+
$expectedX = [math]::Max(0.0, [math]::Min(3200.0, $col * ($s - 1)))
264+
$expectedY = [math]::Max(0.0, [math]::Min(2400.0, $row * ($s - 1)))
265+
ShouldBe $o.X $expectedX
266+
ShouldBe $o.Y $expectedY
267+
}
268+
}
269+
270+
Describe 'Copy-AnnotationList'
271+
It 'returns an empty ArrayList for null input' {
272+
$r = Copy-AnnotationList $null
273+
ShouldBe $r.Count 0
274+
}
275+
It 'returns an empty ArrayList for an empty input' {
276+
$r = Copy-AnnotationList @()
277+
ShouldBe $r.Count 0
278+
}
279+
It 'deep-copies a single highlight annotation' {
280+
$src = @([pscustomobject]@{ Type='highlight'; Color='yellow'; X=10; Y=20; W=100; H=50; Text=$null; FontSize=0 })
281+
$r = Copy-AnnotationList $src
282+
ShouldBe $r.Count 1
283+
ShouldBe $r[0].Type 'highlight'
284+
ShouldBe $r[0].X 10
285+
}
286+
It 'mutations on the copy do not affect the original' {
287+
$orig = @([pscustomobject]@{ Type='rect'; Color='red'; X=5; Y=5; W=50; H=50; Text=$null; FontSize=0 })
288+
$copy = Copy-AnnotationList $orig
289+
$copy[0].X = 999
290+
ShouldBe $orig[0].X 5 # original untouched
291+
ShouldBe $copy[0].X 999
292+
}
293+
It 'preserves mixed annotation types (highlight + rect + arrow + text)' {
294+
$src = @(
295+
[pscustomobject]@{ Type='highlight'; Color='yellow'; X=0; Y=0; W=10; H=10; Text=$null; FontSize=0 }
296+
[pscustomobject]@{ Type='rect'; Color='blue'; X=10; Y=10; W=20; H=20; Text=$null; FontSize=0 }
297+
[pscustomobject]@{ Type='arrow'; Color='red'; X=20; Y=20; W=30; H=30; Text=$null; FontSize=0 }
298+
[pscustomobject]@{ Type='text'; Color='green'; X=30; Y=30; W=40; H=40; Text='hello'; FontSize=24 }
299+
)
300+
$r = Copy-AnnotationList $src
301+
ShouldBe $r.Count 4
302+
ShouldBe $r[0].Type 'highlight'
303+
ShouldBe $r[1].Type 'rect'
304+
ShouldBe $r[2].Type 'arrow'
305+
ShouldBe $r[3].Type 'text'
306+
ShouldBe $r[3].Text 'hello'
307+
ShouldBe $r[3].FontSize 24
308+
}
309+
It 'undo-then-redo round trip: a sequence of copies yields identical content' {
310+
$original = @(
311+
[pscustomobject]@{ Type='highlight'; Color='yellow'; X=1; Y=2; W=3; H=4; Text=$null; FontSize=0 }
312+
[pscustomobject]@{ Type='text'; Color='red'; X=5; Y=6; W=7; H=8; Text='hi'; FontSize=16 }
313+
)
314+
$snap1 = Copy-AnnotationList $original # undo entry
315+
$snap2 = Copy-AnnotationList $snap1 # redo entry after "undo"
316+
$snap3 = Copy-AnnotationList $snap2 # restored after "redo"
317+
ShouldBe $snap3.Count 2
318+
ShouldBe $snap3[0].X 1
319+
ShouldBe $snap3[1].Text 'hi'
320+
# And the original is untouched throughout
321+
ShouldBe $original[0].X 1
322+
}
323+
212324
Describe 'Get-ClampedAnnotationRect'
213325
It 'passes through a rect that is fully inside' {
214326
$r = Get-ClampedAnnotationRect -X 10 -Y 20 -Width 100 -Height 50 `

0 commit comments

Comments
 (0)