make selection box sub-tile aware

This commit is contained in:
2026-08-14 22:44:18 +02:00
parent 1cf7c264c1
commit a1c567715e
13 changed files with 232 additions and 125 deletions

View File

@@ -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

View 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()));
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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.