204 lines
8.4 KiB
C++
204 lines
8.4 KiB
C++
#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::scrolling(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::scrolling(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::scrolling(QSize(800, 400), 0, 0.0f).getTilePx()
|
|
== Approx(1.0f));
|
|
REQUIRE(WorldCoordinates::scrolling(QSize(800, 400), -5, 0.0f).getTilePx()
|
|
== Approx(1.0f));
|
|
}
|
|
|
|
TEST_CASE("A zero-size viewport falls back to a unit tile", "[coords]")
|
|
{
|
|
// A widget that has not been shown yet still has to answer conversions —
|
|
// the arena hit-tests through the same transform.
|
|
REQUIRE(WorldCoordinates::scrolling(QSize(0, 0), 20, 0.0f).getTilePx()
|
|
== Approx(1.0f));
|
|
REQUIRE(WorldCoordinates::fitToWorld(QSize(0, 0), 40, 20).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::scrolling(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);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fitted (non-scrolling) worlds
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("A fitted world takes the tighter of the two axis fits", "[coords]")
|
|
{
|
|
// Height-limited: 400/20 = 20 px per tile beats 800/30 = 26.67.
|
|
REQUIRE(WorldCoordinates::fitToWorld(QSize(800, 400), 30, 20).getTilePx()
|
|
== Approx(20.0f));
|
|
// Width-limited: 800/80 = 10 px per tile beats 400/20 = 20.
|
|
REQUIRE(WorldCoordinates::fitToWorld(QSize(800, 400), 80, 20).getTilePx()
|
|
== Approx(10.0f));
|
|
}
|
|
|
|
TEST_CASE("A fitted world keeps its whole width on screen", "[coords]")
|
|
{
|
|
// The point of taking the tighter fit: the far edge must land inside the
|
|
// viewport, never past it.
|
|
const WorldCoordinates coordinates =
|
|
WorldCoordinates::fitToWorld(QSize(800, 400), 80, 20);
|
|
REQUIRE(coordinates.worldToWidget(QVector2D(80.0f, 0.0f)).x() <= 800.0);
|
|
REQUIRE(coordinates.worldToWidget(QVector2D(0.0f, 20.0f)).y() <= 400.0);
|
|
}
|
|
|
|
TEST_CASE("A fitted world puts the origin at the widget's top-left", "[coords]")
|
|
{
|
|
// No scrolling, so there is no view center to subtract.
|
|
const WorldCoordinates coordinates =
|
|
WorldCoordinates::fitToWorld(QSize(800, 400), 40, 20);
|
|
REQUIRE(coordinates.getViewLeftTiles() == Approx(0.0f));
|
|
REQUIRE(coordinates.tileToWidget(QPoint(0, 0)) == QPointF(0.0, 0.0));
|
|
REQUIRE(coordinates.tileToWidget(QPoint(3, 2)) == QPointF(60.0, 40.0));
|
|
}
|
|
|
|
TEST_CASE("A fitted world round-trips widget points back to world positions",
|
|
"[coords]")
|
|
{
|
|
const WorldCoordinates coordinates =
|
|
WorldCoordinates::fitToWorld(QSize(800, 400), 80, 20);
|
|
const QVector2D world = coordinates.widgetToWorld(QPoint(120, 55));
|
|
const QPointF back = coordinates.worldToWidget(world);
|
|
REQUIRE(back.x() == Approx(120.0));
|
|
REQUIRE(back.y() == Approx(55.0));
|
|
}
|