[Game] Repaint cached zones when the hover zoom moves

`TableZone` cuts the hovered card out of its own paint so zone washes,
indicators, dimming and the land divider never draw over the zoom. It is a
`DeviceCoordinateCache` item, so the card changing geometry never dirtied it
and the stale hole stayed transparent, uncovering the black viewport as soon
as the zoom crossed a zone edge.

- Emit `hoverFootprintChanged(old, new)` from `setCardScale()` and once from
  `setHovered(false)`.
- Repaint every overlapping `TableZone` on that signal.
- Clip with the card's painted outline instead of its bounding rectangle, and
  treat a zoom covering the whole zone as "paint nothing".
This commit is contained in:
Lukas Brübach 2026-11-01 03:58:59 +01:00 • committed by Lukas Brübach
parent abbab61e4f
commit 692edb30dd
6 changed files with 97 additions and 29 deletions

View file

@ -19,6 +19,7 @@
#include <QList>
#include <QListIterator>
#include <QPainter>
#include <QPainterPath>
#include <QRectF>
#include <QSharedPointer>
#include <QString>
@ -34,6 +35,7 @@
#include <libcockatrice/game/zones/table_zone_logic.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/zone_names.h>
#include <optional>
#include <string>
namespace google
@ -121,10 +123,14 @@ void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti
// The hovered (zoomed) card may spill over this zone: cut it out of every layer
// below so washes, indicators, dimming and the land divider never paint over it,
// whatever the stacking order is.
const QRect hoverHole = hoveredCardHole();
const bool hasHole = !hoverHole.isNull();
if (hasHole) {
painter->setClipRegion(QRegion(boundingRect().toRect()) - QRegion(hoverHole));
const std::optional<QPainterPath> hoverHole = hoveredCardHole();
const bool clipped = hoverHole.has_value();
if (clipped && hoverHole->isEmpty()) {
// The zoom covers this zone entirely: there is nothing left of it to paint.
return;
}
if (clipped) {
painter->setClipPath(*hoverHole);
}
if (playmatActive) {
@ -152,31 +158,38 @@ void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti
paintLandDivider(painter);
if (hasHole) {
if (clipped) {
painter->setClipping(false);
}
}
/**
* @brief The hovered card's footprint in zone coordinates, or null when it does not
* overlap this zone. Lets every paint layer dodge the zoomed card instead of painting
* over it.
* @brief This zone's paint area minus the hovered card's painted outline, in zone coordinates.
*
* @c std::nullopt when no card is hovered or it does not overlap this zone; an *empty* path when
* the zoom covers this zone entirely, which means nothing of it is left to paint. Lets every
* paint layer dodge the zoomed card instead of painting over it.
*/
QRect TableZone::hoveredCardHole() const
std::optional<QPainterPath> TableZone::hoveredCardHole() const
{
auto *scene = qobject_cast<GameScene *>(this->scene());
if (!scene) {
return {};
return std::nullopt;
}
CardItem *hovered = scene->getHoveredCard();
if (!hovered) {
return {};
return std::nullopt;
}
QRect hole = mapRectFromScene(hovered->sceneBoundingRect()).toRect();
if (!hole.intersects(boundingRect().toRect())) {
return {};
// Cut out the card's painted outline, not its bounding rectangle: the rectangle's corners
// are never covered by the artwork (rounded corners, antialiased edges), so they would stay
// black around the zoom.
const QPainterPath cardOutline = sceneTransform().inverted().map(hovered->sceneTransform().map(hovered->shape()));
if (!cardOutline.boundingRect().intersects(boundingRect())) {
return std::nullopt;
}
return hole & boundingRect().toRect();
QPainterPath zoneArea;
zoneArea.addRect(boundingRect());
return zoneArea.subtracted(cardOutline);
}
void TableZone::onPlaymatChanged(bool active)