From e635b708a2fc2b11e2e16c42c8981a23b1866b86 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Sat, 15 Aug 2026 22:59:20 +0200 Subject: [PATCH] Improve crop editor and copy publish output to dist Add resize handles, zoom, view brightness, and a multi-frame still strip. Keep the crop box on mouse-up, and copy AmiReel.exe to dist. --- .gitignore | 1 + .../Models/SourceCropSelectionTests.cs | 115 +++++ AmiReel.Tests/Services/ShortsPipelineTests.cs | 33 ++ Controls/SourceCropEditor.xaml | 109 ++++- Controls/SourceCropEditor.xaml.cs | 426 +++++++++++++++--- MainPage.xaml.cs | 4 + Models/SourceCropSelection.cs | 126 +++++- README.md | 5 +- Services/ShortsPipeline.cs | 56 +++ publish-win-x64.ps1 | 22 +- 10 files changed, 801 insertions(+), 96 deletions(-) diff --git a/.gitignore b/.gitignore index cd47807..33e2845 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ [Dd]ebug/ [Rr]elease/ publish/ +dist/ # IDE / tooling .vs/ diff --git a/AmiReel.Tests/Models/SourceCropSelectionTests.cs b/AmiReel.Tests/Models/SourceCropSelectionTests.cs index d14d027..0ecb9e4 100644 --- a/AmiReel.Tests/Models/SourceCropSelectionTests.cs +++ b/AmiReel.Tests/Models/SourceCropSelectionTests.cs @@ -57,6 +57,19 @@ public class SourceCropSelectionTests Assert.AreEqual(30, insets.Bottom); } + [TestMethod] + public void ToInsets_KeepsTheDraggedWidthAndHeight() + { + // Act + PixelInsets insets = SourceCropSelection.ToInsets(3840, 2160, 768, 432, 100, 50, 200, 120); + + // Assert + Assert.AreEqual(500, insets.Left); + Assert.AreEqual(250, insets.Top); + Assert.AreEqual(1000, 3840 - insets.Left - insets.Right); + Assert.AreEqual(600, 2160 - insets.Top - insets.Bottom); + } + [TestMethod] public void ToInsets_DraggedUpAndLeft_NormalizesTheRectangle() { @@ -97,4 +110,106 @@ public class SourceCropSelectionTests Assert.AreEqual(0, x); Assert.AreEqual(60, y); } + + [TestMethod] + public void HitTest_OnNorthWestHandle_ReturnsNorthWest() + { + // Act + CropHit hit = SourceCropSelection.HitTest(20, 30, 80, 40, 20, 30, 12); + + // Assert + Assert.AreEqual(CropHit.NorthWest, hit); + } + + [TestMethod] + public void HitTest_InsideRectangle_ReturnsMove() + { + // Act + CropHit hit = SourceCropSelection.HitTest(20, 30, 80, 40, 50, 50, 12); + + // Assert + Assert.AreEqual(CropHit.Move, hit); + } + + [TestMethod] + public void HitTest_OutsideRectangle_ReturnsNone() + { + // Act + CropHit hit = SourceCropSelection.HitTest(20, 30, 80, 40, 2, 2, 12); + + // Assert + Assert.AreEqual(CropHit.None, hit); + } + + [TestMethod] + public void AdjustRect_Move_KeepsSizeAndClamps() + { + // Act + (double x, double y, double width, double height) = + SourceCropSelection.AdjustRect(10, 10, 40, 20, CropHit.Move, 100, -4, 80, 60); + + // Assert + Assert.AreEqual(40, x); + Assert.AreEqual(6, y); + Assert.AreEqual(40, width); + Assert.AreEqual(20, height); + } + + [TestMethod] + public void AdjustRect_EastHandle_ChangesWidthOnly() + { + // Act + (double x, double y, double width, double height) = + SourceCropSelection.AdjustRect(10, 10, 40, 20, CropHit.East, 8, 5, 80, 60); + + // Assert + Assert.AreEqual(10, x); + Assert.AreEqual(10, y); + Assert.AreEqual(48, width); + Assert.AreEqual(20, height); + } + + [TestMethod] + public void AdjustRect_WestHandle_MovesLeftEdge() + { + // Act + (double x, _, double width, _) = + SourceCropSelection.AdjustRect(20, 10, 40, 20, CropHit.West, 6, 0, 80, 60); + + // Assert + Assert.AreEqual(26, x); + Assert.AreEqual(34, width); + } + + [TestMethod] + public void Nudge_MovesBoxWithoutChangingSize() + { + // Arrange + PixelInsets start = new(10, 20, 8, 12); + + // Act + PixelInsets moved = SourceCropSelection.Nudge(start, 100, 80, 4, 0); + + // Assert + Assert.AreEqual(14, moved.Left); + Assert.AreEqual(16, moved.Right); + Assert.AreEqual(8, moved.Top); + Assert.AreEqual(12, moved.Bottom); + } + + [TestMethod] + public void NudgeEdge_GrowsRightSide() + { + // Arrange + PixelInsets start = new(10, 20, 8, 12); + + // Act + PixelInsets grown = SourceCropSelection.NudgeEdge(start, 100, 80, 0, 0, -4, 0); + + // Assert + Assert.AreEqual(10, grown.Left); + Assert.AreEqual(16, grown.Right); + Assert.AreEqual(8, grown.Top); + Assert.AreEqual(12, grown.Bottom); + } } diff --git a/AmiReel.Tests/Services/ShortsPipelineTests.cs b/AmiReel.Tests/Services/ShortsPipelineTests.cs index fe4ecd0..c0fc463 100644 --- a/AmiReel.Tests/Services/ShortsPipelineTests.cs +++ b/AmiReel.Tests/Services/ShortsPipelineTests.cs @@ -483,6 +483,39 @@ public class ShortsPipelineTests CollectionAssert.Contains(args, "in.mp4"); } + [TestMethod] + public void CropPreviewOffsets_SpreadsFramesAcrossDuration() + { + // Act + IReadOnlyList offsets = ShortsPipeline.CropPreviewOffsets(30, 5); + + // Assert + Assert.AreEqual(5, offsets.Count); + Assert.AreEqual(0, offsets[0]); + Assert.AreEqual(29.85, offsets[^1]); + Assert.AreEqual(14.925, offsets[2]); + } + + [TestMethod] + public void CropPreviewOffsets_WithZeroDuration_ReturnsStartOnly() + { + // Act + IReadOnlyList offsets = ShortsPipeline.CropPreviewOffsets(0); + + // Assert + CollectionAssert.AreEqual(new[] { 0d }, offsets.ToArray()); + } + + [TestMethod] + public void FormatPreviewOffsetLabel_UnderOneHour_UsesMinutesAndSeconds() + { + // Act + string label = ShortsPipeline.FormatPreviewOffsetLabel(75); + + // Assert + Assert.AreEqual("1:15", label); + } + [TestMethod] public void BuildExtractFrameArguments_SeeksToStartAndWritesOnePng() { diff --git a/Controls/SourceCropEditor.xaml b/Controls/SourceCropEditor.xaml index 2e6368c..7e6191d 100644 --- a/Controls/SourceCropEditor.xaml +++ b/Controls/SourceCropEditor.xaml @@ -9,7 +9,7 @@ + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + diff --git a/Controls/SourceCropEditor.xaml.cs b/Controls/SourceCropEditor.xaml.cs index 1685f82..6bb6629 100644 --- a/Controls/SourceCropEditor.xaml.cs +++ b/Controls/SourceCropEditor.xaml.cs @@ -1,21 +1,43 @@ using System.Runtime.InteropServices.WindowsRuntime; using AmiReel.Models; using AmiReel.Services; +using Microsoft.UI; using Microsoft.UI.Input; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using Windows.Foundation; +using Windows.System; +using Windows.UI.Core; namespace AmiReel.Controls; public sealed partial class SourceCropEditor : UserControl { + private const double HandleHitSize = 12; + private const double HandleVisualSize = 10; + private const double MinZoom = 1; + private const double MaxZoom = 4; + private const double ZoomStep = 0.25; + private const int ThumbnailWidth = 128; + + private readonly List