Files

338 lines
14 KiB
C#

namespace MarketAlly.SpotlightTour.Maui;
/// <summary>
/// Utility class for spotlight geometry calculations.
/// Provides centralized methods for positioning, overlap detection, and corner selection.
/// </summary>
public static class SpotlightGeometry
{
/// <summary>
/// Calculates the overlap area between two rectangles in square pixels.
/// </summary>
/// <param name="rect1">The first rectangle.</param>
/// <param name="rect2">The second rectangle.</param>
/// <returns>The overlapping area in square pixels, or 0 if no overlap.</returns>
public static double CalculateOverlap(Rect rect1, Rect rect2)
{
var xOverlap = Math.Max(0, Math.Min(rect1.Right, rect2.Right) - Math.Max(rect1.Left, rect2.Left));
var yOverlap = Math.Max(0, Math.Min(rect1.Bottom, rect2.Bottom) - Math.Max(rect1.Top, rect2.Top));
return xOverlap * yOverlap;
}
/// <summary>
/// Gets the rectangle bounds for a CalloutCorner position.
/// </summary>
/// <param name="corner">The corner position.</param>
/// <param name="containerSize">The container size.</param>
/// <param name="elementWidth">The element width.</param>
/// <param name="elementHeight">The element height.</param>
/// <param name="margin">The margin from the edge.</param>
/// <returns>The rectangle bounds for the corner position.</returns>
public static Rect GetCornerRect(CalloutCorner corner, Size containerSize, double elementWidth, double elementHeight, double margin)
{
return corner switch
{
CalloutCorner.TopLeft => new Rect(margin, margin, elementWidth, elementHeight),
CalloutCorner.TopRight => new Rect(containerSize.Width - elementWidth - margin, margin, elementWidth, elementHeight),
CalloutCorner.BottomLeft => new Rect(margin, containerSize.Height - elementHeight - margin, elementWidth, elementHeight),
CalloutCorner.BottomRight => new Rect(containerSize.Width - elementWidth - margin, containerSize.Height - elementHeight - margin, elementWidth, elementHeight),
_ => new Rect(margin, containerSize.Height - elementHeight - margin, elementWidth, elementHeight)
};
}
/// <summary>
/// Gets the rectangle bounds for a CornerNavigatorPlacement position.
/// </summary>
/// <param name="placement">The corner placement.</param>
/// <param name="containerSize">The container size.</param>
/// <param name="elementWidth">The element width.</param>
/// <param name="elementHeight">The element height.</param>
/// <param name="margin">The margin from the edge.</param>
/// <returns>The rectangle bounds for the corner position.</returns>
public static Rect GetCornerRect(CornerNavigatorPlacement placement, Size containerSize, double elementWidth, double elementHeight, double margin)
{
return placement switch
{
CornerNavigatorPlacement.TopLeft => new Rect(margin, margin, elementWidth, elementHeight),
CornerNavigatorPlacement.TopRight => new Rect(containerSize.Width - elementWidth - margin, margin, elementWidth, elementHeight),
CornerNavigatorPlacement.BottomLeft => new Rect(margin, containerSize.Height - elementHeight - margin, elementWidth, elementHeight),
CornerNavigatorPlacement.BottomRight => new Rect(containerSize.Width - elementWidth - margin, containerSize.Height - elementHeight - margin, elementWidth, elementHeight),
_ => new Rect(containerSize.Width - elementWidth - margin, containerSize.Height - elementHeight - margin, elementWidth, elementHeight)
};
}
/// <summary>
/// Determines the best CalloutCorner that minimizes overlap with a spotlight rectangle.
/// </summary>
/// <param name="spotlightRect">The spotlight rectangle to avoid.</param>
/// <param name="containerSize">The container size.</param>
/// <param name="elementWidth">The element width.</param>
/// <param name="elementHeight">The element height.</param>
/// <param name="margin">The margin from the edge.</param>
/// <param name="config">Optional configuration for thresholds.</param>
/// <returns>The best corner that minimizes overlap.</returns>
public static CalloutCorner DetermineBestCorner(
Rect spotlightRect,
Size containerSize,
double elementWidth,
double elementHeight,
double margin,
TourConfiguration? config = null)
{
config ??= TourConfiguration.Default;
// Calculate the effective element size for collision checking
var actualWidth = Math.Min(containerSize.Width * config.CalloutMaxWidthRatio, elementWidth);
var corners = new[]
{
CalloutCorner.TopLeft,
CalloutCorner.TopRight,
CalloutCorner.BottomLeft,
CalloutCorner.BottomRight
};
CalloutCorner bestCorner = CalloutCorner.BottomLeft;
double minOverlap = double.MaxValue;
foreach (var corner in corners)
{
var cornerRect = GetCornerRect(corner, containerSize, actualWidth, elementHeight, margin);
var overlap = CalculateOverlap(cornerRect, spotlightRect);
if (overlap < minOverlap)
{
minOverlap = overlap;
bestCorner = corner;
}
}
return bestCorner;
}
/// <summary>
/// Determines the best CornerNavigatorPlacement that minimizes overlap with spotlight and optionally a callout.
/// </summary>
/// <param name="spotlightRect">The spotlight rectangle to avoid.</param>
/// <param name="containerSize">The container size.</param>
/// <param name="elementWidth">The element width.</param>
/// <param name="elementHeight">The element height.</param>
/// <param name="margin">The margin from the edge.</param>
/// <param name="calloutBounds">Optional callout bounds to also avoid.</param>
/// <returns>The best placement that minimizes overlap.</returns>
public static CornerNavigatorPlacement DetermineBestNavigatorPlacement(
Rect spotlightRect,
Size containerSize,
double elementWidth,
double elementHeight,
double margin,
Rect? calloutBounds = null)
{
var placements = new[]
{
CornerNavigatorPlacement.TopLeft,
CornerNavigatorPlacement.TopRight,
CornerNavigatorPlacement.BottomLeft,
CornerNavigatorPlacement.BottomRight
};
CornerNavigatorPlacement bestPlacement = CornerNavigatorPlacement.BottomRight;
double minOverlap = double.MaxValue;
foreach (var placement in placements)
{
var placementRect = GetCornerRect(placement, containerSize, elementWidth, elementHeight, margin);
// Calculate overlap with spotlight
var spotlightOverlap = CalculateOverlap(placementRect, spotlightRect);
// Calculate overlap with callout if provided (weight more heavily)
var calloutOverlap = calloutBounds.HasValue
? CalculateOverlap(placementRect, calloutBounds.Value) * 2
: 0;
var totalOverlap = spotlightOverlap + calloutOverlap;
if (totalOverlap < minOverlap)
{
minOverlap = totalOverlap;
bestPlacement = placement;
// If no overlap at all, use this placement
if (totalOverlap <= 0)
break;
}
}
return bestPlacement;
}
/// <summary>
/// Gets the complementary corner (same horizontal side, opposite vertical position).
/// </summary>
/// <param name="calloutCorner">The callout corner.</param>
/// <returns>The complementary navigator placement.</returns>
public static CornerNavigatorPlacement GetComplementCorner(CalloutCorner calloutCorner)
{
return calloutCorner switch
{
CalloutCorner.TopLeft => CornerNavigatorPlacement.BottomLeft,
CalloutCorner.TopRight => CornerNavigatorPlacement.BottomRight,
CalloutCorner.BottomLeft => CornerNavigatorPlacement.TopLeft,
CalloutCorner.BottomRight => CornerNavigatorPlacement.TopRight,
_ => CornerNavigatorPlacement.BottomRight
};
}
/// <summary>
/// Gets the corner on the opposite horizontal side but same vertical position.
/// </summary>
/// <param name="calloutCorner">The callout corner.</param>
/// <returns>The opposite horizontal navigator placement.</returns>
public static CornerNavigatorPlacement GetOppositeHorizontalCorner(CalloutCorner calloutCorner)
{
return calloutCorner switch
{
CalloutCorner.TopLeft => CornerNavigatorPlacement.TopRight,
CalloutCorner.TopRight => CornerNavigatorPlacement.TopLeft,
CalloutCorner.BottomLeft => CornerNavigatorPlacement.BottomRight,
CalloutCorner.BottomRight => CornerNavigatorPlacement.BottomLeft,
_ => CornerNavigatorPlacement.BottomRight
};
}
/// <summary>
/// Clamps a vertical position to stay within container bounds.
/// </summary>
/// <param name="y">The desired Y position.</param>
/// <param name="elementHeight">The element height.</param>
/// <param name="containerHeight">The container height.</param>
/// <param name="margin">The margin from edges.</param>
/// <returns>The clamped Y position.</returns>
public static double ClampVertical(double y, double elementHeight, double containerHeight, double margin)
{
if (y < margin)
return margin;
if (y + elementHeight + margin > containerHeight)
return containerHeight - elementHeight - margin;
return y;
}
/// <summary>
/// Clamps a horizontal position to stay within container bounds.
/// </summary>
/// <param name="x">The desired X position.</param>
/// <param name="elementWidth">The element width.</param>
/// <param name="containerWidth">The container width.</param>
/// <param name="margin">The margin from edges.</param>
/// <returns>The clamped X position.</returns>
public static double ClampHorizontal(double x, double elementWidth, double containerWidth, double margin)
{
if (x < margin)
return margin;
if (x + elementWidth + margin > containerWidth)
return containerWidth - elementWidth - margin;
return x;
}
/// <summary>
/// Determines the best automatic placement for a callout relative to a spotlight.
/// </summary>
/// <param name="spotlightRect">The spotlight rectangle.</param>
/// <param name="containerSize">The container size.</param>
/// <param name="elementHeight">The estimated element height.</param>
/// <param name="margin">The margin from edges.</param>
/// <param name="config">Optional configuration for thresholds.</param>
/// <returns>The best placement direction.</returns>
public static CalloutPlacement DetermineAutoPlacement(
Rect spotlightRect,
Size containerSize,
double elementHeight,
double margin,
TourConfiguration? config = null)
{
config ??= TourConfiguration.Default;
var spaceBelow = containerSize.Height - spotlightRect.Bottom - margin;
var spaceAbove = spotlightRect.Top - margin;
var spaceRight = containerSize.Width - spotlightRect.Right - margin;
var spaceLeft = spotlightRect.Left - margin;
// Prefer bottom, then top, then right, then left
if (spaceBelow >= elementHeight + margin)
return CalloutPlacement.Bottom;
if (spaceAbove >= elementHeight + margin)
return CalloutPlacement.Top;
if (spaceRight >= config.MinSpaceForSidePlacement)
return CalloutPlacement.Right;
if (spaceLeft >= config.MinSpaceForSidePlacement)
return CalloutPlacement.Left;
// Default to bottom if nothing fits well
return CalloutPlacement.Bottom;
}
/// <summary>
/// Determines the best automatic placement for an inline label.
/// </summary>
/// <param name="spotlightRect">The spotlight rectangle.</param>
/// <param name="containerSize">The container size.</param>
/// <param name="margin">The margin from edges.</param>
/// <param name="config">Optional configuration for thresholds.</param>
/// <returns>The best placement direction.</returns>
public static CalloutPlacement DetermineInlineLabelPlacement(
Rect spotlightRect,
Size containerSize,
double margin,
TourConfiguration? config = null)
{
config ??= TourConfiguration.Default;
var spaceBelow = containerSize.Height - spotlightRect.Bottom - margin;
var spaceAbove = spotlightRect.Top - margin;
if (spaceBelow >= config.MinSpaceForPlacement)
return CalloutPlacement.Bottom;
if (spaceAbove >= config.MinSpaceForPlacement)
return CalloutPlacement.Top;
return CalloutPlacement.Bottom;
}
/// <summary>
/// Calculates the center X position for an element relative to a spotlight.
/// </summary>
/// <param name="spotlightRect">The spotlight rectangle.</param>
/// <param name="elementWidth">The element width.</param>
/// <param name="containerWidth">The container width.</param>
/// <param name="margin">The margin from edges.</param>
/// <returns>The clamped center X position.</returns>
public static double CenterXRelativeToSpotlight(
Rect spotlightRect,
double elementWidth,
double containerWidth,
double margin)
{
var centerX = spotlightRect.Center.X - elementWidth / 2;
return ClampHorizontal(centerX, elementWidth, containerWidth, margin);
}
/// <summary>
/// Calculates the center Y position for an element relative to a spotlight.
/// </summary>
/// <param name="spotlightRect">The spotlight rectangle.</param>
/// <param name="elementHeight">The element height.</param>
/// <param name="containerHeight">The container height.</param>
/// <param name="margin">The margin from edges.</param>
/// <returns>The clamped center Y position.</returns>
public static double CenterYRelativeToSpotlight(
Rect spotlightRect,
double elementHeight,
double containerHeight,
double margin)
{
var centerY = spotlightRect.Center.Y - elementHeight / 2;
return ClampVertical(centerY, elementHeight, containerHeight, margin);
}
}