Implement deferred L-shaped belt drag placement

Belts are no longer placed on hover during a drag. Left-press anchors the
drag; as the cursor moves, a rectilinear (L-shaped) path of belt ghosts is
previewed from the anchor to the cursor — first leg parallel to the belt's
current orientation, then orthogonal — with each tile auto-oriented to follow
the path. Construction sites are placed on release: new valid tiles are placed
(subject to cumulative affordability), tiles holding only a belt are re-oriented
in place, and occupied/invalid tiles are skipped. Unaffordable tiles show no
ghost. Right-click during a drag cancels it without leaving belt build mode.

Path geometry is factored into a pure computeBeltDragPath() in lib/core with
Catch2 coverage; GameWorldView owns the classification, affordability, and
rendering against live sim state.

Implements REQ-BLD-BELT-DRAG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
2026-07-21 12:59:21 +02:00
parent 333b72e3b7
commit 42b86785a5
7 changed files with 438 additions and 28 deletions

View File

@@ -0,0 +1,157 @@
#include "catch.hpp"
#include <vector>
#include <QPoint>
#include "BeltDragPath.h"
#include "Rotation.h"
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static std::vector<QPoint> tilesOf(const std::vector<BeltPathTile>& path)
{
std::vector<QPoint> tiles;
for (const BeltPathTile& entry : path) { tiles.push_back(entry.tile); }
return tiles;
}
static std::vector<Rotation> rotationsOf(const std::vector<BeltPathTile>& path)
{
std::vector<Rotation> rotations;
for (const BeltPathTile& entry : path) { rotations.push_back(entry.rotation); }
return rotations;
}
// ---------------------------------------------------------------------------
// Single tile
// ---------------------------------------------------------------------------
TEST_CASE("Single-tile path keeps the belt orientation")
{
for (Rotation orientation : { Rotation::North, Rotation::East,
Rotation::South, Rotation::West })
{
const std::vector<BeltPathTile> path =
computeBeltDragPath(QPoint(3, 4), QPoint(3, 4), orientation);
REQUIRE(path.size() == 1);
REQUIRE(path[0].tile == QPoint(3, 4));
REQUIRE(path[0].rotation == orientation);
}
}
// ---------------------------------------------------------------------------
// Straight runs
// ---------------------------------------------------------------------------
TEST_CASE("Straight horizontal run faces along the row toward the cursor")
{
const std::vector<BeltPathTile> path =
computeBeltDragPath(QPoint(0, 0), QPoint(3, 0), Rotation::East);
REQUIRE(tilesOf(path) == std::vector<QPoint>{
QPoint(0, 0), QPoint(1, 0), QPoint(2, 0), QPoint(3, 0) });
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
Rotation::East, Rotation::East, Rotation::East, Rotation::East });
}
TEST_CASE("Straight horizontal run toward the left faces West")
{
const std::vector<BeltPathTile> path =
computeBeltDragPath(QPoint(0, 0), QPoint(-2, 0), Rotation::East);
REQUIRE(tilesOf(path) == std::vector<QPoint>{
QPoint(0, 0), QPoint(-1, 0), QPoint(-2, 0) });
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
Rotation::West, Rotation::West, Rotation::West });
}
TEST_CASE("Straight vertical run faces along the column toward the cursor")
{
const std::vector<BeltPathTile> path =
computeBeltDragPath(QPoint(0, 0), QPoint(0, 3), Rotation::South);
REQUIRE(tilesOf(path) == std::vector<QPoint>{
QPoint(0, 0), QPoint(0, 1), QPoint(0, 2), QPoint(0, 3) });
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
Rotation::South, Rotation::South, Rotation::South, Rotation::South });
}
// A vertical target with a horizontal orientation still yields a straight vertical
// line (the parallel-axis leg is zero-length).
TEST_CASE("Vertical target with horizontal orientation is a straight vertical line")
{
const std::vector<BeltPathTile> path =
computeBeltDragPath(QPoint(2, 0), QPoint(2, 2), Rotation::East);
REQUIRE(tilesOf(path) == std::vector<QPoint>{
QPoint(2, 0), QPoint(2, 1), QPoint(2, 2) });
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
Rotation::South, Rotation::South, Rotation::South });
}
// ---------------------------------------------------------------------------
// L-shaped paths — horizontal-first (East/West orientation)
// ---------------------------------------------------------------------------
TEST_CASE("East orientation goes horizontal then vertical (down-right)")
{
const std::vector<BeltPathTile> path =
computeBeltDragPath(QPoint(0, 0), QPoint(2, 2), Rotation::East);
// Leg 1 East to the corner (2,0), then Leg 2 South to the cursor (2,2).
REQUIRE(tilesOf(path) == std::vector<QPoint>{
QPoint(0, 0), QPoint(1, 0), QPoint(2, 0), QPoint(2, 1), QPoint(2, 2) });
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
Rotation::East, Rotation::East, Rotation::South, Rotation::South,
Rotation::South });
}
TEST_CASE("East orientation with cursor up-left goes horizontal (West) then vertical (North)")
{
const std::vector<BeltPathTile> path =
computeBeltDragPath(QPoint(0, 0), QPoint(-2, -2), Rotation::East);
REQUIRE(tilesOf(path) == std::vector<QPoint>{
QPoint(0, 0), QPoint(-1, 0), QPoint(-2, 0), QPoint(-2, -1),
QPoint(-2, -2) });
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
Rotation::West, Rotation::West, Rotation::North, Rotation::North,
Rotation::North });
}
TEST_CASE("West orientation is horizontal-first as well (up-right cursor)")
{
const std::vector<BeltPathTile> path =
computeBeltDragPath(QPoint(0, 0), QPoint(2, -2), Rotation::West);
// Horizontal axis first: East toward the cursor to the corner (2,0), then North.
REQUIRE(tilesOf(path) == std::vector<QPoint>{
QPoint(0, 0), QPoint(1, 0), QPoint(2, 0), QPoint(2, -1), QPoint(2, -2) });
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
Rotation::East, Rotation::East, Rotation::North, Rotation::North,
Rotation::North });
}
// ---------------------------------------------------------------------------
// L-shaped paths — vertical-first (North/South orientation)
// ---------------------------------------------------------------------------
TEST_CASE("South orientation goes vertical then horizontal (down-right)")
{
const std::vector<BeltPathTile> path =
computeBeltDragPath(QPoint(0, 0), QPoint(2, 2), Rotation::South);
// Leg 1 South to the corner (0,2), then Leg 2 East to the cursor (2,2).
REQUIRE(tilesOf(path) == std::vector<QPoint>{
QPoint(0, 0), QPoint(0, 1), QPoint(0, 2), QPoint(1, 2), QPoint(2, 2) });
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
Rotation::South, Rotation::South, Rotation::East, Rotation::East,
Rotation::East });
}
TEST_CASE("North orientation is vertical-first (down-left cursor)")
{
const std::vector<BeltPathTile> path =
computeBeltDragPath(QPoint(0, 0), QPoint(-2, 2), Rotation::North);
// Vertical axis first: South toward the cursor to the corner (0,2), then West.
REQUIRE(tilesOf(path) == std::vector<QPoint>{
QPoint(0, 0), QPoint(0, 1), QPoint(0, 2), QPoint(-1, 2), QPoint(-2, 2) });
REQUIRE(rotationsOf(path) == std::vector<Rotation>{
Rotation::South, Rotation::South, Rotation::West, Rotation::West,
Rotation::West });
}

View File

@@ -8,6 +8,7 @@ add_files(
SimulationTest.cpp
BeltSystemTest.cpp
SurfaceMaskTest.cpp
BeltDragPathTest.cpp
BuildingTest.cpp
BuildingConfigTest.cpp
ShipTest.cpp