338 lines
14 KiB
C#
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);
|
|
}
|
|
}
|