move the debug stats panel out of the renderer
It was the one thing in WorldRenderer positioned in pixels rather than tiles, kept there only to preserve its draw order. With the order agreed not to matter, the world-space / screen-space split becomes exact. The real payoff is translation. drawDebugOverlay was the only caller of tr() in the renderer, so Q_DECLARE_TR_FUNCTIONS and the QCoreApplication include go with it. Nothing left in the renderer draws translatable text - its text is config-driven glyphs, ASCII port arrows and numbers - so it no longer needs a tie to the meta-object system at all. That is the argument I should have weighed originally instead of anchoring on draw order. The panel now paints on top of ships and beams rather than under them, and the widget gates it on its own m_debugDraw directly. The frame still carries isDebugDrawEnabled, because the sensor ranges and target lines it also gates are genuinely world-space. Dropped the painter.resetTransform() the function opened with: it was vestigial from the older design that drew the world through painter.translate, and has been a no-op since every conversion became explicit arithmetic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
@@ -331,7 +331,7 @@ Buildings and the belt subsystem stay outside any entity model regardless of wha
|
||||
|
||||
The game world is drawn into a single `GameWorldView` widget that inherits `QOpenGLWidget` and uses `QPainter` for all drawing. This gives the same imperative paint API as a plain `QWidget` with GPU acceleration, comfortably handling the expected scale (hundreds of ships, thousands of belt items) without blocking the main thread on CPU rasterization.
|
||||
|
||||
The drawing itself lives in `WorldRenderer`, not in the widget. `paintGL` is a call sequence: build the frame's `WorldCoordinates`, hand the renderer a `WorldRenderFrame`, then draw the screen-anchored chrome. The split is the world-space / screen-space line: the renderer draws everything positioned in tiles, while the pause and deconstruct vignettes and the replay overlay — which are anchored to the viewport and never took a `WorldCoordinates` — stay with the widget.
|
||||
The drawing itself lives in `WorldRenderer`, not in the widget. `paintGL` is a call sequence: build the frame's `WorldCoordinates`, hand the renderer a `WorldRenderFrame`, then draw the screen-anchored chrome. The split is the world-space / screen-space line, and it is exact: the renderer draws everything positioned in tiles, while everything positioned in pixels — the pause and deconstruct vignettes, the replay overlay, the debug stats panel — stays with the widget. A useful consequence is that the renderer draws no translatable text at all (its text is config-driven glyphs, ASCII port arrows, and numbers), so it needs no `tr()` and no tie to the meta-object system.
|
||||
|
||||
`WorldRenderFrame` is what makes the renderer independent of the widget. The renderer reads the simulation directly, but everything else it draws is interaction state the widget owns — the selection, the active build mode, live beams, the copy-settings feedback, the box-select rectangle. Those are gathered into the frame each `paintGL` and passed by reference, so the renderer keeps no copy that a later click could invalidate. The renderer knows nothing about input: the widget resolves clicks and hit-tests, and the renderer only draws the result.
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <QPen>
|
||||
#include <QPixmap>
|
||||
#include <QRadialGradient>
|
||||
#include <QFontMetrics>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
|
||||
@@ -405,6 +406,7 @@ void GameWorldView::paintGL()
|
||||
|
||||
// Screen-anchored chrome over the finished world.
|
||||
drawScreenSpace(painter);
|
||||
if (m_debugDraw) { drawDebugOverlay(painter); }
|
||||
drawPauseBorder(painter);
|
||||
drawDeconstructBorder(painter);
|
||||
drawReplayOverlay(painter);
|
||||
@@ -821,6 +823,51 @@ void GameWorldView::applyBeltDragPath()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
void GameWorldView::drawDebugOverlay(QPainter& painter)
|
||||
{
|
||||
const QStringList lines = {
|
||||
tr("Accumulated Threat Level: %1")
|
||||
.arg(m_sim->getThreatLevel(), 0, 'f', 1),
|
||||
tr("Time until Wave: %1s")
|
||||
.arg(ticksToSeconds(m_sim->getNormalGapRemainingTicks()), 0, 'f', 1),
|
||||
tr("Threat Accumulation Rate: %1 threat/s")
|
||||
.arg(m_sim->getThreatAccumulationRate(), 0, 'f', 1),
|
||||
tr("Max Factory Production: %1 threat/s")
|
||||
.arg(m_sim->getMaxFactoryProductionThreatRate(), 0, 'f', 1),
|
||||
tr("Current Factory Production: %1 threat/s")
|
||||
.arg(m_sim->getCurrentFactoryProductionThreatRate(), 0, 'f', 1),
|
||||
};
|
||||
|
||||
QFont font = painter.font();
|
||||
font.setPointSize(m_visuals->toast.fontSize);
|
||||
painter.setFont(font);
|
||||
|
||||
const QFontMetrics fm = painter.fontMetrics();
|
||||
const int lineH = fm.height();
|
||||
const int padding = 8;
|
||||
const int spacing = 4;
|
||||
|
||||
int textW = 0;
|
||||
for (const QString& line : lines)
|
||||
{
|
||||
textW = std::max(textW, fm.horizontalAdvance(line));
|
||||
}
|
||||
const int bgW = textW + padding * 2;
|
||||
const int bgH = lineH * lines.size() + spacing * (lines.size() - 1) + padding * 2;
|
||||
|
||||
const QRect bgRect(padding, padding, bgW, bgH);
|
||||
painter.fillRect(bgRect, QColor(0, 0, 0, 160));
|
||||
|
||||
painter.setPen(Qt::white);
|
||||
int y = padding * 2;
|
||||
for (const QString& line : lines)
|
||||
{
|
||||
const QRect textRect(padding * 2, y, textW, lineH);
|
||||
painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, line);
|
||||
y += lineH + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
void GameWorldView::drawScreenSpace(QPainter& /*painter*/)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -150,6 +150,10 @@ private:
|
||||
// Screen-anchored chrome, drawn after the world (see WorldRenderer): these
|
||||
// need no world transform, which is exactly why they stayed here.
|
||||
void drawScreenSpace(QPainter& painter);
|
||||
// Threat and production readout shown while debug draw is on (F3). Positioned
|
||||
// in pixels at the top-left corner, so it belongs with the chrome rather than
|
||||
// with the world.
|
||||
void drawDebugOverlay(QPainter& painter);
|
||||
// Vignette-style border shown while the game is paused (speed 0x, REQ-UI-PAUSE-BORDER)
|
||||
// to make the paused state hard to miss: a black frame whose alpha fades from 50% at
|
||||
// the viewport edges to 0% toward the center.
|
||||
|
||||
@@ -9,14 +9,12 @@
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <QLinearGradient>
|
||||
#include <QPainter>
|
||||
#include <QPen>
|
||||
#include <QPolygonF>
|
||||
#include <QRegion>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringList>
|
||||
#include <QSvgRenderer>
|
||||
|
||||
#include "AttackBehavior.h"
|
||||
@@ -139,7 +137,6 @@ void WorldRenderer::render(QPainter& painter, const WorldCoordinates& coordinate
|
||||
{
|
||||
drawDebugSensorRanges(painter, coordinates, frame);
|
||||
drawDebugTargetLines(painter, coordinates, frame);
|
||||
drawDebugOverlay(painter);
|
||||
}
|
||||
drawShips(painter, coordinates, frame);
|
||||
drawBeams(painter, coordinates, frame);
|
||||
@@ -819,53 +816,6 @@ void WorldRenderer::drawDebugTargetLines(QPainter& painter, const WorldCoordinat
|
||||
});
|
||||
}
|
||||
|
||||
void WorldRenderer::drawDebugOverlay(QPainter& painter)
|
||||
{
|
||||
painter.resetTransform();
|
||||
|
||||
const QStringList lines = {
|
||||
tr("Accumulated Threat Level: %1")
|
||||
.arg(m_sim.getThreatLevel(), 0, 'f', 1),
|
||||
tr("Time until Wave: %1s")
|
||||
.arg(ticksToSeconds(m_sim.getNormalGapRemainingTicks()), 0, 'f', 1),
|
||||
tr("Threat Accumulation Rate: %1 threat/s")
|
||||
.arg(m_sim.getThreatAccumulationRate(), 0, 'f', 1),
|
||||
tr("Max Factory Production: %1 threat/s")
|
||||
.arg(m_sim.getMaxFactoryProductionThreatRate(), 0, 'f', 1),
|
||||
tr("Current Factory Production: %1 threat/s")
|
||||
.arg(m_sim.getCurrentFactoryProductionThreatRate(), 0, 'f', 1),
|
||||
};
|
||||
|
||||
QFont font = painter.font();
|
||||
font.setPointSize(m_visuals.toast.fontSize);
|
||||
painter.setFont(font);
|
||||
|
||||
const QFontMetrics fm = painter.fontMetrics();
|
||||
const int lineH = fm.height();
|
||||
const int padding = 8;
|
||||
const int spacing = 4;
|
||||
|
||||
int textW = 0;
|
||||
for (const QString& line : lines)
|
||||
{
|
||||
textW = std::max(textW, fm.horizontalAdvance(line));
|
||||
}
|
||||
const int bgW = textW + padding * 2;
|
||||
const int bgH = lineH * lines.size() + spacing * (lines.size() - 1) + padding * 2;
|
||||
|
||||
const QRect bgRect(padding, padding, bgW, bgH);
|
||||
painter.fillRect(bgRect, QColor(0, 0, 0, 160));
|
||||
|
||||
painter.setPen(Qt::white);
|
||||
int y = padding * 2;
|
||||
for (const QString& line : lines)
|
||||
{
|
||||
const QRect textRect(padding * 2, y, textW, lineH);
|
||||
painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, line);
|
||||
y += lineH + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
void WorldRenderer::drawBeams(QPainter& painter, const WorldCoordinates& coordinates,
|
||||
const WorldRenderFrame& frame)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QColor>
|
||||
#include <QPoint>
|
||||
#include <QPointF>
|
||||
@@ -70,16 +69,13 @@ struct WorldRenderFrame
|
||||
//
|
||||
// Reads the simulation and never writes it, and knows nothing about input — the
|
||||
// view resolves clicks and owns the interaction state, and hands the parts the
|
||||
// renderer needs over in a WorldRenderFrame. Screen-anchored chrome that is not
|
||||
// part of the world (the pause and deconstruct vignettes, the replay overlay)
|
||||
// stays with the view, which is also why those never took a WorldCoordinates.
|
||||
// renderer needs over in a WorldRenderFrame.
|
||||
//
|
||||
// Everything here is positioned in tiles. Screen-anchored chrome — the pause and
|
||||
// deconstruct vignettes, the replay overlay, the debug stats panel — stays with
|
||||
// the view, which is why nothing in this class draws translatable text.
|
||||
class WorldRenderer
|
||||
{
|
||||
// Not a QObject, but drawDebugOverlay puts text on screen, so it still needs
|
||||
// tr() for translation (REQ-UI-...): this declares the tr() overloads without
|
||||
// dragging in the meta-object system.
|
||||
Q_DECLARE_TR_FUNCTIONS(WorldRenderer)
|
||||
|
||||
public:
|
||||
// `itemIcons` is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); not
|
||||
// owned, must outlive this renderer. `configDir` is used once, to load the
|
||||
@@ -121,10 +117,6 @@ private:
|
||||
const WorldRenderFrame& frame);
|
||||
void drawOverlays(QPainter& painter, const WorldCoordinates& coordinates,
|
||||
const WorldRenderFrame& frame);
|
||||
// Screen-anchored, but drawn mid-world so ships and effects still paint over
|
||||
// it, as they always have.
|
||||
void drawDebugOverlay(QPainter& painter);
|
||||
|
||||
// Draws a single item centered at widget-space `center`, spanning `halfPx` in
|
||||
// each direction (a half-tile). Uses the item's icon when one exists
|
||||
// (REQ-UI-ITEM-ICON), otherwise falls back to the colored square from
|
||||
|
||||
Reference in New Issue
Block a user