color item icon backgrounds in the UI from visuals.toml

This commit is contained in:
2026-08-11 21:18:52 +02:00
parent 37378e3c1b
commit d1da4b2937
8 changed files with 194 additions and 78 deletions

View File

@@ -484,7 +484,7 @@ The screen is a single column: a header bar across the top and the game world vi
Because the contest-zone boundaries shift as the scrollable area grows with each push (REQ-GW-PUSH-EXPAND, REQ-GW-SCROLL-LIMIT), the ramp bands are recomputed from the current contest-zone boundaries. This is a presentation-only concern and does not affect the simulation, consistent with REQ-UI-NO-ZOOM.
- REQ-UI-WORLD-ICON: In the game world, a building is drawn with an icon's glyph symbol centered on its footprint, in place of the letter identity glyph. The icon is an SVG loaded from `data/icons/buildings/`; only the icon's glyph is drawn in the world — in a contrasting ink (white over dark fills, dark over light fills) so it stays legible — and its colored chip background is omitted, because the footprint is already filled with the building's `visuals.toml` fill color. This applies to the production buildings (Miner, Smelter, Assembler, Reprocessing Plant, Shipyard, Salvage Bay), the HQ, and the player and enemy defence stations, wherever the identity label appears: operational buildings, construction sites (REQ-UI-CONSTRUCTION-PROGRESS), and the builder-mode and blueprint-placement ghosts. **Belts, splitters, and tunnels are excluded** — they keep their existing tile rendering so their orientation and flow stay readable (a centered icon would obscure direction). Their build-menu buttons still use icons (REQ-UI-BUILD-ICON); in particular the shared Tunnel button's `tunnel_entry.svg` is a build-button icon only, not a world icon. The directional output-port glyphs (REQ-UI-PORT-GLYPH, REQ-UI-PORT-TARGET-GLYPH) are a separate indicator and are unaffected. A building or station with no icon file falls back to its `visuals.toml` text glyph; a type with neither icon nor glyph shows no identity label. A missing icon is not an error, consistent with REQ-UI-BUILD-ICON.
- REQ-UI-ITEM-ICON: In the game world, an item is drawn as its `visuals.toml` colored square (`fill` + `outline`, the square of REQ-GW-TILE-SIZE) carrying its **item icon** on top. The square is drawn for every item, with or without an icon: it is what gives the item contrast against the tile beneath it, and its outline is what separates neighbouring items where they overlap on a belt. The icon is a self-contained, full-color SVG (rendered as-is, unlike the glyph-only building icons of REQ-UI-WORLD-ICON), loaded at runtime from `data/icons/items/` — a sibling of the config directory, read the same way as the building icons (REQ-UI-BUILD-ICON) — one file per item type named after the item's id (e.g. `iron_ore.svg`). The square fills the item's half-tile rect, keeping the size, spacing, and draw-order rules of REQ-GW-TILE-SIZE; the icon is drawn **inset** within that rect so a frame of the square's color stays visible all around it — required because each icon's viewBox is cropped tight to its artwork, so an icon drawn at the full rect would cover the square entirely. This applies wherever an item is drawn: on belts, splitters, and tunnel ends, and while emerging from or sinking into a building port (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-INPUT-INTAKE). An item type with no icon file shows the colored square alone; a missing icon is not an error, consistent with REQ-UI-BUILD-ICON. The colored square is a game-world treatment only: the item chips and selection dialogs of the UI panels (REQ-UI-SINGLE-SELECTION, REQ-UI-SELECT-BUTTON) show the icon without it. For performance, each item icon is rasterized to a pixmap cached per target pixel size — re-rasterized only when the tile pixel size changes (e.g. on view resize) — rather than re-rendered from vector every frame.
- REQ-UI-ITEM-ICON: In the game world, an item is drawn as its `visuals.toml` colored square (`fill` + `outline`, the square of REQ-GW-TILE-SIZE) carrying its **item icon** on top. The square is drawn for every item, with or without an icon: it is what gives the item contrast against the tile beneath it, and its outline is what separates neighbouring items where they overlap on a belt. The icon is a self-contained, full-color SVG (rendered as-is, unlike the glyph-only building icons of REQ-UI-WORLD-ICON), loaded at runtime from `data/icons/items/` — a sibling of the config directory, read the same way as the building icons (REQ-UI-BUILD-ICON) — one file per item type named after the item's id (e.g. `iron_ore.svg`). The square fills the item's half-tile rect, keeping the size, spacing, and draw-order rules of REQ-GW-TILE-SIZE; the icon is drawn **inset** within that rect so a frame of the square's color stays visible all around it — required because each icon's viewBox is cropped tight to its artwork, so an icon drawn at the full rect would cover the square entirely. This applies wherever an item is drawn: on belts, splitters, and tunnel ends, and while emerging from or sinking into a building port (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-INPUT-INTAKE). An item type with no icon file shows the colored square alone; a missing icon is not an error, consistent with REQ-UI-BUILD-ICON. **The colored square travels with the icon into the UI.** Wherever the UI displays an item *as an item* — the recipe-selection dialog's option buttons (REQ-UI-RECIPE-ICON), the selection panel's item chips (REQ-UI-SINGLE-SELECTION, REQ-UI-HQ-PANEL), and the recipe summary's input and output icons (REQ-UI-RECIPE-SUMMARY) — the icon is drawn on that same square, by the same rules: the item's `fill` and `outline` from `visuals.toml`, the icon inset within the square so a frame of the square's color stays visible all around it, and the square alone when the item has no icon file. The square fills the rect the display allots to the icon, as it fills the half-tile rect in the world. One item therefore reads the same in a panel as it does on a belt. The one exception is the inline `building_block` icon that stands in for the word `Blocks` beside a number (REQ-UI-BLOCKS-ICON, REQ-UI-EXPAND-BUTTON, REQ-UI-BUILD-COST, REQ-UI-MULTI-SELECTION, REQ-UI-BLUEPRINT-CARD): there the icon is a decoration on a line of text rather than an item display, and it is drawn bare, without the square. For performance, each item icon is rasterized to a pixmap cached per target pixel size — re-rasterized only when the tile pixel size changes (e.g. on view resize) — rather than re-rendered from vector every frame.
- REQ-UI-CONSTRUCTION-PROGRESS: Construction sites display the building's identity symbol centered on the footprint (same as an operational building) — its icon glyph, or the text glyph as a fallback (REQ-UI-WORLD-ICON). Below the symbol — or centered on the footprint if the building has neither an icon nor a glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes.
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
- REQ-UI-PORT-TARGET-GLYPH: While in builder mode (REQ-BLD-BUILDER-MODE), the builder-mode ghost additionally shows, for each of the building's output ports, a directional glyph drawn centered in the port's **target cell** — the cell immediately outside the footprint that the port pushes into, i.e. the cell the surface-mask output-port indicator occupies (see Surface Mask Format). As in REQ-UI-PORT-GLYPH the glyph is a `>` rotated to face the port's exit direction (`>` East, `^` North, `<` West, `v` South), previewing where the port's output will go before placement. This is in addition to the on-tile port glyph of REQ-UI-PORT-GLYPH, and — unlike that indicator — is shown only for the builder-mode ghost, not for operational buildings, construction sites, or the blueprint-placement ghost (REQ-UI-BLUEPRINT-PLACE). A building with multiple output ports (e.g. a splitter) shows one target-cell glyph per port. The target-cell glyph is drawn larger than the on-tile port glyph so it stands out as the flow-direction preview. Exceptions: the Tunnel Entry shows no target-cell glyph, because it receives items (which may arrive from any of its non-mouth edges, REQ-BLD-TUNNEL-ENTRY) rather than emitting into a single adjacent cell; the Shipyard shows none either, because its output port is a ship-spawn point (REQ-SHP-SPAWN-PLAYER) rather than a belt-item output (REQ-MAT-OUTPUT-EMERGE).
@@ -574,7 +574,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
- **Debris** — several pieces of debris and nothing else. Their remaining scrap sums into one value (REQ-UI-DEBRIS-PANEL).
Every other multi-selection falls back to the count summary. In particular a selection mixing a splitter with belts does not aggregate (a splitter carries per-object output filters, which have no aggregate), and neither do several production buildings of one type (per-building buffers and cycle progress have no aggregate).
- REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows its symbol and name in the header (REQ-UI-SELECTION-CARD), its current recipe or schematic selection (REQ-UI-SELECT-BUTTON) and recipe summary (REQ-UI-RECIPE-SUMMARY) in the configuration group, and its input and output buffer contents in the runtime group. Each buffered item is shown as an **item chip** bearing that item's icon (REQ-UI-ITEM-ICON) and its current count:
- REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows its symbol and name in the header (REQ-UI-SELECTION-CARD), its current recipe or schematic selection (REQ-UI-SELECT-BUTTON) and recipe summary (REQ-UI-RECIPE-SUMMARY) in the configuration group, and its input and output buffer contents in the runtime group. Each buffered item is shown as an **item chip** bearing that item's icon on its colored square (REQ-UI-ITEM-ICON) and its current count:
- an **input** chip shows the per-cycle amount below the count (the items consumed per run, e.g. `/ 2 per cycle`), or the count alone when the building has no selected recipe or schematic to give one;
- an **output** chip shows the count against the output buffer's capacity as `a / b` (REQ-MAT-OUTPUT-BUFFER), with the item's name below.
@@ -583,7 +583,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
**Only unlocked items are listed.** A building's buffers may carry entries for items the player cannot make yet — an auto-recipe building's buffers are sized over *every* recipe of its type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), including recipes that are still locked. Those entries are left out of both sections, consistent with the rest of the UI hiding what is not unlocked yet (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SPLITTER), so a Smelter shows the ores it can actually smelt rather than every ore in the game.
**An idle auto-recipe building still shows what it handles.** Having no selected recipe (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), it would otherwise show empty sections whenever it happens to be between cycles. Its input and output sections instead list the unlocked items of every recipe of its type — the same union its buffers were sized over — with a count and no per-cycle denominator, since no one recipe is in force. While a cycle is running, that cycle's recipe supplies the denominators as for any other building.
- 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 with its per-cycle amount, an arrow, each output item's icon 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. Auto-recipe buildings (Smelter, Reprocessing Plant — REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) have no player-selected recipe and so show no selection control; they show the summary of the recipe currently in production and, while between cycles, of the one they ran last. They keep it rather than dropping it, because a summary that came and went with each cycle would resize the card in step with the building's status (REQ-UI-SELECTION-STATUS), which is the one thing the panel must not do while the player is reading it (REQ-UI-SELECTION-PANEL). Such a building shows no summary only until it has run its first cycle. A building with no recipe or schematic selected shows no summary.
- 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, 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. Auto-recipe buildings (Smelter, Reprocessing Plant — REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) have no player-selected recipe and so show no selection control; they show the summary of the recipe currently in production and, while between cycles, of the one they ran last. They keep it rather than dropping it, because a summary that came and went with each cycle would resize the card in step with the building's status (REQ-UI-SELECTION-STATUS), which is the one thing the panel must not do while the player is reading it (REQ-UI-SELECTION-PANEL). Such a building shows no summary only until it has run its first cycle. A building with no recipe or schematic selected shows no summary.
- 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-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.
@@ -592,9 +592,9 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
- REQ-UI-SELECT-TOOLTIP: **Selection info tooltip.** Hovering an option button in the selection dialog (REQ-UI-SELECT-BUTTON), and hovering the selection button in the selection panel when a selection is set, displays an info tooltip:
- For a **recipe** (Miner or Assembler): the recipe name; the name and quantity of each input item (no inputs are listed for miner recipes, which consume nothing); the completion time (`duration_seconds`); and the name and quantity of the produced output item.
- For a **ship schematic** (Shipyard): the ship's `display_name`; the name and quantity of each base required material (`[ship.schematic].materials`, excluding any module contributions); the base production time (`[ship.schematic].production_time_seconds`); and "Produces: 1 <ship display name>".
- REQ-UI-RECIPE-ICON: In the recipe-selection dialog (REQ-UI-SELECT-BUTTON) for a Miner or Assembler, each recipe option button shows the icon of the recipe's produced item **instead of** its name caption (icon-only). The item shown is the recipe's `icon` field if set, otherwise its first output item; the icon is that item's icon per REQ-UI-ITEM-ICON. When the item has no icon file, the button falls back to the recipe/item name caption. The recipe name and details remain available on hover via the selection info tooltip (REQ-UI-SELECT-TOOLTIP). The `(None)` option keeps its text caption. This applies only to recipe options; the Shipyard schematic-selection dialog is unaffected and continues to show ship name captions.
- REQ-UI-RECIPE-ICON: In the recipe-selection dialog (REQ-UI-SELECT-BUTTON) for a Miner or Assembler, each recipe option button shows the icon of the recipe's produced item **instead of** its name caption (icon-only). The item shown is the recipe's `icon` field if set, otherwise its first output item; the icon is that item's icon on its colored square per REQ-UI-ITEM-ICON. When the item has no icon file, the button shows that colored square alone rather than a name caption, so every option button keeps the same face. The recipe name and details remain available on hover via the selection info tooltip (REQ-UI-SELECT-TOOLTIP). The `(None)` option keeps its text caption. This applies only to recipe options; the Shipyard schematic-selection dialog is unaffected and continues to show ship name captions.
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel's runtime group shows a **"Clear stuck items"** button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels. The button acts on every selected tile, which is why a selection of belts and tunnel ends aggregates into one content rather than a count summary (REQ-UI-SELECTION-AGGREGATE).
- REQ-UI-HQ-PANEL: When the HQ is selected, the panel shows the HQ's **HP** as a bar labelled `current / maximum` (REQ-HQ-STATS, REQ-UI-HP-BARS) and, beneath it, the **global building blocks stock** — the same value as the header bar's stock display (REQ-UI-BLOCKS-ICON), rendered as an item chip (REQ-UI-SINGLE-SELECTION) carrying the `building_block` icon. The HP comes first, as it does on every card that has it (REQ-UI-SELECTION-CARD). The HQ has no input or output buffers of its own: building blocks delivered by belt go straight into the global stock (REQ-HQ-BELT-INPUT), and showing that stock on the HQ is what tells the player to route blocks there. The HQ has no configuration group and no status indicator (REQ-UI-SELECTION-STATUS), and it is never a construction site.
- REQ-UI-HQ-PANEL: When the HQ is selected, the panel shows the HQ's **HP** as a bar labelled `current / maximum` (REQ-HQ-STATS, REQ-UI-HP-BARS) and, beneath it, the **global building blocks stock** — the same value as the header bar's stock display (REQ-UI-BLOCKS-ICON), rendered as an item chip (REQ-UI-SINGLE-SELECTION) carrying the `building_block` icon on its colored square — a chip is an item display, so it takes the square even though the header bar's inline block icon does not (REQ-UI-ITEM-ICON, REQ-UI-BLOCKS-ICON). The HP comes first, as it does on every card that has it (REQ-UI-SELECTION-CARD). The HQ has no input or output buffers of its own: building blocks delivered by belt go straight into the global stock (REQ-HQ-BELT-INPUT), and showing that stock on the HQ is what tells the player to route blocks there. The HQ has no configuration group and no status indicator (REQ-UI-SELECTION-STATUS), and it is never a construction site.
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. A plain click on a ship or defence station makes it the sole selection, clearing any previous selection. Ships and defence stations can be multi-selected — by Ctrl+clicking individual actors to add or remove them, or by box-drag (REQ-UI-MULTI-SELECT) — and can be selected together with debris and with one another in a single field selection (REQ-UI-SELECTION-CATEGORIES), freely mixing player and enemy actors. Actors cannot be selected together with buildings: selecting a ship or defence station clears any building selection, and selecting a building clears the actors (buildings win). Clicking a piece of debris adds to or establishes a field selection (REQ-UI-DEBRIS-CLICK-SELECT). Clicking empty world space (no building, ship, defence station, or piece of debris) clears the selection.
- REQ-UI-SHIP-STATS-PANEL: When exactly one ship is selected (REQ-UI-ENTITY-CLICK-SELECT) and no debris is selected, the selection panel shows a **ship stats panel**. (If debris is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.) The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. Its header (REQ-UI-SELECTION-CARD) carries the schematic's color swatch and display name, with the ship's current behavior in the right slot (REQ-UI-SHIP-BEHAVIOR). The panel always shows all hull stats: HP (current / maximum) as a **bar** with the two values beside its caption, then max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed as label/value rows. In addition, capability module summaries are shown below the hull stats, each as its own outlined row, conditioned on which module types are installed and using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
- REQ-UI-SHIP-BEHAVIOR: The ship stats panel (REQ-UI-SHIP-STATS-PANEL) additionally displays the selected ship's **current behavior** in its header's right slot (REQ-UI-SELECTION-CARD) — a single label naming the top-priority behavior currently governing the ship's navigation, as resolved by the fixed-priority behavior arbitration. Only the winning behavior is named; lower-priority behaviors that are suppressed are not shown, and neither are the salvage/repair cycles that run regardless of the active behavior (REQ-SHP-SALVAGE, REQ-SHP-REPAIR). The label updates live as the ship's behavior changes, and it is always shown (independent of debug draw mode, unlike the threat-cost line of REQ-UI-SHIP-STATS-PANEL). This applies to both player and enemy ships (REQ-UI-ENTITY-CLICK-SELECT); enemy ships only ever show **Engaging** or **Advancing**. The behavior labels (all wrapped in `tr()`) are:

View File

@@ -2,10 +2,26 @@
#include <QFile>
#include <QPainter>
#include <QRectF>
#include <QSvgRenderer>
ItemIconCache::ItemIconCache(const QString& iconDir)
#include "VisualsConfig.h"
namespace
{
// How far the icon is inset within the item's colored square, as a fraction of the
// square's size on each side (REQ-UI-ITEM-ICON). The inset is what keeps a frame of the
// square's color visible all around the icon: each icon's viewBox is cropped tight to
// its artwork, so an icon drawn at the full rect would cover the square entirely.
const double kIconInsetFraction = 0.15;
} // namespace
ItemIconCache::ItemIconCache(const QString& iconDir, const VisualsConfig* visuals)
: m_iconDir(iconDir)
, m_visuals(visuals)
{
}
@@ -34,14 +50,59 @@ bool ItemIconCache::hasIcon(const std::string& itemId)
return !getSvg(itemId).isEmpty();
}
void ItemIconCache::paintItem(QPainter& painter, const QRectF& rect,
const std::string& itemId)
{
// The colored square from visuals.toml backs every item, icon or not: it is what
// gives the item contrast against the tile beneath it in the world, and its outline
// is what separates neighbouring items where they overlap on a belt (REQ-GW-TILE-SIZE,
// REQ-UI-ITEM-ICON).
if (m_visuals != nullptr)
{
const std::map<std::string, ItemVisuals>::const_iterator it =
m_visuals->items.find(itemId);
if (it != m_visuals->items.end())
{
painter.fillRect(rect, it->second.fill);
painter.setPen(QPen(it->second.outline, 1));
painter.setBrush(Qt::NoBrush);
painter.drawRect(rect);
}
}
if (!hasIcon(itemId)) { return; }
// The icon goes on top, inset so a frame of the square's color stays visible all
// around it. It is rasterized once at the inset pixel size and cached, so this is a
// plain pixmap blit per frame.
const double inset = kIconInsetFraction * rect.width();
const QRectF iconRect = rect.adjusted(inset, inset, -inset, -inset);
int sizePx = qRound(iconRect.width());
if (sizePx < 1) { sizePx = 1; }
painter.drawPixmap(iconRect, getPixmap(itemId, sizePx),
QRectF(0, 0, sizePx, sizePx));
}
QPixmap ItemIconCache::getSquarePixmap(const std::string& itemId, int sizePx)
{
return getPixmap("square:" + itemId, itemId, sizePx, true);
}
QPixmap ItemIconCache::getPixmap(const std::string& itemId, int sizePx)
{
return getPixmap(itemId, itemId, sizePx, false);
}
QPixmap ItemIconCache::getPixmap(const std::string& cacheKey, const std::string& itemId,
int sizePx, bool withSquare)
{
if (sizePx <= 0)
{
return QPixmap();
}
const std::pair<std::string, int> key(itemId, sizePx);
const std::pair<std::string, int> key(cacheKey, sizePx);
const std::map<std::pair<std::string, int>, QPixmap>::const_iterator cached =
m_pixmapCache.find(key);
if (cached != m_pixmapCache.end())
@@ -49,16 +110,45 @@ QPixmap ItemIconCache::getPixmap(const std::string& itemId, int sizePx)
return cached->second;
}
const QByteArray& svg = getSvg(itemId);
QPixmap pixmap;
if (!svg.isEmpty())
if (withSquare)
{
QSvgRenderer renderer(svg);
pixmap = QPixmap(sizePx, sizePx);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing, true);
renderer.render(&painter);
// Null unless the item has something to draw -- a square, an icon, or both --
// so a caller with nothing to show can fall back to text (REQ-UI-RECIPE-ICON).
const bool hasSquare = m_visuals != nullptr
&& m_visuals->items.find(itemId) != m_visuals->items.end();
if (hasSquare || hasIcon(itemId))
{
pixmap = QPixmap(sizePx, sizePx);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
// No antialiasing: the square's edges are axis-aligned and land on pixel
// boundaries, and smoothing a 1-pixel outline only blurs it. The icon is
// blitted into a slightly smaller rect than it was rasterized at, which is
// what the smooth transform is for.
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
// One pixel short of the pixmap so the square's right and bottom edges land
// inside it rather than on its border.
paintItem(painter, QRectF(0, 0, sizePx - 1, sizePx - 1), itemId);
}
}
else
{
const QByteArray& svg = getSvg(itemId);
if (!svg.isEmpty())
{
QSvgRenderer renderer(svg);
pixmap = QPixmap(sizePx, sizePx);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing, true);
renderer.render(&painter);
}
}
return m_pixmapCache.emplace(key, std::move(pixmap)).first->second;
}
void ItemIconCache::clearPixmapCache()
{
m_pixmapCache.clear();
}

View File

@@ -8,38 +8,76 @@
#include <QPixmap>
#include <QString>
// Rasterizes and caches per-item icon SVGs (REQ-UI-ITEM-ICON). Item icons are
// self-contained, full-color SVGs loaded from a directory, one file per item type
// named after the item's id (e.g. "iron_ore.svg"). Shared by the recipe-selection
// dialog (REQ-UI-RECIPE-ICON) and the game world's belt/port item rendering so the
// rasterization is not duplicated.
class QPainter;
class QRectF;
struct VisualsConfig;
// Rasterizes and caches per-item icon SVGs, and composes them onto the item's colored
// square (REQ-UI-ITEM-ICON). Item icons are self-contained, full-color SVGs loaded from
// a directory, one file per item type named after the item's id (e.g. "iron_ore.svg").
//
// A missing icon file is not an error: hasIcon() returns false for it and the caller
// falls back (a colored square in the world, a name caption in the dialog).
// The square is the item's `fill` and `outline` from visuals.toml with the icon inset
// within it, and it backs the icon both in the game world and wherever the UI displays
// an item as an item: the recipe-selection dialog's option buttons (REQ-UI-RECIPE-ICON),
// the selection panel's item chips (REQ-UI-SINGLE-SELECTION, REQ-UI-HQ-PANEL), and the
// recipe summary (REQ-UI-RECIPE-SUMMARY). Defined here once rather than per widget, in
// two forms: paintItem() for the world's fractional geometry, getSquarePixmap() for
// widgets that want a ready-made pixmap.
//
// The bare icon of getPixmap() has one remaining use: the inline building_block icon
// that stands in for the word "Blocks" beside a number (REQ-UI-BLOCKS-ICON), which is a
// decoration on a line of text rather than an item display and takes no square.
//
// A missing icon file is not an error: hasIcon() returns false for it and the item shows
// its colored square alone.
class ItemIconCache
{
public:
// iconDir is the directory holding the "<item_id>.svg" icon files
// (typically "<configDir>/../icons/items").
explicit ItemIconCache(const QString& iconDir);
// iconDir is the directory holding the "<item_id>.svg" icon files (typically
// "<configDir>/../icons/items"). visuals supplies the per-item square colors and
// must outlive the cache; its contents may be replaced on a restart (REQ-CFG-RELOAD),
// which is what clearPixmapCache() is for.
ItemIconCache(const QString& iconDir, const VisualsConfig* visuals);
// True if an icon SVG file exists for the given item id. Loads the file's bytes
// on first query and remembers the result (including absence) so repeated calls
// are cheap.
bool hasIcon(const std::string& itemId);
// Returns the item's icon rasterized to a transparent sizePx*sizePx pixmap,
// cached per (item id, size) so it is rendered once and reused across frames and
// only re-rasterized when the target size changes (REQ-UI-ITEM-ICON). Returns a
// null pixmap if the item has no icon file (callers should gate on hasIcon()).
// Paints the item's colored square into rect and its icon inset within it
// (REQ-UI-ITEM-ICON). Takes the rect as a QRectF so the world can keep painting at
// sub-pixel geometry. An item with no visuals entry gets no square, one with no icon
// file no icon; with neither, this paints nothing.
void paintItem(QPainter& painter, const QRectF& rect, const std::string& itemId);
// The same composition rasterized to a sizePx*sizePx pixmap for widget use, cached
// per (item id, size) so it is rendered once and reused. Returns a null pixmap only
// when the item has neither a visuals entry nor an icon file, which is the one case
// in which a caller has nothing to show and falls back to text.
QPixmap getSquarePixmap(const std::string& itemId, int sizePx);
// Returns the item's icon alone, without its square, rasterized to a transparent
// sizePx*sizePx pixmap and cached per (item id, size) so it is rendered once and
// reused across frames (REQ-UI-ITEM-ICON). Returns a null pixmap if the item has no
// icon file (callers should gate on hasIcon()).
QPixmap getPixmap(const std::string& itemId, int sizePx);
// Drops every rasterized pixmap. Called when the visuals are reloaded on a restart
// (REQ-CFG-RELOAD), because the composed squares carry the colors they were painted
// with; they are re-rasterized on next use.
void clearPixmapCache();
private:
// Returns the raw SVG bytes for an item id, loading and caching them on first
// access. An absent file caches an empty QByteArray so it is not retried.
const QByteArray& getSvg(const std::string& itemId);
// Shared rasterize-and-cache step. cacheKey distinguishes the bare and squared
// variants of one item within the single pixmap cache.
QPixmap getPixmap(const std::string& cacheKey, const std::string& itemId,
int sizePx, bool withSquare);
QString m_iconDir;
const VisualsConfig* m_visuals; // Not owned; lives in MainWindow.
std::map<std::string, QByteArray> m_svgById;
std::map<std::pair<std::string, int>, QPixmap> m_pixmapCache;
};

View File

@@ -55,7 +55,7 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
// REQ-UI-BUILD-ICON).
const QString configDirPath = QString::fromStdString(m_configDir);
m_itemIcons = std::make_unique<ItemIconCache>(
QDir::cleanPath(configDirPath + "/../icons/items"));
QDir::cleanPath(configDirPath + "/../icons/items"), &m_visuals);
m_buildingIcons = std::make_unique<BuildingIconCache>(
QDir::cleanPath(configDirPath + "/../icons/buildings"));
@@ -283,6 +283,9 @@ std::optional<GameConfig> MainWindow::reloadConfig()
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
m_visuals = std::move(newVisuals);
m_dimOverlay->setDimColor(m_visuals.overlays.modalDim);
// The composed item squares carry the colors they were painted with, so they
// are dropped for the new ones to take effect (REQ-UI-ITEM-ICON).
m_itemIcons->clearPixmapCache();
return newConfig;
}
catch (const std::exception& e)

View File

@@ -127,12 +127,17 @@ RecipeSelectionDialog::RecipeSelectionDialog(
const RecipeSelectionOption& option = options[static_cast<std::size_t>(i)];
QPushButton* button = new QPushButton(this);
// Icon-only when the produced item has an icon (REQ-UI-RECIPE-ICON); otherwise
// fall back to the caption. The name stays reachable via the tooltip.
if (!option.iconItemId.empty() && itemIcons->hasIcon(option.iconItemId))
// Icon-only for a recipe option: the produced item's icon on its colored square,
// or the square alone when the item has no icon file (REQ-UI-RECIPE-ICON). The
// name stays reachable via the tooltip. Options carrying no item at all -- the
// "(None)" entry and the Shipyard's schematics -- keep their text caption, as
// does an item with neither square nor icon to show.
const QPixmap icon = option.iconItemId.empty()
? QPixmap()
: itemIcons->getSquarePixmap(option.iconItemId, kOptionIconSize.width());
if (!icon.isNull())
{
button->setIcon(QIcon(itemIcons->getPixmap(
option.iconItemId, kOptionIconSize.width())));
button->setIcon(QIcon(icon));
button->setIconSize(kOptionIconSize);
}
else

View File

@@ -541,37 +541,14 @@ void WorldRenderer::drawPortItems(QPainter& painter, const WorldCoordinates& coo
void WorldRenderer::drawWorldItem(QPainter& painter, const std::string& itemId,
QPointF center, float halfPx)
{
if (!m_itemIcons) { return; }
// The colored square carrying the item's icon (REQ-GW-TILE-SIZE, REQ-UI-ITEM-ICON).
// The composition lives in the icon cache, which draws it the same way here and in
// the UI's item displays, so an item reads the same on a belt as in a panel.
const QRectF itemRect(center.x() - halfPx, center.y() - halfPx,
halfPx * 2, halfPx * 2);
// The colored square from visuals.toml (REQ-GW-TILE-SIZE) backs every item, icon or
// not: it is what gives the item contrast against the tile beneath it, and its dark
// outline is what separates neighbouring items where they overlap on a belt.
const std::map<std::string, ItemVisuals>::const_iterator it =
m_visuals.items.find(itemId);
if (it != m_visuals.items.end())
{
painter.fillRect(itemRect, it->second.fill);
painter.setPen(QPen(it->second.outline, 1));
painter.setBrush(Qt::NoBrush);
painter.drawRect(itemRect);
}
if (!m_itemIcons || !m_itemIcons->hasIcon(itemId)) { return; }
// The icon goes on top, inset so a frame of the square's color stays visible all
// around it (REQ-UI-ITEM-ICON). The inset is needed because each icon's viewBox is
// cropped tight to its artwork: drawn at the full rect, a solid icon would cover the
// square entirely. It is rasterized once at the inset pixel size and cached, so this
// is a plain pixmap blit per frame.
constexpr double kIconInsetFraction = 0.15;
const double inset = kIconInsetFraction * static_cast<double>(halfPx * 2.0f);
const QRectF iconRect = itemRect.adjusted(inset, inset, -inset, -inset);
int sizePx = qRound(iconRect.width());
if (sizePx < 1) { sizePx = 1; }
painter.drawPixmap(iconRect, m_itemIcons->getPixmap(itemId, sizePx),
QRectF(0, 0, sizePx, sizePx));
m_itemIcons->paintItem(painter, itemRect, itemId);
}
void WorldRenderer::drawBeltItems(QPainter& painter, const WorldCoordinates& coordinates,

View File

@@ -8,8 +8,9 @@
namespace
{
// Size the item icon is drawn at inside a chip, in device-independent pixels.
const int kChipIconSizePx = 18;
// Size the item's colored square is drawn at inside a chip, in device-independent
// pixels. Larger than the artwork it carries, which the square insets (REQ-UI-ITEM-ICON).
const int kChipIconSizePx = 22;
// Chips per row. Two fit the panel's capped width side by side; a third would force the
// counts to shrink.
@@ -64,11 +65,10 @@ void ItemChipRow::rebuildChips(const std::vector<Entry>& entries)
for (std::size_t index = 0; index < entries.size(); ++index)
{
const std::string& itemId = entries[index].itemId;
// A missing icon file is not an error (REQ-UI-ITEM-ICON): the chip is then laid
// out around its count alone.
const QPixmap icon = m_itemIcons->hasIcon(itemId)
? m_itemIcons->getPixmap(itemId, kChipIconSizePx)
: QPixmap();
// The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file
// is not an error: the chip then carries the square alone, and only an item with
// no square either leaves the chip laid out around its count alone.
const QPixmap icon = m_itemIcons->getSquarePixmap(itemId, kChipIconSizePx);
ItemChip* chip = new ItemChip(icon, this);
m_layout->addWidget(chip, static_cast<int>(index) / kChipsPerRow,

View File

@@ -10,8 +10,9 @@
namespace
{
// Size the item icons are drawn at on the summary line, in device-independent pixels.
const int kSummaryIconSizePx = 14;
// Size the items' colored squares are drawn at on the summary line, in device-independent
// pixels. Larger than the artwork they carry, which the square insets (REQ-UI-ITEM-ICON).
const int kSummaryIconSizePx = 18;
// Adds a freshly built label to the summary and shows it.
//
@@ -101,13 +102,15 @@ void RecipeSummaryRow::addAmounts(const std::vector<Amount>& amounts)
{
for (const Amount& entry : amounts)
{
// A missing icon file is not an error (REQ-UI-ITEM-ICON): the item's id then
// stands in for its icon.
if (m_itemIcons->hasIcon(entry.itemId))
// The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file
// is not an error: the square stands alone then, and only an item with no square
// either falls back to its id in text.
const QPixmap icon =
m_itemIcons->getSquarePixmap(entry.itemId, kSummaryIconSizePx);
if (!icon.isNull())
{
QLabel* iconLabel = new QLabel(this);
iconLabel->setPixmap(
m_itemIcons->getPixmap(entry.itemId, kSummaryIconSizePx));
iconLabel->setPixmap(icon);
addAndShow(m_layout, iconLabel);
}
else