fix: sidebar bulge at connection with utilities

Basically make sidebar and utilities exclude each other from merging and
manually animate corner radius
Also format c++
This commit is contained in:
2 * r + 2 * t 2026-03-27 20:58:00 +11:00
parent 0f5d2ddc16
commit e8555061ab
12 changed files with 286 additions and 158 deletions

View file

@ -166,6 +166,15 @@ Variants {
panel: panels.sidebar panel: panels.sidebar
bar: bar bar: bar
deformAmount: 0 deformAmount: 0
height: panel.height + 1
exclude: [utilsBg]
bottomLeftRadius: panels.sidebar.visible ? 0 : radius
Behavior on bottomLeftRadius {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
}
}
} }
PanelBg { PanelBg {
@ -193,6 +202,14 @@ Variants {
blobGroup: blobGroup blobGroup: blobGroup
panel: panels.utilities panel: panels.utilities
bar: bar bar: bar
exclude: [sidebarBg]
topLeftRadius: panels.sidebar.visible ? 0 : radius
Behavior on topLeftRadius {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
}
}
} }
PanelBg { PanelBg {

View file

@ -74,20 +74,15 @@ void BlobGroup::markShapeDirty(BlobShape* source) {
// Use cached padded rects to find spatial neighbors // Use cached padded rects to find spatial neighbors
const float pad = static_cast<float>(m_smoothing) * 2.0f; const float pad = static_cast<float>(m_smoothing) * 2.0f;
const QRectF srcRect( const QRectF srcRect(static_cast<double>(source->m_cachedPaddedX - pad),
static_cast<double>(source->m_cachedPaddedX - pad), static_cast<double>(source->m_cachedPaddedY - pad), static_cast<double>(source->m_cachedPaddedW + pad * 2.0f),
static_cast<double>(source->m_cachedPaddedY - pad),
static_cast<double>(source->m_cachedPaddedW + pad * 2.0f),
static_cast<double>(source->m_cachedPaddedH + pad * 2.0f)); static_cast<double>(source->m_cachedPaddedH + pad * 2.0f));
for (auto* shape : std::as_const(m_shapes)) { for (auto* shape : std::as_const(m_shapes)) {
if (shape == source) if (shape == source)
continue; continue;
const QRectF otherRect( const QRectF otherRect(static_cast<double>(shape->m_cachedPaddedX), static_cast<double>(shape->m_cachedPaddedY),
static_cast<double>(shape->m_cachedPaddedX), static_cast<double>(shape->m_cachedPaddedW), static_cast<double>(shape->m_cachedPaddedH));
static_cast<double>(shape->m_cachedPaddedY),
static_cast<double>(shape->m_cachedPaddedW),
static_cast<double>(shape->m_cachedPaddedH));
if (srcRect.intersects(otherRect)) { if (srcRect.intersects(otherRect)) {
shape->polish(); shape->polish();
shape->update(); shape->update();

View file

@ -11,8 +11,7 @@ class BlobInvertedRect;
class BlobGroup : public QObject { class BlobGroup : public QObject {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged)
smoothingChanged)
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
public: public:

View file

@ -13,21 +13,36 @@ BlobInvertedRect::BlobInvertedRect(QQuickItem* parent)
static void setFrameIndices(quint16* idx) { static void setFrameIndices(quint16* idx) {
// Top strip: 0-1-4, 1-5-4 // Top strip: 0-1-4, 1-5-4
idx[0] = 0; idx[1] = 1; idx[2] = 4; idx[0] = 0;
idx[3] = 1; idx[4] = 5; idx[5] = 4; idx[1] = 1;
idx[2] = 4;
idx[3] = 1;
idx[4] = 5;
idx[5] = 4;
// Right strip: 1-2-5, 2-6-5 // Right strip: 1-2-5, 2-6-5
idx[6] = 1; idx[7] = 2; idx[8] = 5; idx[6] = 1;
idx[9] = 2; idx[10] = 6; idx[11] = 5; idx[7] = 2;
idx[8] = 5;
idx[9] = 2;
idx[10] = 6;
idx[11] = 5;
// Bottom strip: 2-3-6, 3-7-6 // Bottom strip: 2-3-6, 3-7-6
idx[12] = 2; idx[13] = 3; idx[14] = 6; idx[12] = 2;
idx[15] = 3; idx[16] = 7; idx[17] = 6; idx[13] = 3;
idx[14] = 6;
idx[15] = 3;
idx[16] = 7;
idx[17] = 6;
// Left strip: 3-0-7, 0-4-7 // Left strip: 3-0-7, 0-4-7
idx[18] = 3; idx[19] = 0; idx[20] = 7; idx[18] = 3;
idx[21] = 0; idx[22] = 4; idx[23] = 7; idx[19] = 0;
idx[20] = 7;
idx[21] = 0;
idx[22] = 4;
idx[23] = 7;
} }
QSGNode* BlobInvertedRect::updatePaintNode( QSGNode* BlobInvertedRect::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
QSGNode* oldNode, UpdatePaintNodeData*) {
if (!m_group) { if (!m_group) {
delete oldNode; delete oldNode;
return nullptr; return nullptr;
@ -55,9 +70,8 @@ QSGNode* BlobInvertedRect::updatePaintNode(
delete oldNode; delete oldNode;
node = new QSGGeometryNode; node = new QSGGeometryNode;
auto* geometry = new QSGGeometry( auto* geometry =
QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, QSGGeometry::UnsignedShortType);
QSGGeometry::UnsignedShortType);
geometry->setDrawingMode(QSGGeometry::DrawTriangles); geometry->setDrawingMode(QSGGeometry::DrawTriangles);
node->setGeometry(geometry); node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry); node->setFlag(QSGNode::OwnsGeometry);
@ -105,13 +119,10 @@ QSGNode* BlobInvertedRect::updatePaintNode(
material->m_color = m_group->color(); material->m_color = m_group->color();
material->m_hasInverted = m_cachedHasInverted ? 1 : 0; material->m_hasInverted = m_cachedHasInverted ? 1 : 0;
material->m_invertedRadius = m_cachedInvertedRadius; material->m_invertedRadius = m_cachedInvertedRadius;
memcpy(material->m_invertedOuter, m_cachedInvertedOuter, memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter));
sizeof(m_cachedInvertedOuter)); memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner));
memcpy(material->m_invertedInner, m_cachedInvertedInner,
sizeof(m_cachedInvertedInner));
const int count = const int count = static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
material->m_rectCount = count; material->m_rectCount = count;
for (int i = 0; i < count; ++i) for (int i = 0; i < count; ++i)
material->m_rects[i] = m_cachedRects[i]; material->m_rects[i] = m_cachedRects[i];

View file

@ -7,14 +7,10 @@
class BlobInvertedRect : public BlobShape { class BlobInvertedRect : public BlobShape {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY borderLeftChanged)
borderLeftChanged) Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY borderRightChanged)
Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY borderTopChanged)
borderRightChanged) Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY borderBottomChanged)
Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY
borderTopChanged)
Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY
borderBottomChanged)
public: public:
explicit BlobInvertedRect(QQuickItem* parent = nullptr); explicit BlobInvertedRect(QQuickItem* parent = nullptr);

View file

@ -7,8 +7,7 @@ QSGMaterialType* BlobMaterial::type() const {
return &s_type; return &s_type;
} }
QSGMaterialShader* BlobMaterial::createShader( QSGMaterialShader* BlobMaterial::createShader(QSGRendererInterface::RenderMode) const {
QSGRendererInterface::RenderMode) const {
return new BlobMaterialShader; return new BlobMaterialShader;
} }
@ -25,8 +24,7 @@ BlobMaterialShader::BlobMaterialShader() {
setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb")); setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb"));
} }
bool BlobMaterialShader::updateUniformData( bool BlobMaterialShader::updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) {
RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) {
Q_UNUSED(oldMaterial); Q_UNUSED(oldMaterial);
auto* mat = static_cast<BlobMaterial*>(newMaterial); auto* mat = static_cast<BlobMaterial*>(newMaterial);
QByteArray* buf = state.uniformData(); QByteArray* buf = state.uniformData();
@ -85,13 +83,13 @@ bool BlobMaterialShader::updateUniformData(
const auto& r = mat->m_rects[i]; const auto& r = mat->m_rects[i];
const int base = 160 + i * 80; const int base = 160 + i * 80;
const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; const float d0[4] = { r.cx, r.cy, r.hw, r.hh };
const float d1[4] = { r.radius, r.offsetX, r.offsetY, r.minEig }; const float d1[4] = { 0.0f, r.offsetX, r.offsetY, r.minEig };
const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f };
memcpy(buf->data() + base, d0, 16); memcpy(buf->data() + base, d0, 16);
memcpy(buf->data() + base + 16, d1, 16); memcpy(buf->data() + base + 16, d1, 16);
memcpy(buf->data() + base + 32, r.invDeform, 16); memcpy(buf->data() + base + 32, r.invDeform, 16);
memcpy(buf->data() + base + 48, d3, 16); memcpy(buf->data() + base + 48, d3, 16);
memcpy(buf->data() + base + 64, r.cornerFill, 16); memcpy(buf->data() + base + 64, r.radius, 16);
} }
return true; return true;

View file

@ -6,22 +6,20 @@
struct BlobRectData { struct BlobRectData {
float cx = 0, cy = 0, hw = 0, hh = 0; float cx = 0, cy = 0, hw = 0, hh = 0;
float radius = 0;
float offsetX = 0, offsetY = 0; float offsetX = 0, offsetY = 0;
float minEig = 1.0f; float minEig = 1.0f;
// Inverse of 2x2 deformation matrix, column-major for GLSL // Inverse of 2x2 deformation matrix, column-major for GLSL
float invDeform[4] = { 1, 0, 0, 1 }; float invDeform[4] = { 1, 0, 0, 1 };
// Screen-space AABB half-extents of the deformed rect // Screen-space AABB half-extents of the deformed rect
float screenHalfX = 0, screenHalfY = 0; float screenHalfX = 0, screenHalfY = 0;
// Pre-computed corner fill factors (tr, br, bl, tl) // Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU
float cornerFill[4] = { 1, 1, 1, 1 }; float radius[4] = { 0, 0, 0, 0 };
}; };
class BlobMaterial : public QSGMaterial { class BlobMaterial : public QSGMaterial {
public: public:
QSGMaterialType* type() const override; QSGMaterialType* type() const override;
QSGMaterialShader* createShader( QSGMaterialShader* createShader(QSGRendererInterface::RenderMode) const override;
QSGRendererInterface::RenderMode) const override;
int compare(const QSGMaterial* other) const override; int compare(const QSGMaterial* other) const override;
float m_paddedX = 0; float m_paddedX = 0;
@ -42,6 +40,5 @@ public:
class BlobMaterialShader : public QSGMaterialShader { class BlobMaterialShader : public QSGMaterialShader {
public: public:
BlobMaterialShader(); BlobMaterialShader();
bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) override;
QSGMaterial* oldMaterial) override;
}; };

View file

@ -17,10 +17,8 @@ void BlobRect::updatePolish() {
if (m_physicsActive) { if (m_physicsActive) {
// Check if deformation is visually imperceptible // Check if deformation is visually imperceptible
float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
+ std::abs(m_dm11 - 1.0f); float totalVel = std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
if (totalDelta < 0.004f && totalVel < 0.05f) { if (totalDelta < 0.004f && totalVel < 0.05f) {
// Snap to rest, no visible deformation // Snap to rest, no visible deformation
@ -62,10 +60,8 @@ void BlobRect::updatePhysics() {
return; return;
} }
const float velX = const float velX = static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt;
static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt; const float velY = static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
const float velY =
static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
m_prevScenePos = scenePos; m_prevScenePos = scenePos;
const float speed = std::sqrt(velX * velX + velY * velY); const float speed = std::sqrt(velX * velX + velY * velY);
@ -86,8 +82,7 @@ void BlobRect::updatePhysics() {
float target11 = 1.0f; float target11 = 1.0f;
if (speed > 5.0f) { if (speed > 5.0f) {
const float targetStretch = const float targetStretch = 1.0f + std::min(speed * kStretchFactor, kMaxStretch);
1.0f + std::min(speed * kStretchFactor, kMaxStretch);
const float targetCompress = 1.0f / targetStretch; const float targetCompress = 1.0f / targetStretch;
const float cosA = velX / speed; const float cosA = velX / speed;
@ -105,35 +100,132 @@ void BlobRect::updatePhysics() {
const float kStiffness = static_cast<float>(m_stiffness); const float kStiffness = static_cast<float>(m_stiffness);
const float kDamping = static_cast<float>(m_damping); const float kDamping = static_cast<float>(m_damping);
const float accel00 = const float accel00 = -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00;
-kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00;
m_dmVel00 += accel00 * dt; m_dmVel00 += accel00 * dt;
m_dm00 += m_dmVel00 * dt; m_dm00 += m_dmVel00 * dt;
const float accel01 = const float accel01 = -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01;
-kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01;
m_dmVel01 += accel01 * dt; m_dmVel01 += accel01 * dt;
m_dm01 += m_dmVel01 * dt; m_dm01 += m_dmVel01 * dt;
const float accel11 = const float accel11 = -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11;
-kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11;
m_dmVel11 += accel11 * dt; m_dmVel11 += accel11 * dt;
m_dm11 += m_dmVel11 * dt; m_dm11 += m_dmVel11 * dt;
m_deformMatrix = QMatrix4x4( m_deformMatrix = QMatrix4x4(m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
checkAtRest(speed); checkAtRest(speed);
} }
void BlobRect::setTopLeftRadius(qreal r) {
if (!qFuzzyCompare(m_topLeftRadius, r)) {
m_topLeftRadius = r;
emit topLeftRadiusChanged();
if (m_group)
m_group->markDirty();
}
}
void BlobRect::setTopRightRadius(qreal r) {
if (!qFuzzyCompare(m_topRightRadius, r)) {
m_topRightRadius = r;
emit topRightRadiusChanged();
if (m_group)
m_group->markDirty();
}
}
void BlobRect::setBottomLeftRadius(qreal r) {
if (!qFuzzyCompare(m_bottomLeftRadius, r)) {
m_bottomLeftRadius = r;
emit bottomLeftRadiusChanged();
if (m_group)
m_group->markDirty();
}
}
void BlobRect::setBottomRightRadius(qreal r) {
if (!qFuzzyCompare(m_bottomRightRadius, r)) {
m_bottomRightRadius = r;
emit bottomRightRadiusChanged();
if (m_group)
m_group->markDirty();
}
}
void BlobRect::cornerRadii(float out[4]) const {
const auto base = static_cast<float>(m_radius);
out[0] = m_topRightRadius >= 0 ? static_cast<float>(m_topRightRadius) : base;
out[1] = m_bottomRightRadius >= 0 ? static_cast<float>(m_bottomRightRadius) : base;
out[2] = m_bottomLeftRadius >= 0 ? static_cast<float>(m_bottomLeftRadius) : base;
out[3] = m_topLeftRadius >= 0 ? static_cast<float>(m_topLeftRadius) : base;
}
bool BlobRect::isExcluded(const BlobShape* other) const {
for (const auto& ptr : m_exclude) {
if (ptr == other)
return true;
}
return false;
}
QQmlListProperty<BlobRect> BlobRect::exclude() {
return QQmlListProperty<BlobRect>(
this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast);
}
void BlobRect::excludeAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect) {
auto* self = static_cast<BlobRect*>(prop->object);
self->m_exclude.append(rect);
if (self->m_group)
self->m_group->markDirty();
emit self->excludeChanged();
}
qsizetype BlobRect::excludeCount(QQmlListProperty<BlobRect>* prop) {
auto* self = static_cast<BlobRect*>(prop->object);
return self->m_exclude.size();
}
BlobRect* BlobRect::excludeAt(QQmlListProperty<BlobRect>* prop, qsizetype index) {
auto* self = static_cast<BlobRect*>(prop->object);
return self->m_exclude.at(index);
}
void BlobRect::excludeClear(QQmlListProperty<BlobRect>* prop) {
auto* self = static_cast<BlobRect*>(prop->object);
if (self->m_exclude.isEmpty())
return;
self->m_exclude.clear();
if (self->m_group)
self->m_group->markDirty();
emit self->excludeChanged();
}
void BlobRect::excludeReplace(QQmlListProperty<BlobRect>* prop, qsizetype index, BlobRect* rect) {
auto* self = static_cast<BlobRect*>(prop->object);
self->m_exclude[index] = rect;
if (self->m_group)
self->m_group->markDirty();
emit self->excludeChanged();
}
void BlobRect::excludeRemoveLast(QQmlListProperty<BlobRect>* prop) {
auto* self = static_cast<BlobRect*>(prop->object);
if (self->m_exclude.isEmpty())
return;
self->m_exclude.removeLast();
if (self->m_group)
self->m_group->markDirty();
emit self->excludeChanged();
}
void BlobRect::checkAtRest(float speed) { void BlobRect::checkAtRest(float speed) {
constexpr float kEpsilon = 0.002f; constexpr float kEpsilon = 0.002f;
const bool atRest = const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon &&
std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && speed < 5.0f;
speed < 5.0f;
if (atRest) { if (atRest) {
m_dm00 = 1.0f; m_dm00 = 1.0f;

View file

@ -3,17 +3,22 @@
#include "blobshape.hpp" #include "blobshape.hpp"
#include <qelapsedtimer.h> #include <qelapsedtimer.h>
#include <qpointer.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmllist.h>
class BlobRect : public BlobShape { class BlobRect : public BlobShape {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY stiffnessChanged)
stiffnessChanged) Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged)
Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged)
Q_PROPERTY(QQmlListProperty<BlobRect> exclude READ exclude NOTIFY excludeChanged)
Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged)
Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged)
Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged)
Q_PROPERTY( Q_PROPERTY(
qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) qreal bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius NOTIFY bottomRightRadiusChanged)
Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY
deformScaleChanged)
public: public:
explicit BlobRect(QQuickItem* parent = nullptr); explicit BlobRect(QQuickItem* parent = nullptr);
@ -46,10 +51,36 @@ public:
} }
} }
QQmlListProperty<BlobRect> exclude();
bool isExcluded(const BlobShape* other) const override;
void cornerRadii(float out[4]) const override;
qreal topLeftRadius() const { return m_topLeftRadius; }
void setTopLeftRadius(qreal r);
qreal topRightRadius() const { return m_topRightRadius; }
void setTopRightRadius(qreal r);
qreal bottomLeftRadius() const { return m_bottomLeftRadius; }
void setBottomLeftRadius(qreal r);
qreal bottomRightRadius() const { return m_bottomRightRadius; }
void setBottomRightRadius(qreal r);
signals: signals:
void stiffnessChanged(); void stiffnessChanged();
void dampingChanged(); void dampingChanged();
void deformScaleChanged(); void deformScaleChanged();
void excludeChanged();
void topLeftRadiusChanged();
void topRightRadiusChanged();
void bottomLeftRadiusChanged();
void bottomRightRadiusChanged();
protected: protected:
void updatePolish() override; void updatePolish() override;
@ -78,4 +109,18 @@ private:
qreal m_stiffness = 200.0; qreal m_stiffness = 200.0;
qreal m_damping = 16.0; qreal m_damping = 16.0;
qreal m_deformScale = 0.0005; qreal m_deformScale = 0.0005;
qreal m_topLeftRadius = -1;
qreal m_topRightRadius = -1;
qreal m_bottomLeftRadius = -1;
qreal m_bottomRightRadius = -1;
QList<QPointer<BlobRect>> m_exclude;
static void excludeAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect);
static qsizetype excludeCount(QQmlListProperty<BlobRect>* prop);
static BlobRect* excludeAt(QQmlListProperty<BlobRect>* prop, qsizetype index);
static void excludeClear(QQmlListProperty<BlobRect>* prop);
static void excludeReplace(QQmlListProperty<BlobRect>* prop, qsizetype index, BlobRect* rect);
static void excludeRemoveLast(QQmlListProperty<BlobRect>* prop);
}; };

View file

@ -19,8 +19,7 @@ static float deformPadding(const QMatrix4x4& dm, float hw, float hh) {
return std::max(extraX, extraY); return std::max(extraX, extraY);
} }
static float cpuSdBox(float px, float py, float cx, float cy, float hw, static float cpuSdBox(float px, float py, float cx, float cy, float hw, float hh) {
float hh) {
const float dx = std::abs(px - cx) - hw; const float dx = std::abs(px - cx) - hw;
const float dy = std::abs(py - cy) - hh; const float dy = std::abs(py - cy) - hh;
const float mdx = std::max(dx, 0.0f); const float mdx = std::max(dx, 0.0f);
@ -66,8 +65,7 @@ void BlobShape::componentComplete() {
registerWithGroup(); registerWithGroup();
} }
void BlobShape::geometryChange( void BlobShape::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) {
const QRectF& newGeometry, const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry); QQuickItem::geometryChange(newGeometry, oldGeometry);
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
if (m_group) { if (m_group) {
@ -92,6 +90,14 @@ void BlobShape::updateCenteredDeformMatrix() {
emit deformMatrixChanged(); emit deformMatrixChanged();
} }
void BlobShape::cornerRadii(float out[4]) const {
const auto r = static_cast<float>(m_radius);
out[0] = r;
out[1] = r;
out[2] = r;
out[3] = r;
}
void BlobShape::registerWithGroup() { void BlobShape::registerWithGroup() {
if (m_group) if (m_group)
m_group->addShape(this); m_group->addShape(this);
@ -127,19 +133,15 @@ void BlobShape::updatePolish() {
m_cachedPaddedY = static_cast<float>(scenePos.y()) - totalPad; m_cachedPaddedY = static_cast<float>(scenePos.y()) - totalPad;
m_cachedPaddedW = static_cast<float>(width()) + 2.0f * totalPad; m_cachedPaddedW = static_cast<float>(width()) + 2.0f * totalPad;
m_cachedPaddedH = static_cast<float>(height()) + 2.0f * totalPad; m_cachedPaddedH = static_cast<float>(height()) + 2.0f * totalPad;
m_localPaddedRect = QRectF(static_cast<double>(-totalPad), m_localPaddedRect = QRectF(static_cast<double>(-totalPad), static_cast<double>(-totalPad),
static_cast<double>(-totalPad), width() + 2.0 * static_cast<double>(totalPad), height() + 2.0 * static_cast<double>(totalPad));
width() + 2.0 * static_cast<double>(totalPad),
height() + 2.0 * static_cast<double>(totalPad));
} }
// Filter nearby normal rects // Filter nearby normal rects
m_cachedRects.clear(); m_cachedRects.clear();
m_cachedMyIndex = -2; m_cachedMyIndex = -2;
const QRectF myPadded(static_cast<double>(m_cachedPaddedX), const QRectF myPadded(static_cast<double>(m_cachedPaddedX), static_cast<double>(m_cachedPaddedY),
static_cast<double>(m_cachedPaddedY), static_cast<double>(m_cachedPaddedW), static_cast<double>(m_cachedPaddedH));
static_cast<double>(m_cachedPaddedW),
static_cast<double>(m_cachedPaddedH));
for (BlobShape* other : m_group->shapes()) { for (BlobShape* other : m_group->shapes()) {
if (other->isInvertedRect()) if (other->isInvertedRect())
@ -149,6 +151,9 @@ void BlobShape::updatePolish() {
if (other->width() <= 0 || other->height() <= 0) if (other->width() <= 0 || other->height() <= 0)
continue; continue;
if (isExcluded(other))
continue;
const QPointF otherScene = other->mapToScene(QPointF(0, 0)); const QPointF otherScene = other->mapToScene(QPointF(0, 0));
bool include = false; bool include = false;
@ -157,12 +162,9 @@ void BlobShape::updatePolish() {
} else { } else {
const float otherHW = static_cast<float>(other->width()) * 0.5f; const float otherHW = static_cast<float>(other->width()) * 0.5f;
const float otherHH = static_cast<float>(other->height()) * 0.5f; const float otherHH = static_cast<float>(other->height()) * 0.5f;
const float otherPad = const float otherPad = pad + deformPadding(other->m_deformMatrix, otherHW, otherHH);
pad + deformPadding(other->m_deformMatrix, otherHW, otherHH); const QRectF otherPadded(otherScene.x() - static_cast<double>(otherPad),
const QRectF otherPadded( otherScene.y() - static_cast<double>(otherPad), other->width() + 2.0 * static_cast<double>(otherPad),
otherScene.x() - static_cast<double>(otherPad),
otherScene.y() - static_cast<double>(otherPad),
other->width() + 2.0 * static_cast<double>(otherPad),
other->height() + 2.0 * static_cast<double>(otherPad)); other->height() + 2.0 * static_cast<double>(otherPad));
include = myPadded.intersects(otherPadded); include = myPadded.intersects(otherPadded);
} }
@ -180,14 +182,13 @@ void BlobShape::updatePolish() {
r.cy = static_cast<float>(otherScene.y() + other->height() / 2.0); r.cy = static_cast<float>(otherScene.y() + other->height() / 2.0);
r.hw = static_cast<float>(other->width() / 2.0); r.hw = static_cast<float>(other->width() / 2.0);
r.hh = static_cast<float>(other->height() / 2.0); r.hh = static_cast<float>(other->height() / 2.0);
r.radius = static_cast<float>(other->radius()); other->cornerRadii(r.radius);
r.offsetX = dm(0, 3); r.offsetX = dm(0, 3);
r.offsetY = dm(1, 3); r.offsetY = dm(1, 3);
// Pre-compute inverse deformation matrix // Pre-compute inverse deformation matrix
const float det = a * d - c * b; const float det = a * d - c * b;
const float invDet = const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f;
std::abs(det) > 1e-6f ? 1.0f / det : 1.0f;
r.invDeform[0] = d * invDet; r.invDeform[0] = d * invDet;
r.invDeform[1] = -b * invDet; r.invDeform[1] = -b * invDet;
r.invDeform[2] = -c * invDet; r.invDeform[2] = -c * invDet;
@ -218,25 +219,15 @@ void BlobShape::updatePolish() {
auto* inv = m_group->invertedRect(); auto* inv = m_group->invertedRect();
if (inv) { if (inv) {
const QPointF invScene = inv->mapToScene(QPointF(0, 0)); const QPointF invScene = inv->mapToScene(QPointF(0, 0));
const float outerCX = const float outerCX = static_cast<float>(invScene.x() + inv->width() / 2.0);
static_cast<float>(invScene.x() + inv->width() / 2.0); const float outerCY = static_cast<float>(invScene.y() + inv->height() / 2.0);
const float outerCY =
static_cast<float>(invScene.y() + inv->height() / 2.0);
const float outerHW = static_cast<float>(inv->width() / 2.0); const float outerHW = static_cast<float>(inv->width() / 2.0);
const float outerHH = static_cast<float>(inv->height() / 2.0); const float outerHH = static_cast<float>(inv->height() / 2.0);
const float innerCX = const float innerCX = outerCX + static_cast<float>((inv->borderLeft() - inv->borderRight()) / 2.0);
outerCX + const float innerCY = outerCY + static_cast<float>((inv->borderTop() - inv->borderBottom()) / 2.0);
static_cast<float>((inv->borderLeft() - inv->borderRight()) / 2.0); const float innerHW = outerHW - static_cast<float>((inv->borderLeft() + inv->borderRight()) / 2.0);
const float innerCY = const float innerHH = outerHH - static_cast<float>((inv->borderTop() + inv->borderBottom()) / 2.0);
outerCY +
static_cast<float>((inv->borderTop() - inv->borderBottom()) / 2.0);
const float innerHW =
outerHW -
static_cast<float>((inv->borderLeft() + inv->borderRight()) / 2.0);
const float innerHH =
outerHH -
static_cast<float>((inv->borderTop() + inv->borderBottom()) / 2.0);
// Check if this rect is near the border (within 2x smoothing of inner edge) // Check if this rect is near the border (within 2x smoothing of inner edge)
bool nearBorder = isInvertedRect(); bool nearBorder = isInvertedRect();
@ -247,10 +238,8 @@ void BlobShape::updatePolish() {
const float myHW = m_cachedPaddedW * 0.5f; const float myHW = m_cachedPaddedW * 0.5f;
const float myHH = m_cachedPaddedH * 0.5f; const float myHH = m_cachedPaddedH * 0.5f;
// Near border if any edge of padded rect is within margin of inner edge // Near border if any edge of padded rect is within margin of inner edge
nearBorder = (myCX - myHW < innerCX - innerHW + margin) || nearBorder = (myCX - myHW < innerCX - innerHW + margin) || (myCX + myHW > innerCX + innerHW - margin) ||
(myCX + myHW > innerCX + innerHW - margin) || (myCY - myHH < innerCY - innerHH + margin) || (myCY + myHH > innerCY + innerHH - margin);
(myCY - myHH < innerCY - innerHH + margin) ||
(myCY + myHH > innerCY + innerHH - margin);
} }
if (nearBorder) { if (nearBorder) {
@ -269,8 +258,9 @@ void BlobShape::updatePolish() {
} }
} }
// Pre-compute corner fill factors (moves O(N²) work from GPU to CPU) // Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU)
const float smoothFactor = pad; const float smoothFactor = pad;
constexpr float minR = 2.0f;
const auto rectCount = m_cachedRects.size(); const auto rectCount = m_cachedRects.size();
for (qsizetype i = 0; i < rectCount; ++i) { for (qsizetype i = 0; i < rectCount; ++i) {
auto& ri = m_cachedRects[i]; auto& ri = m_cachedRects[i];
@ -285,14 +275,10 @@ void BlobShape::updatePolish() {
if (j == i) if (j == i)
continue; continue;
const auto& rj = m_cachedRects[j]; const auto& rj = m_cachedRects[j];
fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh)));
cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh)));
fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh)));
cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh)));
fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor,
cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh)));
fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor,
cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh)));
} }
if (m_cachedHasInverted) { if (m_cachedHasInverted) {
@ -300,20 +286,17 @@ void BlobShape::updatePolish() {
const float icy = m_cachedInvertedInner[1]; const float icy = m_cachedInvertedInner[1];
const float ihw = m_cachedInvertedInner[2]; const float ihw = m_cachedInvertedInner[2];
const float ihh = m_cachedInvertedInner[3]; const float ihh = m_cachedInvertedInner[3];
fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh)));
-cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh))); fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh)));
fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh)));
-cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh))); fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh)));
fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor,
-cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh)));
fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor,
-cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh)));
} }
ri.cornerFill[0] = fTr; // Combine base radii with fill factors into effective per-corner radii
ri.cornerFill[1] = fBr; ri.radius[0] = std::max(ri.radius[0] * fTr, minR);
ri.cornerFill[2] = fBl; ri.radius[1] = std::max(ri.radius[1] * fBr, minR);
ri.cornerFill[3] = fTl; ri.radius[2] = std::max(ri.radius[2] * fBl, minR);
ri.radius[3] = std::max(ri.radius[3] * fTl, minR);
} }
} }
@ -327,8 +310,7 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
if (!node) { if (!node) {
node = new QSGGeometryNode; node = new QSGGeometryNode;
auto* geometry = new QSGGeometry( auto* geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4);
QSGGeometry::defaultAttributes_TexturedPoint2D(), 4);
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
node->setGeometry(geometry); node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry); node->setFlag(QSGNode::OwnsGeometry);
@ -366,13 +348,10 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
material->m_color = m_group->color(); material->m_color = m_group->color();
material->m_hasInverted = m_cachedHasInverted ? 1 : 0; material->m_hasInverted = m_cachedHasInverted ? 1 : 0;
material->m_invertedRadius = m_cachedInvertedRadius; material->m_invertedRadius = m_cachedInvertedRadius;
memcpy(material->m_invertedOuter, m_cachedInvertedOuter, memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter));
sizeof(m_cachedInvertedOuter)); memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner));
memcpy(material->m_invertedInner, m_cachedInvertedInner,
sizeof(m_cachedInvertedInner));
const int count = const int count = static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16)));
material->m_rectCount = count; material->m_rectCount = count;
for (int i = 0; i < count; ++i) for (int i = 0; i < count; ++i)
material->m_rects[i] = m_cachedRects[i]; material->m_rects[i] = m_cachedRects[i];

View file

@ -12,8 +12,7 @@ class BlobShape : public QQuickItem {
Q_OBJECT Q_OBJECT
Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged) Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged)
Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged)
Q_PROPERTY( Q_PROPERTY(QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged)
QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged)
friend class BlobGroup; friend class BlobGroup;
@ -38,13 +37,16 @@ signals:
protected: protected:
void componentComplete() override; void componentComplete() override;
void geometryChange( void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override;
const QRectF& newGeometry, const QRectF& oldGeometry) override;
void updatePolish() override; void updatePolish() override;
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override;
virtual bool isInvertedRect() const { return false; } virtual bool isInvertedRect() const { return false; }
virtual bool isExcluded(const BlobShape* /*other*/) const { return false; }
virtual void cornerRadii(float out[4]) const;
virtual void updatePhysics() {} virtual void updatePhysics() {}
virtual void registerWithGroup(); virtual void registerWithGroup();

View file

@ -80,7 +80,7 @@ void main() {
vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig
vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix
vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0
vec4 fills = rectData[i * 5 + 4]; // f_tr, f_br, f_bl, f_tl vec4 radii = rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl)
// Offset center for asymmetric deformation // Offset center for asymmetric deformation
vec2 center = rect.xy + props.yz; vec2 center = rect.xy + props.yz;
@ -94,10 +94,7 @@ void main() {
mat2 invDeform = mat2(invDm.xy, invDm.zw); mat2 invDeform = mat2(invDm.xy, invDm.zw);
vec2 transformedPixel = center + invDeform * (pixel - center); vec2 transformedPixel = center + invDeform * (pixel - center);
// Use pre-computed corner fill factors // Use pre-computed effective per-corner radii
float br = props.x;
float minR = 2.0;
vec4 radii = max(br * fills, vec4(minR));
float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii);
// Use pre-computed minimum eigenvalue for SDF correction // Use pre-computed minimum eigenvalue for SDF correction