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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user