make selection box sub-tile aware
This commit is contained in:
@@ -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