make selection box sub-tile aware
This commit is contained in:
@@ -17,6 +17,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanelPlacement.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionBox.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.h
|
||||
|
||||
34
src/lib/core/SelectionBox.h
Normal file
34
src/lib/core/SelectionBox.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPoint>
|
||||
#include <QRectF>
|
||||
#include <QVector2D>
|
||||
|
||||
// The coverage rules of a selection box (REQ-UI-MULTI-SELECT, REQ-BLD-DECONSTRUCT-BOX).
|
||||
//
|
||||
// The box is a rectangle in world coordinates — tiles as the unit, but fractional,
|
||||
// because the drag follows the mouse and is not snapped to the tile grid. The two
|
||||
// rules below are the whole of what "covered by the box" means; they live here so the
|
||||
// building query and the entity queries answer it identically.
|
||||
//
|
||||
// Both callers pass a normalized rectangle: neither rule normalizes on its own.
|
||||
|
||||
// Whether the box overlaps the unit square of `tile` — the rule for anything that
|
||||
// occupies whole tiles (buildings, construction sites, defence station bodies). The
|
||||
// comparisons are inclusive, so a box that only grazes the tile's edge still covers
|
||||
// it, and a box with no area covers the tile it lies on.
|
||||
inline bool boxCoversTile(const QRectF& worldBox, QPoint tile)
|
||||
{
|
||||
return worldBox.left() <= static_cast<qreal>(tile.x()) + 1.0
|
||||
&& worldBox.right() >= static_cast<qreal>(tile.x())
|
||||
&& worldBox.top() <= static_cast<qreal>(tile.y()) + 1.0
|
||||
&& worldBox.bottom() >= static_cast<qreal>(tile.y());
|
||||
}
|
||||
|
||||
// Whether the box contains `worldPos` — the rule for anything that has a position
|
||||
// rather than a footprint (ships, debris). Their centre is what the box must enclose,
|
||||
// so that what the rectangle visibly holds is what the drag selects.
|
||||
inline bool boxCoversPoint(const QRectF& worldBox, QVector2D worldPos)
|
||||
{
|
||||
return worldBox.contains(QPointF(worldPos.x(), worldPos.y()));
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cmath>
|
||||
|
||||
#include "EntityAdmin.h"
|
||||
#include "SelectionBox.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "DebrisComponent.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
@@ -82,45 +83,29 @@ entt::entity debrisAtWorldPos(EntityAdmin& admin, QVector2D worldPos)
|
||||
return bestDebris;
|
||||
}
|
||||
|
||||
std::vector<entt::entity> debrisInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB)
|
||||
std::vector<entt::entity> debrisInBox(EntityAdmin& admin, const QRectF& worldBox)
|
||||
{
|
||||
const int minX = std::min(tileA.x(), tileB.x());
|
||||
const int maxX = std::max(tileA.x(), tileB.x());
|
||||
const int minY = std::min(tileA.y(), tileB.y());
|
||||
const int maxY = std::max(tileA.y(), tileB.y());
|
||||
|
||||
std::vector<entt::entity> result;
|
||||
admin.forEach<DebrisComponent, PositionComponent>(
|
||||
[&](entt::entity entity, const DebrisComponent& /*sd*/, const PositionComponent& pos)
|
||||
{
|
||||
const int tileX = static_cast<int>(std::floor(pos.value.x()));
|
||||
const int tileY = static_cast<int>(std::floor(pos.value.y()));
|
||||
if (tileX >= minX && tileX <= maxX && tileY >= minY && tileY <= maxY)
|
||||
{
|
||||
result.push_back(entity);
|
||||
}
|
||||
if (boxCoversPoint(worldBox, pos.value)) { result.push_back(entity); }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<entt::entity> actorsInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB)
|
||||
std::vector<entt::entity> actorsInBox(EntityAdmin& admin, const QRectF& worldBox)
|
||||
{
|
||||
const int minX = std::min(tileA.x(), tileB.x());
|
||||
const int maxX = std::max(tileA.x(), tileB.x());
|
||||
const int minY = std::min(tileA.y(), tileB.y());
|
||||
const int maxY = std::max(tileA.y(), tileB.y());
|
||||
|
||||
std::vector<entt::entity> result;
|
||||
|
||||
// Stations: included when any occupied body cell lies in the box.
|
||||
// Stations occupy whole tiles: included when the box overlaps any occupied cell.
|
||||
admin.forEach<StationBodyComponent, HealthComponent>(
|
||||
[&](entt::entity entity, const StationBodyComponent& sb, const HealthComponent& h)
|
||||
{
|
||||
if (h.hp <= 0.0f) { return; }
|
||||
for (const QPoint& cell : sb.bodyCells)
|
||||
{
|
||||
if (cell.x() >= minX && cell.x() <= maxX
|
||||
&& cell.y() >= minY && cell.y() <= maxY)
|
||||
if (boxCoversTile(worldBox, cell))
|
||||
{
|
||||
result.push_back(entity);
|
||||
return;
|
||||
@@ -128,19 +113,15 @@ std::vector<entt::entity> actorsInBox(EntityAdmin& admin, QPoint tileA, QPoint t
|
||||
}
|
||||
});
|
||||
|
||||
// Ships: included when the floored position tile lies in the box. Requiring
|
||||
// ShipIdentityComponent excludes the HQ proxy and any station bodies.
|
||||
// Ships have a position rather than a footprint: included when the box contains
|
||||
// that position. Requiring ShipIdentityComponent excludes the HQ proxy and any
|
||||
// station bodies.
|
||||
admin.forEach<ShipIdentityComponent, PositionComponent, HealthComponent>(
|
||||
[&](entt::entity entity, const ShipIdentityComponent& /*id*/,
|
||||
const PositionComponent& pos, const HealthComponent& h)
|
||||
{
|
||||
if (h.hp <= 0.0f) { return; }
|
||||
const int tileX = static_cast<int>(std::floor(pos.value.x()));
|
||||
const int tileY = static_cast<int>(std::floor(pos.value.y()));
|
||||
if (tileX >= minX && tileX <= maxX && tileY >= minY && tileY <= maxY)
|
||||
{
|
||||
result.push_back(entity);
|
||||
}
|
||||
if (boxCoversPoint(worldBox, pos.value)) { result.push_back(entity); }
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
#include <QRectF>
|
||||
#include <QVector2D>
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
@@ -16,13 +17,13 @@ entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos);
|
||||
// after actors: entityAtWorldPos never returns debris (debris has no HealthComponent).
|
||||
entt::entity debrisAtWorldPos(EntityAdmin& admin, QVector2D worldPos);
|
||||
|
||||
// Returns every piece of debris whose position falls within the inclusive tile rectangle
|
||||
// spanned by tileA and tileB, in any corner order (REQ-UI-DEBRIS-MULTI-SELECT).
|
||||
std::vector<entt::entity> debrisInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB);
|
||||
// Returns every piece of debris the selection box covers — that is, whose position it
|
||||
// contains, per boxCoversPoint (REQ-UI-DEBRIS-MULTI-SELECT). `worldBox` is in world
|
||||
// coordinates and normalized; it is not snapped to tiles.
|
||||
std::vector<entt::entity> debrisInBox(EntityAdmin& admin, const QRectF& worldBox);
|
||||
|
||||
// Returns every living actor (ship or defence station, player or enemy) that falls
|
||||
// within the inclusive tile rectangle spanned by tileA and tileB, in any corner order
|
||||
// (REQ-UI-MULTI-SELECT, REQ-UI-ENTITY-CLICK-SELECT). A ship is included when its floored
|
||||
// position tile lies in the box; a station is included when any of its body cells does.
|
||||
// Dead actors (hp <= 0) and the HQ proxy are excluded.
|
||||
std::vector<entt::entity> actorsInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB);
|
||||
// Returns every living actor (ship or defence station, player or enemy) the selection
|
||||
// box covers (REQ-UI-MULTI-SELECT, REQ-UI-ENTITY-CLICK-SELECT): a ship when the box
|
||||
// contains its position, a station when the box overlaps any of its body cells — the
|
||||
// two rules of SelectionBox.h. Dead actors (hp <= 0) and the HQ proxy are excluded.
|
||||
std::vector<entt::entity> actorsInBox(EntityAdmin& admin, const QRectF& worldBox);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "PortGeometry.h"
|
||||
#include "ProductionRules.h"
|
||||
#include "SelectionBox.h"
|
||||
#include "SurfaceMask.h"
|
||||
|
||||
#include "Item.h"
|
||||
@@ -185,22 +186,13 @@ getSiteSplitterInfo(const FactoryState& state, const GameConfig& config, Buildin
|
||||
|
||||
|
||||
std::vector<BuildingId> buildingsInBox(const FactoryState& state,
|
||||
QPoint cornerA, QPoint cornerB)
|
||||
const QRectF& worldBox)
|
||||
{
|
||||
const int x0 = std::min(cornerA.x(), cornerB.x());
|
||||
const int y0 = std::min(cornerA.y(), cornerB.y());
|
||||
const int x1 = std::max(cornerA.x(), cornerB.x());
|
||||
const int y1 = std::max(cornerA.y(), cornerB.y());
|
||||
|
||||
const auto covers = [&](const std::vector<QPoint>& bodyCells)
|
||||
{
|
||||
for (const QPoint& cell : bodyCells)
|
||||
{
|
||||
if (cell.x() >= x0 && cell.x() <= x1
|
||||
&& cell.y() >= y0 && cell.y() <= y1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (boxCoversTile(worldBox, cell)) { return true; }
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
#include <QRectF>
|
||||
#include <QVector2D>
|
||||
|
||||
#include "Building.h"
|
||||
@@ -71,11 +72,12 @@ std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(const FactoryState&
|
||||
const GameConfig& config,
|
||||
BuildingId id);
|
||||
|
||||
// Ids of all buildings and construction sites whose footprint intersects the tile
|
||||
// box spanned by the two (unordered) corner tiles (REQ-UI-MULTI-SELECT,
|
||||
// REQ-BLD-DECONSTRUCT-BOX).
|
||||
// Ids of all buildings and construction sites the selection box covers — those with
|
||||
// a body cell the box overlaps, per boxCoversTile (REQ-UI-MULTI-SELECT,
|
||||
// REQ-BLD-DECONSTRUCT-BOX). `worldBox` is in world coordinates and normalized; it is
|
||||
// not snapped to tiles.
|
||||
std::vector<BuildingId> buildingsInBox(const FactoryState& state,
|
||||
QPoint cornerA, QPoint cornerB);
|
||||
const QRectF& worldBox);
|
||||
|
||||
// Every tunnel entry and exit, built or still a construction site, indexed by its
|
||||
// single-cell tile. Shared by the placement preview and the selection highlight.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
#include <QRectF>
|
||||
|
||||
#include "BeltSystem.h"
|
||||
#include "Building.h"
|
||||
@@ -135,6 +136,26 @@ TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building
|
||||
REQUIRE_FALSE(isTileOccupied(f.state, QPoint(1, 1)));
|
||||
}
|
||||
|
||||
TEST_CASE("buildingsInBox covers a body cell the box only reaches into", "[building]")
|
||||
{
|
||||
PlacementFixture f;
|
||||
|
||||
const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0),
|
||||
Rotation::East, 0).value();
|
||||
|
||||
// Body at (0,0),(1,0),(0,1). The box is unsnapped and lies wholly within cell
|
||||
// (1,0) without filling it, which is enough: a building is covered when the box
|
||||
// overlaps any of its body cells (REQ-UI-MULTI-SELECT, Coverage).
|
||||
const std::vector<BuildingId> grazed =
|
||||
buildingsInBox(f.state, QRectF(1.6, 0.4, 0.2, 0.2));
|
||||
REQUIRE(grazed.size() == 1);
|
||||
REQUIRE(grazed.front() == id);
|
||||
|
||||
// (1,1) is the output-port tile, not a body cell, so a box inside it covers
|
||||
// nothing even though it is surrounded by the miner's cells.
|
||||
REQUIRE(buildingsInBox(f.state, QRectF(1.2, 1.2, 0.5, 0.5)).empty());
|
||||
}
|
||||
|
||||
// -- World-bounds rejection (REQ-BLD-PLACE-VALID) ---------------------------
|
||||
|
||||
TEST_CASE("BuildingSystem: place rejects a building above the world (y < 0)", "[building]")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "catch.hpp"
|
||||
|
||||
#include <QRectF>
|
||||
#include <QSize>
|
||||
#include <QVector2D>
|
||||
|
||||
@@ -210,17 +211,16 @@ TEST_CASE("entityAtWorldPos never returns debris", "[debris]")
|
||||
REQUIRE((entityAtWorldPos(admin, QVector2D(3.0f, 4.0f)) == entt::null));
|
||||
}
|
||||
|
||||
TEST_CASE("debrisInBox returns exactly the debris inside the tile rectangle", "[debris]")
|
||||
TEST_CASE("debrisInBox returns exactly the debris the box encloses", "[debris]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
DebrisSystem ss(admin);
|
||||
|
||||
const entt::entity inA = ss.spawn(QVector2D(1.2f, 2.7f), 1, 100); // tile (1,2)
|
||||
const entt::entity inB = ss.spawn(QVector2D(4.9f, 5.1f), 1, 100); // tile (4,5)
|
||||
const entt::entity inA = ss.spawn(QVector2D(1.2f, 2.7f), 1, 100);
|
||||
const entt::entity inB = ss.spawn(QVector2D(4.9f, 5.1f), 1, 100);
|
||||
const entt::entity outX = ss.spawn(QVector2D(10.0f, 10.0f), 1, 100);
|
||||
|
||||
// Box given in reversed corner order to confirm normalization.
|
||||
const std::vector<entt::entity> hit = debrisInBox(admin, QPoint(5, 5), QPoint(0, 0));
|
||||
const std::vector<entt::entity> hit = debrisInBox(admin, QRectF(0.0, 0.0, 6.0, 6.0));
|
||||
|
||||
REQUIRE(hit.size() == 2);
|
||||
REQUIRE(contains(hit, inA));
|
||||
@@ -228,6 +228,23 @@ TEST_CASE("debrisInBox returns exactly the debris inside the tile rectangle", "[
|
||||
REQUIRE_FALSE(contains(hit, outX));
|
||||
}
|
||||
|
||||
TEST_CASE("debrisInBox cuts within a tile, not along the tile grid", "[debris]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
DebrisSystem ss(admin);
|
||||
|
||||
const entt::entity inTile = ss.spawn(QVector2D(1.8f, 2.5f), 1, 100);
|
||||
// Same tile (1,2) as the piece above, but on the far side of the box's left edge:
|
||||
// a tile-snapped box would take both (REQ-UI-MULTI-SELECT, Coverage).
|
||||
const entt::entity outTile = ss.spawn(QVector2D(1.2f, 2.5f), 1, 100);
|
||||
|
||||
const std::vector<entt::entity> hit = debrisInBox(admin, QRectF(1.5, 2.0, 4.0, 4.0));
|
||||
|
||||
REQUIRE(hit.size() == 1);
|
||||
REQUIRE(contains(hit, inTile));
|
||||
REQUIRE_FALSE(contains(hit, outTile));
|
||||
}
|
||||
|
||||
TEST_CASE("actorsInBox returns living ships and stations, excluding debris and dead actors",
|
||||
"[actor]")
|
||||
{
|
||||
@@ -236,10 +253,10 @@ TEST_CASE("actorsInBox returns living ships and stations, excluding debris and d
|
||||
// Two living ships inside the box: one player, one enemy.
|
||||
const entt::entity playerShip = admin.spawnShip(
|
||||
QVector2D(1.5f, 2.5f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
|
||||
"fighter", false); // tile (1,2)
|
||||
"fighter", false);
|
||||
const entt::entity enemyShip = admin.spawnShip(
|
||||
QVector2D(4.2f, 5.8f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
|
||||
"raider", true); // tile (4,5)
|
||||
"raider", true);
|
||||
|
||||
// A dead ship inside the box is excluded.
|
||||
const entt::entity deadShip = admin.spawnShip(
|
||||
@@ -251,7 +268,7 @@ TEST_CASE("actorsInBox returns living ships and stations, excluding debris and d
|
||||
QVector2D(20.0f, 20.0f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
|
||||
"fighter", false);
|
||||
|
||||
// A station is included when any body cell lies inside the box.
|
||||
// A station is included when the box overlaps any body cell.
|
||||
const std::vector<QPoint> stationCells{ QPoint(2, 2), QPoint(3, 2) };
|
||||
const entt::entity station = admin.spawnStation(
|
||||
QPoint(2, 2), QSize(2, 1), stationCells, 200.0f, 200.0f, true);
|
||||
@@ -260,7 +277,7 @@ TEST_CASE("actorsInBox returns living ships and stations, excluding debris and d
|
||||
admin.spawnDebris(QVector2D(1.0f, 1.0f), 5, Tick(1000));
|
||||
admin.spawnHqProxy(QVector2D(0.5f, 0.5f), 500.0f, 500.0f);
|
||||
|
||||
const std::vector<entt::entity> hit = actorsInBox(admin, QPoint(5, 5), QPoint(0, 0));
|
||||
const std::vector<entt::entity> hit = actorsInBox(admin, QRectF(0.0, 0.0, 6.0, 6.0));
|
||||
|
||||
REQUIRE(hit.size() == 3);
|
||||
REQUIRE(contains(hit, playerShip));
|
||||
@@ -269,3 +286,32 @@ TEST_CASE("actorsInBox returns living ships and stations, excluding debris and d
|
||||
REQUIRE_FALSE(contains(hit, deadShip));
|
||||
REQUIRE_FALSE(contains(hit, outsideShip));
|
||||
}
|
||||
|
||||
TEST_CASE("actorsInBox takes a ship by its position and a station by its footprint",
|
||||
"[actor]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
|
||||
// Ship inside the tile the box only reaches into: taken, because the box contains
|
||||
// its position (REQ-UI-MULTI-SELECT, Coverage).
|
||||
const entt::entity shipInside = admin.spawnShip(
|
||||
QVector2D(3.9f, 3.9f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
|
||||
"fighter", false);
|
||||
// Same tile (3,3), outside the box: a tile-snapped box would take it too.
|
||||
const entt::entity shipOutside = admin.spawnShip(
|
||||
QVector2D(3.1f, 3.1f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
|
||||
"fighter", false);
|
||||
|
||||
// The box reaches 0.5 tiles into the station's only cell, which is enough: a
|
||||
// station is covered when the box overlaps its footprint.
|
||||
const std::vector<QPoint> stationCells{ QPoint(4, 3) };
|
||||
const entt::entity station = admin.spawnStation(
|
||||
QPoint(4, 3), QSize(1, 1), stationCells, 200.0f, 200.0f, true);
|
||||
|
||||
const std::vector<entt::entity> hit = actorsInBox(admin, QRectF(3.5, 3.5, 1.0, 1.0));
|
||||
|
||||
REQUIRE(hit.size() == 2);
|
||||
REQUIRE(contains(hit, shipInside));
|
||||
REQUIRE(contains(hit, station));
|
||||
REQUIRE_FALSE(contains(hit, shipOutside));
|
||||
}
|
||||
|
||||
@@ -295,16 +295,11 @@ void GameWorldView::onFrame()
|
||||
const bool viewMoved =
|
||||
m_camera.advance(m_panDirection, elapsed, getScrollBounds());
|
||||
|
||||
// While the view scrolls, the tile under a stationary cursor changes,
|
||||
// so refresh the box-select rectangle even though no mouse move fires.
|
||||
// While the view scrolls, the world position under a stationary cursor
|
||||
// changes, so refresh the box even though no mouse move fires.
|
||||
if (m_boxSelecting && viewMoved)
|
||||
{
|
||||
m_boxCurrentTile =
|
||||
getCoordinates().widgetToTile(mapFromGlobal(QCursor::pos()));
|
||||
// The cursor has not travelled a pixel, so the drag threshold above never
|
||||
// trips; but the scroll has grown the box past the tile it started on,
|
||||
// which has to become visible.
|
||||
if (m_boxCurrentTile != m_boxStartTile) { m_boxDragMoved = true; }
|
||||
updateBoxDrag(mapFromGlobal(QCursor::pos()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,9 +420,14 @@ void GameWorldView::paintGL()
|
||||
|
||||
WorldRenderFrame GameWorldView::makeRenderFrame() const
|
||||
{
|
||||
return WorldRenderFrame{m_selection, m_buildMode, m_activeBeams,
|
||||
m_boxSelecting, m_boxDragMoved, m_boxStartTile,
|
||||
m_boxCurrentTile, m_debugDraw};
|
||||
// A box only reaches the renderer once the gesture reads as a drag: below the
|
||||
// threshold there is nothing to draw and nothing the box marks that hovering does
|
||||
// not mark already (REQ-UI-MULTI-SELECT).
|
||||
std::optional<QRectF> boxWorldRect;
|
||||
if (m_boxSelecting && m_boxDragMoved) { boxWorldRect = getBoxWorldRect(); }
|
||||
|
||||
return WorldRenderFrame{m_selection, m_buildMode, m_activeBeams, boxWorldRect,
|
||||
m_debugDraw};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1227,11 +1227,10 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
case ControlAction::ToggleDeconstruct:
|
||||
// Start a deconstruct box drag; a plain click resolves as a 1x1 box on
|
||||
// release (REQ-BLD-DECONSTRUCT-CLICK, REQ-BLD-DECONSTRUCT-BOX).
|
||||
m_boxSelecting = true;
|
||||
m_boxStartTile = tile;
|
||||
m_boxCurrentTile = tile;
|
||||
m_boxStartPos = event->pos();
|
||||
m_boxDragMoved = false;
|
||||
m_boxSelecting = true;
|
||||
m_boxStartWorld = coordinates.widgetToWorld(event->pos());
|
||||
m_boxCurrentWorld = m_boxStartWorld;
|
||||
m_boxDragMoved = false;
|
||||
break;
|
||||
|
||||
case ControlAction::Select:
|
||||
@@ -1244,9 +1243,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
// selectAtPoint has already cleared the selection unless Ctrl is
|
||||
// preserving it for an additive drag.
|
||||
m_boxSelecting = true;
|
||||
m_boxStartTile = tile;
|
||||
m_boxCurrentTile = tile;
|
||||
m_boxStartPos = event->pos();
|
||||
m_boxStartWorld = coordinates.widgetToWorld(event->pos());
|
||||
m_boxCurrentWorld = m_boxStartWorld;
|
||||
m_boxDragMoved = false;
|
||||
}
|
||||
break;
|
||||
@@ -1300,8 +1298,10 @@ void GameWorldView::selectInBox(bool additive)
|
||||
// mode: a Ctrl box adds and never deselects, where a Ctrl click toggles.
|
||||
const SelectionMode mode = additive ? SelectionMode::Add : SelectionMode::Replace;
|
||||
|
||||
const QRectF worldBox = getBoxWorldRect();
|
||||
|
||||
const std::vector<BuildingId> boxIds =
|
||||
buildingsInBox(m_sim->getFactoryState(), m_boxStartTile, m_boxCurrentTile);
|
||||
buildingsInBox(m_sim->getFactoryState(), worldBox);
|
||||
if (!boxIds.empty())
|
||||
{
|
||||
publishSelectionAnchor(mode, boxIds, {}, {});
|
||||
@@ -1309,10 +1309,8 @@ void GameWorldView::selectInBox(bool additive)
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<entt::entity> boxActors =
|
||||
actorsInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
|
||||
const std::vector<entt::entity> boxDebris =
|
||||
debrisInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
|
||||
const std::vector<entt::entity> boxActors = actorsInBox(m_sim->getAdmin(), worldBox);
|
||||
const std::vector<entt::entity> boxDebris = debrisInBox(m_sim->getAdmin(), worldBox);
|
||||
if (!boxActors.empty() || !boxDebris.empty())
|
||||
{
|
||||
publishSelectionAnchor(mode, {}, boxActors, boxDebris);
|
||||
@@ -1324,6 +1322,35 @@ void GameWorldView::selectInBox(bool additive)
|
||||
if (!additive) { m_selection.clearAll(); }
|
||||
}
|
||||
|
||||
void GameWorldView::updateBoxDrag(QPoint cursorWidgetPos)
|
||||
{
|
||||
const WorldCoordinates coordinates = getCoordinates();
|
||||
m_boxCurrentWorld = coordinates.widgetToWorld(cursorWidgetPos);
|
||||
|
||||
// Measured against where the anchor sits on screen right now, not against where
|
||||
// the button went down: a view that scrolls under a held button moves the anchor
|
||||
// away from a motionless cursor, and that is a drag as much as moving the mouse
|
||||
// is (REQ-UI-MULTI-SELECT).
|
||||
const QPointF anchorWidgetPos = coordinates.worldToWidget(m_boxStartWorld);
|
||||
const qreal travel_px = std::abs(cursorWidgetPos.x() - anchorWidgetPos.x())
|
||||
+ std::abs(cursorWidgetPos.y() - anchorWidgetPos.y());
|
||||
if (travel_px >= kBoxDragThresholdPixels) { m_boxDragMoved = true; }
|
||||
}
|
||||
|
||||
QRectF GameWorldView::getBoxWorldRect() const
|
||||
{
|
||||
if (!m_boxDragMoved)
|
||||
{
|
||||
// Still a click: the rectangle it spans has no area and would cover nothing,
|
||||
// so the box is the whole tile the button went down on instead — what the
|
||||
// click points at (REQ-UI-MULTI-SELECT, REQ-BLD-DECONSTRUCT-CLICK).
|
||||
return QRectF(std::floor(m_boxStartWorld.x()), std::floor(m_boxStartWorld.y()),
|
||||
1.0, 1.0);
|
||||
}
|
||||
return QRectF(QPointF(m_boxStartWorld.x(), m_boxStartWorld.y()),
|
||||
QPointF(m_boxCurrentWorld.x(), m_boxCurrentWorld.y())).normalized();
|
||||
}
|
||||
|
||||
void GameWorldView::publishSelectionAnchor(SelectionMode mode,
|
||||
const std::vector<BuildingId>& buildings,
|
||||
const std::vector<entt::entity>& actors,
|
||||
@@ -1359,14 +1386,6 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
||||
const QPoint tile = coordinates.widgetToTile(event->pos());
|
||||
m_cursorWorldPos = coordinates.widgetToWorld(event->pos());
|
||||
|
||||
// A press is a drag once the cursor has travelled far enough from it; below that
|
||||
// it stays a click and shows no rectangle (REQ-UI-MULTI-SELECT).
|
||||
if (m_boxSelecting
|
||||
&& (event->pos() - m_boxStartPos).manhattanLength() >= kBoxDragThresholdPixels)
|
||||
{
|
||||
m_boxDragMoved = true;
|
||||
}
|
||||
|
||||
if (m_buildMode.isBuilderMode())
|
||||
{
|
||||
m_buildMode.setGhostTile(tile);
|
||||
@@ -1410,11 +1429,11 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
||||
else if (m_buildMode.isDeconstructMode())
|
||||
{
|
||||
m_buildMode.setDeconstructHoverBuildingId(buildingAtTile(tile));
|
||||
if (m_boxSelecting) { m_boxCurrentTile = tile; }
|
||||
if (m_boxSelecting) { updateBoxDrag(event->pos()); }
|
||||
}
|
||||
else if (m_boxSelecting)
|
||||
{
|
||||
m_boxCurrentTile = tile;
|
||||
updateBoxDrag(event->pos());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1435,7 +1454,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
||||
m_boxSelecting = false;
|
||||
|
||||
const std::vector<BuildingId> boxIds =
|
||||
buildingsInBox(m_sim->getFactoryState(), m_boxStartTile, m_boxCurrentTile);
|
||||
buildingsInBox(m_sim->getFactoryState(), getBoxWorldRect());
|
||||
|
||||
const bool controlHeld = (event->modifiers() & Qt::ControlModifier) != 0;
|
||||
const ControlAction dragAction =
|
||||
|
||||
@@ -226,6 +226,17 @@ private:
|
||||
// which is the only case that goes on to start a box drag.
|
||||
bool selectAtPoint(QPoint tile, QVector2D worldPos, bool additive);
|
||||
void selectInBox(bool additive);
|
||||
// Moves the running box drag's far corner to the world position under
|
||||
// `cursorWidgetPos` and, once the cursor sits far enough from where the anchor is
|
||||
// drawn, promotes the gesture from a click to a drag. Every corner update goes
|
||||
// through here, including the ones a scrolling view causes under a cursor that
|
||||
// has not moved (REQ-UI-MULTI-SELECT).
|
||||
void updateBoxDrag(QPoint cursorWidgetPos);
|
||||
// The box the drag currently spans, in world coordinates and normalized: the
|
||||
// rectangle between its two corners once it reads as a drag, and the whole tile
|
||||
// the button went down on before that (REQ-UI-MULTI-SELECT). Both what is drawn
|
||||
// and what is selected come from here, so they can never disagree.
|
||||
QRectF getBoxWorldRect() const;
|
||||
// Publishes where on the screen the selection about to be made sits, so the
|
||||
// selection panel can be placed beside it (REQ-UI-SELECTION-PANEL). Called with
|
||||
// what is about to be selected, immediately before selecting it, and publishes
|
||||
@@ -313,16 +324,16 @@ private:
|
||||
// Not owned; set after construction, so null until MainWindow has built it.
|
||||
const BlueprintLibrary* m_blueprintLibrary = nullptr;
|
||||
bool m_boxSelecting;
|
||||
QPoint m_boxStartTile;
|
||||
QPoint m_boxCurrentTile;
|
||||
// Where the button went down, in widget pixels; the origin the drag threshold
|
||||
// below measures from. Pixels, not tiles: the threshold separates a click from a
|
||||
// drag, which is a hand-steadiness question and not a tile-sized one.
|
||||
QPoint m_boxStartPos;
|
||||
// Whether the cursor has moved far enough from m_boxStartPos for this to read as
|
||||
// a drag. Until it has, the rectangle is not drawn (REQ-UI-MULTI-SELECT): a plain
|
||||
// click would otherwise flash a one-tile rectangle. Sticky for the rest of the
|
||||
// drag, so coming back to the press position does not hide the rectangle again.
|
||||
// The drag's two corners in world coordinates, unsnapped: where the button went
|
||||
// down and where the cursor is now (REQ-UI-MULTI-SELECT). World rather than
|
||||
// widget coordinates so the anchor keeps the spot in the world it was placed on
|
||||
// when the view scrolls under a held button.
|
||||
QVector2D m_boxStartWorld;
|
||||
QVector2D m_boxCurrentWorld;
|
||||
// Whether the cursor has moved far enough from the anchor for this to read as a
|
||||
// drag. Until it has, the rectangle is not drawn and the box resolves as the
|
||||
// whole anchor tile (REQ-UI-MULTI-SELECT). Sticky for the rest of the drag, so
|
||||
// coming back to the press position does not hide the rectangle again.
|
||||
bool m_boxDragMoved;
|
||||
|
||||
// Interprets this widget's key events into semantic actions and publishes them
|
||||
|
||||
@@ -984,9 +984,9 @@ void WorldRenderer::drawOverlays(QPainter& painter, const WorldCoordinates& coor
|
||||
|
||||
// Deconstruct tint: while dragging a deconstruct box, tint every covered
|
||||
// building/site (REQ-BLD-DECONSTRUCT-BOX); otherwise tint the hovered one.
|
||||
if (frame.buildMode.isDeconstructMode() && frame.isBoxSelecting)
|
||||
if (frame.buildMode.isDeconstructMode() && frame.boxWorldRect.has_value())
|
||||
{
|
||||
for (BuildingId id : buildingsInBox(m_sim.getFactoryState(), frame.boxStartTile, frame.boxCurrentTile))
|
||||
for (BuildingId id : buildingsInBox(m_sim.getFactoryState(), *frame.boxWorldRect))
|
||||
{
|
||||
const Building* b = findBuilding(m_sim.getFactoryState(), id);
|
||||
if (b && b->type == BuildingType::Hq) { continue; }
|
||||
@@ -1019,16 +1019,14 @@ void WorldRenderer::drawOverlays(QPainter& painter, const WorldCoordinates& coor
|
||||
}
|
||||
}
|
||||
|
||||
// Box-select rectangle. Not drawn until the drag has moved far enough to read as
|
||||
// one, so a plain click does not flash a rectangle (REQ-UI-MULTI-SELECT).
|
||||
if (frame.isBoxSelecting && frame.isBoxDragMoved)
|
||||
// Box-select rectangle, drawn from the world rectangle itself and unsnapped, so
|
||||
// the outline sits where the mouse went rather than on the tile grid
|
||||
// (REQ-UI-MULTI-SELECT).
|
||||
if (frame.boxWorldRect.has_value())
|
||||
{
|
||||
const QPoint tl(std::min(frame.boxStartTile.x(), frame.boxCurrentTile.x()),
|
||||
std::min(frame.boxStartTile.y(), frame.boxCurrentTile.y()));
|
||||
const QPoint br(std::max(frame.boxStartTile.x(), frame.boxCurrentTile.x()) + 1,
|
||||
std::max(frame.boxStartTile.y(), frame.boxCurrentTile.y()) + 1);
|
||||
const QRectF selRect(coordinates.tileToWidget(tl),
|
||||
coordinates.tileToWidget(br));
|
||||
const QRectF selRect(
|
||||
coordinates.worldToWidget(QVector2D(frame.boxWorldRect->topLeft())),
|
||||
coordinates.worldToWidget(QVector2D(frame.boxWorldRect->bottomRight())));
|
||||
// In deconstruct mode the box marks buildings for demolition, so it is
|
||||
// drawn in the deconstruct red instead of the selection color; the
|
||||
// tint's alpha governs only the fills it tints, never this outline
|
||||
|
||||
@@ -54,12 +54,11 @@ struct WorldRenderFrame
|
||||
const SelectionController& selection;
|
||||
const BuildModeController& buildMode;
|
||||
const std::vector<ActiveBeam>& beams;
|
||||
bool isBoxSelecting;
|
||||
// Whether that box drag has passed the movement threshold that tells it apart
|
||||
// from a click; until it has, the rectangle is not drawn (REQ-UI-MULTI-SELECT).
|
||||
bool isBoxDragMoved;
|
||||
QPoint boxStartTile;
|
||||
QPoint boxCurrentTile;
|
||||
// The box being dragged, in world coordinates and normalized, or nullopt when no
|
||||
// drag is in progress — a press that has not passed the movement threshold is
|
||||
// still a click and offers none (REQ-UI-MULTI-SELECT). It is the same rectangle
|
||||
// the view selects by, so what is drawn and what is selected cannot disagree.
|
||||
std::optional<QRectF> boxWorldRect;
|
||||
bool isDebugDrawEnabled;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user