Files
riojoy/src/RioJoy.Core/Overlay/OverlayRegionEdit.cs
T
CydandClaude Fable 5 d1a938dd43 Phase 7: region box editor in the wallpaper maker
"Edit cell boxes" mode: the selected cell grows resize handles on the
preview -- drag to move/resize with live outline tracking and a re-render
on drop, or type exact X/Y/W/H in the new geometry fields. Plain clicks
still select and cycle stacked cells (click-vs-drag resolved by a movement
threshold on mouse-up). The move/resize math is pure and unit-tested
(OverlayRegionEdit: corner/edge/move handle hit-testing with tolerance,
min-size clamped resizing that pins the opposite edge). "Save template"
persists the shared cell geometry back to the regions.json; "Reload
template" re-reads it to discard unsaved box edits. Cursor feedback per
handle; template save/reload disabled when no template path was supplied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 01:14:36 -05:00

98 lines
3.6 KiB
C#

namespace RioJoy.Core.Overlay;
/// <summary>Which part of a region's box a drag grabs.</summary>
public enum OverlayBoxHandle
{
None,
Move,
North, South, East, West,
NorthWest, NorthEast, SouthWest, SouthEast,
}
/// <summary>
/// The pure math behind the wallpaper maker's box editor: which handle a
/// template-space point grabs on a region, and the region's new rectangle after
/// dragging that handle. Kept UI-free so it is unit-testable; the canvas only
/// converts mouse pixels to template space and applies the result.
/// </summary>
public static class OverlayRegionEdit
{
/// <summary>
/// The handle at a template-space point: corners win over edges, edges over
/// <see cref="OverlayBoxHandle.Move"/> (anywhere else inside the box), all
/// within <paramref name="tolerance"/> template pixels of the border.
/// </summary>
public static OverlayBoxHandle HitHandle(OverlayRegion region, double x, double y, double tolerance)
{
if (region is null) throw new ArgumentNullException(nameof(region));
double l = region.X, t = region.Y, r = region.X + region.Width, b = region.Y + region.Height;
if (x < l - tolerance || x > r + tolerance || y < t - tolerance || y > b + tolerance)
return OverlayBoxHandle.None;
bool west = Math.Abs(x - l) <= tolerance;
bool east = Math.Abs(x - r) <= tolerance;
bool north = Math.Abs(y - t) <= tolerance;
bool south = Math.Abs(y - b) <= tolerance;
if (north && west) return OverlayBoxHandle.NorthWest;
if (north && east) return OverlayBoxHandle.NorthEast;
if (south && west) return OverlayBoxHandle.SouthWest;
if (south && east) return OverlayBoxHandle.SouthEast;
if (north) return OverlayBoxHandle.North;
if (south) return OverlayBoxHandle.South;
if (west) return OverlayBoxHandle.West;
if (east) return OverlayBoxHandle.East;
return x >= l && x < r && y >= t && y < b ? OverlayBoxHandle.Move : OverlayBoxHandle.None;
}
/// <summary>
/// The rectangle after dragging <paramref name="handle"/> by (dx, dy) template
/// pixels from the drag-start rectangle. Resizes clamp so width/height never
/// drop below <paramref name="minSize"/> (the opposite edge stays put).
/// </summary>
public static (double X, double Y, double Width, double Height) Apply(
double x, double y, double width, double height,
OverlayBoxHandle handle, double dx, double dy, double minSize = 4)
{
double l = x, t = y, r = x + width, b = y + height;
switch (handle)
{
case OverlayBoxHandle.Move:
return (x + dx, y + dy, width, height);
case OverlayBoxHandle.West:
case OverlayBoxHandle.NorthWest:
case OverlayBoxHandle.SouthWest:
l = Math.Min(l + dx, r - minSize);
break;
}
switch (handle)
{
case OverlayBoxHandle.East:
case OverlayBoxHandle.NorthEast:
case OverlayBoxHandle.SouthEast:
r = Math.Max(r + dx, l + minSize);
break;
}
switch (handle)
{
case OverlayBoxHandle.North:
case OverlayBoxHandle.NorthWest:
case OverlayBoxHandle.NorthEast:
t = Math.Min(t + dy, b - minSize);
break;
case OverlayBoxHandle.South:
case OverlayBoxHandle.SouthWest:
case OverlayBoxHandle.SouthEast:
b = Math.Max(b + dy, t + minSize);
break;
}
return (l, t, r - l, b - t);
}
}