list what a belt is carrying, and clear it by name

The belt card's only content was a button reading "Clear stuck items", which
assumed a state the items need not be in. It now reads "Clear items", and above
it the card lists what the selected tiles hold -- one item chip per type, the
same chip the buffer sections and the HQ's block stock draw -- so a line's
contents can be read before they are removed, and can be read at all: items on a
moving belt are too small and too transient to count by eye, and items inside a
tunnel are drawn nowhere.

BeltSystem gains countItems(tiles), a query of the same kind as
forEachVisualItem: a method rather than exposed tile containers, so the per-tile
representation stays swappable. It walks the same five containers as clearTiles,
in the same order, so the list and the button cannot drift apart.

The tunnel's two ends are now told apart. Items in transit are counted on the
exit they are travelling toward, and a clear removes exactly what the panel
listed for the tile it acts on: the exit discards them, the entry leaves them
travelling. BeltSystemTest's tunnel case splits in two accordingly.

Splitters now aggregate with belts and tunnel ends. Their output filters are
per-object configuration, which an aggregate simply does not show -- a splitter
selected alone still gets them. Both cards share one BeltItemList widget, and
both derive their tiles from collectBeltTiles, so nothing is stated twice.

The mixed count summary loses the clear action it carried: a button acting on
part of a selection is worse than no button, and the tiles can be selected by
themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-19 14:38:45 +02:00
parent 7547d954f1
commit fbb1af85e3
18 changed files with 444 additions and 92 deletions

View File

@@ -408,14 +408,11 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
m_tunnelEntries.find(key(tile));
if (teIt != m_tunnelEntries.end())
{
// Only what sits on the entry itself: the items already inside the tunnel
// are counted on its exit (REQ-UI-BELT-ITEMS) and are cleared from there, so
// a clear never removes more than the panel showed for the tile it acted on
// (REQ-BLD-TUNNEL-TRANSIT). Emptying a tunnel is done from its exit.
teIt->second.itemSlots.clear();
for (TunnelLink& link : m_tunnelLinks)
{
if (link.entryTile == tile)
{
link.items.clear();
}
}
}
const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
@@ -434,6 +431,83 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
}
}
std::map<ItemType, int> BeltSystem::countItems(const std::vector<QPoint>& tiles) const
{
std::map<ItemType, int> counts;
// The same five containers clearTiles walks, in the same order, so that what the
// panel lists and what the button removes cannot drift apart (REQ-UI-BELT-ITEMS,
// REQ-UI-BELT-CLEAR).
for (const QPoint& tile : tiles)
{
const std::map<std::pair<int, int>, BeltTile>::const_iterator bIt =
m_belts.find(key(tile));
if (bIt != m_belts.end())
{
for (const BeltItemSlot& slot : bIt->second.itemSlots)
{
counts[slot.item.type]++;
}
}
const std::map<std::pair<int, int>, SplitterTile>::const_iterator sIt =
m_splitters.find(key(tile));
if (sIt != m_splitters.end())
{
const SplitterTile& splitter = sIt->second;
for (const BeltItemSlot& slot : splitter.back)
{
counts[slot.item.type]++;
}
if (splitter.frontA)
{
counts[splitter.frontA->item.type]++;
}
if (splitter.frontB)
{
counts[splitter.frontB->item.type]++;
}
}
const std::map<std::pair<int, int>, TunnelEntryTile>::const_iterator teIt =
m_tunnelEntries.find(key(tile));
if (teIt != m_tunnelEntries.end())
{
for (const BeltItemSlot& slot : teIt->second.itemSlots)
{
counts[slot.item.type]++;
}
}
const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
m_tunnelExits.find(key(tile));
if (txIt != m_tunnelExits.end())
{
for (const BeltItemSlot& slot : txIt->second.itemSlots)
{
counts[slot.item.type]++;
}
// The tunnel's invisible cargo, counted on the end it is travelling toward.
// This is the only place the player can see it at all, the tunnel drawing
// nothing of what is inside it (REQ-BLD-TUNNEL-TRANSIT).
for (const TunnelLink& link : m_tunnelLinks)
{
if (link.exitTile != tile)
{
continue;
}
for (const TunnelTransitItem& transit : link.items)
{
counts[transit.item.type]++;
}
}
}
}
return counts;
}
// ---------------------------------------------------------------------------
// Tick
// ---------------------------------------------------------------------------

View File

@@ -95,13 +95,26 @@ public:
double getProgressPerTick_tpt() const { return m_progressPerTick_tpt; }
// -- Maintenance ---------------------------------------------------------
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
// Removes every item from the given tiles (REQ-UI-BELT-CLEAR). A tunnel exit takes
// the items in transit through its tunnel with it; a tunnel entry does not, so what
// a clear removes is exactly what countItems reported for the same tile.
void clearTiles(const std::vector<QPoint>& tiles);
void tick();
// -- Rendering -----------------------------------------------------------
void forEachVisualItem(QRect viewportTiles,
std::function<void(VisualItem)> visit) const;
// -- Inspection ----------------------------------------------------------
// What the given tiles carry, summed per item type (REQ-UI-BELT-ITEMS). Items in
// transit through a tunnel are counted on its exit, that being where they will
// arrive and the only end whose clear discards them (REQ-BLD-TUNNEL-TRANSIT).
//
// A method rather than an exposed container: what a tile holds and how it holds it
// is this subsystem's alone to know, so the per-tile representation stays swappable
// (architecture.md, Belt Subsystem).
std::map<ItemType, int> countItems(const std::vector<QPoint>& tiles) const;
// -- Determinism ---------------------------------------------------------
// Folds all transport state (belt/splitter/tunnel tiles and their items)
// into the hasher in deterministic order (see docs/replay_design.md).

View File

@@ -233,3 +233,25 @@ TunnelTileMap collectTunnelTiles(const FactoryState& state)
}
return tunnels;
}
std::vector<QPoint> collectBeltTiles(const FactoryState& state,
const std::vector<BuildingId>& ids)
{
std::vector<QPoint> tiles;
for (BuildingId id : ids)
{
// findBuilding fails for a construction site, which is what keeps a site out:
// its tile carries nothing and is not registered with the belt subsystem until
// it is built (REQ-BLD-SITE-CONFIG).
const Building* building = findBuilding(state, id);
if (!building || !isBeltSubsystemType(building->type))
{
continue;
}
for (const QPoint& cell : building->bodyCells)
{
tiles.push_back(cell);
}
}
return tiles;
}

View File

@@ -82,3 +82,11 @@ std::vector<BuildingId> buildingsInBox(const FactoryState& state,
// 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.
TunnelTileMap collectTunnelTiles(const FactoryState& state);
// The tiles of the belt-subsystem buildings among the given ids, in the order the ids
// arrive; anything else in the selection is skipped, as are construction sites, whose
// tiles the belt subsystem does not know yet (REQ-BLD-SITE-CONFIG). Shared by the panel's
// item list and its clear action, so both speak of exactly the same tiles
// (REQ-UI-BELT-ITEMS, REQ-UI-BELT-CLEAR).
std::vector<QPoint> collectBeltTiles(const FactoryState& state,
const std::vector<BuildingId>& ids);