Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"Bash(\"/mnt/c/Program Files/dotnet/dotnet\" build)",
"PowerShell(git diff *)",
"PowerShell(dotnet build *)",
"Bash(gh issue list *)"
"Bash(gh issue list *)",
"Bash(dotnet test *)"
],
"deny": []
}
Expand Down
114 changes: 114 additions & 0 deletions MagickCrop.Tests/Helpers/GeometryMathHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System.Windows;
using MagickCrop.Helpers;

namespace MagickCrop.Tests.Helpers;

[TestClass]
public class GeometryMathHelperTests
{
[TestMethod]
public void MidPoint_ReturnsAverageOfCoordinates()
{
Point result = GeometryMathHelper.MidPoint(new Point(0, 0), new Point(10, 20));

Assert.AreEqual(5, result.X);
Assert.AreEqual(10, result.Y);
}

[TestMethod]
public void Distance_ForHorizontalSegment_ReturnsDeltaX()
{
double result = GeometryMathHelper.Distance(new Point(0, 0), new Point(3, 0));

Assert.AreEqual(3, result, 1e-9);
}

[TestMethod]
public void Distance_For3_4_5Triangle_ReturnsFive()
{
double result = GeometryMathHelper.Distance(new Point(0, 0), new Point(3, 4));

Assert.AreEqual(5, result, 1e-9);
}

[TestMethod]
public void PolygonPerimeter_ClosedTriangle_SumsAllThreeEdges()
{
Point[] triangle = [new(0, 0), new(4, 0), new(0, 3)];

double result = GeometryMathHelper.PolygonPerimeter(triangle, isClosed: true);

// 4 + 5 + 3 = 12
Assert.AreEqual(12, result, 1e-9);
}

[TestMethod]
public void PolygonPerimeter_OpenPolyline_ExcludesClosingEdge()
{
Point[] triangle = [new(0, 0), new(4, 0), new(0, 3)];

double result = GeometryMathHelper.PolygonPerimeter(triangle, isClosed: false);

// 4 + 5 = 9 (no closing edge back to the start)
Assert.AreEqual(9, result, 1e-9);
}

[TestMethod]
public void PolygonPerimeter_FewerThanTwoVertices_ReturnsZero()
{
Assert.AreEqual(0, GeometryMathHelper.PolygonPerimeter([], isClosed: true));
Assert.AreEqual(0, GeometryMathHelper.PolygonPerimeter([new Point(1, 1)], isClosed: true));
}

[TestMethod]
public void PolygonArea_UnitSquare_ReturnsOne()
{
Point[] square = [new(0, 0), new(1, 0), new(1, 1), new(0, 1)];

double result = GeometryMathHelper.PolygonArea(square);

Assert.AreEqual(1, result, 1e-9);
}

[TestMethod]
public void PolygonArea_FewerThanThreeVertices_ReturnsZero()
{
Point[] segment = [new(0, 0), new(1, 1)];

Assert.AreEqual(0, GeometryMathHelper.PolygonArea(segment));
}

[TestMethod]
public void TryGetCircumcircle_ForPointsOnUnitCircle_FindsOriginAndRadiusOne()
{
bool found = GeometryMathHelper.TryGetCircumcircle(
new Point(1, 0), new Point(0, 1), new Point(-1, 0), out Point center, out double radius);

Assert.IsTrue(found);
Assert.AreEqual(0, center.X, 1e-9);
Assert.AreEqual(0, center.Y, 1e-9);
Assert.AreEqual(1, radius, 1e-9);
}

[TestMethod]
public void TryGetCircumcircle_ForCollinearPoints_ReturnsFalse()
{
bool found = GeometryMathHelper.TryGetCircumcircle(
new Point(0, 0), new Point(1, 1), new Point(2, 2), out _, out _);

Assert.IsFalse(found);
}

[TestMethod]
public void BezierControlFromPassThrough_ForMidpointOnStraightLine_ReturnsThatSamePoint()
{
Point start = new(0, 0);
Point end = new(10, 0);
Point mid = new(5, 0);

Point control = GeometryMathHelper.BezierControlFromPassThrough(start, mid, end);

Assert.AreEqual(5, control.X, 1e-9);
Assert.AreEqual(0, control.Y, 1e-9);
}
}
39 changes: 39 additions & 0 deletions MagickCrop.Tests/Helpers/MeasurementFormattingHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using MagickCrop.Helpers;

namespace MagickCrop.Tests.Helpers;

[TestClass]
public class MeasurementFormattingHelperTests
{
[TestMethod]
public void FormatPerimeter_IncludesUnitsAndTwoDecimalPlaces()
{
string result = MeasurementFormattingHelper.FormatPerimeter(12.3456, "cm");

Assert.AreEqual("P: 12.35 cm", result);
}

[TestMethod]
public void FormatPerimeterArea_SquaresTheLinearUnits()
{
string result = MeasurementFormattingHelper.FormatPerimeterArea(10, 6.25, "cm");

Assert.AreEqual("P: 10.00 cm, A: 6.25 cm²", result);
}

[TestMethod]
public void FormatNeedMorePoints_IncludesRemainingCount()
{
string result = MeasurementFormattingHelper.FormatNeedMorePoints(4.5, "px", 2);

Assert.AreEqual("P: 4.50 px (Need 2 more points)", result);
}

[TestMethod]
public void FormatClickToClose_IncludesInstructionalText()
{
string result = MeasurementFormattingHelper.FormatClickToClose(4.5, "px");

Assert.AreEqual("P: 4.50 px (Click orange point to close)", result);
}
}
1 change: 1 addition & 0 deletions MagickCrop.Tests/MSTestSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
40 changes: 40 additions & 0 deletions MagickCrop.Tests/MagickCrop.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0-windows10.0.20348.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Platforms>x64;ARM64</Platforms>
<!--
Default to the host CPU architecture when no Platform is specified (e.g. plain
`dotnet test`/`dotnet build`, or a solution build via the "Any CPU" row) so this
resolves correctly on both x64 and arm64 dev machines. Pass -p:Platform=x64 or
-p:Platform=ARM64 explicitly to override.
-->
<Platform Condition="('$(Platform)' == '' Or '$(Platform)' == 'AnyCPU' Or '$(Platform)' == 'Any CPU') And '$(PROCESSOR_ARCHITECTURE)' == 'ARM64'">ARM64</Platform>
<Platform Condition="'$(Platform)' == '' Or '$(Platform)' == 'AnyCPU' Or '$(Platform)' == 'Any CPU'">x64</Platform>
<PlatformTarget>$(Platform)</PlatformTarget>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MSTest" Version="4.0.2" />
<PackageReference Include="Moq" Version="4.20.72" />
</ItemGroup>

<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>

<ItemGroup>
<!-- Forces MagickCrop to build for the same architecture as this test project,
regardless of what Platform the outer build (e.g. a solution build) passes in. -->
<ProjectReference Include="..\MagickCrop\MagickCrop.csproj">
<SetPlatform>Platform=$(Platform)</SetPlatform>
</ProjectReference>
</ItemGroup>

</Project>
111 changes: 111 additions & 0 deletions MagickCrop.Tests/UndoRedoTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using MagickCrop;

namespace MagickCrop.Tests;

[TestClass]
public class UndoRedoTests
{
private sealed class FakeUndoRedoItem : UndoRedoItem
{
public int UndoCalls { get; private set; }
public int RedoCalls { get; private set; }
public string UndoResult { get; set; } = string.Empty;
public string RedoResult { get; set; } = string.Empty;

public override string Undo()
{
UndoCalls++;
return UndoResult;
}

public override string Redo()
{
RedoCalls++;
return RedoResult;
}
}

[TestMethod]
public void NewUndoRedo_HasNothingToUndoOrRedo()
{
UndoRedo undoRedo = new();

Assert.IsFalse(undoRedo.CanUndo);
Assert.IsFalse(undoRedo.CanRedo);
}

[TestMethod]
public void AddUndo_MakesCanUndoTrueAndCanRedoFalse()
{
UndoRedo undoRedo = new();

undoRedo.AddUndo(new FakeUndoRedoItem());

Assert.IsTrue(undoRedo.CanUndo);
Assert.IsFalse(undoRedo.CanRedo);
}

[TestMethod]
public void Undo_InvokesItemUndoAndMovesItToRedoStack()
{
UndoRedo undoRedo = new();
FakeUndoRedoItem item = new() { UndoResult = "previous.png" };
undoRedo.AddUndo(item);

string result = undoRedo.Undo();

Assert.AreEqual("previous.png", result);
Assert.AreEqual(1, item.UndoCalls);
Assert.IsFalse(undoRedo.CanUndo);
Assert.IsTrue(undoRedo.CanRedo);
}

[TestMethod]
public void Redo_AfterUndo_InvokesItemRedoAndMovesItBackToUndoStack()
{
UndoRedo undoRedo = new();
FakeUndoRedoItem item = new() { RedoResult = "next.png" };
undoRedo.AddUndo(item);
undoRedo.Undo();

string result = undoRedo.Redo();

Assert.AreEqual("next.png", result);
Assert.AreEqual(1, item.RedoCalls);
Assert.IsTrue(undoRedo.CanUndo);
Assert.IsFalse(undoRedo.CanRedo);
}

[TestMethod]
public void Undo_WithEmptyStack_ReturnsEmptyStringAndDoesNotThrow()
{
UndoRedo undoRedo = new();

string result = undoRedo.Undo();

Assert.AreEqual(string.Empty, result);
}

[TestMethod]
public void Redo_WithEmptyStack_ReturnsEmptyStringAndDoesNotThrow()
{
UndoRedo undoRedo = new();

string result = undoRedo.Redo();

Assert.AreEqual(string.Empty, result);
}

[TestMethod]
public void AddUndo_AfterAnUndo_ClearsTheRedoStack()
{
UndoRedo undoRedo = new();
undoRedo.AddUndo(new FakeUndoRedoItem());
undoRedo.Undo();
Assert.IsTrue(undoRedo.CanRedo);

undoRedo.AddUndo(new FakeUndoRedoItem());

Assert.IsFalse(undoRedo.CanRedo);
}
}
22 changes: 22 additions & 0 deletions MagickCrop.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MagickCrop", "MagickCrop\Ma
EndProject
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "MagickCrop-Package", "MagickCrop-Package\MagickCrop-Package.wapproj", "{F6C2CFAD-9A9B-48F6-A5F3-DB3BCC25CACB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MagickCrop.Tests", "MagickCrop.Tests\MagickCrop.Tests.csproj", "{113A3DCE-5333-4003-9B94-61C46F5BB543}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -71,6 +73,26 @@ Global
{F6C2CFAD-9A9B-48F6-A5F3-DB3BCC25CACB}.Release|x86.ActiveCfg = Release|x86
{F6C2CFAD-9A9B-48F6-A5F3-DB3BCC25CACB}.Release|x86.Build.0 = Release|x86
{F6C2CFAD-9A9B-48F6-A5F3-DB3BCC25CACB}.Release|x86.Deploy.0 = Release|x86
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Debug|Any CPU.ActiveCfg = Debug|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Debug|Any CPU.Build.0 = Debug|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Debug|ARM.ActiveCfg = Debug|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Debug|ARM.Build.0 = Debug|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Debug|ARM64.ActiveCfg = Debug|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Debug|ARM64.Build.0 = Debug|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Debug|x64.ActiveCfg = Debug|x64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Debug|x64.Build.0 = Debug|x64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Debug|x86.ActiveCfg = Debug|x64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Debug|x86.Build.0 = Debug|x64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Release|Any CPU.ActiveCfg = Release|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Release|Any CPU.Build.0 = Release|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Release|ARM.ActiveCfg = Release|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Release|ARM.Build.0 = Release|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Release|ARM64.ActiveCfg = Release|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Release|ARM64.Build.0 = Release|ARM64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Release|x64.ActiveCfg = Release|x64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Release|x64.Build.0 = Release|x64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Release|x86.ActiveCfg = Release|x64
{113A3DCE-5333-4003-9B94-61C46F5BB543}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
4 changes: 4 additions & 0 deletions MagickCrop/Controls/AngleMeasurementControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
Click="CopyMeasurementMenuItem_Click"
Header="Copy Measurement"
ToolTip="Copy the angle value" />
<MenuItem
Click="ChangeColorMenuItem_Click"
Header="Change Color"
ToolTip="Change the color of this measurement" />
<MenuItem
Click="RemoveMeasurementMenuItem_Click"
Header="Remove Measurement"
Expand Down
Loading
Loading