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

@@ -591,7 +591,9 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
- REQ-UI-RECIPE-SUMMARY: Below the recipe/schematic selection control, a building running a recipe or schematic shows a one-line **recipe summary**: each input item's icon on its colored square (REQ-UI-ITEM-ICON) with its per-cycle amount and the inputs separated by `+`, an arrow, each output item's icon on its colored square with its per-cycle amount, and the cycle time in seconds. It restates what the building will do without opening the selection dialog, and it is the panel's only display of the cycle time. For a Shipyard the summary is built from the schematic's materials and production time including the placed modules' contributions (REQ-BLD-SHIPYARD, REQ-MOD-STAT-CALC), matching the buffers beneath it. A building with no recipe or schematic selected shows no summary — including an auto-recipe building that has yet to select one (REQ-BLD-AUTO-RECIPE), which shows none until it does and keeps it from then on, so the card does not resize in step with the building's status (REQ-UI-SELECTION-STATUS, REQ-UI-SELECTION-PANEL). - REQ-UI-RECIPE-SUMMARY: Below the recipe/schematic selection control, a building running a recipe or schematic shows a one-line **recipe summary**: each input item's icon on its colored square (REQ-UI-ITEM-ICON) with its per-cycle amount and the inputs separated by `+`, an arrow, each output item's icon on its colored square with its per-cycle amount, and the cycle time in seconds. It restates what the building will do without opening the selection dialog, and it is the panel's only display of the cycle time. For a Shipyard the summary is built from the schematic's materials and production time including the placed modules' contributions (REQ-BLD-SHIPYARD, REQ-MOD-STAT-CALC), matching the buffers beneath it. A building with no recipe or schematic selected shows no summary — including an auto-recipe building that has yet to select one (REQ-BLD-AUTO-RECIPE), which shows none until it does and keeps it from then on, so the card does not resize in step with the building's status (REQ-UI-SELECTION-STATUS, REQ-UI-SELECTION-PANEL).
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the panel's runtime group shows a captioned **production section** between the input and output buffer sections (REQ-UI-SINGLE-SELECTION): a horizontal progress bar filled to the completion of the active production cycle, with that completion beside the caption as an integer percentage (e.g. `72%`), or the text `idle` in place of the percentage and an empty bar when no production cycle is active. The cycle time is shown in the recipe summary (REQ-UI-RECIPE-SUMMARY) rather than repeated here. When no recipe or schematic is selected, the production section is not shown at all. - REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the panel's runtime group shows a captioned **production section** between the input and output buffer sections (REQ-UI-SINGLE-SELECTION): a horizontal progress bar filled to the completion of the active production cycle, with that completion beside the caption as an integer percentage (e.g. `72%`), or the text `idle` in place of the percentage and an empty bar when no production cycle is active. The cycle time is shown in the recipe summary (REQ-UI-RECIPE-SUMMARY) rather than repeated here. When no recipe or schematic is selected, the production section is not shown at all.
- REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or debris selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT). - REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or debris selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
- **When the rectangle appears.** The rectangle is drawn only once the cursor has moved at least **2 pixels** from the position the button went down at — a press alone draws nothing, so a plain click does not flash a one-tile rectangle. The threshold is in screen pixels rather than tiles because it only separates a click from a drag, which is a question of hand steadiness: a drag that stays inside the starting tile still shows its rectangle. Once shown, the rectangle stays shown for the rest of the drag, including when the cursor comes back to where it started. Scrolling the view while the button is held (REQ-UI-SCROLL) moves no cursor but does grow the box, so it too shows the rectangle as soon as the box covers more than the starting tile. This governs the drawing only: what the gesture selects (or, in deconstruct mode, marks) on release is unchanged, and a click that never passes the threshold still resolves as a one-tile box. - **Rectangle geometry.** The box is **not** snapped to tiles: its two corners are the exact world positions the button went down at and the cursor is at now, so the rectangle is drawn where the mouse actually went and follows it pixel by pixel. The corners are held in world coordinates rather than screen ones, so the anchor stays on the spot in the world it was placed on when the view scrolls under a held button (REQ-UI-SCROLL).
- **Coverage.** What the box covers follows the same rectangle, not the tiles it touches. An object that occupies whole tiles — a building, a construction site, a defence station — is covered when the rectangle overlaps any of its body cells, so grazing a building's tile selects it. An object that has a position rather than a footprint — a ship, a piece of debris — is covered when the rectangle contains its centre, so what the player sees enclosed by the rectangle is exactly what the release selects. This is also what makes box and click agree: a click already hit-tests ships and debris against their world positions (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-CLICK-SELECT), not against the tile they stand on.
- **When the rectangle appears.** The rectangle is drawn only once the cursor has moved at least **2 pixels** from the position the button went down at — a press alone draws nothing, so a plain click does not flash a rectangle. The threshold is in screen pixels because it only separates a click from a drag, which is a question of hand steadiness. Once shown, the rectangle stays shown for the rest of the drag, including when the cursor comes back to where it started. It is measured against where the anchor sits on screen at that moment, so scrolling the view while the button is held moves no cursor but still crosses the threshold, the box having grown all the same. Below the threshold the gesture is a click, and the box it resolves on release is the **whole tile** the button went down on — that is what makes a click select or mark what it points at (REQ-UI-SELECTION-CATEGORIES, REQ-BLD-DECONSTRUCT-CLICK) rather than the empty rectangle a motionless cursor spans.
- **Rectangle color.** While dragging, the selection rectangle is drawn as an outline in `visuals.toml [overlays].selected_outline` — the same color and config entry as the outline drawn in the world around the objects that end up selected, so the box and the selection it produces read as one thing. **Exception:** while deconstruct mode is active (REQ-UI-DECONSTRUCT-BUTTON, REQ-UI-HOTKEYS), the box drag marks buildings for demolition instead (REQ-BLD-DECONSTRUCT-BOX) and the rectangle is drawn in the deconstruct color — the RGB of `visuals.toml [overlays].deconstruct_tint`, drawn **fully opaque**. That entry's alpha channel governs only the fills it tints (the deconstruct-mode hover tint and queued buildings, REQ-UI-DECONSTRUCT-BORDER, REQ-BLD-DECON-QUEUE) and is not applied to this outline, which would otherwise be too faint to see. The rectangle's geometry is the same in both modes. - **Rectangle color.** While dragging, the selection rectangle is drawn as an outline in `visuals.toml [overlays].selected_outline` — the same color and config entry as the outline drawn in the world around the objects that end up selected, so the box and the selection it produces read as one thing. **Exception:** while deconstruct mode is active (REQ-UI-DECONSTRUCT-BUTTON, REQ-UI-HOTKEYS), the box drag marks buildings for demolition instead (REQ-BLD-DECONSTRUCT-BOX) and the rectangle is drawn in the deconstruct color — the RGB of `visuals.toml [overlays].deconstruct_tint`, drawn **fully opaque**. That entry's alpha channel governs only the fills it tints (the deconstruct-mode hover tint and queued buildings, REQ-UI-DECONSTRUCT-BORDER, REQ-BLD-DECON-QUEUE) and is not applied to this outline, which would otherwise be too faint to see. The rectangle's geometry is the same in both modes.
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected and the selection does not aggregate (REQ-UI-SELECTION-AGGREGATE), the panel shows a count summary. Its header names the size of the selection as `<n> buildings` in place of an object name, and carries no symbol and nothing in its right slot. Below it is one row per selected building type — the type's symbol, its name, and the number selected as `x<count>` — one type per row, and no per-building detail. A final row shows the **total building block cost** of the selection, captioned `Total cost` with the value followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON): the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button bar); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-CARD). Construction sites count at their building type's full placement cost regardless of construction progress. - REQ-UI-MULTI-SELECTION: When multiple buildings are selected and the selection does not aggregate (REQ-UI-SELECTION-AGGREGATE), the panel shows a count summary. Its header names the size of the selection as `<n> buildings` in place of an object name, and carries no symbol and nothing in its right slot. Below it is one row per selected building type — the type's symbol, its name, and the number selected as `x<count>` — one type per row, and no per-building detail. A final row shows the **total building block cost** of the selection, captioned `Total cost` with the value followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON): the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button bar); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-CARD). Construction sites count at their building type's full placement cost regardless of construction progress.
- REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel, in its configuration group (REQ-UI-SELECTION-CARD). Recipe selection (miner, assembler, smelter, reprocessing plant) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW). - REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel, in its configuration group (REQ-UI-SELECTION-CARD). Recipe selection (miner, assembler, smelter, reprocessing plant) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW).

View File

@@ -17,6 +17,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h ${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h ${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanelPlacement.h ${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanelPlacement.h
${CMAKE_CURRENT_SOURCE_DIR}/SelectionBox.h
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h ${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.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 <cmath>
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "SelectionBox.h"
#include "PositionComponent.h" #include "PositionComponent.h"
#include "DebrisComponent.h" #include "DebrisComponent.h"
#include "ShipIdentityComponent.h" #include "ShipIdentityComponent.h"
@@ -82,45 +83,29 @@ entt::entity debrisAtWorldPos(EntityAdmin& admin, QVector2D worldPos)
return bestDebris; 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; std::vector<entt::entity> result;
admin.forEach<DebrisComponent, PositionComponent>( admin.forEach<DebrisComponent, PositionComponent>(
[&](entt::entity entity, const DebrisComponent& /*sd*/, const PositionComponent& pos) [&](entt::entity entity, const DebrisComponent& /*sd*/, const PositionComponent& pos)
{ {
const int tileX = static_cast<int>(std::floor(pos.value.x())); if (boxCoversPoint(worldBox, pos.value)) { result.push_back(entity); }
const int tileY = static_cast<int>(std::floor(pos.value.y()));
if (tileX >= minX && tileX <= maxX && tileY >= minY && tileY <= maxY)
{
result.push_back(entity);
}
}); });
return result; 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; 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>( admin.forEach<StationBodyComponent, HealthComponent>(
[&](entt::entity entity, const StationBodyComponent& sb, const HealthComponent& h) [&](entt::entity entity, const StationBodyComponent& sb, const HealthComponent& h)
{ {
if (h.hp <= 0.0f) { return; } if (h.hp <= 0.0f) { return; }
for (const QPoint& cell : sb.bodyCells) for (const QPoint& cell : sb.bodyCells)
{ {
if (cell.x() >= minX && cell.x() <= maxX if (boxCoversTile(worldBox, cell))
&& cell.y() >= minY && cell.y() <= maxY)
{ {
result.push_back(entity); result.push_back(entity);
return; 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 // Ships have a position rather than a footprint: included when the box contains
// ShipIdentityComponent excludes the HQ proxy and any station bodies. // that position. Requiring ShipIdentityComponent excludes the HQ proxy and any
// station bodies.
admin.forEach<ShipIdentityComponent, PositionComponent, HealthComponent>( admin.forEach<ShipIdentityComponent, PositionComponent, HealthComponent>(
[&](entt::entity entity, const ShipIdentityComponent& /*id*/, [&](entt::entity entity, const ShipIdentityComponent& /*id*/,
const PositionComponent& pos, const HealthComponent& h) const PositionComponent& pos, const HealthComponent& h)
{ {
if (h.hp <= 0.0f) { return; } if (h.hp <= 0.0f) { return; }
const int tileX = static_cast<int>(std::floor(pos.value.x())); if (boxCoversPoint(worldBox, pos.value)) { result.push_back(entity); }
const int tileY = static_cast<int>(std::floor(pos.value.y()));
if (tileX >= minX && tileX <= maxX && tileY >= minY && tileY <= maxY)
{
result.push_back(entity);
}
}); });
return result; return result;

View File

@@ -3,6 +3,7 @@
#include <vector> #include <vector>
#include <QPoint> #include <QPoint>
#include <QRectF>
#include <QVector2D> #include <QVector2D>
#include "entt/entity/entity.hpp" #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). // after actors: entityAtWorldPos never returns debris (debris has no HealthComponent).
entt::entity debrisAtWorldPos(EntityAdmin& admin, QVector2D worldPos); entt::entity debrisAtWorldPos(EntityAdmin& admin, QVector2D worldPos);
// Returns every piece of debris whose position falls within the inclusive tile rectangle // Returns every piece of debris the selection box covers — that is, whose position it
// spanned by tileA and tileB, in any corner order (REQ-UI-DEBRIS-MULTI-SELECT). // contains, per boxCoversPoint (REQ-UI-DEBRIS-MULTI-SELECT). `worldBox` is in world
std::vector<entt::entity> debrisInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB); // 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 // Returns every living actor (ship or defence station, player or enemy) the selection
// within the inclusive tile rectangle spanned by tileA and tileB, in any corner order // box covers (REQ-UI-MULTI-SELECT, REQ-UI-ENTITY-CLICK-SELECT): a ship when the box
// (REQ-UI-MULTI-SELECT, REQ-UI-ENTITY-CLICK-SELECT). A ship is included when its floored // contains its position, a station when the box overlaps any of its body cells — the
// position tile lies in the box; a station is included when any of its body cells does. // two rules of SelectionBox.h. Dead actors (hp <= 0) and the HQ proxy are excluded.
// Dead actors (hp <= 0) and the HQ proxy are excluded. std::vector<entt::entity> actorsInBox(EntityAdmin& admin, const QRectF& worldBox);
std::vector<entt::entity> actorsInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB);

View File

@@ -5,6 +5,7 @@
#include "PortGeometry.h" #include "PortGeometry.h"
#include "ProductionRules.h" #include "ProductionRules.h"
#include "SelectionBox.h"
#include "SurfaceMask.h" #include "SurfaceMask.h"
#include "Item.h" #include "Item.h"
@@ -185,22 +186,13 @@ getSiteSplitterInfo(const FactoryState& state, const GameConfig& config, Buildin
std::vector<BuildingId> buildingsInBox(const FactoryState& state, 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) const auto covers = [&](const std::vector<QPoint>& bodyCells)
{ {
for (const QPoint& cell : bodyCells) for (const QPoint& cell : bodyCells)
{ {
if (cell.x() >= x0 && cell.x() <= x1 if (boxCoversTile(worldBox, cell)) { return true; }
&& cell.y() >= y0 && cell.y() <= y1)
{
return true;
}
} }
return false; return false;
}; };

View File

@@ -3,6 +3,7 @@
#include <vector> #include <vector>
#include <QPoint> #include <QPoint>
#include <QRectF>
#include <QVector2D> #include <QVector2D>
#include "Building.h" #include "Building.h"
@@ -71,11 +72,12 @@ std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(const FactoryState&
const GameConfig& config, const GameConfig& config,
BuildingId id); BuildingId id);
// Ids of all buildings and construction sites whose footprint intersects the tile // Ids of all buildings and construction sites the selection box covers — those with
// box spanned by the two (unordered) corner tiles (REQ-UI-MULTI-SELECT, // a body cell the box overlaps, per boxCoversTile (REQ-UI-MULTI-SELECT,
// REQ-BLD-DECONSTRUCT-BOX). // REQ-BLD-DECONSTRUCT-BOX). `worldBox` is in world coordinates and normalized; it is
// not snapped to tiles.
std::vector<BuildingId> buildingsInBox(const FactoryState& state, 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 // 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. // single-cell tile. Shared by the placement preview and the selection highlight.

View File

@@ -11,6 +11,7 @@
#include <vector> #include <vector>
#include <QPoint> #include <QPoint>
#include <QRectF>
#include "BeltSystem.h" #include "BeltSystem.h"
#include "Building.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))); 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) --------------------------- // -- World-bounds rejection (REQ-BLD-PLACE-VALID) ---------------------------
TEST_CASE("BuildingSystem: place rejects a building above the world (y < 0)", "[building]") TEST_CASE("BuildingSystem: place rejects a building above the world (y < 0)", "[building]")

View File

@@ -1,5 +1,6 @@
#include "catch.hpp" #include "catch.hpp"
#include <QRectF>
#include <QSize> #include <QSize>
#include <QVector2D> #include <QVector2D>
@@ -210,17 +211,16 @@ TEST_CASE("entityAtWorldPos never returns debris", "[debris]")
REQUIRE((entityAtWorldPos(admin, QVector2D(3.0f, 4.0f)) == entt::null)); 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; EntityAdmin admin;
DebrisSystem ss(admin); DebrisSystem ss(admin);
const entt::entity inA = ss.spawn(QVector2D(1.2f, 2.7f), 1, 100); // tile (1,2) 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); // tile (4,5) 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); 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, QRectF(0.0, 0.0, 6.0, 6.0));
const std::vector<entt::entity> hit = debrisInBox(admin, QPoint(5, 5), QPoint(0, 0));
REQUIRE(hit.size() == 2); REQUIRE(hit.size() == 2);
REQUIRE(contains(hit, inA)); REQUIRE(contains(hit, inA));
@@ -228,6 +228,23 @@ TEST_CASE("debrisInBox returns exactly the debris inside the tile rectangle", "[
REQUIRE_FALSE(contains(hit, outX)); 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", TEST_CASE("actorsInBox returns living ships and stations, excluding debris and dead actors",
"[actor]") "[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. // Two living ships inside the box: one player, one enemy.
const entt::entity playerShip = admin.spawnShip( 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, 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( 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, 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. // A dead ship inside the box is excluded.
const entt::entity deadShip = admin.spawnShip( 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, QVector2D(20.0f, 20.0f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
"fighter", false); "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 std::vector<QPoint> stationCells{ QPoint(2, 2), QPoint(3, 2) };
const entt::entity station = admin.spawnStation( const entt::entity station = admin.spawnStation(
QPoint(2, 2), QSize(2, 1), stationCells, 200.0f, 200.0f, true); 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.spawnDebris(QVector2D(1.0f, 1.0f), 5, Tick(1000));
admin.spawnHqProxy(QVector2D(0.5f, 0.5f), 500.0f, 500.0f); 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(hit.size() == 3);
REQUIRE(contains(hit, playerShip)); 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, deadShip));
REQUIRE_FALSE(contains(hit, outsideShip)); 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));
}

View File

@@ -295,16 +295,11 @@ void GameWorldView::onFrame()
const bool viewMoved = const bool viewMoved =
m_camera.advance(m_panDirection, elapsed, getScrollBounds()); m_camera.advance(m_panDirection, elapsed, getScrollBounds());
// While the view scrolls, the tile under a stationary cursor changes, // While the view scrolls, the world position under a stationary cursor
// so refresh the box-select rectangle even though no mouse move fires. // changes, so refresh the box even though no mouse move fires.
if (m_boxSelecting && viewMoved) if (m_boxSelecting && viewMoved)
{ {
m_boxCurrentTile = updateBoxDrag(mapFromGlobal(QCursor::pos()));
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; }
} }
} }
@@ -425,9 +420,14 @@ void GameWorldView::paintGL()
WorldRenderFrame GameWorldView::makeRenderFrame() const WorldRenderFrame GameWorldView::makeRenderFrame() const
{ {
return WorldRenderFrame{m_selection, m_buildMode, m_activeBeams, // A box only reaches the renderer once the gesture reads as a drag: below the
m_boxSelecting, m_boxDragMoved, m_boxStartTile, // threshold there is nothing to draw and nothing the box marks that hovering does
m_boxCurrentTile, m_debugDraw}; // 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};
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1228,9 +1228,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
// Start a deconstruct box drag; a plain click resolves as a 1x1 box on // Start a deconstruct box drag; a plain click resolves as a 1x1 box on
// release (REQ-BLD-DECONSTRUCT-CLICK, REQ-BLD-DECONSTRUCT-BOX). // release (REQ-BLD-DECONSTRUCT-CLICK, REQ-BLD-DECONSTRUCT-BOX).
m_boxSelecting = true; m_boxSelecting = true;
m_boxStartTile = tile; m_boxStartWorld = coordinates.widgetToWorld(event->pos());
m_boxCurrentTile = tile; m_boxCurrentWorld = m_boxStartWorld;
m_boxStartPos = event->pos();
m_boxDragMoved = false; m_boxDragMoved = false;
break; break;
@@ -1244,9 +1243,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
// selectAtPoint has already cleared the selection unless Ctrl is // selectAtPoint has already cleared the selection unless Ctrl is
// preserving it for an additive drag. // preserving it for an additive drag.
m_boxSelecting = true; m_boxSelecting = true;
m_boxStartTile = tile; m_boxStartWorld = coordinates.widgetToWorld(event->pos());
m_boxCurrentTile = tile; m_boxCurrentWorld = m_boxStartWorld;
m_boxStartPos = event->pos();
m_boxDragMoved = false; m_boxDragMoved = false;
} }
break; break;
@@ -1300,8 +1298,10 @@ void GameWorldView::selectInBox(bool additive)
// mode: a Ctrl box adds and never deselects, where a Ctrl click toggles. // mode: a Ctrl box adds and never deselects, where a Ctrl click toggles.
const SelectionMode mode = additive ? SelectionMode::Add : SelectionMode::Replace; const SelectionMode mode = additive ? SelectionMode::Add : SelectionMode::Replace;
const QRectF worldBox = getBoxWorldRect();
const std::vector<BuildingId> boxIds = const std::vector<BuildingId> boxIds =
buildingsInBox(m_sim->getFactoryState(), m_boxStartTile, m_boxCurrentTile); buildingsInBox(m_sim->getFactoryState(), worldBox);
if (!boxIds.empty()) if (!boxIds.empty())
{ {
publishSelectionAnchor(mode, boxIds, {}, {}); publishSelectionAnchor(mode, boxIds, {}, {});
@@ -1309,10 +1309,8 @@ void GameWorldView::selectInBox(bool additive)
return; return;
} }
const std::vector<entt::entity> boxActors = const std::vector<entt::entity> boxActors = actorsInBox(m_sim->getAdmin(), worldBox);
actorsInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile); const std::vector<entt::entity> boxDebris = debrisInBox(m_sim->getAdmin(), worldBox);
const std::vector<entt::entity> boxDebris =
debrisInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
if (!boxActors.empty() || !boxDebris.empty()) if (!boxActors.empty() || !boxDebris.empty())
{ {
publishSelectionAnchor(mode, {}, boxActors, boxDebris); publishSelectionAnchor(mode, {}, boxActors, boxDebris);
@@ -1324,6 +1322,35 @@ void GameWorldView::selectInBox(bool additive)
if (!additive) { m_selection.clearAll(); } 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, void GameWorldView::publishSelectionAnchor(SelectionMode mode,
const std::vector<BuildingId>& buildings, const std::vector<BuildingId>& buildings,
const std::vector<entt::entity>& actors, const std::vector<entt::entity>& actors,
@@ -1359,14 +1386,6 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
const QPoint tile = coordinates.widgetToTile(event->pos()); const QPoint tile = coordinates.widgetToTile(event->pos());
m_cursorWorldPos = coordinates.widgetToWorld(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()) if (m_buildMode.isBuilderMode())
{ {
m_buildMode.setGhostTile(tile); m_buildMode.setGhostTile(tile);
@@ -1410,11 +1429,11 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
else if (m_buildMode.isDeconstructMode()) else if (m_buildMode.isDeconstructMode())
{ {
m_buildMode.setDeconstructHoverBuildingId(buildingAtTile(tile)); m_buildMode.setDeconstructHoverBuildingId(buildingAtTile(tile));
if (m_boxSelecting) { m_boxCurrentTile = tile; } if (m_boxSelecting) { updateBoxDrag(event->pos()); }
} }
else if (m_boxSelecting) else if (m_boxSelecting)
{ {
m_boxCurrentTile = tile; updateBoxDrag(event->pos());
} }
} }
@@ -1435,7 +1454,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
m_boxSelecting = false; m_boxSelecting = false;
const std::vector<BuildingId> boxIds = 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 bool controlHeld = (event->modifiers() & Qt::ControlModifier) != 0;
const ControlAction dragAction = const ControlAction dragAction =

View File

@@ -226,6 +226,17 @@ private:
// which is the only case that goes on to start a box drag. // which is the only case that goes on to start a box drag.
bool selectAtPoint(QPoint tile, QVector2D worldPos, bool additive); bool selectAtPoint(QPoint tile, QVector2D worldPos, bool additive);
void selectInBox(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 // 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 // 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 // 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. // Not owned; set after construction, so null until MainWindow has built it.
const BlueprintLibrary* m_blueprintLibrary = nullptr; const BlueprintLibrary* m_blueprintLibrary = nullptr;
bool m_boxSelecting; bool m_boxSelecting;
QPoint m_boxStartTile; // The drag's two corners in world coordinates, unsnapped: where the button went
QPoint m_boxCurrentTile; // down and where the cursor is now (REQ-UI-MULTI-SELECT). World rather than
// Where the button went down, in widget pixels; the origin the drag threshold // widget coordinates so the anchor keeps the spot in the world it was placed on
// below measures from. Pixels, not tiles: the threshold separates a click from a // when the view scrolls under a held button.
// drag, which is a hand-steadiness question and not a tile-sized one. QVector2D m_boxStartWorld;
QPoint m_boxStartPos; QVector2D m_boxCurrentWorld;
// Whether the cursor has moved far enough from m_boxStartPos for this to read as // Whether the cursor has moved far enough from the anchor for this to read as a
// a drag. Until it has, the rectangle is not drawn (REQ-UI-MULTI-SELECT): a plain // drag. Until it has, the rectangle is not drawn and the box resolves as the
// click would otherwise flash a one-tile rectangle. Sticky for the rest of the // whole anchor tile (REQ-UI-MULTI-SELECT). Sticky for the rest of the drag, so
// drag, so coming back to the press position does not hide the rectangle again. // coming back to the press position does not hide the rectangle again.
bool m_boxDragMoved; bool m_boxDragMoved;
// Interprets this widget's key events into semantic actions and publishes them // Interprets this widget's key events into semantic actions and publishes them

View File

@@ -984,9 +984,9 @@ void WorldRenderer::drawOverlays(QPainter& painter, const WorldCoordinates& coor
// Deconstruct tint: while dragging a deconstruct box, tint every covered // Deconstruct tint: while dragging a deconstruct box, tint every covered
// building/site (REQ-BLD-DECONSTRUCT-BOX); otherwise tint the hovered one. // 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); const Building* b = findBuilding(m_sim.getFactoryState(), id);
if (b && b->type == BuildingType::Hq) { continue; } 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 // Box-select rectangle, drawn from the world rectangle itself and unsnapped, so
// one, so a plain click does not flash a rectangle (REQ-UI-MULTI-SELECT). // the outline sits where the mouse went rather than on the tile grid
if (frame.isBoxSelecting && frame.isBoxDragMoved) // (REQ-UI-MULTI-SELECT).
if (frame.boxWorldRect.has_value())
{ {
const QPoint tl(std::min(frame.boxStartTile.x(), frame.boxCurrentTile.x()), const QRectF selRect(
std::min(frame.boxStartTile.y(), frame.boxCurrentTile.y())); coordinates.worldToWidget(QVector2D(frame.boxWorldRect->topLeft())),
const QPoint br(std::max(frame.boxStartTile.x(), frame.boxCurrentTile.x()) + 1, coordinates.worldToWidget(QVector2D(frame.boxWorldRect->bottomRight())));
std::max(frame.boxStartTile.y(), frame.boxCurrentTile.y()) + 1);
const QRectF selRect(coordinates.tileToWidget(tl),
coordinates.tileToWidget(br));
// In deconstruct mode the box marks buildings for demolition, so it is // In deconstruct mode the box marks buildings for demolition, so it is
// drawn in the deconstruct red instead of the selection color; the // drawn in the deconstruct red instead of the selection color; the
// tint's alpha governs only the fills it tints, never this outline // tint's alpha governs only the fills it tints, never this outline

View File

@@ -54,12 +54,11 @@ struct WorldRenderFrame
const SelectionController& selection; const SelectionController& selection;
const BuildModeController& buildMode; const BuildModeController& buildMode;
const std::vector<ActiveBeam>& beams; const std::vector<ActiveBeam>& beams;
bool isBoxSelecting; // The box being dragged, in world coordinates and normalized, or nullopt when no
// Whether that box drag has passed the movement threshold that tells it apart // drag is in progress — a press that has not passed the movement threshold is
// from a click; until it has, the rectangle is not drawn (REQ-UI-MULTI-SELECT). // still a click and offers none (REQ-UI-MULTI-SELECT). It is the same rectangle
bool isBoxDragMoved; // the view selects by, so what is drawn and what is selected cannot disagree.
QPoint boxStartTile; std::optional<QRectF> boxWorldRect;
QPoint boxCurrentTile;
bool isDebugDrawEnabled; bool isDebugDrawEnabled;
}; };