extract the world<->widget transform into WorldCoordinates

The nine coordinate helpers on GameWorldView were the first seam of the
planned decomposition: stateless math that every draw method, the placement
validation, and the belt-drag code reached into private state to get at.

WorldCoordinates is an immutable value built from the viewport size, the world
height, and the scroll center. It lives in lib/core rather than ui, following
BeltDragPath and TunnelCompletion — UI-only helpers kept there so Catch2 can
reach them. That is what lets the transform math be unit-tested at all, since
the test target links lib only.

Rather than leave delegating one-liners behind, the snapshot is threaded
through the call graph: paintGL builds one per frame, every world-space draw
takes a const WorldCoordinates&, and the screen-space draws (vignettes, replay
overlay, debug text) take none. Those methods no longer touch m_scrollXTiles,
width(), or height(), which is most of the groundwork for extracting
WorldRenderer later. The mouse handlers each take their own snapshot.

The snapshot is deliberately not cached in a member: a resize or a scroll
would silently invalidate it, and a stale transform surfaces as misaligned
hit-testing rather than as a visible failure.

widgetToTile and widgetToWorld had the same arithmetic inlined separately;
the former now goes through the latter.

The architecture doc's coordinate section was already stale — it described a
painter.translate approach and a hardcoded tilePx = 20, neither of which was
true — so it is rewritten to match the code.

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:04:14 +02:00
parent 949937d2c2
commit 4f6de79352
8 changed files with 475 additions and 211 deletions

View File

@@ -49,6 +49,7 @@
#include "TickDriver.h"
#include "TunnelCompletion.h"
#include "VisualsConfig.h"
#include "WorldCoordinates.h"
struct Command;
struct ParsedReplay;
@@ -129,28 +130,32 @@ private:
// Used to pre-validate placements whose UI follow-up depends on success.
bool canAfford(BuildingType type) const;
void drawTiles(QPainter& painter);
void drawPortItems(QPainter& painter);
void drawBuildings(QPainter& painter);
void drawSelectionHighlights(QPainter& painter);
void drawCopyConfigFeedback(QPainter& painter);
void drawStations(QPainter& painter);
void drawBeltItems(QPainter& painter);
// World-space drawing takes the frame's transform (see getCoordinates()) rather
// than reaching for the scroll position and viewport size itself; the
// screen-space draws below need neither.
void drawTiles(QPainter& painter, const WorldCoordinates& coordinates);
void drawPortItems(QPainter& painter, const WorldCoordinates& coordinates);
void drawBuildings(QPainter& painter, const WorldCoordinates& coordinates);
void drawSelectionHighlights(QPainter& painter, const WorldCoordinates& coordinates);
void drawCopyConfigFeedback(QPainter& painter, const WorldCoordinates& coordinates);
void drawStations(QPainter& painter, const WorldCoordinates& coordinates);
void drawBeltItems(QPainter& painter, const WorldCoordinates& coordinates);
// Draws a single item centered at widget-space `center`, spanning `halfPx` in
// each direction (a half-tile). Uses the item's icon when one exists
// (REQ-UI-ITEM-ICON), otherwise falls back to the colored square from
// visuals.toml. Shared by drawBeltItems and drawPortItems.
void drawWorldItem(QPainter& painter, const std::string& itemId,
QPointF center, float halfPx);
void drawDebris(QPainter& painter);
void drawShips(QPainter& painter);
void drawHpBar(QPainter& painter, qreal left, qreal top, qreal width,
void drawDebris(QPainter& painter, const WorldCoordinates& coordinates);
void drawShips(QPainter& painter, const WorldCoordinates& coordinates);
void drawHpBar(QPainter& painter, const WorldCoordinates& coordinates,
qreal left, qreal top, qreal width,
float fraction, bool isEnemy);
void drawDebugSensorRanges(QPainter& painter);
void drawDebugTargetLines(QPainter& painter);
void drawDebugSensorRanges(QPainter& painter, const WorldCoordinates& coordinates);
void drawDebugTargetLines(QPainter& painter, const WorldCoordinates& coordinates);
void drawDebugOverlay(QPainter& painter);
void drawBeams(QPainter& painter);
void drawOverlays(QPainter& painter);
void drawBeams(QPainter& painter, const WorldCoordinates& coordinates);
void drawOverlays(QPainter& painter, const WorldCoordinates& coordinates);
void drawScreenSpace(QPainter& painter);
// Vignette-style border shown while the game is paused (speed 0x, REQ-UI-PAUSE-BORDER)
// to make the paused state hard to miss: a black frame whose alpha fades from 50% at
@@ -166,20 +171,16 @@ private:
void drawVignetteBorder(QPainter& painter, const QColor& edgeColor);
void drawReplayOverlay(QPainter& painter);
float getTilePx() const;
float getViewportWidthTiles() const;
// World-X (tiles) at the left edge of the viewport. m_scrollXTiles stores the
// view center; this derives the left edge the world<->widget conversions need.
float getViewLeftTiles() const;
QPointF worldToWidget(QVector2D worldPos) const;
QPointF tileToWidget(QPoint tile) const;
QPoint widgetToTile(QPoint widgetPt) const;
QRectF tileRect(QPoint tile) const;
QRect getViewportRect() const;
// The world <-> widget transform for the current viewport size and scroll
// position. Cheap to build and deliberately not cached: it is a snapshot that
// a resize or a scroll invalidates, so every user takes a fresh one.
WorldCoordinates getCoordinates() const;
// Widget-space rectangle covering a building or construction site's footprint,
// or nullopt if the id resolves to neither. Shared by the selection highlight
// and the copy-settings feedback (REQ-BLD-COPY-CONFIG-FEEDBACK).
std::optional<QRectF> footprintWidgetRect(BuildingId id) const;
std::optional<QRectF> footprintWidgetRect(const WorldCoordinates& coordinates,
BuildingId id) const;
float getAsteroidLeftEdge() const;
float getEnemyStationRightEdge() const;
@@ -193,13 +194,13 @@ private:
// Ids of all buildings and construction sites whose footprint intersects
// the tile box spanned by the two (unordered) corner tiles.
std::vector<BuildingId> buildingsInBox(QPoint cornerA, QPoint cornerB) const;
QVector2D widgetToWorld(QPoint widgetPt) const;
void drawPortGlyph(QPainter& painter, QPoint tile,
Rotation direction, const QColor& color,
void drawPortGlyph(QPainter& painter, const WorldCoordinates& coordinates,
QPoint tile, Rotation direction, const QColor& color,
bool centered);
void drawBuildingGhost(QPainter& painter, BuildingType type,
void drawBuildingGhost(QPainter& painter, const WorldCoordinates& coordinates,
BuildingType type,
QPoint anchorTile, Rotation rotation, bool valid,
bool showPortTargetGlyphs);
@@ -213,7 +214,8 @@ private:
// Draws a building's world icon glyph centered in box, choosing the white or
// dark pre-rendered variant by fill luminance so it stays legible. Returns
// false if the type has no world icon (caller falls back to the text glyph).
bool drawBuildingIcon(QPainter& painter, BuildingType type,
bool drawBuildingIcon(QPainter& painter, const WorldCoordinates& coordinates,
BuildingType type,
const QRectF& box, const QColor& fill) const;
void placeBlueprintAtTile(QPoint center);
@@ -254,7 +256,8 @@ private:
static TunnelLookup makeTunnelLookup(const TunnelTileMap& tunnels);
// Draws the green connection highlight for every selected tunnel end that has a
// matching end (REQ-BLD-TUNNEL-SELECT-HIGHLIGHT).
void drawSelectedTunnelConnections(QPainter& painter);
void drawSelectedTunnelConnections(QPainter& painter,
const WorldCoordinates& coordinates);
// Belt drag placement (REQ-BLD-BELT-DRAG).
// Per-path-tile decision, shared by ghost drawing and release-time placement.