408 lines
13 KiB
C++
408 lines
13 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());
|
|
|
|
// Which shape is the source for the bar edge.
|
|
resolveSources(entryShapes);
|
|
|
|
// 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)));
|
|
}
|
|
|
|
// A point on the bar edge can only have one source. A shape that is not pushing is not
|
|
// the source: it still fills its own area, but inside the border strip it must not add
|
|
// its own outline to a span another shape is already shaping. That is the case that
|
|
// matters — one panel at rest next to one that is bouncing — where the edge showed
|
|
// several steps instead of one smooth indent of the closing panel. Only one shape can be
|
|
// pushing at a time, so the flag is just that, and the shader gates the strip with it.
|
|
void BlobGroup2::resolveSources(const QVector<BlobShape2*>& entryShapes) {
|
|
const int count = static_cast<int>(m_data.rects.size());
|
|
|
|
// With nothing pushing there is no span being shaped, so a shape at rest keeps its
|
|
// own outline and nothing is gated.
|
|
bool anyPushing = false;
|
|
for (int i = 0; i < count; ++i)
|
|
if (entryShapes[i]->isMoving()) {
|
|
anyPushing = true;
|
|
break;
|
|
}
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
m_data.rects[i].source = !anyPushing || entryShapes[i]->isMoving();
|
|
}
|
|
|
|
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;
|
|
}
|