Skip to content

Commit 1206966

Browse files
authored
[MettaScope] camera clamping (#1557)
- It's possible to lose the map completely if you scroll too far off - Let's clamp the map panning to ensure some part of the map is always on-screen [Asana Task](https://app.asana.com/1/1209016784099267/project/1210348820405981/task/1210835069727884)
1 parent 3bf945b commit 1206966

2 files changed

Lines changed: 68 additions & 0 deletions

File tree

configs/user/monofuel.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# @package __global__
2+
3+
# This file is private property of Andrew
4+
# DO NOT MODIFY. or do, if you want.
5+
# I'm not your boss I'm just a comment in a git repo.
6+
7+
run: monofuel_local_2025_07_22
8+
9+
wandb:
10+
enabled: false
11+
12+
trainer:
13+
simulation:
14+
replay_dir: ${run_dir}/replays/local # disable uploading replays to s3

mettascope/src/worldmap.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,57 @@ import { parseHtmlColor, find } from './htmlutils.js'
99
import { updateHoverPanel, updateReadout, HoverPanel } from './hoverpanels.js'
1010
import { search, searchMatch } from './search.js'
1111

12+
/**
13+
* Clamps the map panel's pan position so that the world map always remains at
14+
* least partially visible within the panel.
15+
*/
16+
function clampMapPan(panel: PanelInfo) {
17+
if (state.replay === null) {
18+
return
19+
}
20+
21+
// The bounds of the world map in world-space coordinates. Tiles are drawn
22+
// starting at (−TILE_SIZE/2, −TILE_SIZE/2).
23+
const mapMinX = -Common.TILE_SIZE / 2
24+
const mapMinY = -Common.TILE_SIZE / 2
25+
const mapMaxX = state.replay.map_size[0] * Common.TILE_SIZE - Common.TILE_SIZE / 2
26+
const mapMaxY = state.replay.map_size[1] * Common.TILE_SIZE - Common.TILE_SIZE / 2
27+
28+
// Dimensions of the visible area in world-space coordinates.
29+
const rect = panel.rectInner()
30+
const viewHalfWidth = rect.width / (2 * panel.zoomLevel)
31+
const viewHalfHeight = rect.height / (2 * panel.zoomLevel)
32+
33+
// Current viewport centre in world-space.
34+
let cx = -panel.panPos.x()
35+
let cy = -panel.panPos.y()
36+
37+
const mapWidth = mapMaxX - mapMinX
38+
const mapHeight = mapMaxY - mapMinY
39+
40+
// Minimum number of pixels of the map that must remain visible.
41+
const minVisiblePixels = 500
42+
43+
// Convert to world coordinates based on current zoom level.
44+
const minVisibleWorldUnits = minVisiblePixels / panel.zoomLevel
45+
46+
// Ensure the required visible area doesn't exceed the actual map size.
47+
const maxVisibleUnitsX = Math.min(minVisibleWorldUnits, mapWidth / 2)
48+
const maxVisibleUnitsY = Math.min(minVisibleWorldUnits, mapHeight / 2)
49+
50+
// Clamp horizontally.
51+
const minCenterX = mapMinX + maxVisibleUnitsX - viewHalfWidth
52+
const maxCenterX = mapMaxX - maxVisibleUnitsX + viewHalfWidth
53+
cx = Math.max(minCenterX, Math.min(cx, maxCenterX))
54+
55+
// Clamp vertically.
56+
const minCenterY = mapMinY + maxVisibleUnitsY - viewHalfHeight
57+
const maxCenterY = mapMaxY - maxVisibleUnitsY + viewHalfHeight
58+
cy = Math.max(minCenterY, Math.min(cy, maxCenterY))
59+
60+
panel.panPos = new Vec2f(-cx, -cy)
61+
}
62+
1263
/** Generates a color from an agent ID. */
1364
function colorFromId(agentId: number) {
1465
let n = agentId + Math.PI + Math.E + Math.SQRT2
@@ -767,6 +818,9 @@ export function drawMap(panel: PanelInfo) {
767818
panel.panPos = new Vec2f(-x * Common.TILE_SIZE, -y * Common.TILE_SIZE)
768819
}
769820

821+
// Ensure that at least a portion of the map remains visible.
822+
clampMapPan(panel)
823+
770824
ctx.save()
771825
const rect = panel.rectInner()
772826
ctx.setScissorRect(rect.x, rect.y, rect.width, rect.height)

0 commit comments

Comments
 (0)