Files
z-bar-qt/Plugins/ZShell/Blobs/blobgroup2.cpp
T
zach c5608dc9b9
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 25s
Python / static (pull_request) Successful in 34s
Rust / build (pull_request) Successful in 51s
Python / verify (pull_request) Failing after 1m25s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 59s
fix: blobs now allow to be drawn in device pixels to stay sharp
2026-10-30 01:52:33 +01:00

414 lines
12 KiB
C++

#include "blobgroup2.hpp"
#include "blobinvertedrect2.hpp"
#include "blobshape2.hpp"
#include <qsggeometry.h>
#include <qsgnode.h>
#include <algorithm>
#include <cmath>
#include <cstring>
static float cpuSdBox(
float px, float py, float cx, float cy, float hw, float hh) {
const float dx = std::abs(px - cx) - hw;
const float dy = std::abs(py - cy) - hh;
const float mdx = std::max(dx, 0.0f);
const float mdy = std::max(dy, 0.0f);
return std::sqrt(mdx * mdx + mdy * mdy) + std::min(std::max(dx, dy), 0.0f);
}
static float cpuSmoothstep(float edge0, float edge1, float x) {
const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
static float cornerFillFactor(float sd, float smoothFactor) {
const float outside = cpuSmoothstep(0.0f, smoothFactor, sd);
const float inside = cpuSmoothstep(0.0f, -smoothFactor, sd);
return std::max(outside, inside);
}
namespace {
constexpr int k_pumpMs = 16;
} // namespace
BlobGroup2::BlobGroup2(QQuickItem* parent) : QQuickItem(parent) {
setFlag(ItemHasContents);
m_physicsPump.setInterval(k_pumpMs);
m_physicsPump.setTimerType(Qt::CoarseTimer);
connect(&m_physicsPump, &QTimer::timeout, this, &BlobGroup2::pumpPhysics);
}
BlobGroup2::~BlobGroup2() {
for (auto* shape : std::as_const(m_shapes))
shape->m_group = nullptr;
if (m_invertedRect)
static_cast<BlobShape2*>(m_invertedRect)->m_group = nullptr;
}
void BlobGroup2::setSmoothing(qreal s) {
if (s < 0.5) s = 0.5;
if (qFuzzyCompare(m_smoothing, s)) return;
m_smoothing = s;
emit smoothingChanged();
markDirty();
}
void BlobGroup2::setColor(const QColor& c) {
if (m_color == c) return;
m_color = c;
emit colorChanged();
markDirty();
}
void BlobGroup2::setCornerFill(bool e) {
if (m_cornerFill == e) return;
m_cornerFill = e;
emit cornerFillChanged();
markDirty();
}
void BlobGroup2::setShadowColor(const QColor& c) {
if (m_shadowColor == c) return;
m_shadowColor = c;
emit shadowColorChanged();
markDirty();
}
void BlobGroup2::setShadowRadius(qreal r) {
if (qFuzzyCompare(m_shadowRadius, r)) return;
m_shadowRadius = r;
emit shadowRadiusChanged();
markDirty();
}
void BlobGroup2::addShape(BlobShape2* shape) {
if (!shape || m_shapes.contains(shape)) return;
m_shapes.append(shape);
markDirty();
}
void BlobGroup2::removeShape(BlobShape2* shape) {
m_shapes.removeOne(shape);
markDirty();
}
void BlobGroup2::setInvertedRect(BlobInvertedRect2* rect) {
if (m_invertedRect == rect) return;
m_invertedRect = rect;
markDirty();
}
void BlobGroup2::clearInvertedRect(BlobInvertedRect2* rect) {
if (m_invertedRect != rect) return;
m_invertedRect = nullptr;
markDirty();
}
void BlobGroup2::markDirty() {
m_physicsUpdated = false;
m_dataBuilt = false;
polish();
update();
}
void BlobGroup2::updatePolish() {
ensureDataBuilt();
}
void BlobGroup2::ensurePhysicsUpdated() {
if (m_physicsUpdated) return;
m_physicsUpdated = true;
for (auto* shape : std::as_const(m_shapes))
shape->updatePhysics();
}
void BlobGroup2::requestPhysics() {
if (!m_physicsPump.isActive()) m_physicsPump.start();
}
void BlobGroup2::pumpPhysics() {
for (auto* shape : std::as_const(m_shapes))
shape->updatePhysics();
m_physicsUpdated = true;
const bool anyMoving = std::any_of(
m_shapes.cbegin(), m_shapes.cend(), [](const BlobShape2* shape) {
return shape->needsFrame();
});
if (!anyMoving) {
m_physicsPump.stop();
return;
}
m_dataBuilt = false;
polish();
update();
}
void BlobGroup2::ensureDataBuilt() {
if (m_dataBuilt) return;
ensurePhysicsUpdated();
m_dataBuilt = true;
m_data.rects.clear();
m_bounds = QRectF();
m_data.hasInverted = false;
m_data.invertedRadius = 0;
memset(m_data.invertedOuter, 0, sizeof(m_data.invertedOuter));
memset(m_data.invertedInner, 0, sizeof(m_data.invertedInner));
memset(m_data.invertedOuterRadii, 0, sizeof(m_data.invertedOuterRadii));
const float smoothFactor = static_cast<float>(m_smoothing);
const double extent = static_cast<double>(smoothFactor) * 1.5 +
static_cast<double>(m_shadowRadius);
const bool dropAtRest = m_invertedRect != nullptr;
QVector<BlobShape2*> entryShapes;
for (auto* shape : std::as_const(m_shapes)) {
if (dropAtRest && !shape->isMoving()) continue;
if (shape->width() <= 0 || shape->height() <= 0) continue;
if (static_cast<int>(m_data.rects.size()) >= BlobGroupData::kMaxRects)
continue;
const QPointF scene = shape->mapToScene(QPointF(0, 0));
const QMatrix4x4& dm = shape->m_deformMatrix;
BlobRectData r;
r.cx = static_cast<float>(scene.x() + shape->width() / 2.0);
r.cy = static_cast<float>(scene.y() + shape->height() / 2.0);
r.hw = static_cast<float>(shape->width()) * 0.5f;
r.hh = static_cast<float>(shape->height()) * 0.5f;
shape->cornerRadii(r.radius);
r.offsetX = dm(0, 3);
r.offsetY = dm(1, 3);
const float a = dm(0, 0), b = dm(1, 0);
const float c = dm(0, 1), e = dm(1, 1);
const float det = a * e - c * b;
const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f;
r.invDeform[0] = e * invDet;
r.invDeform[1] = -b * invDet;
r.invDeform[2] = -c * invDet;
r.invDeform[3] = a * invDet;
const float halfTr = 0.5f * (a + e);
const float halfDiff = 0.5f * (a - e);
r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c);
r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh;
r.screenHalfY = std::abs(b) * r.hw + std::abs(e) * r.hh;
m_data.rects.append(r);
m_bounds = m_bounds.united(QRectF(
static_cast<double>(r.cx) + static_cast<double>(r.offsetX) -
static_cast<double>(r.screenHalfX) - extent,
static_cast<double>(r.cy) + static_cast<double>(r.offsetY) -
static_cast<double>(r.screenHalfY) - extent,
2.0 * (static_cast<double>(r.screenHalfX) + extent),
2.0 * (static_cast<double>(r.screenHalfY) + extent)));
entryShapes.append(shape);
}
const int count = static_cast<int>(m_data.rects.size());
for (int i = 0; i < count; ++i) {
int mask = 0;
BlobShape2* const si = entryShapes[i];
for (int j = 0; j < count; ++j) {
if (j == i) continue;
BlobShape2* const sj = entryShapes[j];
if (si->isExcluded(sj) || sj->isExcluded(si)) mask |= (1 << j);
}
m_data.rects[i].excludeMask = mask;
}
constexpr float minR = 2.0f;
const bool cornerFill = m_cornerFill;
for (int i = 0; i < count; ++i) {
auto& ri = m_data.rects[i];
BlobShape2* const si = entryShapes[i];
float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f;
const float dcx = ri.cx + ri.offsetX, dcy = ri.cy + ri.offsetY;
const float cTrX = dcx + ri.hw, cTrY = dcy - ri.hh;
const float cBrX = dcx + ri.hw, cBrY = dcy + ri.hh;
const float cBlX = dcx - ri.hw, cBlY = dcy + ri.hh;
const float cTlX = dcx - ri.hw, cTlY = dcy - ri.hh;
if (cornerFill) {
for (int j = 0; j < count; ++j) {
if (j == i) continue;
if (ri.excludeMask & (1 << j)) continue;
BlobShape2* const sj = entryShapes[j];
if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si))
continue;
const auto& rj = m_data.rects[j];
const float jcx = rj.cx + rj.offsetX;
const float jcy = rj.cy + rj.offsetY;
fTr = std::min(
fTr,
cornerFillFactor(
cpuSdBox(cTrX, cTrY, jcx, jcy, rj.hw, rj.hh),
smoothFactor));
fBr = std::min(
fBr,
cornerFillFactor(
cpuSdBox(cBrX, cBrY, jcx, jcy, rj.hw, rj.hh),
smoothFactor));
fBl = std::min(
fBl,
cornerFillFactor(
cpuSdBox(cBlX, cBlY, jcx, jcy, rj.hw, rj.hh),
smoothFactor));
fTl = std::min(
fTl,
cornerFillFactor(
cpuSdBox(cTlX, cTlY, jcx, jcy, rj.hw, rj.hh),
smoothFactor));
}
}
ri.radius[0] = ri.radius[0] > 0.0f ? std::max(ri.radius[0] * fTr, minR)
: 0.0f;
ri.radius[1] = ri.radius[1] > 0.0f ? std::max(ri.radius[1] * fBr, minR)
: 0.0f;
ri.radius[2] = ri.radius[2] > 0.0f ? std::max(ri.radius[2] * fBl, minR)
: 0.0f;
ri.radius[3] = ri.radius[3] > 0.0f ? std::max(ri.radius[3] * fTl, minR)
: 0.0f;
}
if (!m_invertedRect) return;
auto* inv = static_cast<BlobShape2*>(m_invertedRect);
if (inv->width() <= 0 || inv->height() <= 0) return;
const QPointF invScene = inv->mapToScene(QPointF(0, 0));
const float outerCX = static_cast<float>(invScene.x() + inv->width() / 2.0);
const float outerCY =
static_cast<float>(invScene.y() + inv->height() / 2.0);
const float outerHW = static_cast<float>(inv->width()) * 0.5f;
const float outerHH = static_cast<float>(inv->height()) * 0.5f;
const auto* invRect = m_invertedRect;
const float innerCX =
outerCX + static_cast<float>(
(invRect->borderLeft() - invRect->borderRight()) / 2.0);
const float innerCY =
outerCY + static_cast<float>(
(invRect->borderTop() - invRect->borderBottom()) / 2.0);
const float innerHW =
outerHW - static_cast<float>(
(invRect->borderLeft() + invRect->borderRight()) / 2.0);
const float innerHH =
outerHH - static_cast<float>(
(invRect->borderTop() + invRect->borderBottom()) / 2.0);
m_data.hasInverted = true;
m_data.invertedRadius = static_cast<float>(invRect->radius());
m_data.invertedOuter[0] = outerCX;
m_data.invertedOuter[1] = outerCY;
m_data.invertedOuter[2] = outerHW;
m_data.invertedOuter[3] = outerHH;
m_data.invertedInner[0] = innerCX;
m_data.invertedInner[1] = innerCY;
m_data.invertedInner[2] = innerHW;
m_data.invertedInner[3] = innerHH;
inv->outerCornerRadii(m_data.invertedOuterRadii);
m_bounds = m_bounds.united(QRectF(
static_cast<double>(outerCX) - static_cast<double>(outerHW) - extent,
static_cast<double>(outerCY) - static_cast<double>(outerHH) - extent,
2.0 * (static_cast<double>(outerHW) + extent),
2.0 * (static_cast<double>(outerHH) + extent)));
}
void BlobGroup2::fillMaterial(BlobMaterial* material) {
material->m_paddedX = static_cast<float>(m_bounds.x());
material->m_paddedY = static_cast<float>(m_bounds.y());
material->m_paddedW = static_cast<float>(m_bounds.width());
material->m_paddedH = static_cast<float>(m_bounds.height());
material->m_smoothFactor = static_cast<float>(m_smoothing);
material->m_myIndex = -2;
material->m_color = m_color;
material->m_hasInverted = m_data.hasInverted ? 1 : 0;
material->m_invertedRadius = data().invertedRadius;
memcpy(
material->m_invertedOuter,
data().invertedOuter,
sizeof(data().invertedOuter));
memcpy(
material->m_invertedInner,
data().invertedInner,
sizeof(data().invertedInner));
memcpy(
material->m_invertedOuterRadii,
data().invertedOuterRadii,
sizeof(data().invertedOuterRadii));
material->m_shadowRadius = static_cast<float>(m_shadowRadius);
material->m_shadowColor[0] = static_cast<float>(m_shadowColor.redF());
material->m_shadowColor[1] = static_cast<float>(m_shadowColor.greenF());
material->m_shadowColor[2] = static_cast<float>(m_shadowColor.blueF());
material->m_shadowColor[3] = static_cast<float>(m_shadowColor.alphaF());
const int count =
static_cast<int>(qMin(data().rects.size(), qsizetype(16)));
material->m_rectCount = count;
for (int i = 0; i < count; ++i)
material->m_rects[i] = data().rects[i];
}
QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
ensureDataBuilt();
if (m_data.rects.isEmpty() && !m_data.hasInverted) {
delete oldNode;
return nullptr;
}
auto* node = static_cast<QSGGeometryNode*>(oldNode);
if (!node) {
node = new QSGGeometryNode;
auto* geometry =
new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4);
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);
auto* material = new BlobMaterial;
material->setFlag(QSGMaterial::Blending);
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);
}
auto* v = node->geometry()->vertexDataAsTexturedPoint2D();
const QPointF p0 = mapFromScene(m_bounds.topLeft());
const QPointF p1 = mapFromScene(m_bounds.bottomRight());
v[0].set(static_cast<float>(p0.x()), static_cast<float>(p0.y()), 0.0f, 0.0f);
v[1].set(static_cast<float>(p1.x()), static_cast<float>(p0.y()), 1.0f, 0.0f);
v[2].set(static_cast<float>(p0.x()), static_cast<float>(p1.y()), 0.0f, 1.0f);
v[3].set(static_cast<float>(p1.x()), static_cast<float>(p1.y()), 1.0f, 1.0f);
node->markDirty(QSGNode::DirtyGeometry);
fillMaterial(static_cast<BlobMaterial*>(node->material()));
node->markDirty(QSGNode::DirtyMaterial);
return node;
}