Skip to content

Commit 5e86626

Browse files
committed
feat(hunts): fit the detail zone map's initial view to the spawn cluster
The zone map always opened fit to the whole texture, so a mob whose candidates cluster in a small corner of the map (as little as 47% of the panel for outer_la_noscea) left the dots small and clustered together even though their positions were correct. Add PhotoZoomView.FocusOn(stage, size, normalizedBounds, padding), on top of a new SnapTo that clamps to the existing zoom range and reuses the existing pan clamp, so a target region can be framed immediately without the user ever seeing the unfocused full-texture view. Hunts computes the bounding box of the current detailMapPoints (not the aetheryte points, which can sit far from the candidate cluster and would fight the zoom) and focuses on it once per newly opened mob, deferred to the first Draw of the map panel since the stage and texture size aren't known until then. Live phase/zone narrowing while a page stays open does not re-focus, to avoid yanking the view under the user; only opening a different mob does.
1 parent 76d1322 commit 5e86626

3 files changed

Lines changed: 111 additions & 1 deletion

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System.Numerics;
2+
using Aetherphone.Core;
3+
using Aetherphone.Windows.Components;
4+
using Xunit;
5+
6+
namespace Aetherphone.Tests;
7+
8+
public sealed class PhotoZoomViewTests
9+
{
10+
private static readonly Rect Stage = new(Vector2.Zero, new Vector2(260f, 260f));
11+
private static readonly Vector2 TextureSize = new(2048f, 2048f);
12+
13+
[Fact]
14+
public void FocusOnATightClusterClampsToMaxZoom()
15+
{
16+
var view = new PhotoZoomView();
17+
var bounds = new Rect(new Vector2(0.45f, 0.45f), new Vector2(0.55f, 0.55f));
18+
19+
view.FocusOn(Stage, TextureSize, bounds);
20+
21+
Assert.Equal(4f, view.Zoom, 3);
22+
}
23+
24+
[Fact]
25+
public void FocusOnTheFullCanvasStaysAtMinZoomWithNoPan()
26+
{
27+
var view = new PhotoZoomView();
28+
var bounds = new Rect(Vector2.Zero, Vector2.One);
29+
30+
view.FocusOn(Stage, TextureSize, bounds, paddingFraction: 0f);
31+
32+
Assert.Equal(1f, view.Zoom, 3);
33+
Assert.Equal(0f, view.Pan.X, 3);
34+
Assert.Equal(0f, view.Pan.Y, 3);
35+
}
36+
37+
[Fact]
38+
public void FocusOnAnOffCenterClusterPansTowardItWithoutExceedingTheClamp()
39+
{
40+
var view = new PhotoZoomView();
41+
var bounds = new Rect(new Vector2(0.05f, 0.4f), new Vector2(0.25f, 0.6f));
42+
43+
view.FocusOn(Stage, TextureSize, bounds);
44+
45+
Assert.True(view.Zoom > 1f);
46+
var fit = PhotoZoomView.FitScale(Stage, TextureSize);
47+
var drawn = TextureSize * fit * view.Zoom;
48+
var maxPanX = MathF.Max(0f, (drawn.X - Stage.Width) * 0.5f);
49+
Assert.InRange(view.Pan.X, -maxPanX, maxPanX);
50+
}
51+
52+
[Fact]
53+
public void SnapToClampsZoomToTheConfiguredRange()
54+
{
55+
var view = new PhotoZoomView();
56+
57+
view.SnapTo(Stage, TextureSize, 50f, Vector2.Zero);
58+
59+
Assert.Equal(4f, view.Zoom, 3);
60+
}
61+
}

src/Aetherphone/Apps/Hunts/HuntsApp.Detail.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ internal sealed partial class HuntsApp
4949
private readonly Dictionary<(uint TerritoryId, string ZoneId, string Language), string> zoneLabelCache = new();
5050
private readonly PhotoZoomView detailMapZoom = new();
5151
private bool detailMapHovered;
52+
private bool detailMapPendingFocus;
5253

5354
private (int WindowNum, int PhaseNum)? detailMapActivePhase;
5455
private string? detailMapConfirmedZoneId;
@@ -85,7 +86,7 @@ private void OpenDetailFor(string mobId, string worldId, int zoneInstance)
8586
detailMapActivePhase = hunts.PhaseFor(mobId, worldId, zoneInstance);
8687
detailMapConfirmedZoneId = hunts.ZoneIdFor(mobId, worldId, zoneInstance);
8788
ResolveDetailMap(mobCatalog.Find(mobId), detailMapActivePhase, detailMapConfirmedZoneId);
88-
detailMapZoom.Reset();
89+
detailMapPendingFocus = true;
8990
detailMapHovered = false;
9091
}
9192

@@ -522,6 +523,12 @@ private string ResolveZoneLabel(string zoneId, uint territoryId)
522523
private void DrawDetailZoneMapContent(Rect stage, IDalamudTextureWrap texture, float scale, int? confirmedPoiId,
523524
HuntsView view, uint territoryId)
524525
{
526+
if (detailMapPendingFocus)
527+
{
528+
detailMapPendingFocus = false;
529+
FocusDetailMap(stage, texture.Size);
530+
}
531+
525532
var drawList = ImGui.GetWindowDrawList();
526533
detailMapZoom.Draw(stage, texture, frameTheme, Metrics.Radius.Card * scale, showButtons: false);
527534

@@ -577,6 +584,27 @@ private void DrawDetailZoneMapContent(Rect stage, IDalamudTextureWrap texture, f
577584
drawList.PopClipRect();
578585
}
579586

587+
private void FocusDetailMap(Rect stage, Vector2 textureSize)
588+
{
589+
if (detailMapPoints.Count == 0)
590+
{
591+
detailMapZoom.Reset();
592+
return;
593+
}
594+
595+
var min = new Vector2(float.MaxValue, float.MaxValue);
596+
var max = new Vector2(float.MinValue, float.MinValue);
597+
for (var index = 0; index < detailMapPoints.Count; index++)
598+
{
599+
var (rawX, rawY) = detailMapPoints[index].ParsedLocation();
600+
var (normalizedX, normalizedY) = MapPixelMath.NormalizeToFullCanvas(rawX, rawY);
601+
min = new Vector2(MathF.Min(min.X, normalizedX), MathF.Min(min.Y, normalizedY));
602+
max = new Vector2(MathF.Max(max.X, normalizedX), MathF.Max(max.Y, normalizedY));
603+
}
604+
605+
detailMapZoom.FocusOn(stage, textureSize, new Rect(min, max));
606+
}
607+
580608
private void DrawSpawnDot(ImDrawListPtr drawList, Vector2 center, float scale, int poiId, bool confirmed,
581609
bool finalLocation)
582610
{

src/Aetherphone/Windows/Components/Media/PhotoZoomView.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,27 @@ public void Reset()
4343
dragging = false;
4444
}
4545

46+
public void SnapTo(Rect stage, Vector2 size, float zoomValue, Vector2 panValue)
47+
{
48+
targetZoom = Math.Clamp(zoomValue, MinZoom, MaxZoom);
49+
targetPan = panValue;
50+
ClampPan(stage, size);
51+
zoom.SnapTo(targetZoom);
52+
panX.SnapTo(targetPan.X);
53+
panY.SnapTo(targetPan.Y);
54+
dragging = false;
55+
}
56+
57+
public void FocusOn(Rect stage, Vector2 size, Rect normalizedBounds, float paddingFraction = 0.3f)
58+
{
59+
var span = MathF.Max(normalizedBounds.Width, normalizedBounds.Height) * (1f + paddingFraction);
60+
var focusZoom = span > 0f ? 1f / span : MaxZoom;
61+
var drawn = size * FitScale(stage, size) * Math.Clamp(focusZoom, MinZoom, MaxZoom);
62+
var center = normalizedBounds.Center;
63+
var pan = new Vector2(drawn.X * (0.5f - center.X), drawn.Y * (0.5f - center.Y));
64+
SnapTo(stage, size, focusZoom, pan);
65+
}
66+
4667
public bool Draw(Rect stage, IDalamudTextureWrap texture, PhoneTheme theme, float rounding,
4768
bool showButtons = true, Rect? controls = null)
4869
{

0 commit comments

Comments
 (0)