From 5f6ecbf6c809e4b9743c7a9b7bc9a2db77fa8509 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Thu, 9 Jul 2026 21:33:24 +0200 Subject: [PATCH] implement visual feedback for copy building settings with shift + click gesture --- bin/app/data/config/visuals.toml | 1 + src/ui/GameWorldView.cpp | 100 +++++++++++++++++++++++++------ src/ui/GameWorldView.h | 17 ++++++ src/ui/VisualsConfig.h | 1 + src/ui/VisualsLoader.cpp | 1 + 5 files changed, 102 insertions(+), 18 deletions(-) diff --git a/bin/app/data/config/visuals.toml b/bin/app/data/config/visuals.toml index 7a31445..fc5e624 100644 --- a/bin/app/data/config/visuals.toml +++ b/bin/app/data/config/visuals.toml @@ -348,6 +348,7 @@ demolish_tint = "#ff000033" # demolish-mode hover tint selection_rect = "#00ff00" # box-drag selection rectangle (REQ-UI-MULTI-SELECT) tile_highlight = "#ffffff22" # tile under cursor selected_outline = "#ffff00" # outline drawn around currently-selected building(s) +copy_config = "#33ccff66" # copy-settings eligible-target tint + copy/paste flash (REQ-BLD-COPY-CONFIG-FEEDBACK) # ----------------------------------------------------------------------------- # Schematic-drop toasts (REQ-UI-SCHEMATIC-TOAST) diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index b5b28df..c424eeb 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -271,6 +271,20 @@ void GameWorldView::onFrame() m_activeBeams = std::move(live); } + // Expire copy/paste flashes. Lifetime is wall-clock (the frame delta), so the + // flash plays for a fixed real duration regardless of game speed, including + // while the game is paused (REQ-BLD-COPY-CONFIG-FEEDBACK). + if (!m_copyConfigFlashes.empty()) + { + std::vector live; + for (CopyConfigFlash flash : m_copyConfigFlashes) + { + flash.remainingMs -= elapsed; + if (flash.remainingMs > 0) { live.push_back(flash); } + } + m_copyConfigFlashes = std::move(live); + } + // Apply held scroll { // Pan speed depends on where the view is centered (REQ-UI-SCROLL-SPEED). @@ -384,6 +398,7 @@ void GameWorldView::paintGL() drawTiles(painter); drawBuildings(painter); + drawCopyConfigFeedback(painter); drawStations(painter); drawBeltItems(painter); drawScrap(painter); @@ -998,6 +1013,29 @@ void GameWorldView::drawBuildings(QPainter& painter) drawSelectionHighlights(painter); } +std::optional GameWorldView::footprintWidgetRect(BuildingId id) const +{ + std::optional anchor; + std::optional footprint; + + if (const Building* b = m_sim->buildings().findBuilding(id)) + { + anchor = b->anchor; + footprint = b->footprint; + } + else if (const ConstructionSite* s = m_sim->buildings().findSite(id)) + { + anchor = s->anchor; + footprint = s->footprint; + } + if (!anchor.has_value() || !footprint.has_value()) { return std::nullopt; } + + const QPointF tl = tileToWidget(*anchor); + return QRectF(tl.x(), tl.y(), + footprint->width() * static_cast(tilePx()), + footprint->height() * static_cast(tilePx())); +} + void GameWorldView::drawSelectionHighlights(QPainter& painter) { painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2)); @@ -1005,26 +1043,47 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter) for (BuildingId selId : m_selectedBuildingIds) { - std::optional anchor; - std::optional footprint; + const std::optional rect = footprintWidgetRect(selId); + if (!rect.has_value()) { continue; } + // Outline sits 1px outside the footprint (into adjacent tiles). + painter.drawRect(rect->adjusted(-1, -1, 1, 1)); + } +} - if (const Building* b = m_sim->buildings().findBuilding(selId)) - { - anchor = b->anchor; - footprint = b->footprint; - } - else if (const ConstructionSite* s = m_sim->buildings().findSite(selId)) - { - anchor = s->anchor; - footprint = s->footprint; - } - if (!anchor.has_value() || !footprint.has_value()) { continue; } +void GameWorldView::drawCopyConfigFeedback(QPainter& painter) +{ + const QColor color = m_visuals->overlays.copyConfig; - const QPointF tl = tileToWidget(*anchor); - const QRectF bboxRect(tl.x(), tl.y(), - footprint->width() * static_cast(tilePx()), - footprint->height() * static_cast(tilePx())); - painter.drawRect(bboxRect.adjusted(-1, -1, 1, 1)); + // Eligible-target tint: while a configuration is cached, every same-type + // building and site (the source included) is a valid paste target and is + // washed in the copy-settings color (REQ-BLD-COPY-CONFIG-FEEDBACK). + if (m_copiedConfig.has_value()) + { + painter.setPen(Qt::NoPen); + painter.setBrush(color); + const BuildingType type = m_copiedConfig->type; + for (const Building& b : m_sim->buildings().allBuildings()) + { + if (b.type != type) { continue; } + const std::optional rect = footprintWidgetRect(b.id); + if (rect.has_value()) { painter.drawRect(*rect); } + } + for (const ConstructionSite& s : m_sim->buildings().allSites()) + { + if (s.type != type) { continue; } + const std::optional rect = footprintWidgetRect(s.id); + if (rect.has_value()) { painter.drawRect(*rect); } + } + } + + // Copy / paste flashes: a brief outline in the same color, drawn like the + // selection outline (REQ-BLD-COPY-CONFIG-FEEDBACK). + painter.setPen(QPen(color, 2)); + painter.setBrush(Qt::NoBrush); + for (const CopyConfigFlash& flash : m_copyConfigFlashes) + { + const std::optional rect = footprintWidgetRect(flash.id); + if (rect.has_value()) { painter.drawRect(rect->adjusted(-1, -1, 1, 1)); } } } @@ -1905,6 +1964,7 @@ void GameWorldView::copyConfigFrom(BuildingId id) if (!config->recipeId.has_value() && !config->isSplitter) { return; } m_copiedConfig = config; + m_copyConfigFlashes.push_back({ id, kCopyFlashDurationMs }); } void GameWorldView::pasteConfigTo(BuildingId id) @@ -1914,6 +1974,9 @@ void GameWorldView::pasteConfigTo(BuildingId id) const std::optional target = readBuildingConfig(*m_sim, id); if (!target.has_value() || target->type != m_copiedConfig->type) { return; } + // The paste applies below; flash the target to confirm (REQ-BLD-COPY-CONFIG-FEEDBACK). + m_copyConfigFlashes.push_back({ id, kCopyFlashDurationMs }); + const BuildingConfig& source = *m_copiedConfig; // The cached settings were valid on a same-type source building, so they are @@ -2041,6 +2104,7 @@ void GameWorldView::resetForNewGame() std::make_shared(false)); m_selectedBuildingIds.clear(); m_copiedConfig = std::nullopt; + m_copyConfigFlashes.clear(); m_boxSelecting = false; m_scrollXTiles = 0.0f; m_scrollLeft = false; diff --git a/src/ui/GameWorldView.h b/src/ui/GameWorldView.h index dd5ede1..5e84f04 100644 --- a/src/ui/GameWorldView.h +++ b/src/ui/GameWorldView.h @@ -117,6 +117,7 @@ private: void drawTiles(QPainter& painter); void drawBuildings(QPainter& painter); void drawSelectionHighlights(QPainter& painter); + void drawCopyConfigFeedback(QPainter& painter); void drawStations(QPainter& painter); void drawBeltItems(QPainter& painter); void drawScrap(QPainter& painter); @@ -138,6 +139,10 @@ private: QPoint widgetToTile(QPoint widgetPt) const; QRectF tileRect(QPoint tile) const; QRect viewportRect() const; + // Widget-space rectangle covering a building or construction site's footprint, + // or nullopt if the id resolves to neither. Shared by the selection highlight + // and the copy-settings feedback (REQ-BLD-COPY-CONFIG-FEEDBACK). + std::optional footprintWidgetRect(BuildingId id) const; float asteroidLeftEdge() const; float enemyStationRightEdge() const; @@ -226,6 +231,18 @@ private: // only while Shift is down and cleared on Shift release. std::optional m_copiedConfig; + // Brief outline flash shown on a building when settings are copied from it or + // pasted onto it (REQ-BLD-COPY-CONFIG-FEEDBACK). remainingMs counts down in + // wall-clock time so the flash plays at a fixed length regardless of game speed + // (and while paused). + struct CopyConfigFlash + { + BuildingId id; + qint64 remainingMs; + }; + std::vector m_copyConfigFlashes; + static constexpr qint64 kCopyFlashDurationMs = 300; + bool m_demolishMode; BuildingId m_demolishHoverBuildingId; bool m_debugDraw; diff --git a/src/ui/VisualsConfig.h b/src/ui/VisualsConfig.h index 7e9ebb3..3d4842c 100644 --- a/src/ui/VisualsConfig.h +++ b/src/ui/VisualsConfig.h @@ -48,6 +48,7 @@ struct OverlayVisuals QColor selectionRect; QColor tileHighlight; QColor selectedOutline; + QColor copyConfig; }; struct ToastVisuals diff --git a/src/ui/VisualsLoader.cpp b/src/ui/VisualsLoader.cpp index 0af3ac7..08d529f 100644 --- a/src/ui/VisualsLoader.cpp +++ b/src/ui/VisualsLoader.cpp @@ -224,6 +224,7 @@ VisualsConfig VisualsLoader::load(const std::string& path) cfg.overlays.selectionRect = parseColor(requireString(ov, "selection_rect", "overlays"), "overlays.selection_rect"); cfg.overlays.tileHighlight = parseColor(requireString(ov, "tile_highlight", "overlays"), "overlays.tile_highlight"); cfg.overlays.selectedOutline = parseColor(requireString(ov, "selected_outline", "overlays"), "overlays.selected_outline"); + cfg.overlays.copyConfig = parseColor(requireString(ov, "copy_config", "overlays"), "overlays.copy_config"); } // Toast