Skip to content

Commit 9c55231

Browse files
HandyS11claude
andcommitted
fix(map): anchor the grid at the world's north-west corner, lattice over the whole image
The previous fix kept the partial edge cell but anchored rows at the SOUTH edge, leaving the remainder as a squished sliver row at the top — rows sat visibly displaced versus RustMaps and the companion app (columns were fine because they were already west-anchored). Rust, the app and RustMaps anchor the grid at the world's NW corner: rows step south from the top and the partial cell sits at the south/east, bleeding past the world edge so every visible cell looks full-size. They also draw the grid lattice across the WHOLE map image (ocean margin included), not clipped to the world square. - MapRenderer.DrawGrid: lattice anchored at the NW world corner, lines spanning the full image in all directions; labels (A0..) only for the world's ceil-count cells, top-left corner placement unchanged. - MapGrid.LabelFor: rows bin from the north edge (floor((worldSize - y) / CellSize)) so grid references match the visible grid; columns unchanged. - Regression tests lock the north anchoring (1500 world: y=1360 → A0, y=10 → partial row A10). Verified against RustMaps 1500/1234 (tmp/images/image.png) and the companion-app geometry with an ocean-margin base tile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0326ff7 commit 9c55231

3 files changed

Lines changed: 52 additions & 27 deletions

File tree

src/RustPlusBot.Abstractions/Connections/MapGrid.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ namespace RustPlusBot.Abstractions.Connections;
55

66
/// <summary>
77
/// Rust map grid math shared by the map renderer and grid-reference formatting.
8-
/// One cell is 146.25 game units; the grid covers the whole world <em>including</em> the partial edge
9-
/// cell (Rust / RustMaps / companion-app behaviour). Columns are lettered west→east from A; rows are
10-
/// numbered north→south from 0.
8+
/// One cell is 146.25 game units; the grid is anchored at the world's NORTH-WEST corner
9+
/// (Rust / RustMaps / companion-app behaviour): columns are lettered west→east from A, rows are
10+
/// numbered north→south from 0, and the partial edge cell (when the world size is not a whole
11+
/// multiple of the cell size) sits along the south and east edges.
1112
/// </summary>
1213
public static class MapGrid
1314
{
@@ -16,9 +17,9 @@ public static class MapGrid
1617

1718
/// <summary>
1819
/// Number of grid cells per axis, covering the whole world size — <c>ceil(worldSize / CellSize)</c>.
19-
/// The final cell along each axis is a partial (narrower) edge cell whenever the world size is not a
20-
/// whole multiple of <see cref="CellSize"/>; it is still a labelled cell, matching how Rust, the Rust+
21-
/// companion app and RustMaps draw the grid. (A 1500 world → 11 cells: A–K, rows 0–10.)
20+
/// The last cell (east-most column / south-most row) is a partial edge cell whenever the world size
21+
/// is not a whole multiple of <see cref="CellSize"/>; it is still a labelled cell, matching how Rust,
22+
/// the Rust+ companion app and RustMaps draw the grid. (A 1500 world → 11 cells: A–K, rows 0–10.)
2223
/// </summary>
2324
/// <param name="worldSize">The world size in game units.</param>
2425
/// <returns>The cell count (at least 1).</returns>
@@ -52,10 +53,11 @@ public static string ColumnLetters(int index)
5253
/// <returns>The grid label, rows numbered from the top; out-of-world coordinates clamp to the edge cell.</returns>
5354
public static string LabelFor(float x, float y, uint worldSize)
5455
{
56+
// Rows bin from the NORTH edge (the grid anchor), not the south — with a partial edge cell the
57+
// two disagree, and the north anchoring is what the in-game map, the app and RustMaps use.
5558
var cells = CellCount(worldSize);
5659
var col = Math.Clamp((int)MathF.Floor(x / CellSize), 0, cells - 1);
57-
var rowFromBottom = Math.Clamp((int)MathF.Floor(y / CellSize), 0, cells - 1);
58-
var row = cells - 1 - rowFromBottom;
60+
var row = Math.Clamp((int)MathF.Floor((worldSize - y) / CellSize), 0, cells - 1);
5961
return string.Create(CultureInfo.InvariantCulture, $"{ColumnLetters(col)}{row}");
6062
}
6163
}

src/RustPlusBot.Features.Map/Rendering/MapRenderer.cs

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -112,38 +112,45 @@ private static void DrawGrid(Image<Rgba32> image, MapProjection projection)
112112

113113
var lineColor = Color.FromRgba(255, 255, 255, 80);
114114
var labelColor = Color.FromRgba(255, 255, 255, 140);
115-
var worldSize = projection.WorldSize;
116-
var cells = MapGrid.CellCount(worldSize);
117-
var (left, top) = projection.ToPixel(0f, worldSize);
118-
var (right, bottom) = projection.ToPixel(worldSize, 0f);
115+
var cells = MapGrid.CellCount(projection.WorldSize);
116+
117+
// The lattice is anchored at the world's NORTH-WEST corner and spans the whole image, ocean
118+
// margin included — exactly how the in-game map, the companion app and RustMaps draw it. The
119+
// partial edge cell (world size not a whole multiple of the cell size) sits at the south/east
120+
// and simply bleeds past the world edge, so every rendered cell looks full-size.
121+
var (anchorX, anchorY) = projection.ToPixel(0f, projection.WorldSize);
122+
var (cellEndX, cellEndY) = projection.ToPixel(MapGrid.CellSize, projection.WorldSize - MapGrid.CellSize);
123+
var stepX = cellEndX - anchorX;
124+
var stepY = cellEndY - anchorY;
125+
if (stepX <= 0f || stepY <= 0f)
126+
{
127+
return; // Degenerate projection (no drawable world area).
128+
}
119129

120130
image.Mutate(ctx =>
121131
{
122-
for (var i = 0; i <= cells; i++)
132+
for (var k = (int)MathF.Ceiling(-anchorX / stepX); anchorX + (k * stepX) <= image.Width; k++)
133+
{
134+
var x = anchorX + (k * stepX);
135+
ctx.DrawLine(lineColor, OutlinePenWidth, new PointF(x, 0f), new PointF(x, image.Height));
136+
}
137+
138+
for (var k = (int)MathF.Ceiling(-anchorY / stepY); anchorY + (k * stepY) <= image.Height; k++)
123139
{
124-
// Cover the whole world including the partial edge cell: the final boundary is clamped to
125-
// worldSize (Rust / RustMaps behaviour), so the last cell is narrower when the world size
126-
// is not a whole multiple of MapGrid.CellSize.
127-
var boundary = MathF.Min(i * MapGrid.CellSize, worldSize);
128-
var (vx, _) = projection.ToPixel(boundary, 0f);
129-
ctx.DrawLine(lineColor, OutlinePenWidth, new PointF(vx, top), new PointF(vx, bottom));
130-
var (_, hy) = projection.ToPixel(0f, boundary);
131-
ctx.DrawLine(lineColor, OutlinePenWidth, new PointF(left, hy), new PointF(right, hy));
140+
var y = anchorY + (k * stepY);
141+
ctx.DrawLine(lineColor, OutlinePenWidth, new PointF(0f, y), new PointF(image.Width, y));
132142
}
133143

144+
// Only the world's cells carry labels (A0 in the north-west corner), each just inside its
145+
// cell's top-left corner (companion-app placement).
134146
for (var col = 0; col < cells; col++)
135147
{
136148
for (var row = 0; row < cells; row++)
137149
{
138-
// Label sits just inside each cell's top-left corner (companion-app placement). Row 0 is
139-
// the northernmost cell, so its top edge is the world's north edge (worldSize).
140-
var worldX = col * MapGrid.CellSize;
141-
var worldY = MathF.Min((cells - row) * MapGrid.CellSize, worldSize);
142-
var (lx, ly) = projection.ToPixel(worldX, worldY);
143150
var label = MapGrid.ColumnLetters(col) + row.ToString(CultureInfo.InvariantCulture);
144151
ctx.DrawText(new RichTextOptions(GridLabelFont)
145152
{
146-
Origin = new PointF(lx + 2f, ly + 2f)
153+
Origin = new PointF(anchorX + (col * stepX) + 2f, anchorY + (row * stepY) + 2f)
147154
},
148155
label, labelColor);
149156
}

tests/RustPlusBot.Abstractions.Tests/Connections/MapGridTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ public void LabelFor_north_west_corner_is_A0()
3535
Assert.Equal("A0", MapGrid.LabelFor(0f, 3999f, 4000u));
3636
}
3737

38+
[Fact]
39+
public void LabelFor_rows_bin_from_the_north_edge()
40+
{
41+
// 1500 world -> 11 cells with a 37.5-unit partial edge cell. y = 1360 is 140 units below the
42+
// north edge: row 0 with north-anchored rows (in-game / app / RustMaps behaviour); binning from
43+
// the south would put it in row 1. Locks in the anchoring.
44+
Assert.Equal("A0", MapGrid.LabelFor(0f, 1360f, 1500u));
45+
}
46+
47+
[Fact]
48+
public void LabelFor_south_edge_falls_in_the_partial_row()
49+
{
50+
// 1500 world: the southernmost 37.5 units are the partial row 10.
51+
Assert.Equal("A10", MapGrid.LabelFor(0f, 10f, 1500u));
52+
}
53+
3854
[Fact]
3955
public void LabelFor_beyond_world_size_clamps_to_last_cell()
4056
{

0 commit comments

Comments
 (0)