use WorldCoordinates in ArenaView too

ArenaView carried its own copy of the same five transforms. It differs from the
game view in exactly two respects: the arena is a fixed world shown whole, so
the tile size is the tighter of the two axis fits rather than the height fit,
and there is no scrolling, so the left edge is always 0.

That is small enough to absorb into WorldCoordinates as a second named factory.
The raw constructor becomes private and both call sites go through
scrolling(...) or fitToWorld(...), which also reads better than three
positional ints at the call site.

ArenaView is threaded the same way GameWorldView was: paintGL builds one
snapshot, every draw takes a const WorldCoordinates&, and mousePressEvent takes
its own.

ArenaView::widgetToWorld guarded against a near-zero tile size; that guard now
lives in WorldCoordinates as a tilePx fallback to 1.0, alongside the existing
degenerate-world-size fallback, so both factories are total and no conversion
can divide by zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
2026-08-05 10:18:52 +02:00
parent 4f6de79352
commit 8ec413e1b9
7 changed files with 208 additions and 120 deletions

View File

@@ -1,21 +1,61 @@
#include "WorldCoordinates.h"
#include <algorithm>
#include <cmath>
WorldCoordinates::WorldCoordinates(QSize widgetSize_px, int worldHeight_tiles,
float viewCenterX_tiles)
: m_tilePx(1.0f)
, m_viewportWidthTiles(0.0f)
, m_viewLeftTiles(0.0f)
, m_worldHeightTiles(worldHeight_tiles)
namespace
{
// A zero-height widget, a not-yet-shown widget, or a degenerate world size
// would otherwise make every conversion divide by zero.
float sanitizeTilePx(float tilePx)
{
return tilePx > 0.0f ? tilePx : 1.0f;
}
}
WorldCoordinates WorldCoordinates::scrolling(QSize widgetSize_px,
int worldHeight_tiles,
float viewCenterX_tiles)
{
float tilePx = 1.0f;
if (worldHeight_tiles > 0)
{
m_tilePx = static_cast<float>(widgetSize_px.height())
/ static_cast<float>(worldHeight_tiles);
tilePx = static_cast<float>(widgetSize_px.height())
/ static_cast<float>(worldHeight_tiles);
}
m_viewportWidthTiles = static_cast<float>(widgetSize_px.width()) / m_tilePx;
m_viewLeftTiles = viewCenterX_tiles - m_viewportWidthTiles / 2.0f;
tilePx = sanitizeTilePx(tilePx);
const float viewportWidthTiles = static_cast<float>(widgetSize_px.width()) / tilePx;
return WorldCoordinates(tilePx, static_cast<float>(widgetSize_px.width()),
viewCenterX_tiles - viewportWidthTiles / 2.0f,
worldHeight_tiles);
}
WorldCoordinates WorldCoordinates::fitToWorld(QSize widgetSize_px,
int worldWidth_tiles,
int worldHeight_tiles)
{
float tilePx = 1.0f;
if (worldWidth_tiles > 0 && worldHeight_tiles > 0)
{
// The tighter of the two fits, so the whole world stays on screen.
tilePx = std::min(
static_cast<float>(widgetSize_px.height()) / static_cast<float>(worldHeight_tiles),
static_cast<float>(widgetSize_px.width()) / static_cast<float>(worldWidth_tiles));
}
tilePx = sanitizeTilePx(tilePx);
return WorldCoordinates(tilePx, static_cast<float>(widgetSize_px.width()),
0.0f, worldHeight_tiles);
}
WorldCoordinates::WorldCoordinates(float tilePx, float viewportWidth_px,
float viewLeft_tiles, int worldHeight_tiles)
: m_tilePx(tilePx)
, m_viewportWidthTiles(viewportWidth_px / tilePx)
, m_viewLeftTiles(viewLeft_tiles)
, m_worldHeightTiles(worldHeight_tiles)
{
}
float WorldCoordinates::getTilePx() const

View File

@@ -8,28 +8,37 @@
#include <QVector2D>
// Immutable snapshot of the world <-> widget transform for one viewport state
// (REQ-GW-COORDS, REQ-GW-TILE-SIZE). Tiles are square and sized so the world
// height exactly fills the viewport height; there is no zoom (REQ-UI-NO-ZOOM),
// so the only free parameter is the horizontal scroll position.
// (REQ-GW-COORDS). Tiles are square; the two factories below differ only in how
// the tile size and the left edge are derived, and everything downstream of that
// is shared.
//
// The transform is a value: it is constructed from the viewport size, the world
// height, and the view center, and never observes them again. A caller therefore
// builds one per frame (or per event) rather than holding one across a resize or
// a scroll, which would silently go stale.
// The transform is a value: it is constructed from the viewport size and the view
// state, and never observes them again. A caller therefore builds one per frame
// (or per event) rather than holding one across a resize or a scroll, which would
// silently go stale.
class WorldCoordinates
{
public:
// `viewCenterX_tiles` is the world X at the center of the viewport, matching
// how the scroll position is stored and clamped (REQ-GW-SCROLL-LIMIT).
WorldCoordinates(QSize widgetSize_px, int worldHeight_tiles,
float viewCenterX_tiles);
// The scrolling game world: the tile size is whatever makes the world height
// exactly fill the viewport height (REQ-GW-TILE-SIZE, no zoom per
// REQ-UI-NO-ZOOM), and the view pans horizontally. `viewCenterX_tiles` is the
// world X at the center of the viewport, matching how the scroll position is
// stored and clamped (REQ-GW-SCROLL-LIMIT).
static WorldCoordinates scrolling(QSize widgetSize_px, int worldHeight_tiles,
float viewCenterX_tiles);
// Side length of one tile in pixels. Degenerate world heights yield 1.0 so
// callers never divide by zero.
// A whole world shown at once with no scrolling, as the balancing tool's arena
// does: the tile size is whichever axis is the tighter fit, so nothing is cut
// off, and the world origin sits at the widget's top-left. A viewport wider
// than the fitted world leaves empty space to the right rather than centering.
static WorldCoordinates fitToWorld(QSize widgetSize_px, int worldWidth_tiles,
int worldHeight_tiles);
// Side length of one tile in pixels. Always positive: a degenerate world or
// viewport size falls back to 1.0 so no conversion below divides by zero.
float getTilePx() const;
float getViewportWidthTiles() const;
// World X (tiles) at the left edge of the viewport, derived from the view
// center the constructor was given.
// World X (tiles) at the left edge of the viewport.
float getViewLeftTiles() const;
QPointF worldToWidget(QVector2D worldPos) const;
@@ -44,6 +53,9 @@ public:
QRect getViewportRect() const;
private:
WorldCoordinates(float tilePx, float viewportWidth_px, float viewLeft_tiles,
int worldHeight_tiles);
float m_tilePx;
float m_viewportWidthTiles;
float m_viewLeftTiles;