Files
z-bar-qt/Plugins/ZShell/Blobs/blobgroup2.cpp
T

383 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);
}
BlobGroup2::BlobGroup2(QQuickItem* parent) : QQuickItem(parent) {
setFlag(ItemHasContents);
}
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 (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;
// Physics runs during polish, before the render pass. The panel content is
// transformed by deformMatrix from QML, so the matrix has to be final for this
// frame; building the data in updatePaintNode would let the transform lag one
// frame behind the material.
polish();
update();
}
void BlobGroup2::updatePolish() {
// The group is the only thing that draws, so it also drives the shapes'
// physics.
ensureDataBuilt();
}
void BlobGroup2::ensurePhysicsUpdated() {
if (m_physicsUpdated) return;
m_physicsUpdated = true;
for (auto* shape : std::as_const(m_shapes))
shape->updatePhysics();
}
void BlobGroup2::ensureDataBuilt() {
if (m_dataBuilt) return;
ensurePhysicsUpdated();
// Physics can request the next frame through markDirty(); the data is
// refreshed here anyway, so the flag is set after it runs.
m_dataBuilt = true;
m_data.rects.clear();
m_bounds = QRectF();
const float smoothFactor = static_cast<float>(m_smoothing);
// The same reach the shader uses to decide whether a rect can contribute, plus
// the shadow, so every pixel the shader can give a value to is inside the node.
const double extent = static_cast<double>(smoothFactor) * 1.5 +
static_cast<double>(m_shadowRadius);
// Group order, no pruning: the shader sees this list for every pixel.
// entryShapes maps a shared entry back to the shape it came from.
QVector<BlobShape2*> entryShapes;
for (auto* shape : std::as_const(m_shapes)) {
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());
// Exclusion gates only the smooth blend.
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 cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh;
const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh;
const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh;
const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - 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];
fTr = std::min(
fTr,
cornerFillFactor(
cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh),
smoothFactor));
fBr = std::min(
fBr,
cornerFillFactor(
cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh),
smoothFactor));
fBl = std::min(
fBl,
cornerFillFactor(
cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh),
smoothFactor));
fTl = std::min(
fTl,
cornerFillFactor(
cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh),
smoothFactor));
}
}
ri.radius[0] = std::max(ri.radius[0] * fTr, minR);
ri.radius[1] = std::max(ri.radius[1] * fBr, minR);
ri.radius[2] = std::max(ri.radius[2] * fBl, minR);
ri.radius[3] = std::max(ri.radius[3] * fTl, minR);
}
// Inverted rect. The frame term is evaluated for every pixel in the bounds,
// so the group flag cannot be gated per shape.
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;
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);
// The single group draw has no owner, so the shader skips the ownership test.
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));
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*) {
// Data is built during polish. This only covers a render pass that happens
// without one.
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);
}
// The shader evaluates the field at the scene position a vertex maps to, so
// the group item itself must not be transformed. The vertex positions are the
// bounds in this item's frame; the material carries the same rect in scene
// coordinates.
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;
}