Skip to content

Commit ee5bacb

Browse files
committed
Enhance quadrilateral selection and hover handling
- Added hover interactions with `MouseEnter` and `MouseLeave` events. - Introduced hover highlight functionality in `MainWindow`. - Improved duplicate detection in `QuadrilateralDetector`. - Integrated new `QuadrilateralSelector` into the UI. - Refactored and cleaned up unused code for better maintainability.
1 parent f59bc5a commit ee5bacb

File tree

6 files changed

+179
-45
lines changed

6 files changed

+179
-45
lines changed

MagickCrop/Controls/QuadrilateralSelector.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
CornerRadius="4"
3636
Cursor="Hand"
3737
MouseDown="QuadrilateralItem_MouseDown"
38+
MouseEnter="QuadrilateralItem_MouseEnter"
39+
MouseLeave="QuadrilateralItem_MouseLeave"
3840
ToolTip="Click to use this quadrilateral">
3941
<Border.Style>
4042
<Style TargetType="Border">

MagickCrop/Controls/QuadrilateralSelector.xaml.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ private static PointCollection ScalePointsForPreview(QuadrilateralDetector.Detec
6262
public event EventHandler<QuadrilateralDetector.DetectedQuadrilateral>? QuadrilateralSelected;
6363
public event EventHandler? ManualSelection;
6464
public event EventHandler? Cancelled;
65+
public event EventHandler<QuadrilateralDetector.DetectedQuadrilateral>? QuadrilateralHoverEnter;
66+
public event EventHandler? QuadrilateralHoverExit;
6567

6668
public QuadrilateralSelector()
6769
{
@@ -82,6 +84,19 @@ private void QuadrilateralItem_MouseDown(object sender, MouseButtonEventArgs e)
8284
}
8385
}
8486

87+
private void QuadrilateralItem_MouseEnter(object sender, MouseEventArgs e)
88+
{
89+
if (sender is Border border && border.DataContext is QuadrilateralViewModel vm)
90+
{
91+
QuadrilateralHoverEnter?.Invoke(this, vm.Quadrilateral);
92+
}
93+
}
94+
95+
private void QuadrilateralItem_MouseLeave(object sender, MouseEventArgs e)
96+
{
97+
QuadrilateralHoverExit?.Invoke(this, EventArgs.Empty);
98+
}
99+
85100
private void ManualButton_Click(object sender, RoutedEventArgs e)
86101
{
87102
ManualSelection?.Invoke(this, EventArgs.Empty);

MagickCrop/Helpers/QuadrilateralDetector.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public static class QuadrilateralDetector
2121
private const double SizeWeight = 0.6;
2222
private const double RectangularityWeight = 0.4;
2323

24+
// Duplicate detection threshold - quadrilaterals with corners closer than this are considered duplicates
25+
private const double DuplicateDistanceThreshold = 5.0; // pixels
26+
2427
/// <summary>
2528
/// Represents a detected quadrilateral with its corner points
2629
/// </summary>
@@ -183,6 +186,9 @@ public static DetectionResult DetectQuadrilateralsWithDimensions(string imagePat
183186
}
184187
}
185188

189+
// Remove duplicates before sorting
190+
result.Quadrilaterals = FilterDuplicates(result.Quadrilaterals);
191+
186192
// Sort by confidence (highest first) and take top results
187193
result.Quadrilaterals = result.Quadrilaterals.OrderByDescending(q => q.Confidence).Take(maxResults).ToList();
188194
}
@@ -296,6 +302,61 @@ private static double CalculateAngle(System.Windows.Point p1, System.Windows.Poi
296302
return angleDiff;
297303
}
298304

305+
/// <summary>
306+
/// Filter out duplicate quadrilaterals that have very similar corner positions
307+
/// </summary>
308+
private static List<DetectedQuadrilateral> FilterDuplicates(List<DetectedQuadrilateral> quadrilaterals)
309+
{
310+
var filtered = new List<DetectedQuadrilateral>();
311+
312+
foreach (var quad in quadrilaterals)
313+
{
314+
bool isDuplicate = false;
315+
foreach (var existing in filtered)
316+
{
317+
if (AreDuplicates(quad, existing))
318+
{
319+
isDuplicate = true;
320+
break;
321+
}
322+
}
323+
324+
if (!isDuplicate)
325+
{
326+
filtered.Add(quad);
327+
}
328+
}
329+
330+
return filtered;
331+
}
332+
333+
/// <summary>
334+
/// Check if two quadrilaterals are duplicates based on corner proximity
335+
/// </summary>
336+
private static bool AreDuplicates(DetectedQuadrilateral quad1, DetectedQuadrilateral quad2)
337+
{
338+
// Calculate average distance between corresponding corners
339+
double totalDistance =
340+
Distance(quad1.TopLeft, quad2.TopLeft) +
341+
Distance(quad1.TopRight, quad2.TopRight) +
342+
Distance(quad1.BottomRight, quad2.BottomRight) +
343+
Distance(quad1.BottomLeft, quad2.BottomLeft);
344+
345+
double averageDistance = totalDistance / 4.0;
346+
347+
return averageDistance < DuplicateDistanceThreshold;
348+
}
349+
350+
/// <summary>
351+
/// Calculate Euclidean distance between two points
352+
/// </summary>
353+
private static double Distance(System.Windows.Point p1, System.Windows.Point p2)
354+
{
355+
double dx = p1.X - p2.X;
356+
double dy = p1.Y - p2.Y;
357+
return Math.Sqrt(dx * dx + dy * dy);
358+
}
359+
299360
/// <summary>
300361
/// Scale detected quadrilateral points to fit display dimensions
301362
/// </summary>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using MagickCrop.Helpers;
2+
using System.Windows;
3+
using System.Windows.Media;
4+
using System.Windows.Shapes;
5+
6+
namespace MagickCrop;
7+
8+
public partial class MainWindow
9+
{
10+
private void QuadrilateralSelector_HoverEnter(object? sender, QuadrilateralDetector.DetectedQuadrilateral quad)
11+
{
12+
ShowHoverHighlight(quad);
13+
}
14+
15+
private void QuadrilateralSelector_HoverExit(object? sender, EventArgs e)
16+
{
17+
RemoveHoverHighlight();
18+
}
19+
20+
private void ShowHoverHighlight(QuadrilateralDetector.DetectedQuadrilateral quad)
21+
{
22+
// Remove existing highlight if any
23+
RemoveHoverHighlight();
24+
25+
// Create highlight polygon
26+
hoverHighlightPolygon = new Polygon
27+
{
28+
Stroke = new SolidColorBrush(Color.FromArgb(255, 255, 165, 0)), // Orange
29+
StrokeThickness = 3,
30+
Fill = new SolidColorBrush(Color.FromArgb(60, 255, 165, 0)), // Semi-transparent orange
31+
IsHitTestVisible = false,
32+
StrokeLineJoin = PenLineJoin.Round,
33+
Points = new PointCollection
34+
{
35+
quad.TopLeft,
36+
quad.TopRight,
37+
quad.BottomRight,
38+
quad.BottomLeft
39+
}
40+
};
41+
42+
// Add to canvas
43+
ShapeCanvas.Children.Add(hoverHighlightPolygon);
44+
}
45+
46+
private void RemoveHoverHighlight()
47+
{
48+
if (hoverHighlightPolygon != null)
49+
{
50+
ShapeCanvas.Children.Remove(hoverHighlightPolygon);
51+
hoverHighlightPolygon = null;
52+
}
53+
}
54+
55+
private void UnwireQuadrilateralHoverEvents()
56+
{
57+
QuadrilateralSelectorControl.QuadrilateralHoverEnter -= QuadrilateralSelector_HoverEnter;
58+
QuadrilateralSelectorControl.QuadrilateralHoverExit -= QuadrilateralSelector_HoverExit;
59+
RemoveHoverHighlight();
60+
}
61+
62+
private void HideQuadrilateralSelector()
63+
{
64+
QuadrilateralSelectorControl.Visibility = Visibility.Collapsed;
65+
UnwireQuadrilateralHoverEvents();
66+
}
67+
68+
private void ShowQuadrilateralSelector()
69+
{
70+
QuadrilateralSelectorControl.Visibility = Visibility.Visible;
71+
}
72+
}

MagickCrop/MainWindow.xaml

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,16 @@
840840
<wpfui:SymbolIcon Symbol="ShapeIntersect24" />
841841
</wpfui:Button.Icon>
842842
</wpfui:Button>
843+
844+
<!-- Quadrilateral Selector in Sidebar -->
845+
<c:QuadrilateralSelector
846+
x:Name="QuadrilateralSelectorControl"
847+
Margin="0,8,0,0"
848+
Visibility="Collapsed"
849+
Cancelled="QuadrilateralSelector_Cancelled"
850+
ManualSelection="QuadrilateralSelector_ManualSelection"
851+
QuadrilateralSelected="QuadrilateralSelector_Selected" />
852+
843853
<wpfui:Button
844854
x:Name="ApplyTransformButton"
845855
Width="180"
@@ -1066,44 +1076,9 @@
10661076
ToolTip="Processing..."
10671077
Visibility="Collapsed" />
10681078

1069-
<!-- Quadrilateral Selector Overlay -->
1070-
<Border
1071-
x:Name="QuadrilateralSelectorOverlay"
1072-
Grid.Row="1"
1073-
Grid.ColumnSpan="2"
1074-
Background="#CC000000"
1075-
Visibility="Collapsed">
1076-
<Border
1077-
MaxWidth="450"
1078-
MaxHeight="400"
1079-
Margin="40"
1080-
HorizontalAlignment="Center"
1081-
VerticalAlignment="Center"
1082-
Background="{DynamicResource ApplicationBackgroundBrush}"
1083-
BorderBrush="{DynamicResource ControlStrongStrokeColorDefaultBrush}"
1084-
BorderThickness="1"
1085-
CornerRadius="8">
1086-
<c:QuadrilateralSelector
1087-
x:Name="QuadrilateralSelectorControl"
1088-
Cancelled="QuadrilateralSelector_Cancelled"
1089-
ManualSelection="QuadrilateralSelector_ManualSelection"
1090-
QuadrilateralSelected="QuadrilateralSelector_Selected" />
1091-
</Border>
1092-
</Border>
1093-
10941079
<ContentPresenter
10951080
x:Name="Presenter"
1096-
Grid.Row="1"
1097-
Grid.ColumnSpan="2" />
1098-
1099-
<Grid.ContextMenu>
1100-
<ContextMenu>
1101-
<MenuItem
1102-
x:Name="ResetMenuItem"
1103-
Click="ResetMenuItem_Click"
1104-
Header="Reset"
1105-
ToolTip="Reset the workspace" />
1106-
</ContextMenu>
1107-
</Grid.ContextMenu>
1081+
Grid.Row="1"
1082+
Grid.ColumnSpan="2" />
11081083
</Grid>
11091084
</wpfui:FluentWindow>

MagickCrop/MainWindow.xaml.cs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ public partial class MainWindow : FluentWindow
131131
private AdornerLayer? rotateAdornerLayer;
132132
private bool isAdornerRotatingDrag = false; // true while adorner has the mouse captured
133133

134+
// Hover highlight polygon for quadrilateral selector
135+
private Polygon? hoverHighlightPolygon;
136+
134137
public MainWindow()
135138
{
136139
ThemeService themeService = new();
@@ -1808,6 +1811,9 @@ private void HideTransformControls()
18081811

18091812
foreach (UIElement element in _polygonElements)
18101813
element.Visibility = Visibility.Collapsed;
1814+
1815+
if (lines is not null)
1816+
lines.Visibility = Visibility.Collapsed;
18111817
}
18121818

18131819
private async void DetectShapeButton_Click(object sender, RoutedEventArgs e)
@@ -1848,10 +1854,12 @@ private async void DetectShapeButton_Click(object sender, RoutedEventArgs e)
18481854

18491855
// Show selector
18501856
QuadrilateralSelectorControl.SetQuadrilaterals(scaledQuads);
1851-
QuadrilateralSelectorOverlay.Visibility = Visibility.Visible;
1852-
}
1857+
QuadrilateralSelectorControl.QuadrilateralHoverEnter += QuadrilateralSelector_HoverEnter;
1858+
QuadrilateralSelectorControl.QuadrilateralHoverExit += QuadrilateralSelector_HoverExit;
1859+
ShowQuadrilateralSelector();
1860+
}
18531861
}
1854-
catch (Exception ex)
1862+
catch (Exception ex)
18551863
{
18561864
_ = System.Windows.MessageBox.Show(
18571865
$"Error detecting quadrilaterals: {ex.Message}",
@@ -1868,7 +1876,7 @@ private async void DetectShapeButton_Click(object sender, RoutedEventArgs e)
18681876
private void QuadrilateralSelector_Selected(object? sender, Helpers.QuadrilateralDetector.DetectedQuadrilateral quad)
18691877
{
18701878
// Hide selector overlay
1871-
QuadrilateralSelectorOverlay.Visibility = Visibility.Collapsed;
1879+
HideQuadrilateralSelector();
18721880

18731881
// Position the corner markers
18741882
PositionCornerMarkers(quad);
@@ -1877,13 +1885,13 @@ private void QuadrilateralSelector_Selected(object? sender, Helpers.Quadrilatera
18771885
private void QuadrilateralSelector_ManualSelection(object? sender, EventArgs e)
18781886
{
18791887
// Hide selector overlay and let user position markers manually
1880-
QuadrilateralSelectorOverlay.Visibility = Visibility.Collapsed;
1888+
HideQuadrilateralSelector();
18811889
}
18821890

18831891
private void QuadrilateralSelector_Cancelled(object? sender, EventArgs e)
18841892
{
18851893
// Hide selector overlay
1886-
QuadrilateralSelectorOverlay.Visibility = Visibility.Collapsed;
1894+
HideQuadrilateralSelector();
18871895
}
18881896

18891897
private void PositionCornerMarkers(Helpers.QuadrilateralDetector.DetectedQuadrilateral quad)
@@ -2395,7 +2403,7 @@ private void MeasurementPoint_MouseDown(object sender, MouseButtonEventArgs? e)
23952403
{
23962404
if (isAdornerRotatingDrag)
23972405
{
2398-
if (e is not null) e.Handled = true;
2406+
e.Handled = true;
23992407
return;
24002408
}
24012409
if (sender is Ellipse senderEllipse
@@ -2614,7 +2622,6 @@ private void SaveMeasurementsPackageToFile()
26142622
{
26152623
package.Measurements.PolygonMeasurements.Add(control.ToDto());
26162624
}
2617-
Debug.WriteLine($"Saved {polygonMeasurementTools.Count} polygon measurements");
26182625

26192626
foreach (VerticalLineControl control in verticalLineControls)
26202627
package.Measurements.VerticalLines.Add(control.ToDto());
@@ -3547,6 +3554,7 @@ private void ApplyPreviewRotation()
35473554
{
35483555
if (previewRotateTransform == null)
35493556
return;
3557+
35503558
previewRotateTransform.Angle = currentPreviewRotation;
35513559
}
35523560

@@ -3567,6 +3575,7 @@ private void RotateAngleSlider_ValueChanged(object sender, RoutedPropertyChanged
35673575
{
35683576
if (suppressRotateEvents || !isRotateMode)
35693577
return;
3578+
35703579
currentPreviewRotation = e.NewValue;
35713580
UpdateRotationUiValues(currentPreviewRotation); // keep number box in sync
35723581
ApplyPreviewRotation();

0 commit comments

Comments
 (0)