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

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