move blueprints out of the sidebar into Ctrl+C / Ctrl+V dialogs

The blueprint panel is gone; the side panel column now holds the selected
building panel alone at full height. Saving is Ctrl+C on the current
selection, and picking one for placement is a new frameless modal card grid
opened with Ctrl+V, or handed to directly after a confirmed save.

BlueprintPanel sheds its widget half and becomes BlueprintLibrary: the list,
its disk round-trip, capture, and the T hotkey, with no widget of its own.
MainWindow owns it and drives both dialogs, because ModalPauseScope needs a
GameWorldView and only MainWindow has one. Both handlers hold a single pause
scope and a single dim scope across the save-to-select handoff, so the dim
does not blink and the simulation is not resumed in between.

The two facts a card derives -- the per-type contents summary and the plain
cost total -- go into lib/sim next to captureBlueprintFromSelection so they
can be unit tested; src/ui is off the test include path. They are kept apart
from GameWorldView's placement total, which excludes locked types and
rotate-in-place targets and is a different number by design.

A card's delete icon is a sibling of the card body rather than one of its
children: Qt disables a widget's children along with it, and the delete has
to stay live on an unaffordable card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
2026-08-06 11:29:48 +02:00
parent d2d93347ad
commit a86cbd3fbd
21 changed files with 975 additions and 385 deletions

View File

@@ -3,10 +3,14 @@
#include <algorithm>
#include <climits>
#include <cstddef>
#include <map>
#include "BeltSystem.h"
#include "Building.h"
#include "BuildingsConfig.h"
#include "BuildingSystem.h"
#include "DisplayName.h"
#include "Simulation.h"
namespace
@@ -49,6 +53,21 @@ std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, Building
resolved.bodyCells = building ? &building->bodyCells : &site->bodyCells;
return resolved;
}
// Position of a building type in buildings.toml, which is the order the build button
// bar lays out its buttons (REQ-UI-BUILD-BAR) and so the tie-break order for a
// blueprint's contents line. A type with no config entry sorts after every known one.
std::size_t configOrderIndex(BuildingType type, const BuildingsConfig& buildings)
{
for (std::size_t i = 0; i < buildings.buildings.size(); ++i)
{
if (buildings.buildings[i].type == type)
{
return i;
}
}
return buildings.buildings.size();
}
} // namespace
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
@@ -166,3 +185,56 @@ bool selectionHasPlaceableBuilding(const Simulation& sim,
}
return false;
}
std::vector<BlueprintContentEntry> summarizeBlueprintContents(
const Blueprint& blueprint, const BuildingsConfig& buildings)
{
std::map<BuildingType, int> counts;
for (const BlueprintBuilding& building : blueprint.buildings)
{
++counts[building.type];
}
// The config index is carried through the sort so the comparator stays a strict
// total order; the enum value is the final tie-break, which only two config-less
// types could ever reach.
struct RankedType
{
BuildingType type;
int count;
std::size_t order;
};
std::vector<RankedType> ranked;
ranked.reserve(counts.size());
for (const std::pair<const BuildingType, int>& entry : counts)
{
ranked.push_back({entry.first, entry.second,
configOrderIndex(entry.first, buildings)});
}
std::sort(ranked.begin(), ranked.end(),
[](const RankedType& left, const RankedType& right)
{
if (left.count != right.count) { return left.count > right.count; }
if (left.order != right.order) { return left.order < right.order; }
return static_cast<int>(left.type) < static_cast<int>(right.type);
});
std::vector<BlueprintContentEntry> summary;
summary.reserve(ranked.size());
for (const RankedType& entry : ranked)
{
summary.push_back({toDisplayName(buildingTypeId(entry.type)), entry.count});
}
return summary;
}
int computeBlueprintCost(const Blueprint& blueprint, const BuildingsConfig& buildings)
{
int total = 0;
for (const BlueprintBuilding& building : blueprint.buildings)
{
const BuildingDef* def = buildings.findBuildingDef(building.type);
if (def) { total += def->cost; }
}
return total;
}