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

@@ -357,9 +357,10 @@ Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly
### Coordinates and Scrolling ### Coordinates and Scrolling
- `GameWorldView` holds a continuous `scrollXTiles` (float). A / D input pans this smoothly (REQ-UI-SCROLL). - `GameWorldView` holds a continuous `scrollXTiles` (float), the world X at the *center* of the viewport. A / D input pans this smoothly (REQ-UI-SCROLL) at a position-dependent speed (REQ-UI-SCROLL-SPEED).
- At the start of `paintEvent`, a single `painter.translate(-scrollXTiles * tilePx, 0)` maps world tile units into widget pixels (`tilePx = 20`, per REQ-GW-TILE-SIZE). - The world↔widget transform itself lives in `WorldCoordinates` (`lib/core/`), not in the view. It is an immutable value built from the viewport size, the world height, and the scroll center; `tilePx` is derived so the world height fills the viewport (REQ-GW-TILE-SIZE) rather than being fixed. Being a plain value with no Qt Widgets dependency, it is unit-tested (`WorldCoordinatesTest`) even though the widgets around it are not.
- Mouse input converts the other way: `worldX = mouseX / tilePx + scrollXTiles`; apply `floor` for a tile. Asteroid tiles (`x < 0`) need no special casing — they share the coordinate system with space tiles. - `GameWorldView::getCoordinates()` builds one per frame in `paintGL` and per event in the mouse handlers, and passes it down: every world-space `draw<X>` takes a `const WorldCoordinates&`, while the screen-space draws (vignette borders, replay overlay, debug text) take none. The snapshot is deliberately never cached in a member — a resize or a scroll would silently invalidate it.
- Conversions are per-call arithmetic rather than a `painter.translate`, because hit-testing needs the inverse (`widgetToWorld` / `widgetToTile`, flooring for a tile) as often as drawing needs the forward direction. Asteroid tiles (`x < 0`) need no special casing — they share the coordinate system with space tiles, which is why the flooring must not be truncation.
### Culling ### Culling

View File

@@ -14,6 +14,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h ${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h ${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h ${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
PARENT_SCOPE PARENT_SCOPE
) )
@@ -25,6 +26,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp ${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
PARENT_SCOPE PARENT_SCOPE
) )

View File

@@ -0,0 +1,78 @@
#include "WorldCoordinates.h"
#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)
{
if (worldHeight_tiles > 0)
{
m_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;
}
float WorldCoordinates::getTilePx() const
{
return m_tilePx;
}
float WorldCoordinates::getViewportWidthTiles() const
{
return m_viewportWidthTiles;
}
float WorldCoordinates::getViewLeftTiles() const
{
return m_viewLeftTiles;
}
QPointF WorldCoordinates::worldToWidget(QVector2D worldPos) const
{
return QPointF(
static_cast<qreal>((worldPos.x() - m_viewLeftTiles) * m_tilePx),
static_cast<qreal>(worldPos.y() * m_tilePx));
}
QPointF WorldCoordinates::tileToWidget(QPoint tile) const
{
return worldToWidget(QVector2D(static_cast<float>(tile.x()),
static_cast<float>(tile.y())));
}
QPoint WorldCoordinates::widgetToTile(QPoint widgetPoint) const
{
const QVector2D world = widgetToWorld(widgetPoint);
return QPoint(static_cast<int>(std::floor(world.x())),
static_cast<int>(std::floor(world.y())));
}
QVector2D WorldCoordinates::widgetToWorld(QPoint widgetPoint) const
{
return QVector2D(
static_cast<float>(widgetPoint.x()) / m_tilePx + m_viewLeftTiles,
static_cast<float>(widgetPoint.y()) / m_tilePx);
}
QRectF WorldCoordinates::tileRect(QPoint tile) const
{
const QPointF topLeft = tileToWidget(tile);
return QRectF(topLeft.x(), topLeft.y(),
static_cast<qreal>(m_tilePx), static_cast<qreal>(m_tilePx));
}
QRect WorldCoordinates::getViewportRect() const
{
const int left = static_cast<int>(std::floor(m_viewLeftTiles)) - 1;
const int top = 0;
const int right = static_cast<int>(
std::ceil(m_viewLeftTiles + m_viewportWidthTiles)) + 1;
const int bottom = m_worldHeightTiles;
return QRect(left, top, right - left, bottom - top);
}

View File

@@ -0,0 +1,51 @@
#pragma once
#include <QPoint>
#include <QPointF>
#include <QRect>
#include <QRectF>
#include <QSize>
#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.
//
// 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.
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);
// Side length of one tile in pixels. Degenerate world heights yield 1.0 so
// callers never divide 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.
float getViewLeftTiles() const;
QPointF worldToWidget(QVector2D worldPos) const;
QPointF tileToWidget(QPoint tile) const;
QPoint widgetToTile(QPoint widgetPoint) const;
QVector2D widgetToWorld(QPoint widgetPoint) const;
// Widget-space rect covering the whole of `tile`.
QRectF tileRect(QPoint tile) const;
// Tile-space rect of everything currently on screen, widened by one column on
// each side so items straddling an edge are still drawn.
QRect getViewportRect() const;
private:
float m_tilePx;
float m_viewportWidthTiles;
float m_viewLeftTiles;
int m_worldHeightTiles;
};

View File

@@ -12,6 +12,7 @@ add_files(
SurfaceMaskTest.cpp SurfaceMaskTest.cpp
BeltDragPathTest.cpp BeltDragPathTest.cpp
TunnelCompletionTest.cpp TunnelCompletionTest.cpp
WorldCoordinatesTest.cpp
BuildingTest.cpp BuildingTest.cpp
BuildingConfigTest.cpp BuildingConfigTest.cpp
ShipTest.cpp ShipTest.cpp

View File

@@ -0,0 +1,145 @@
#include "catch.hpp"
#include <QPoint>
#include <QSize>
#include <QVector2D>
#include "WorldCoordinates.h"
// A 800x400 viewport over a 20-tile-high world gives exactly 20 px per tile and a
// 40-tile-wide view, so every expectation below is a whole number.
static WorldCoordinates makeCoordinates(float viewCenterX_tiles)
{
return WorldCoordinates(QSize(800, 400), 20, viewCenterX_tiles);
}
// ---------------------------------------------------------------------------
// Tile size and viewport extent
// ---------------------------------------------------------------------------
TEST_CASE("Tile size makes the world height fill the viewport height", "[coords]")
{
// REQ-GW-TILE-SIZE: tiles are square and sized so the world height exactly
// fills the view's height.
REQUIRE(makeCoordinates(0.0f).getTilePx() == Approx(20.0f));
REQUIRE(WorldCoordinates(QSize(800, 600), 20, 0.0f).getTilePx() == Approx(30.0f));
}
TEST_CASE("A degenerate world height falls back to a unit tile", "[coords]")
{
// Guards the division in every conversion; a zero or negative height would
// otherwise produce infinities.
REQUIRE(WorldCoordinates(QSize(800, 400), 0, 0.0f).getTilePx() == Approx(1.0f));
REQUIRE(WorldCoordinates(QSize(800, 400), -5, 0.0f).getTilePx() == Approx(1.0f));
}
TEST_CASE("Viewport width in tiles follows the widget width", "[coords]")
{
REQUIRE(makeCoordinates(0.0f).getViewportWidthTiles() == Approx(40.0f));
REQUIRE(WorldCoordinates(QSize(400, 400), 20, 0.0f).getViewportWidthTiles()
== Approx(20.0f));
}
TEST_CASE("The view left edge is half a viewport left of the center", "[coords]")
{
REQUIRE(makeCoordinates(0.0f).getViewLeftTiles() == Approx(-20.0f));
REQUIRE(makeCoordinates(100.0f).getViewLeftTiles() == Approx(80.0f));
}
// ---------------------------------------------------------------------------
// World <-> widget conversion
// ---------------------------------------------------------------------------
TEST_CASE("World positions map to widget pixels relative to the view left edge",
"[coords]")
{
const WorldCoordinates coordinates = makeCoordinates(0.0f); // left edge at -20
REQUIRE(coordinates.worldToWidget(QVector2D(-20.0f, 0.0f)).x() == Approx(0.0));
REQUIRE(coordinates.worldToWidget(QVector2D(0.0f, 0.0f)).x() == Approx(400.0));
// Y is not scrolled: world Y maps straight through the tile size.
REQUIRE(coordinates.worldToWidget(QVector2D(0.0f, 3.5f)).y() == Approx(70.0));
}
TEST_CASE("Scrolling right shifts the world left on screen", "[coords]")
{
const QVector2D worldPos(10.0f, 5.0f);
const qreal atOrigin = makeCoordinates(0.0f).worldToWidget(worldPos).x();
const qreal scrolled = makeCoordinates(4.0f).worldToWidget(worldPos).x();
// Panning the view 4 tiles right moves the same world point 4 tiles (80 px) left.
REQUIRE(scrolled == Approx(atOrigin - 80.0));
}
TEST_CASE("A tile's widget position is its top-left corner", "[coords]")
{
const WorldCoordinates coordinates = makeCoordinates(0.0f);
REQUIRE(coordinates.tileToWidget(QPoint(-20, 0)) == QPointF(0.0, 0.0));
REQUIRE(coordinates.tileToWidget(QPoint(0, 2)) == QPointF(400.0, 40.0));
}
TEST_CASE("A tile rect covers exactly one tile", "[coords]")
{
const QRectF rect = makeCoordinates(0.0f).tileRect(QPoint(-19, 1));
REQUIRE(rect.left() == Approx(20.0));
REQUIRE(rect.top() == Approx(20.0));
REQUIRE(rect.width() == Approx(20.0));
REQUIRE(rect.height() == Approx(20.0));
}
// ---------------------------------------------------------------------------
// Widget -> world conversion
// ---------------------------------------------------------------------------
TEST_CASE("Widget points map back to the world position they came from", "[coords]")
{
const WorldCoordinates coordinates = makeCoordinates(7.0f);
const QVector2D world = coordinates.widgetToWorld(QPoint(250, 130));
const QPointF back = coordinates.worldToWidget(world);
REQUIRE(back.x() == Approx(250.0));
REQUIRE(back.y() == Approx(130.0));
}
TEST_CASE("Widget points resolve to the tile that contains them", "[coords]")
{
const WorldCoordinates coordinates = makeCoordinates(0.0f); // left edge at -20
// Anywhere inside a tile's 20px cell resolves to that tile.
REQUIRE(coordinates.widgetToTile(QPoint(0, 0)) == QPoint(-20, 0));
REQUIRE(coordinates.widgetToTile(QPoint(19, 19)) == QPoint(-20, 0));
REQUIRE(coordinates.widgetToTile(QPoint(20, 20)) == QPoint(-19, 1));
REQUIRE(coordinates.widgetToTile(QPoint(405, 45)) == QPoint(0, 2));
}
TEST_CASE("Tile resolution floors, so negative world positions round down", "[coords]")
{
// Truncation toward zero would map the whole strip from -1 to 1 onto tile 0,
// making the tile under the cursor wrong on the asteroid side (REQ-GW-COORDS:
// all asteroid tiles have x < 0).
const WorldCoordinates coordinates = makeCoordinates(0.0f); // left edge at -20
REQUIRE(coordinates.widgetToTile(QPoint(399, 0)) == QPoint(-1, 0));
REQUIRE(coordinates.widgetToTile(QPoint(400, 0)) == QPoint(0, 0));
}
// ---------------------------------------------------------------------------
// Viewport rect
// ---------------------------------------------------------------------------
TEST_CASE("The viewport rect spans the visible tiles with a one-column margin",
"[coords]")
{
// The margin keeps items that straddle an edge from popping in and out.
const QRect rect = makeCoordinates(0.0f).getViewportRect(); // left edge at -20
REQUIRE(rect.left() == -21);
REQUIRE(rect.right() == 20);
REQUIRE(rect.top() == 0);
REQUIRE(rect.height() == 20); // the full world height
}
TEST_CASE("A fractional scroll position widens the viewport rect outward", "[coords]")
{
// Left edge at -19.5: the rect must still cover the partially visible columns
// on both sides, so it floors on the left and ceils on the right.
const QRect rect = makeCoordinates(0.5f).getViewportRect();
REQUIRE(rect.left() == -21);
REQUIRE(rect.right() == 21);
}

View File

@@ -379,7 +379,8 @@ void GameWorldView::onFrame()
// so refresh the box-select rectangle even though no mouse move fires. // so refresh the box-select rectangle even though no mouse move fires.
if (m_boxSelecting && m_scrollXTiles != scrollBefore) if (m_boxSelecting && m_scrollXTiles != scrollBefore)
{ {
m_boxCurrentTile = widgetToTile(mapFromGlobal(QCursor::pos())); m_boxCurrentTile =
getCoordinates().widgetToTile(mapFromGlobal(QCursor::pos()));
} }
} }
@@ -488,26 +489,30 @@ void GameWorldView::paintGL()
QPainter painter(this); QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, false); painter.setRenderHint(QPainter::Antialiasing, false);
drawTiles(painter); // One transform snapshot for the whole frame; every world-space draw below
drawBuildings(painter); // reads the viewport through it.
const WorldCoordinates coordinates = getCoordinates();
drawTiles(painter, coordinates);
drawBuildings(painter, coordinates);
// Port items are drawn over the buildings but clipped to a thin margin at each // Port items are drawn over the buildings but clipped to a thin margin at each
// machine's edges (see drawPortItems), so items appear to emerge from / sink // machine's edges (see drawPortItems), so items appear to emerge from / sink
// into the port and stay visible while crossing directly between two touching // into the port and stay visible while crossing directly between two touching
// buildings (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-INPUT-INTAKE, REQ-MAT-DIRECT-COUPLE). // buildings (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-INPUT-INTAKE, REQ-MAT-DIRECT-COUPLE).
drawPortItems(painter); drawPortItems(painter, coordinates);
drawCopyConfigFeedback(painter); drawCopyConfigFeedback(painter, coordinates);
drawStations(painter); drawStations(painter, coordinates);
drawBeltItems(painter); drawBeltItems(painter, coordinates);
drawDebris(painter); drawDebris(painter, coordinates);
if (m_debugDraw) if (m_debugDraw)
{ {
drawDebugSensorRanges(painter); drawDebugSensorRanges(painter, coordinates);
drawDebugTargetLines(painter); drawDebugTargetLines(painter, coordinates);
drawDebugOverlay(painter); drawDebugOverlay(painter);
} }
drawShips(painter); drawShips(painter, coordinates);
drawBeams(painter); drawBeams(painter, coordinates);
drawOverlays(painter); drawOverlays(painter, coordinates);
drawScreenSpace(painter); drawScreenSpace(painter);
drawPauseBorder(painter); drawPauseBorder(painter);
drawDeconstructBorder(painter); drawDeconstructBorder(painter);
@@ -518,63 +523,9 @@ void GameWorldView::paintGL()
// Coordinate helpers // Coordinate helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
float GameWorldView::getTilePx() const WorldCoordinates GameWorldView::getCoordinates() const
{ {
if (m_config->world.heightTiles <= 0) { return 1.0f; } return WorldCoordinates(size(), m_config->world.heightTiles, m_scrollXTiles);
return static_cast<float>(height()) / static_cast<float>(m_config->world.heightTiles);
}
float GameWorldView::getViewportWidthTiles() const
{
return static_cast<float>(width()) / getTilePx();
}
float GameWorldView::getViewLeftTiles() const
{
return m_scrollXTiles - getViewportWidthTiles() / 2.0f;
}
QPointF GameWorldView::worldToWidget(QVector2D worldPos) const
{
return QPointF(
static_cast<qreal>((worldPos.x() - getViewLeftTiles()) * getTilePx()),
static_cast<qreal>(worldPos.y() * getTilePx()));
}
QPointF GameWorldView::tileToWidget(QPoint tile) const
{
return worldToWidget(QVector2D(static_cast<float>(tile.x()),
static_cast<float>(tile.y())));
}
QPoint GameWorldView::widgetToTile(QPoint widgetPt) const
{
const float wx = static_cast<float>(widgetPt.x()) / getTilePx() + getViewLeftTiles();
const float wy = static_cast<float>(widgetPt.y()) / getTilePx();
return QPoint(static_cast<int>(std::floor(wx)), static_cast<int>(std::floor(wy)));
}
QVector2D GameWorldView::widgetToWorld(QPoint widgetPt) const
{
const float wx = static_cast<float>(widgetPt.x()) / getTilePx() + getViewLeftTiles();
const float wy = static_cast<float>(widgetPt.y()) / getTilePx();
return QVector2D(wx, wy);
}
QRectF GameWorldView::tileRect(QPoint tile) const
{
const QPointF tl = tileToWidget(tile);
return QRectF(tl.x(), tl.y(),
static_cast<qreal>(getTilePx()), static_cast<qreal>(getTilePx()));
}
QRect GameWorldView::getViewportRect() const
{
const int left = static_cast<int>(std::floor(getViewLeftTiles())) - 1;
const int top = 0;
const int right = static_cast<int>(std::ceil(getViewLeftTiles() + getViewportWidthTiles())) + 1;
const int bottom = m_config->world.heightTiles;
return QRect(left, top, right - left, bottom - top);
} }
float GameWorldView::getAsteroidLeftEdge() const float GameWorldView::getAsteroidLeftEdge() const
@@ -1178,12 +1129,13 @@ void GameWorldView::applyBeltDragPath()
// Port glyph helper // Port glyph helper
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void GameWorldView::drawPortGlyph(QPainter& painter, QPoint tile, void GameWorldView::drawPortGlyph(QPainter& painter,
const WorldCoordinates& coordinates, QPoint tile,
Rotation direction, const QColor& color, Rotation direction, const QColor& color,
bool centered) bool centered)
{ {
const float px = getTilePx(); const float px = coordinates.getTilePx();
const QRectF tr = tileRect(tile); const QRectF tr = coordinates.tileRect(tile);
const QPointF center(tr.x() + static_cast<qreal>(px) * 0.5, const QPointF center(tr.x() + static_cast<qreal>(px) * 0.5,
tr.y() + static_cast<qreal>(px) * 0.5); tr.y() + static_cast<qreal>(px) * 0.5);
@@ -1220,10 +1172,11 @@ void GameWorldView::drawPortGlyph(QPainter& painter, QPoint tile,
// Rendering // Rendering
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void GameWorldView::drawTiles(QPainter& painter) void GameWorldView::drawTiles(QPainter& painter, const WorldCoordinates& coordinates)
{ {
const int leftTile = static_cast<int>(std::floor(getViewLeftTiles())) - 1; const int leftTile = static_cast<int>(std::floor(coordinates.getViewLeftTiles())) - 1;
const int rightTile = leftTile + static_cast<int>(std::ceil(getViewportWidthTiles())) + 2; const int rightTile = leftTile
+ static_cast<int>(std::ceil(coordinates.getViewportWidthTiles())) + 2;
const int bottomTile = m_config->world.heightTiles; const int bottomTile = m_config->world.heightTiles;
// Asteroid columns left of the buildable edge are not yet unlocked by // Asteroid columns left of the buildable edge are not yet unlocked by
@@ -1239,7 +1192,7 @@ void GameWorldView::drawTiles(QPainter& painter)
const bool locked = (x < buildableLeftX); const bool locked = (x < buildableLeftX);
for (int y = 0; y < bottomTile; ++y) for (int y = 0; y < bottomTile; ++y)
{ {
const QRectF rect = tileRect(QPoint(x, y)); const QRectF rect = coordinates.tileRect(QPoint(x, y));
painter.fillRect(rect, fill); painter.fillRect(rect, fill);
if (locked) if (locked)
{ {
@@ -1273,7 +1226,9 @@ void GameWorldView::loadBuildingIcons(const std::string& configDir)
} }
} }
bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type, bool GameWorldView::drawBuildingIcon(QPainter& painter,
const WorldCoordinates& coordinates,
BuildingType type,
const QRectF& box, const QColor& fill) const const QRectF& box, const QColor& fill) const
{ {
const std::map<BuildingType, BuildingIconRenderers>::const_iterator it = const std::map<BuildingType, BuildingIconRenderers>::const_iterator it =
@@ -1282,7 +1237,7 @@ bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type,
// Every icon is the same fixed on-screen size, centered on the footprint, // Every icon is the same fixed on-screen size, centered on the footprint,
// regardless of footprint size (REQ-UI-WORLD-ICON). // regardless of footprint size (REQ-UI-WORLD-ICON).
const qreal side = static_cast<qreal>(getTilePx()) * kWorldIconTileFactor; const qreal side = static_cast<qreal>(coordinates.getTilePx()) * kWorldIconTileFactor;
const QRectF target(box.center().x() - side / 2.0, const QRectF target(box.center().x() - side / 2.0,
box.center().y() - side / 2.0, side, side); box.center().y() - side / 2.0, side, side);
QSvgRenderer* renderer = isLightFill(fill) ? it->second.dark.get() QSvgRenderer* renderer = isLightFill(fill) ? it->second.dark.get()
@@ -1297,7 +1252,7 @@ bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type,
return true; return true;
} }
void GameWorldView::drawBuildings(QPainter& painter) void GameWorldView::drawBuildings(QPainter& painter, const WorldCoordinates& coordinates)
{ {
for (const Building& b : getAllBuildings(m_sim->getFactoryState())) for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
{ {
@@ -1309,13 +1264,13 @@ void GameWorldView::drawBuildings(QPainter& painter)
painter.setPen(Qt::NoPen); painter.setPen(Qt::NoPen);
for (const QPoint& cell : b.bodyCells) for (const QPoint& cell : b.bodyCells)
{ {
painter.fillRect(tileRect(cell), bv.fill); painter.fillRect(coordinates.tileRect(cell), bv.fill);
} }
const QPointF tl = tileToWidget(b.anchor); const QPointF tl = coordinates.tileToWidget(b.anchor);
const QRectF bboxRect(tl.x(), tl.y(), const QRectF bboxRect(tl.x(), tl.y(),
b.footprint.width() * static_cast<qreal>(getTilePx()), b.footprint.width() * static_cast<qreal>(coordinates.getTilePx()),
b.footprint.height() * static_cast<qreal>(getTilePx())); b.footprint.height() * static_cast<qreal>(coordinates.getTilePx()));
painter.setPen(QPen(bv.outline, 1)); painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
@@ -1323,7 +1278,8 @@ void GameWorldView::drawBuildings(QPainter& painter)
// Icon glyph over the fill (REQ-UI-WORLD-ICON); falls back to the text // Icon glyph over the fill (REQ-UI-WORLD-ICON); falls back to the text
// glyph for building types without a world icon (e.g. tunnels). // glyph for building types without a world icon (e.g. tunnels).
if (!drawBuildingIcon(painter, b.type, bboxRect, bv.fill) && !bv.glyph.isEmpty()) if (!drawBuildingIcon(painter, coordinates, b.type, bboxRect, bv.fill)
&& !bv.glyph.isEmpty())
{ {
painter.setPen(bv.outline); painter.setPen(bv.outline);
painter.drawText(bboxRect, Qt::AlignCenter, bv.glyph); painter.drawText(bboxRect, Qt::AlignCenter, bv.glyph);
@@ -1331,7 +1287,8 @@ void GameWorldView::drawBuildings(QPainter& painter)
for (const Port& port : b.outputPorts) for (const Port& port : b.outputPorts)
{ {
drawPortGlyph(painter, outputBodyTile(port.tile, port.direction), drawPortGlyph(painter, coordinates,
outputBodyTile(port.tile, port.direction),
port.direction, bv.outline, /*centered*/ false); port.direction, bv.outline, /*centered*/ false);
} }
@@ -1343,7 +1300,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
if (const std::optional<ProductionStatus> status = if (const std::optional<ProductionStatus> status =
getProductionStatus(m_sim->getConfig(), b)) getProductionStatus(m_sim->getConfig(), b))
{ {
const float px = getTilePx(); const float px = coordinates.getTilePx();
const float r = px * 0.18f; const float r = px * 0.18f;
const float inset = r + px * 0.12f; const float inset = r + px * 0.12f;
// Default orientation (East) puts the light at the top-right corner; // Default orientation (East) puts the light at the top-right corner;
@@ -1375,13 +1332,13 @@ void GameWorldView::drawBuildings(QPainter& painter)
for (const QPoint& cell : s.bodyCells) for (const QPoint& cell : s.bodyCells)
{ {
painter.fillRect(tileRect(cell), bv.fill); painter.fillRect(coordinates.tileRect(cell), bv.fill);
} }
const QPointF tl = tileToWidget(s.anchor); const QPointF tl = coordinates.tileToWidget(s.anchor);
const QRectF bboxRect(tl.x(), tl.y(), const QRectF bboxRect(tl.x(), tl.y(),
s.footprint.width() * static_cast<qreal>(getTilePx()), s.footprint.width() * static_cast<qreal>(coordinates.getTilePx()),
s.footprint.height() * static_cast<qreal>(getTilePx())); s.footprint.height() * static_cast<qreal>(coordinates.getTilePx()));
painter.setPen(QPen(bv.outline, 1, Qt::DashLine)); painter.setPen(QPen(bv.outline, 1, Qt::DashLine));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
painter.drawRect(bboxRect); painter.drawRect(bboxRect);
@@ -1406,7 +1363,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
// Identity symbol with the progress percentage below it // Identity symbol with the progress percentage below it
// (REQ-UI-CONSTRUCTION-PROGRESS): the world icon centered on the // (REQ-UI-CONSTRUCTION-PROGRESS): the world icon centered on the
// footprint where the type has one, otherwise the text glyph. // footprint where the type has one, otherwise the text glyph.
if (drawBuildingIcon(painter, s.type, bboxRect, bv.fill)) if (drawBuildingIcon(painter, coordinates, s.type, bboxRect, bv.fill))
{ {
painter.drawText(bboxRect, Qt::AlignHCenter | Qt::AlignBottom, pctText); painter.drawText(bboxRect, Qt::AlignHCenter | Qt::AlignBottom, pctText);
} }
@@ -1432,8 +1389,8 @@ void GameWorldView::drawBuildings(QPainter& painter)
{ {
const QPoint absBody = s.anchor const QPoint absBody = s.anchor
+ outputBodyTile(port.tile, port.direction); + outputBodyTile(port.tile, port.direction);
drawPortGlyph(painter, absBody, port.direction, bv.outline, drawPortGlyph(painter, coordinates, absBody, port.direction,
/*centered*/ false); bv.outline, /*centered*/ false);
} }
} }
} }
@@ -1446,17 +1403,18 @@ void GameWorldView::drawBuildings(QPainter& painter)
for (const Building& b : getAllBuildings(m_sim->getFactoryState())) for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
{ {
if (b.type != BuildingType::Hq) { continue; } if (b.type != BuildingType::Hq) { continue; }
const QPointF tl = tileToWidget(b.anchor); const QPointF tl = coordinates.tileToWidget(b.anchor);
const QRectF bboxRect(tl.x(), tl.y(), const QRectF bboxRect(tl.x(), tl.y(),
b.footprint.width() * static_cast<qreal>(getTilePx()), b.footprint.width() * static_cast<qreal>(coordinates.getTilePx()),
b.footprint.height() * static_cast<qreal>(getTilePx())); b.footprint.height() * static_cast<qreal>(coordinates.getTilePx()));
m_sim->getAdmin().forEach<HqProxyComponent, FactionComponent, HealthComponent>( m_sim->getAdmin().forEach<HqProxyComponent, FactionComponent, HealthComponent>(
[&](entt::entity /*e*/, const HqProxyComponent& /*hq*/, [&](entt::entity /*e*/, const HqProxyComponent& /*hq*/,
const FactionComponent& f, const HealthComponent& h) const FactionComponent& f, const HealthComponent& h)
{ {
if (h.maxHp > 0.0f) if (h.maxHp > 0.0f)
{ {
drawHpBar(painter, bboxRect.left(), bboxRect.bottom() + 1.0, drawHpBar(painter, coordinates,
bboxRect.left(), bboxRect.bottom() + 1.0,
bboxRect.width(), h.hp / h.maxHp, f.isEnemy); bboxRect.width(), h.hp / h.maxHp, f.isEnemy);
} }
}); });
@@ -1466,10 +1424,11 @@ void GameWorldView::drawBuildings(QPainter& painter)
// site fill, so a selected building surrounded by neighbours keeps its outline: // site fill, so a selected building surrounded by neighbours keeps its outline:
// the highlight sits 1px outside the footprint (into adjacent tiles), and drawing // the highlight sits 1px outside the footprint (into adjacent tiles), and drawing
// it inline would let later-drawn neighbours overpaint it with their body fill. // it inline would let later-drawn neighbours overpaint it with their body fill.
drawSelectionHighlights(painter); drawSelectionHighlights(painter, coordinates);
} }
std::optional<QRectF> GameWorldView::footprintWidgetRect(BuildingId id) const std::optional<QRectF> GameWorldView::footprintWidgetRect(
const WorldCoordinates& coordinates, BuildingId id) const
{ {
std::optional<QPoint> anchor; std::optional<QPoint> anchor;
std::optional<QSize> footprint; std::optional<QSize> footprint;
@@ -1486,20 +1445,21 @@ std::optional<QRectF> GameWorldView::footprintWidgetRect(BuildingId id) const
} }
if (!anchor.has_value() || !footprint.has_value()) { return std::nullopt; } if (!anchor.has_value() || !footprint.has_value()) { return std::nullopt; }
const QPointF tl = tileToWidget(*anchor); const QPointF tl = coordinates.tileToWidget(*anchor);
return QRectF(tl.x(), tl.y(), return QRectF(tl.x(), tl.y(),
footprint->width() * static_cast<qreal>(getTilePx()), footprint->width() * static_cast<qreal>(coordinates.getTilePx()),
footprint->height() * static_cast<qreal>(getTilePx())); footprint->height() * static_cast<qreal>(coordinates.getTilePx()));
} }
void GameWorldView::drawSelectionHighlights(QPainter& painter) void GameWorldView::drawSelectionHighlights(QPainter& painter,
const WorldCoordinates& coordinates)
{ {
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2)); painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
for (BuildingId selId : m_selectedBuildingIds) for (BuildingId selId : m_selectedBuildingIds)
{ {
const std::optional<QRectF> rect = footprintWidgetRect(selId); const std::optional<QRectF> rect = footprintWidgetRect(coordinates, selId);
if (!rect.has_value()) { continue; } if (!rect.has_value()) { continue; }
// Outline sits 1px outside the footprint (into adjacent tiles). // Outline sits 1px outside the footprint (into adjacent tiles).
painter.drawRect(rect->adjusted(-1, -1, 1, 1)); painter.drawRect(rect->adjusted(-1, -1, 1, 1));
@@ -1509,17 +1469,20 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter)
// rendered circle (radius getTilePx()*0.2, matching drawDebris) (REQ-UI-DEBRIS-CLICK-SELECT). // rendered circle (radius getTilePx()*0.2, matching drawDebris) (REQ-UI-DEBRIS-CLICK-SELECT).
if (!m_selectedDebris.empty()) if (!m_selectedDebris.empty())
{ {
const qreal outlineRadius = static_cast<qreal>(getTilePx() * 0.2f) + 3.0; const qreal outlineRadius =
static_cast<qreal>(coordinates.getTilePx() * 0.2f) + 3.0;
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin())) for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
{ {
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), debris.entity) if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), debris.entity)
== m_selectedDebris.end()) { continue; } == m_selectedDebris.end()) { continue; }
painter.drawEllipse(worldToWidget(debris.position), outlineRadius, outlineRadius); painter.drawEllipse(coordinates.worldToWidget(debris.position),
outlineRadius, outlineRadius);
} }
} }
} }
void GameWorldView::drawCopyConfigFeedback(QPainter& painter) void GameWorldView::drawCopyConfigFeedback(QPainter& painter,
const WorldCoordinates& coordinates)
{ {
const QColor color = m_visuals->overlays.copyConfig; const QColor color = m_visuals->overlays.copyConfig;
@@ -1534,13 +1497,13 @@ void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
for (const Building& b : getAllBuildings(m_sim->getFactoryState())) for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
{ {
if (b.type != type) { continue; } if (b.type != type) { continue; }
const std::optional<QRectF> rect = footprintWidgetRect(b.id); const std::optional<QRectF> rect = footprintWidgetRect(coordinates, b.id);
if (rect.has_value()) { painter.drawRect(*rect); } if (rect.has_value()) { painter.drawRect(*rect); }
} }
for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState())) for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState()))
{ {
if (s.type != type) { continue; } if (s.type != type) { continue; }
const std::optional<QRectF> rect = footprintWidgetRect(s.id); const std::optional<QRectF> rect = footprintWidgetRect(coordinates, s.id);
if (rect.has_value()) { painter.drawRect(*rect); } if (rect.has_value()) { painter.drawRect(*rect); }
} }
} }
@@ -1551,14 +1514,14 @@ void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
for (const CopyConfigFlash& flash : m_copyConfigFlashes) for (const CopyConfigFlash& flash : m_copyConfigFlashes)
{ {
const std::optional<QRectF> rect = footprintWidgetRect(flash.id); const std::optional<QRectF> rect = footprintWidgetRect(coordinates, flash.id);
if (rect.has_value()) { painter.drawRect(rect->adjusted(-1, -1, 1, 1)); } if (rect.has_value()) { painter.drawRect(rect->adjusted(-1, -1, 1, 1)); }
} }
} }
void GameWorldView::drawPortItems(QPainter& painter) void GameWorldView::drawPortItems(QPainter& painter, const WorldCoordinates& coordinates)
{ {
const float halfPx = getTilePx() * 0.5f * 0.5f; const float halfPx = coordinates.getTilePx() * 0.5f * 0.5f;
// Port items are drawn over the buildings (drawBuildings runs first) but clipped // Port items are drawn over the buildings (drawBuildings runs first) but clipped
// to a thin margin at each machine's edges: the clip region is the whole view // to a thin margin at each machine's edges: the clip region is the whole view
@@ -1569,7 +1532,7 @@ void GameWorldView::drawPortItems(QPainter& painter)
// (REQ-MAT-DIRECT-COUPLE). Transport tiles are not machines and never occlude, so // (REQ-MAT-DIRECT-COUPLE). Transport tiles are not machines and never occlude, so
// items on belts stay fully visible. // items on belts stay fully visible.
constexpr double kPortMarginTiles = 0.2; constexpr double kPortMarginTiles = 0.2;
const double margin = kPortMarginTiles * static_cast<double>(getTilePx()); const double margin = kPortMarginTiles * static_cast<double>(coordinates.getTilePx());
QRegion clip(rect()); QRegion clip(rect());
for (const Building& b : getAllBuildings(m_sim->getFactoryState())) for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
@@ -1588,7 +1551,8 @@ void GameWorldView::drawPortItems(QPainter& painter)
const double t = cells.count(cell + QPoint( 0, -1)) ? 0.0 : margin; const double t = cells.count(cell + QPoint( 0, -1)) ? 0.0 : margin;
const double r = cells.count(cell + QPoint( 1, 0)) ? 0.0 : margin; const double r = cells.count(cell + QPoint( 1, 0)) ? 0.0 : margin;
const double d = cells.count(cell + QPoint( 0, 1)) ? 0.0 : margin; const double d = cells.count(cell + QPoint( 0, 1)) ? 0.0 : margin;
clip = clip.subtracted(QRegion(tileRect(cell).adjusted(l, t, -r, -d).toRect())); clip = clip.subtracted(
QRegion(coordinates.tileRect(cell).adjusted(l, t, -r, -d).toRect()));
} }
} }
@@ -1597,7 +1561,7 @@ void GameWorldView::drawPortItems(QPainter& painter)
const std::function<void(const ItemType&, QPointF)> drawItem = const std::function<void(const ItemType&, QPointF)> drawItem =
[&](const ItemType& type, QPointF worldPos) [&](const ItemType& type, QPointF worldPos)
{ {
const QPointF center = worldToWidget( const QPointF center = coordinates.worldToWidget(
QVector2D(static_cast<float>(worldPos.x()), QVector2D(static_cast<float>(worldPos.x()),
static_cast<float>(worldPos.y()))); static_cast<float>(worldPos.y())));
drawWorldItem(painter, type.id, center, halfPx); drawWorldItem(painter, type.id, center, halfPx);
@@ -1637,26 +1601,26 @@ void GameWorldView::drawWorldItem(QPainter& painter, const std::string& itemId,
painter.drawRect(itemRect); painter.drawRect(itemRect);
} }
void GameWorldView::drawBeltItems(QPainter& painter) void GameWorldView::drawBeltItems(QPainter& painter, const WorldCoordinates& coordinates)
{ {
const float halfPx = getTilePx() * 0.5f * 0.5f; const float halfPx = coordinates.getTilePx() * 0.5f * 0.5f;
const QRect vr = getViewportRect(); const QRect vr = coordinates.getViewportRect();
m_sim->getBelts().forEachVisualItem(vr, [&](const VisualItem& vi) m_sim->getBelts().forEachVisualItem(vr, [&](const VisualItem& vi)
{ {
const QPointF center = worldToWidget( const QPointF center = coordinates.worldToWidget(
QVector2D(static_cast<float>(vi.worldPos.x()), QVector2D(static_cast<float>(vi.worldPos.x()),
static_cast<float>(vi.worldPos.y()))); static_cast<float>(vi.worldPos.y())));
drawWorldItem(painter, vi.type.id, center, halfPx); drawWorldItem(painter, vi.type.id, center, halfPx);
}); });
} }
void GameWorldView::drawDebris(QPainter& painter) void GameWorldView::drawDebris(QPainter& painter, const WorldCoordinates& coordinates)
{ {
const float r = getTilePx() * 0.2f; const float r = coordinates.getTilePx() * 0.2f;
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin())) for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
{ {
const QPointF center = worldToWidget(debris.position); const QPointF center = coordinates.worldToWidget(debris.position);
painter.setBrush(QColor(128, 110, 90)); painter.setBrush(QColor(128, 110, 90));
painter.setPen(QPen(QColor(50, 40, 30), 1)); painter.setPen(QPen(QColor(50, 40, 30), 1));
painter.drawEllipse(center, painter.drawEllipse(center,
@@ -1664,7 +1628,7 @@ void GameWorldView::drawDebris(QPainter& painter)
} }
} }
void GameWorldView::drawStations(QPainter& painter) void GameWorldView::drawStations(QPainter& painter, const WorldCoordinates& coordinates)
{ {
m_sim->getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( m_sim->getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f, [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f,
@@ -1681,13 +1645,14 @@ void GameWorldView::drawStations(QPainter& painter)
painter.setPen(Qt::NoPen); painter.setPen(Qt::NoPen);
for (const QPoint& cell : sb.bodyCells) for (const QPoint& cell : sb.bodyCells)
{ {
painter.fillRect(tileRect(cell), bv.fill); painter.fillRect(coordinates.tileRect(cell), bv.fill);
} }
const QPointF tl = tileToWidget(QPoint(sb.anchor.x(), sb.anchor.y())); const QPointF tl =
coordinates.tileToWidget(QPoint(sb.anchor.x(), sb.anchor.y()));
const QRectF bboxRect(tl.x(), tl.y(), const QRectF bboxRect(tl.x(), tl.y(),
sb.footprint.width() * static_cast<qreal>(getTilePx()), sb.footprint.width() * static_cast<qreal>(coordinates.getTilePx()),
sb.footprint.height() * static_cast<qreal>(getTilePx())); sb.footprint.height() * static_cast<qreal>(coordinates.getTilePx()));
painter.setPen(QPen(bv.outline, 1)); painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
@@ -1695,7 +1660,7 @@ void GameWorldView::drawStations(QPainter& painter)
// Station icon over the fill (REQ-UI-WORLD-ICON); the same symbol is // Station icon over the fill (REQ-UI-WORLD-ICON); the same symbol is
// colored blue/red by the player/enemy fill it is drawn over. // colored blue/red by the player/enemy fill it is drawn over.
drawBuildingIcon(painter, visType, bboxRect, bv.fill); drawBuildingIcon(painter, coordinates, visType, bboxRect, bv.fill);
if (isEntitySelected(e)) if (isEntitySelected(e))
{ {
@@ -1707,13 +1672,14 @@ void GameWorldView::drawStations(QPainter& painter)
// HP bar below footprint. // HP bar below footprint.
if (h.maxHp > 0.0f) if (h.maxHp > 0.0f)
{ {
drawHpBar(painter, bboxRect.left(), bboxRect.bottom() + 1.0, drawHpBar(painter, coordinates,
bboxRect.left(), bboxRect.bottom() + 1.0,
bboxRect.width(), h.hp / h.maxHp, f.isEnemy); bboxRect.width(), h.hp / h.maxHp, f.isEnemy);
} }
}); });
} }
void GameWorldView::drawShips(QPainter& painter) void GameWorldView::drawShips(QPainter& painter, const WorldCoordinates& coordinates)
{ {
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent, m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, HealthComponent>( FactionComponent, HealthComponent>(
@@ -1725,12 +1691,12 @@ void GameWorldView::drawShips(QPainter& painter)
m_visuals->ships.find(si.schematicId); m_visuals->ships.find(si.schematicId);
if (it == m_visuals->ships.end()) { return; } if (it == m_visuals->ships.end()) { return; }
const QPointF center = worldToWidget(pos.value); const QPointF center = coordinates.worldToWidget(pos.value);
const QVector2D dir(std::cos(facing.radians), std::sin(facing.radians)); const QVector2D dir(std::cos(facing.radians), std::sin(facing.radians));
const QVector2D perp(-dir.y(), dir.x()); const QVector2D perp(-dir.y(), dir.x());
const float fwd = getTilePx() * 0.45f; const float fwd = coordinates.getTilePx() * 0.45f;
const float side = getTilePx() * 0.25f; const float side = coordinates.getTilePx() * 0.25f;
QPolygonF tri; QPolygonF tri;
tri << QPointF(center.x() + static_cast<qreal>(dir.x() * fwd), tri << QPointF(center.x() + static_cast<qreal>(dir.x() * fwd),
@@ -1757,22 +1723,25 @@ void GameWorldView::drawShips(QPainter& painter)
const qreal barW = static_cast<qreal>(fwd) * 2.0; const qreal barW = static_cast<qreal>(fwd) * 2.0;
const qreal barX = center.x() - static_cast<qreal>(fwd); const qreal barX = center.x() - static_cast<qreal>(fwd);
const qreal barY = center.y() + static_cast<qreal>(fwd) + 1.0; const qreal barY = center.y() + static_cast<qreal>(fwd) + 1.0;
drawHpBar(painter, barX, barY, barW, h.hp / h.maxHp, fac.isEnemy); drawHpBar(painter, coordinates, barX, barY, barW,
h.hp / h.maxHp, fac.isEnemy);
} }
}); });
} }
void GameWorldView::drawHpBar(QPainter& painter, qreal left, qreal top, qreal width, void GameWorldView::drawHpBar(QPainter& painter, const WorldCoordinates& coordinates,
qreal left, qreal top, qreal width,
float fraction, bool isEnemy) float fraction, bool isEnemy)
{ {
const qreal barH = static_cast<qreal>(getTilePx()) * 0.12; const qreal barH = static_cast<qreal>(coordinates.getTilePx()) * 0.12;
const float clamped = std::max(0.0f, fraction); const float clamped = std::max(0.0f, fraction);
painter.fillRect(QRectF(left, top, width, barH), QColor(60, 60, 60)); painter.fillRect(QRectF(left, top, width, barH), QColor(60, 60, 60));
painter.fillRect(QRectF(left, top, width * static_cast<qreal>(clamped), barH), painter.fillRect(QRectF(left, top, width * static_cast<qreal>(clamped), barH),
isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60)); isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60));
} }
void GameWorldView::drawDebugSensorRanges(QPainter& painter) void GameWorldView::drawDebugSensorRanges(QPainter& painter,
const WorldCoordinates& coordinates)
{ {
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent, m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
@@ -1785,9 +1754,9 @@ void GameWorldView::drawDebugSensorRanges(QPainter& painter)
m_visuals->ships.find(si.schematicId); m_visuals->ships.find(si.schematicId);
if (it == m_visuals->ships.end()) { return; } if (it == m_visuals->ships.end()) { return; }
const QPointF center = worldToWidget(pos.value); const QPointF center = coordinates.worldToWidget(pos.value);
const qreal radiusPx = static_cast<qreal>(sensor.value_tiles) const qreal radiusPx = static_cast<qreal>(sensor.value_tiles)
* static_cast<qreal>(getTilePx()); * static_cast<qreal>(coordinates.getTilePx());
QColor circleColor = it->second.outline; QColor circleColor = it->second.outline;
circleColor.setAlpha(77); circleColor.setAlpha(77);
painter.setPen(QPen(circleColor, 1)); painter.setPen(QPen(circleColor, 1));
@@ -1795,7 +1764,8 @@ void GameWorldView::drawDebugSensorRanges(QPainter& painter)
}); });
} }
void GameWorldView::drawDebugTargetLines(QPainter& painter) void GameWorldView::drawDebugTargetLines(QPainter& painter,
const WorldCoordinates& coordinates)
{ {
// Draw a thin translucent line from a ship to a target, colored by the ship's // Draw a thin translucent line from a ship to a target, colored by the ship's
// own schematic fill. Shared by the attack, repair and salvage target lines. // own schematic fill. Shared by the attack, repair and salvage target lines.
@@ -1810,7 +1780,8 @@ void GameWorldView::drawDebugTargetLines(QPainter& painter)
QColor lineColor = it->second.fill; QColor lineColor = it->second.fill;
lineColor.setAlpha(128); lineColor.setAlpha(128);
painter.setPen(QPen(lineColor, 1)); painter.setPen(QPen(lineColor, 1));
painter.drawLine(worldToWidget(from), worldToWidget(to)); painter.drawLine(coordinates.worldToWidget(from),
coordinates.worldToWidget(to));
}; };
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, AttackBehavior>( m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, AttackBehavior>(
@@ -1896,7 +1867,7 @@ void GameWorldView::drawDebugOverlay(QPainter& painter)
} }
} }
void GameWorldView::drawBeams(QPainter& painter) void GameWorldView::drawBeams(QPainter& painter, const WorldCoordinates& coordinates)
{ {
const QPainter::RenderHints savedHints = painter.renderHints(); const QPainter::RenderHints savedHints = painter.renderHints();
painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::Antialiasing, true);
@@ -1915,8 +1886,8 @@ void GameWorldView::drawBeams(QPainter& painter)
case BeamKind::Salvage: color = m_visuals->beams.salvageColor; break; case BeamKind::Salvage: color = m_visuals->beams.salvageColor; break;
} }
const QPointF s = worldToWidget(*shooterPos); const QPointF s = coordinates.worldToWidget(*shooterPos);
const QPointF t = worldToWidget(*targetPos + beam.targetOffset); const QPointF t = coordinates.worldToWidget(*targetPos + beam.targetOffset);
// Unit direction/perpendicular of the beam in widget space. A degenerate // Unit direction/perpendicular of the beam in widget space. A degenerate
// zero-length beam (shooter and target coincide) has no direction to // zero-length beam (shooter and target coincide) has no direction to
@@ -1965,7 +1936,8 @@ void GameWorldView::drawBeams(QPainter& painter)
painter.setRenderHints(savedHints); painter.setRenderHints(savedHints);
} }
void GameWorldView::drawSelectedTunnelConnections(QPainter& painter) void GameWorldView::drawSelectedTunnelConnections(QPainter& painter,
const WorldCoordinates& coordinates)
{ {
if (m_selectedBuildingIds.empty()) { return; } if (m_selectedBuildingIds.empty()) { return; }
@@ -2015,14 +1987,14 @@ void GameWorldView::drawSelectedTunnelConnections(QPainter& painter)
const QColor green = m_visuals->overlays.tunnelPreview; const QColor green = m_visuals->overlays.tunnelPreview;
for (const QPoint& tile : highlightTiles) for (const QPoint& tile : highlightTiles)
{ {
painter.fillRect(tileRect(tile), green); painter.fillRect(coordinates.tileRect(tile), green);
} }
} }
void GameWorldView::drawOverlays(QPainter& painter) void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coordinates)
{ {
// Green connection highlight for any selected tunnel end (REQ-BLD-TUNNEL-SELECT-HIGHLIGHT). // Green connection highlight for any selected tunnel end (REQ-BLD-TUNNEL-SELECT-HIGHLIGHT).
drawSelectedTunnelConnections(painter); drawSelectedTunnelConnections(painter, coordinates);
// Builder-mode ghost // Builder-mode ghost
if (m_builderType.has_value()) if (m_builderType.has_value())
@@ -2041,8 +2013,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
continue; continue;
} }
const BeltPathTile& entry = m_beltDragPath[index]; const BeltPathTile& entry = m_beltDragPath[index];
drawBuildingGhost(painter, BuildingType::Belt, entry.tile, drawBuildingGhost(painter, coordinates, BuildingType::Belt,
entry.rotation, entry.tile, entry.rotation,
/*valid*/ item.action != BeltTileAction::Invalid, /*valid*/ item.action != BeltTileAction::Invalid,
/*showPortTargetGlyphs*/ true); /*showPortTargetGlyphs*/ true);
} }
@@ -2056,7 +2028,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
if (inTunnelMode() && m_ghostValid && m_tunnelPartnerTile.has_value()) if (inTunnelMode() && m_ghostValid && m_tunnelPartnerTile.has_value())
{ {
const QColor green = m_visuals->overlays.tunnelPreview; const QColor green = m_visuals->overlays.tunnelPreview;
painter.fillRect(tileRect(*m_tunnelPartnerTile), green); painter.fillRect(coordinates.tileRect(*m_tunnelPartnerTile), green);
// Partner and ghost tile are colinear along the tunnel run; tint the // Partner and ghost tile are colinear along the tunnel run; tint the
// tiles strictly between them. // tiles strictly between them.
@@ -2065,12 +2037,12 @@ void GameWorldView::drawOverlays(QPainter& painter)
(delta.y() > 0) - (delta.y() < 0)); (delta.y() > 0) - (delta.y() < 0));
for (QPoint t = m_ghostTile + step; t != *m_tunnelPartnerTile; t += step) for (QPoint t = m_ghostTile + step; t != *m_tunnelPartnerTile; t += step)
{ {
painter.fillRect(tileRect(t), green); painter.fillRect(coordinates.tileRect(t), green);
} }
} }
drawBuildingGhost(painter, effectiveBuilderType(), m_ghostTile, drawBuildingGhost(painter, coordinates, effectiveBuilderType(),
m_ghostRotation, m_ghostValid, m_ghostTile, m_ghostRotation, m_ghostValid,
/*showPortTargetGlyphs*/ true); /*showPortTargetGlyphs*/ true);
} }
} }
@@ -2085,8 +2057,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; } if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
const QPoint anchor = m_blueprintGhostTile + bb.offset; const QPoint anchor = m_blueprintGhostTile + bb.offset;
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation); const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
drawBuildingGhost(painter, bb.type, anchor, bb.rotation, valid, drawBuildingGhost(painter, coordinates, bb.type, anchor, bb.rotation,
/*showPortTargetGlyphs*/ false); valid, /*showPortTargetGlyphs*/ false);
} }
} }
@@ -2097,7 +2069,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
if (!b.queuedForDeconstruction) { continue; } if (!b.queuedForDeconstruction) { continue; }
for (const QPoint& cell : b.bodyCells) for (const QPoint& cell : b.bodyCells)
{ {
painter.fillRect(tileRect(cell), m_visuals->overlays.deconstructTint); painter.fillRect(coordinates.tileRect(cell),
m_visuals->overlays.deconstructTint);
} }
} }
@@ -2117,7 +2090,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
{ {
for (const QPoint& cell : *cells) for (const QPoint& cell : *cells)
{ {
painter.fillRect(tileRect(cell), m_visuals->overlays.deconstructTint); painter.fillRect(coordinates.tileRect(cell),
m_visuals->overlays.deconstructTint);
} }
} }
} }
@@ -2129,7 +2103,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
{ {
for (const QPoint& cell : b->bodyCells) for (const QPoint& cell : b->bodyCells)
{ {
painter.fillRect(tileRect(cell), m_visuals->overlays.deconstructTint); painter.fillRect(coordinates.tileRect(cell),
m_visuals->overlays.deconstructTint);
} }
} }
} }
@@ -2141,14 +2116,17 @@ void GameWorldView::drawOverlays(QPainter& painter)
std::min(m_boxStartTile.y(), m_boxCurrentTile.y())); std::min(m_boxStartTile.y(), m_boxCurrentTile.y()));
const QPoint br(std::max(m_boxStartTile.x(), m_boxCurrentTile.x()) + 1, const QPoint br(std::max(m_boxStartTile.x(), m_boxCurrentTile.x()) + 1,
std::max(m_boxStartTile.y(), m_boxCurrentTile.y()) + 1); std::max(m_boxStartTile.y(), m_boxCurrentTile.y()) + 1);
const QRectF selRect(tileToWidget(tl), tileToWidget(br)); const QRectF selRect(coordinates.tileToWidget(tl),
coordinates.tileToWidget(br));
painter.setPen(QPen(m_visuals->overlays.selectionRect, 1)); painter.setPen(QPen(m_visuals->overlays.selectionRect, 1));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
painter.drawRect(selRect); painter.drawRect(selRect);
} }
} }
void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type, void GameWorldView::drawBuildingGhost(QPainter& painter,
const WorldCoordinates& coordinates,
BuildingType type,
QPoint anchorTile, Rotation rotation, QPoint anchorTile, Rotation rotation,
bool valid, bool showPortTargetGlyphs) bool valid, bool showPortTargetGlyphs)
{ {
@@ -2179,17 +2157,17 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
QPoint maxCell = parsed.bodyCells.front(); QPoint maxCell = parsed.bodyCells.front();
for (const QPoint& cell : parsed.bodyCells) for (const QPoint& cell : parsed.bodyCells)
{ {
painter.fillRect(tileRect(anchorTile + cell), fillColor); painter.fillRect(coordinates.tileRect(anchorTile + cell), fillColor);
minCell.setX(std::min(minCell.x(), cell.x())); minCell.setX(std::min(minCell.x(), cell.x()));
minCell.setY(std::min(minCell.y(), cell.y())); minCell.setY(std::min(minCell.y(), cell.y()));
maxCell.setX(std::max(maxCell.x(), cell.x())); maxCell.setX(std::max(maxCell.x(), cell.x()));
maxCell.setY(std::max(maxCell.y(), cell.y())); maxCell.setY(std::max(maxCell.y(), cell.y()));
} }
const QPointF tl = tileToWidget(anchorTile + minCell); const QPointF tl = coordinates.tileToWidget(anchorTile + minCell);
const QRectF bboxRect(tl.x(), tl.y(), const QRectF bboxRect(tl.x(), tl.y(),
(maxCell.x() - minCell.x() + 1) * static_cast<qreal>(getTilePx()), (maxCell.x() - minCell.x() + 1) * static_cast<qreal>(coordinates.getTilePx()),
(maxCell.y() - minCell.y() + 1) * static_cast<qreal>(getTilePx())); (maxCell.y() - minCell.y() + 1) * static_cast<qreal>(coordinates.getTilePx()));
painter.setPen(QPen(lineColor, 1)); painter.setPen(QPen(lineColor, 1));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
@@ -2197,7 +2175,8 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
// Icon glyph over the ghost fill (REQ-UI-WORLD-ICON); an invalid ghost's fill // Icon glyph over the ghost fill (REQ-UI-WORLD-ICON); an invalid ghost's fill
// is the red invalid color, which auto-contrasts to a white icon. // is the red invalid color, which auto-contrasts to a white icon.
if (!drawBuildingIcon(painter, type, bboxRect, fillColor) && !bv.glyph.isEmpty()) if (!drawBuildingIcon(painter, coordinates, type, bboxRect, fillColor)
&& !bv.glyph.isEmpty())
{ {
painter.setPen(lineColor); painter.setPen(lineColor);
painter.drawText(bboxRect, Qt::AlignCenter, bv.glyph); painter.drawText(bboxRect, Qt::AlignCenter, bv.glyph);
@@ -2205,7 +2184,8 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
for (const Port& port : parsed.outputPorts) for (const Port& port : parsed.outputPorts)
{ {
drawPortGlyph(painter, anchorTile + outputBodyTile(port.tile, port.direction), drawPortGlyph(painter, coordinates,
anchorTile + outputBodyTile(port.tile, port.direction),
port.direction, lineColor, /*centered*/ false); port.direction, lineColor, /*centered*/ false);
} }
@@ -2221,8 +2201,8 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
{ {
for (const Port& port : parsed.outputPorts) for (const Port& port : parsed.outputPorts)
{ {
drawPortGlyph(painter, anchorTile + port.tile, port.direction, drawPortGlyph(painter, coordinates, anchorTile + port.tile,
lineColor, /*centered*/ true); port.direction, lineColor, /*centered*/ true);
} }
} }
@@ -2483,6 +2463,8 @@ void GameWorldView::keyReleaseEvent(QKeyEvent* event)
void GameWorldView::mousePressEvent(QMouseEvent* event) void GameWorldView::mousePressEvent(QMouseEvent* event)
{ {
const WorldCoordinates coordinates = getCoordinates();
if (event->button() != Qt::LeftButton) if (event->button() != Qt::LeftButton)
{ {
if (event->button() == Qt::RightButton) if (event->button() == Qt::RightButton)
@@ -2507,7 +2489,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
{ {
// Shift + right-click copies a building's settings, but only in the // Shift + right-click copies a building's settings, but only in the
// default selection mode (REQ-BLD-COPY-CONFIG). // default selection mode (REQ-BLD-COPY-CONFIG).
const QPoint tile = widgetToTile(event->pos()); const QPoint tile = coordinates.widgetToTile(event->pos());
std::optional<BuildingId> id = buildingAtTile(tile); std::optional<BuildingId> id = buildingAtTile(tile);
if (!id.has_value()) { id = siteAtTile(tile); } if (!id.has_value()) { id = siteAtTile(tile); }
if (id.has_value()) { copyConfigFrom(*id); } if (id.has_value()) { copyConfigFrom(*id); }
@@ -2516,7 +2498,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
return; return;
} }
const QPoint tile = widgetToTile(event->pos()); const QPoint tile = coordinates.widgetToTile(event->pos());
if (m_builderType.has_value()) if (m_builderType.has_value())
{ {
@@ -2527,7 +2509,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
// is placed until release (REQ-BLD-BELT-DRAG). // is placed until release (REQ-BLD-BELT-DRAG).
m_dragging = true; m_dragging = true;
m_beltDragAnchor = tile; m_beltDragAnchor = tile;
m_cursorWorldPos = widgetToWorld(event->pos()); m_cursorWorldPos = coordinates.widgetToWorld(event->pos());
recomputeBeltDragPath(tile); recomputeBeltDragPath(tile);
} }
else else
@@ -2564,7 +2546,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
} }
const bool ctrl = (event->modifiers() & Qt::ControlModifier) != 0; const bool ctrl = (event->modifiers() & Qt::ControlModifier) != 0;
const QVector2D worldPos = widgetToWorld(event->pos()); const QVector2D worldPos = coordinates.widgetToWorld(event->pos());
// Point hit-test precedence: buildings win over actors, which win over debris // Point hit-test precedence: buildings win over actors, which win over debris
// (REQ-UI-SELECTION-CATEGORIES). // (REQ-UI-SELECTION-CATEGORIES).
@@ -2690,8 +2672,9 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
void GameWorldView::mouseMoveEvent(QMouseEvent* event) void GameWorldView::mouseMoveEvent(QMouseEvent* event)
{ {
const QPoint tile = widgetToTile(event->pos()); const WorldCoordinates coordinates = getCoordinates();
m_cursorWorldPos = widgetToWorld(event->pos()); const QPoint tile = coordinates.widgetToTile(event->pos());
m_cursorWorldPos = coordinates.widgetToWorld(event->pos());
if (m_builderType.has_value()) if (m_builderType.has_value())
{ {

View File

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