Skip to content
Merged
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
96 changes: 96 additions & 0 deletions Darling/Darling.Tests/Viewer4580Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,100 @@ public void ClickToScrollOffset_ReturnsZero_WhenScaleIsZeroOrLess()
Assert.Equal(0, offsetX);
Assert.Equal(0, offsetY);
}

// -- Part 2: resize clamp, double-click zoom-and-center, minimap node hit-testing --
// These members are new on this branch; the RED for this section is a compile failure on the
// pre-fix branch (b3317c250, part 1), same as the class overall.

[Fact]
public void ClampPanelSize_ClampsToPerformanceStudiosBounds()
{
Assert.Equal(MinimapLayout.MinPanelSize, MinimapLayout.ClampPanelSize(50));
Assert.Equal(MinimapLayout.MaxPanelSize, MinimapLayout.ClampPanelSize(9999));
Assert.Equal(300, MinimapLayout.ClampPanelSize(300));
}

[Fact]
public void ResizeFromDrag_SubtractsDeltaFromStartSize_ClampedToBounds()
{
// Dragging toward the top-left (negative delta) grows the panel pinned bottom-right.
var (width, height) = MinimapLayout.ResizeFromDrag(220, 220, -30, -10);
Assert.Equal(250, width);
Assert.Equal(230, height);
}

[Fact]
public void ResizeFromDrag_ClampsBelowMinAndAboveMax()
{
var (width, height) = MinimapLayout.ResizeFromDrag(170, 170, 500, 500);
Assert.Equal(MinimapLayout.MinPanelSize, width);
Assert.Equal(MinimapLayout.MinPanelSize, height);

var (width2, height2) = MinimapLayout.ResizeFromDrag(480, 480, -500, -500);
Assert.Equal(MinimapLayout.MaxPanelSize, width2);
Assert.Equal(MinimapLayout.MaxPanelSize, height2);
}

[Fact]
public void GetZoomToNodeLevel_FitsNodeToAboutAThirdOfTheViewport()
{
// Node 200x60, viewport 900x600: width-bound fitZoom = 900/600 = 1.5, height-bound = 600/180 = 3.33.
// The smaller wins, clamped to [0.1, 3.0].
var zoom = MinimapLayout.GetZoomToNodeLevel(200, 60, 900, 600, 0.1, 3.0);
Assert.Equal(1.5, zoom, 3);
}

[Fact]
public void GetZoomToNodeLevel_ClampsToMaxZoom()
{
var zoom = MinimapLayout.GetZoomToNodeLevel(10, 10, 900, 600, 0.1, 3.0);
Assert.Equal(3.0, zoom, 3);
}

[Fact]
public void GetZoomToNodeLevel_ReturnsMinZoom_WhenViewportOrNodeHasNoSize()
{
Assert.Equal(0.1, MinimapLayout.GetZoomToNodeLevel(0, 60, 900, 600, 0.1, 3.0));
Assert.Equal(0.1, MinimapLayout.GetZoomToNodeLevel(200, 60, 0, 600, 0.1, 3.0));
}

[Fact]
public void GetNodeCenterOffset_CentersNodeInViewport_ClampedToZero()
{
// Node at (100, 50), 20x10, zoom 1.0, viewport 400x300: center=(110,55), offset=(110-200,55-150) -> clamped to 0.
var (offsetX, offsetY) = MinimapLayout.GetNodeCenterOffset(100, 50, 20, 10, 1.0, 400, 300);
Assert.Equal(0, offsetX);
Assert.Equal(0, offsetY);
}

[Fact]
public void GetNodeCenterOffset_CentersWithoutClamping_WhenFarFromTheEdge()
{
var (offsetX, offsetY) = MinimapLayout.GetNodeCenterOffset(1000, 500, 20, 10, 1.0, 200, 100);
Assert.Equal(1000 + 10 - 100, offsetX, 3);
Assert.Equal(500 + 5 - 50, offsetY, 3);
}

[Fact]
public void FindNodeAt_ReturnsTheNodeWhoseRectangleContainsThePoint()
{
// Rows far enough apart (500) that nodes' rectangles never overlap, so the match is unambiguous.
var root = MakeNode(0, 0);
var child = MakeNode(0, 500);
var grandchild = MakeNode(0, 1000);
child.Children.Add(grandchild);
root.Children.Add(child);

// At scale 1, grandchild's rect is Y=1000..1000+height, X=0..NodeWidth. Pick a point inside it.
var found = MinimapLayout.FindNodeAt(root, 10, 1002, 1.0);
Assert.Same(grandchild, found);
}

[Fact]
public void FindNodeAt_ReturnsNull_WhenNoNodeContainsThePoint()
{
var root = MakeNode(0, 0);
var found = MinimapLayout.FindNodeAt(root, 9999, 9999, 1.0);
Assert.Null(found);
}
}
67 changes: 67 additions & 0 deletions PerformanceMonitor.PlanAnalysis/MinimapLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,73 @@ public static (double X, double Y, double Width, double Height) GetViewportRect(
return (x, y, width, height);
}

/// <summary>The minimap panel's minimum width/height while resizing.</summary>
public const double MinPanelSize = 160;

/// <summary>The minimap panel's maximum width/height while resizing.</summary>
public const double MaxPanelSize = 500;

/// <summary>Clamps a minimap panel width or height to <see cref="MinPanelSize"/>..<see cref="MaxPanelSize"/>.</summary>
public static double ClampPanelSize(double size) => Math.Max(MinPanelSize, Math.Min(MaxPanelSize, size));

/// <summary>
/// The panel's new width/height from a resize-grip drag. The grip sits in the panel's top-left
/// corner while the panel itself is pinned bottom-right, so dragging toward the top-left (away
/// from the pinned corner) is what grows the panel: the deltas subtract from the size the drag
/// started at. Both dimensions are clamped to <see cref="MinPanelSize"/>..<see cref="MaxPanelSize"/>.
/// </summary>
public static (double Width, double Height) ResizeFromDrag(double startWidth, double startHeight, double deltaX, double deltaY)
=> (ClampPanelSize(startWidth - deltaX), ClampPanelSize(startHeight - deltaY));

/// <summary>
/// The zoom level that fits a node to about a third of the viewport, for double-click-to-zoom.
/// Clamped to <paramref name="minZoom"/>..<paramref name="maxZoom"/>. Returns <paramref name="minZoom"/>
/// when the viewport or node has no usable size (nothing sensible to fit to).
/// </summary>
public static double GetZoomToNodeLevel(double nodeWidth, double nodeHeight, double viewportWidth, double viewportHeight, double minZoom, double maxZoom)
{
if (viewportWidth <= 0 || viewportHeight <= 0 || nodeWidth <= 0 || nodeHeight <= 0)
return minZoom;

var fitZoom = Math.Min(viewportWidth / (nodeWidth * 3), viewportHeight / (nodeHeight * 3));
return Math.Max(minZoom, Math.Min(maxZoom, fitZoom));
}

/// <summary>
/// The main scroll viewer's offset that centers <paramref name="nodeX"/>/<paramref name="nodeY"/>
/// (a node's plan-space position) in the viewport at <paramref name="zoomLevel"/>. Clamped to zero
/// so a node near the top-left edge never asks for a negative offset. Used for both double-click
/// zoom-and-center and single-click center-on-node.
/// </summary>
public static (double OffsetX, double OffsetY) GetNodeCenterOffset(
double nodeX, double nodeY, double nodeWidth, double nodeHeight,
double zoomLevel, double viewportWidth, double viewportHeight)
{
var centerX = (nodeX + nodeWidth / 2) * zoomLevel - viewportWidth / 2;
var centerY = (nodeY + nodeHeight / 2) * zoomLevel - viewportHeight / 2;
return (Math.Max(0, centerX), Math.Max(0, centerY));
}

/// <summary>
/// Finds the node whose minimap rectangle (see <see cref="GetNodeRect"/>) contains a minimap-space
/// point, searching this node then its descendants depth-first. Returns null when no node's
/// rectangle contains the point.
/// </summary>
public static PlanNode? FindNodeAt(PlanNode node, double pointX, double pointY, double scale)
{
var (x, y, width, height) = GetNodeRect(node, scale);
if (pointX >= x && pointX <= x + width && pointY >= y && pointY <= y + height)
return node;

foreach (var child in node.Children)
{
var found = FindNodeAt(child, pointX, pointY, scale);
if (found != null) return found;
}

return null;
}

/// <summary>
/// The main scroll viewer's target offset for a click at a minimap point: convert the click
/// back to plan content coordinates, then center the viewport on that point. Clamped to zero
Expand Down
Loading
Loading