From c5608dc9b94281990a22b95d5f0ffeae04c955df Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 30 Oct 2026 01:52:33 +0100 Subject: [PATCH] fix: blobs now allow to be drawn in device pixels to stay sharp --- Drawers/Panels.qml | 11 +- Modules/Bar/Bar.qml | 3 +- Modules/Bar/Popouts/ClipWrapper.qml | 8 +- Modules/Bar/Popouts/Wrapper.qml | 5 +- Modules/Dashboard/Media/LyricsInfo.qml | 8 +- Modules/Settings/Common/PageBase.qml | 2 +- Modules/Settings/Common/SectionHeader.qml | 3 +- Modules/Settings/Pages/Apps/AllApps.qml | 8 +- Plugins/ZShell/Blobs/blobgroup.cpp | 41 ++++- Plugins/ZShell/Blobs/blobgroup.hpp | 6 + Plugins/ZShell/Blobs/blobgroup2.cpp | 104 +++++++---- Plugins/ZShell/Blobs/blobgroup2.hpp | 20 +- Plugins/ZShell/Blobs/blobinvertedrect.cpp | 22 +-- Plugins/ZShell/Blobs/blobinvertedrect.hpp | 9 +- Plugins/ZShell/Blobs/blobinvertedrect2.cpp | 60 ++++++ Plugins/ZShell/Blobs/blobinvertedrect2.hpp | 56 ++++++ Plugins/ZShell/Blobs/blobmaterial.cpp | 15 -- Plugins/ZShell/Blobs/blobmaterial.hpp | 10 - Plugins/ZShell/Blobs/blobrect.cpp | 204 +++++++++++++-------- Plugins/ZShell/Blobs/blobrect.hpp | 7 +- Plugins/ZShell/Blobs/blobrect2.cpp | 159 +++++++++++----- Plugins/ZShell/Blobs/blobrect2.hpp | 26 ++- Plugins/ZShell/Blobs/blobshape.cpp | 55 ++++-- Plugins/ZShell/Blobs/blobshape.hpp | 8 +- Plugins/ZShell/Blobs/blobshape2.cpp | 8 +- Plugins/ZShell/Blobs/blobshape2.hpp | 20 +- Plugins/ZShell/Blobs/shaders/blob.frag | 129 ++++--------- Plugins/ZShell/Blobs/shaders/frag.spv | Bin 0 -> 24680 bytes Plugins/ZShell/Components/snaplayout.cpp | 184 ++++++++++++++----- Plugins/ZShell/Components/snaplayout.hpp | 38 +++- 30 files changed, 794 insertions(+), 435 deletions(-) create mode 100644 Plugins/ZShell/Blobs/shaders/frag.spv diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 0807b23..6bd1eed 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -154,8 +154,10 @@ Item { property real offsetScale: dashboard.shouldBeActive ? 0 : 1 - anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top + + x: ZUtils.snap((parent.width - implicitWidth) / 2, dashboardWrapper) + clip: offsetScale > 0 implicitHeight: dashboard.implicitHeight * (1 - offsetScale) implicitWidth: dashboard.implicitWidth @@ -194,8 +196,11 @@ Item { Settings.Wrapper { id: settings - anchors.centerIn: parent - anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale + x: ZUtils.snap((parent.width - implicitWidth) / 2, settings) + y: ZUtils.snap((parent.height - implicitHeight) / 2 - (implicitHeight + 5 + (parent.height - implicitHeight) / 2) * offsetScale, settings) + + // anchors.centerIn: parent + // anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale panels: root screen: root.screen screenState: root.screenState diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index ba6cf14..83a95c2 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -8,6 +8,7 @@ import ZShell.Components import qs.Components import qs.Modules.Bar.Components import qs.Modules.Bar.Popouts +import ZShell SnapLayout { id: root @@ -23,7 +24,7 @@ SnapLayout { function axisCenterOf(item: Item): real { const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2); - return horizontal ? Math.floor(c.x) : Math.floor(c.y); + return horizontal ? ZUtils.snap(c.x, item) : ZUtils.snap(c.y, item); } function checkPopout(pos: real): void { diff --git a/Modules/Bar/Popouts/ClipWrapper.qml b/Modules/Bar/Popouts/ClipWrapper.qml index b9471f5..078de91 100644 --- a/Modules/Bar/Popouts/ClipWrapper.qml +++ b/Modules/Bar/Popouts/ClipWrapper.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import qs.Components import ZShell.Config +import ZShell Item { id: root @@ -36,7 +37,7 @@ Item { const diff = parent.width - Math.floor(off + content.nonAnimWidth); if (diff < 0) return off + diff; - return Math.floor(Math.max(off, 0)); + return ZUtils.snap(Math.max(off, 0), root); } y: { if (horizontal) @@ -46,12 +47,11 @@ Item { const diff = parent.height - Math.floor(off + content.nonAnimHeight); if (diff < 0) return off + diff; - return Math.max(off, 0); + return ZUtils.snap(Math.max(off, 0), root); } Behavior on offsetScale { - Anim { - } + Anim {} } Behavior on x { enabled: !root.horizontal || root.offsetScale < 1 diff --git a/Modules/Bar/Popouts/Wrapper.qml b/Modules/Bar/Popouts/Wrapper.qml index 92f2833..be16ac1 100644 --- a/Modules/Bar/Popouts/Wrapper.qml +++ b/Modules/Bar/Popouts/Wrapper.qml @@ -2,6 +2,7 @@ import Quickshell import QtQuick import qs.Components import ZShell.Config +import ZShell Item { id: root @@ -26,8 +27,8 @@ Item { detachedMode = ""; } - implicitHeight: nonAnimHeight - implicitWidth: nonAnimWidth + implicitHeight: ZUtils.snap(nonAnimHeight, root) + implicitWidth: ZUtils.snap(nonAnimWidth, root) Behavior on implicitHeight { enabled: root.offsetScale < 1 diff --git a/Modules/Dashboard/Media/LyricsInfo.qml b/Modules/Dashboard/Media/LyricsInfo.qml index c82939f..e36cad6 100644 --- a/Modules/Dashboard/Media/LyricsInfo.qml +++ b/Modules/Dashboard/Media/LyricsInfo.qml @@ -17,7 +17,7 @@ Item { implicitWidth: btn.implicitWidth * 0.9 implicitHeight: btn.implicitHeight * 0.9 - BlobGroup { + BlobGroup2 { id: blobGroup color: Colors.palette.m3surfaceContainerHighest @@ -29,7 +29,7 @@ Item { } } - BlobRect { + BlobRect2 { id: btnRect anchors.fill: parent @@ -42,7 +42,7 @@ Item { } } - BlobRect { + BlobRect2 { id: rect anchors.right: parent.right @@ -143,8 +143,8 @@ Item { ] SnapLayout { -orientation: Qt.Vertical id: layout + orientation: Qt.Vertical anchors.centerIn: parent spacing: Tokens.spacing.extraSmall diff --git a/Modules/Settings/Common/PageBase.qml b/Modules/Settings/Common/PageBase.qml index 976cfd1..bf0fe35 100644 --- a/Modules/Settings/Common/PageBase.qml +++ b/Modules/Settings/Common/PageBase.qml @@ -9,7 +9,6 @@ import ZShell.Config import qs.Services SnapLayout { -orientation: Qt.Vertical id: root property bool animateScroll: false @@ -69,6 +68,7 @@ orientation: Qt.Vertical } spacing: Tokens.spacing.largeIncreased + orientation: Qt.Vertical Component.onCompleted: applySearchAnchor() diff --git a/Modules/Settings/Common/SectionHeader.qml b/Modules/Settings/Common/SectionHeader.qml index 07c8acb..f0be9e4 100644 --- a/Modules/Settings/Common/SectionHeader.qml +++ b/Modules/Settings/Common/SectionHeader.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Layouts +import ZShell.Components import ZShell.Config import qs.Components import qs.Services @@ -10,7 +11,7 @@ CustomText { Layout.bottomMargin: Tokens.spacing.extraSmall Layout.fillWidth: true Layout.leftMargin: Tokens.padding.extraSmall - Layout.topMargin: first ? 0 : Tokens.spacing.largeIncreased - ((parent as ColumnLayout).spacing ?? 0) + Layout.topMargin: first ? 0 : Tokens.spacing.largeIncreased - ((parent as SnapLayout).spacing ?? 0) color: Colors.palette.m3onSurfaceVariant elide: Text.ElideRight font: Tokens.font.label.medium diff --git a/Modules/Settings/Pages/Apps/AllApps.qml b/Modules/Settings/Pages/Apps/AllApps.qml index 48e4e50..4e16fd5 100644 --- a/Modules/Settings/Pages/Apps/AllApps.qml +++ b/Modules/Settings/Pages/Apps/AllApps.qml @@ -18,10 +18,12 @@ PageBase { title: qsTr("All apps") SnapLayout { -orientation: Qt.Vertical anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top + spacing: Tokens.spacing.extraSmall / 2 + orientation: Qt.Vertical + width: root.cappedWidth Repeater { @@ -54,6 +56,7 @@ orientation: Qt.Vertical anchors.leftMargin: Tokens.padding.largeIncreased anchors.margins: Tokens.padding.small anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium IconImage { @@ -63,8 +66,9 @@ orientation: Qt.Vertical } SnapLayout { -orientation: Qt.Vertical Layout.fillWidth: true + + orientation: Qt.Vertical spacing: 0 CustomText { diff --git a/Plugins/ZShell/Blobs/blobgroup.cpp b/Plugins/ZShell/Blobs/blobgroup.cpp index c286650..b993c73 100644 --- a/Plugins/ZShell/Blobs/blobgroup.cpp +++ b/Plugins/ZShell/Blobs/blobgroup.cpp @@ -2,7 +2,17 @@ #include "blobinvertedrect.hpp" #include "blobshape.hpp" -BlobGroup::BlobGroup(QObject* parent) : QObject(parent) {} +#include + +namespace { +constexpr int k_pumpMs = 16; +} // namespace + +BlobGroup::BlobGroup(QObject* parent) : QObject(parent) { + m_physicsPump.setInterval(k_pumpMs); + m_physicsPump.setTimerType(Qt::CoarseTimer); + connect(&m_physicsPump, &QTimer::timeout, this, &BlobGroup::pumpPhysics); +} BlobGroup::~BlobGroup() { for (auto* shape : std::as_const(m_shapes)) @@ -12,6 +22,7 @@ BlobGroup::~BlobGroup() { } void BlobGroup::setSmoothing(qreal s) { + if (s < 0.5) s = 0.5; if (qFuzzyCompare(m_smoothing, s)) return; m_smoothing = s; emit smoothingChanged(); @@ -73,7 +84,6 @@ void BlobGroup::markShapeDirty(BlobShape* source) { source->polish(); source->update(); - // Use cached padded rects to find spatial neighbors const float pad = static_cast(m_smoothing) * 2.0f; const QRectF srcRect( static_cast(source->m_cachedPaddedX - pad), @@ -103,6 +113,33 @@ void BlobGroup::markShapeDirty(BlobShape* source) { void BlobGroup::ensurePhysicsUpdated() { if (m_physicsUpdated) return; m_physicsUpdated = true; + for (auto* shape : std::as_const(m_shapes)) { + shape->updatePhysics(); + if (shape->needsFrame()) { + shape->polish(); + shape->update(); + } + } +} + +void BlobGroup::requestPhysics() { + if (!m_physicsPump.isActive()) m_physicsPump.start(); +} + +void BlobGroup::pumpPhysics() { for (auto* shape : std::as_const(m_shapes)) shape->updatePhysics(); + + const bool anyMoving = std::any_of( + m_shapes.cbegin(), m_shapes.cend(), [](const BlobShape* shape) { + return shape->needsFrame(); + }); + + if (!anyMoving) { + m_physicsPump.stop(); + return; + } + + markDirty(); + m_physicsUpdated = true; } diff --git a/Plugins/ZShell/Blobs/blobgroup.hpp b/Plugins/ZShell/Blobs/blobgroup.hpp index eb3ca35..a98709b 100644 --- a/Plugins/ZShell/Blobs/blobgroup.hpp +++ b/Plugins/ZShell/Blobs/blobgroup.hpp @@ -4,6 +4,7 @@ #include #include #include +#include class BlobShape; class BlobInvertedRect; @@ -51,16 +52,21 @@ class BlobGroup : public QObject { void markShapeDirty(BlobShape* source); void ensurePhysicsUpdated(); + void requestPhysics(); + signals: void smoothingChanged(); void colorChanged(); void cornerFillChanged(); private: + void pumpPhysics(); + qreal m_smoothing = 32.0; QColor m_color{0x44, 0x88, 0xff}; bool m_cornerFill = true; QList m_shapes; BlobInvertedRect* m_invertedRect = nullptr; bool m_physicsUpdated = false; + QTimer m_physicsPump; }; diff --git a/Plugins/ZShell/Blobs/blobgroup2.cpp b/Plugins/ZShell/Blobs/blobgroup2.cpp index 0f642ed..c298e90 100644 --- a/Plugins/ZShell/Blobs/blobgroup2.cpp +++ b/Plugins/ZShell/Blobs/blobgroup2.cpp @@ -29,8 +29,15 @@ static float cornerFillFactor(float sd, float smoothFactor) { 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() { @@ -41,6 +48,7 @@ BlobGroup2::~BlobGroup2() { } void BlobGroup2::setSmoothing(qreal s) { + if (s < 0.5) s = 0.5; if (qFuzzyCompare(m_smoothing, s)) return; m_smoothing = s; emit smoothingChanged(); @@ -101,17 +109,11 @@ void BlobGroup2::clearInvertedRect(BlobInvertedRect2* rect) { 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(); } @@ -122,32 +124,53 @@ void BlobGroup2::ensurePhysicsUpdated() { 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(); - // 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(); + 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(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(smoothFactor) * 1.5 + static_cast(m_shadowRadius); - // Group order, no pruning by proximity: the shader sees this list for every pixel. A - // shape that is not pushing is not in the list at all: shapes here are data for the - // group draw, so leaving a panel at rest out means it cannot define any part of the - // edge. A closing panel is still in the list while its curve runs out, even with no - // size left, which is what keeps the bounce. - // entryShapes maps a shared entry back to the shape it came from. + const bool dropAtRest = m_invertedRect != nullptr; QVector entryShapes; for (auto* shape : std::as_const(m_shapes)) { - if (!shape->isMoving()) continue; + if (dropAtRest && !shape->isMoving()) continue; if (shape->width() <= 0 || shape->height() <= 0) continue; if (static_cast(m_data.rects.size()) >= BlobGroupData::kMaxRects) continue; @@ -194,7 +217,6 @@ void BlobGroup2::ensureDataBuilt() { const int count = static_cast(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]; @@ -214,10 +236,11 @@ void BlobGroup2::ensureDataBuilt() { 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; + 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) { @@ -227,37 +250,41 @@ void BlobGroup2::ensureDataBuilt() { 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, rj.cx, rj.cy, rj.hw, rj.hh), + cpuSdBox(cTrX, cTrY, jcx, jcy, rj.hw, rj.hh), smoothFactor)); fBr = std::min( fBr, cornerFillFactor( - cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh), + cpuSdBox(cBrX, cBrY, jcx, jcy, rj.hw, rj.hh), smoothFactor)); fBl = std::min( fBl, cornerFillFactor( - cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh), + cpuSdBox(cBlX, cBlY, jcx, jcy, rj.hw, rj.hh), smoothFactor)); fTl = std::min( fTl, cornerFillFactor( - cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh), + cpuSdBox(cTlX, cTlY, jcx, jcy, 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); + 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; } - // 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(m_invertedRect); @@ -297,6 +324,8 @@ void BlobGroup2::ensureDataBuilt() { m_data.invertedInner[2] = innerHW; m_data.invertedInner[3] = innerHH; + inv->outerCornerRadii(m_data.invertedOuterRadii); + m_bounds = m_bounds.united(QRectF( static_cast(outerCX) - static_cast(outerHW) - extent, static_cast(outerCY) - static_cast(outerHH) - extent, @@ -311,7 +340,6 @@ void BlobGroup2::fillMaterial(BlobMaterial* material) { material->m_paddedH = static_cast(m_bounds.height()); material->m_smoothFactor = static_cast(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; @@ -324,6 +352,10 @@ void BlobGroup2::fillMaterial(BlobMaterial* material) { material->m_invertedInner, data().invertedInner, sizeof(data().invertedInner)); + memcpy( + material->m_invertedOuterRadii, + data().invertedOuterRadii, + sizeof(data().invertedOuterRadii)); material->m_shadowRadius = static_cast(m_shadowRadius); material->m_shadowColor[0] = static_cast(m_shadowColor.redF()); @@ -339,8 +371,6 @@ void BlobGroup2::fillMaterial(BlobMaterial* material) { } 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) { @@ -364,10 +394,6 @@ QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { 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()); diff --git a/Plugins/ZShell/Blobs/blobgroup2.hpp b/Plugins/ZShell/Blobs/blobgroup2.hpp index 9f922e6..23ca501 100644 --- a/Plugins/ZShell/Blobs/blobgroup2.hpp +++ b/Plugins/ZShell/Blobs/blobgroup2.hpp @@ -9,22 +9,13 @@ #include #include #include +#include #include class BlobShape2; class BlobInvertedRect2; -// One shared valuation for a whole group, drawn by one node. -// -// The shapes here are data only: they say where the material should be. The group -// draws a single quad over everything the shader can evaluate, so a pixel cannot -// be evaluated by one shape and missed by another, and no second draw can -// disagree with the first. That is what makes overlapping shapes read as one -// surface - one value, one writer, one alpha per pixel. struct BlobGroupData { - // The shader reads rectData[80] = 16 entries, and excludeMask is a 32-bit - // mask. The shared list is capped at 16, in group order, so the cap is the - // same list for every pixel. static constexpr int kMaxRects = 16; QVector rects; @@ -32,6 +23,7 @@ struct BlobGroupData { float invertedRadius = 0; float invertedOuter[4] = {}; float invertedInner[4] = {}; + float invertedOuterRadii[4] = {}; }; class BlobGroup2 : public QQuickItem { @@ -91,10 +83,10 @@ class BlobGroup2 : public QQuickItem { [[nodiscard]] const BlobGroupData& data() const { return m_data; } - // One draw per group, so a change to any shape changes the value for every - // pixel. Refresh the whole group. void markDirty(); + void requestPhysics(); + signals: void smoothingChanged(); void colorChanged(); @@ -110,6 +102,7 @@ class BlobGroup2 : public QQuickItem { void ensurePhysicsUpdated(); void ensureDataBuilt(); void fillMaterial(BlobMaterial* material); + void pumpPhysics(); qreal m_smoothing = 32.0; QColor m_color{0x44, 0x88, 0xff}; @@ -120,9 +113,8 @@ class BlobGroup2 : public QQuickItem { BlobInvertedRect2* m_invertedRect = nullptr; bool m_physicsUpdated = false; bool m_dataBuilt = false; + QTimer m_physicsPump; BlobGroupData m_data; - // Scene-space union of what the shader can evaluate for any shape. The node - // covers exactly this, so gaps are not possible. QRectF m_bounds; }; diff --git a/Plugins/ZShell/Blobs/blobinvertedrect.cpp b/Plugins/ZShell/Blobs/blobinvertedrect.cpp index 0e4336c..2b5f218 100644 --- a/Plugins/ZShell/Blobs/blobinvertedrect.cpp +++ b/Plugins/ZShell/Blobs/blobinvertedrect.cpp @@ -11,28 +11,24 @@ BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {} static void setFrameIndices(quint16* idx) { - // Top strip: 0-1-4, 1-5-4 idx[0] = 0; idx[1] = 1; idx[2] = 4; idx[3] = 1; idx[4] = 5; idx[5] = 4; - // Right strip: 1-2-5, 2-6-5 idx[6] = 1; idx[7] = 2; idx[8] = 5; idx[9] = 2; idx[10] = 6; idx[11] = 5; - // Bottom strip: 2-3-6, 3-7-6 idx[12] = 2; idx[13] = 3; idx[14] = 6; idx[15] = 3; idx[16] = 7; idx[17] = 6; - // Left strip: 3-0-7, 0-4-7 idx[18] = 3; idx[19] = 0; idx[20] = 7; @@ -50,17 +46,20 @@ QSGNode* BlobInvertedRect::updatePaintNode( const float pad = static_cast(m_group->smoothing()); - // Compute inner hole boundary in local coords - // Inset past the inner border edge by 2x smoothing to cover the blend zone const float inset = pad * 2.0f; const float holeLeft = static_cast(m_borderLeft) + inset; const float holeTop = static_cast(m_borderTop) + inset; const float holeRight = static_cast(width() - m_borderRight) - inset; const float holeBot = static_cast(height() - m_borderBottom) - inset; - // If the hole is too small or invalid, fall back to full quad - if (holeLeft >= holeRight || holeTop >= holeBot) + if (holeLeft >= holeRight || holeTop >= holeBot) { + auto* oldGeoNode = static_cast(oldNode); + if (oldNode && oldGeoNode->geometry()->vertexCount() != 4) { + delete oldNode; + oldNode = nullptr; + } return BlobShape::updatePaintNode(oldNode, nullptr); + } auto* node = static_cast(oldNode); @@ -87,7 +86,6 @@ QSGNode* BlobInvertedRect::updatePaintNode( node->setFlag(QSGNode::OwnsMaterial); } - // Outer bounds (local coords) const float x0 = static_cast(m_localPaddedRect.x()); const float y0 = static_cast(m_localPaddedRect.y()); const float x1 = x0 + static_cast(m_localPaddedRect.width()); @@ -95,15 +93,12 @@ QSGNode* BlobInvertedRect::updatePaintNode( const float w = x1 - x0; const float h = y1 - y0; - // Update vertex positions and texture coordinates auto* v = node->geometry()->vertexDataAsTexturedPoint2D(); - // Outer corners v[0].set(x0, y0, 0.0f, 0.0f); v[1].set(x1, y0, 1.0f, 0.0f); v[2].set(x1, y1, 1.0f, 1.0f); v[3].set(x0, y1, 0.0f, 1.0f); - // Inner corners (hole) v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h); v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h); v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h); @@ -111,7 +106,6 @@ QSGNode* BlobInvertedRect::updatePaintNode( node->markDirty(QSGNode::DirtyGeometry); - // Update material uniforms auto* material = static_cast(node->material()); material->m_paddedX = m_cachedPaddedX; material->m_paddedY = m_cachedPaddedY; @@ -218,7 +212,7 @@ void BlobInvertedRect::outerCornerRadii(float out[4]) const { const auto base = std::min(static_cast(m_outerRadius), maxR); out[0] = std::min( m_outerTopRightRadius >= 0 ? static_cast(m_outerTopRightRadius) - : base, + : base, maxR); out[1] = std::min( m_outerBottomRightRadius >= 0 diff --git a/Plugins/ZShell/Blobs/blobinvertedrect.hpp b/Plugins/ZShell/Blobs/blobinvertedrect.hpp index 60121d0..6ef26ef 100644 --- a/Plugins/ZShell/Blobs/blobinvertedrect.hpp +++ b/Plugins/ZShell/Blobs/blobinvertedrect.hpp @@ -59,12 +59,15 @@ class BlobInvertedRect : public BlobShape { void setOuterRadius(qreal v); - // Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect). - [[nodiscard]] qreal outerTopLeftRadius() const { return m_outerTopLeftRadius; } + [[nodiscard]] qreal outerTopLeftRadius() const { + return m_outerTopLeftRadius; + } void setOuterTopLeftRadius(qreal v); - [[nodiscard]] qreal outerTopRightRadius() const { return m_outerTopRightRadius; } + [[nodiscard]] qreal outerTopRightRadius() const { + return m_outerTopRightRadius; + } void setOuterTopRightRadius(qreal v); diff --git a/Plugins/ZShell/Blobs/blobinvertedrect2.cpp b/Plugins/ZShell/Blobs/blobinvertedrect2.cpp index 2972cf2..5f4f078 100644 --- a/Plugins/ZShell/Blobs/blobinvertedrect2.cpp +++ b/Plugins/ZShell/Blobs/blobinvertedrect2.cpp @@ -1,6 +1,8 @@ #include "blobinvertedrect2.hpp" #include "blobgroup2.hpp" +#include + BlobInvertedRect2::BlobInvertedRect2(QQuickItem* parent) : BlobShape2(parent) {} BlobInvertedRect2::~BlobInvertedRect2() { @@ -35,6 +37,64 @@ void BlobInvertedRect2::setBorderBottom(qreal v) { if (m_group) m_group->markDirty(); } +void BlobInvertedRect2::setOuterRadius(qreal v) { + if (qFuzzyCompare(m_outerRadius, v)) return; + m_outerRadius = v; + emit outerRadiusChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect2::setOuterTopLeftRadius(qreal v) { + if (qFuzzyCompare(m_outerTopLeftRadius, v)) return; + m_outerTopLeftRadius = v; + emit outerTopLeftRadiusChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect2::setOuterTopRightRadius(qreal v) { + if (qFuzzyCompare(m_outerTopRightRadius, v)) return; + m_outerTopRightRadius = v; + emit outerTopRightRadiusChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect2::setOuterBottomLeftRadius(qreal v) { + if (qFuzzyCompare(m_outerBottomLeftRadius, v)) return; + m_outerBottomLeftRadius = v; + emit outerBottomLeftRadiusChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect2::setOuterBottomRightRadius(qreal v) { + if (qFuzzyCompare(m_outerBottomRightRadius, v)) return; + m_outerBottomRightRadius = v; + emit outerBottomRightRadiusChanged(); + if (m_group) m_group->markDirty(); +} + +void BlobInvertedRect2::outerCornerRadii(float out[4]) const { + const auto maxR = static_cast(std::min(width(), height())) * 0.5f; + const auto base = std::min(static_cast(m_outerRadius), maxR); + out[0] = std::min( + m_outerTopRightRadius >= 0 ? static_cast(m_outerTopRightRadius) + : base, + maxR); + out[1] = std::min( + m_outerBottomRightRadius >= 0 + ? static_cast(m_outerBottomRightRadius) + : base, + maxR); + out[2] = std::min( + m_outerBottomLeftRadius >= 0 + ? static_cast(m_outerBottomLeftRadius) + : base, + maxR); + out[3] = std::min( + m_outerTopLeftRadius >= 0 ? static_cast(m_outerTopLeftRadius) + : base, + maxR); +} + void BlobInvertedRect2::registerWithGroup() { if (m_group) m_group->setInvertedRect(this); } diff --git a/Plugins/ZShell/Blobs/blobinvertedrect2.hpp b/Plugins/ZShell/Blobs/blobinvertedrect2.hpp index 4b7fb85..6e63172 100644 --- a/Plugins/ZShell/Blobs/blobinvertedrect2.hpp +++ b/Plugins/ZShell/Blobs/blobinvertedrect2.hpp @@ -19,6 +19,21 @@ class BlobInvertedRect2 : public BlobShape2 { Q_PROPERTY( qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY borderBottomChanged) + Q_PROPERTY( + qreal outerRadius READ outerRadius WRITE setOuterRadius NOTIFY + outerRadiusChanged) + Q_PROPERTY( + qreal outerTopLeftRadius READ outerTopLeftRadius WRITE + setOuterTopLeftRadius NOTIFY outerTopLeftRadiusChanged) + Q_PROPERTY( + qreal outerTopRightRadius READ outerTopRightRadius WRITE + setOuterTopRightRadius NOTIFY outerTopRightRadiusChanged) + Q_PROPERTY( + qreal outerBottomLeftRadius READ outerBottomLeftRadius WRITE + setOuterBottomLeftRadius NOTIFY outerBottomLeftRadiusChanged) + Q_PROPERTY( + qreal outerBottomRightRadius READ outerBottomRightRadius WRITE + setOuterBottomRightRadius NOTIFY outerBottomRightRadiusChanged) public: explicit BlobInvertedRect2(QQuickItem* parent = nullptr); @@ -40,13 +55,48 @@ class BlobInvertedRect2 : public BlobShape2 { void setBorderBottom(qreal v); + [[nodiscard]] qreal outerRadius() const { return m_outerRadius; } + + void setOuterRadius(qreal v); + + [[nodiscard]] qreal outerTopLeftRadius() const { + return m_outerTopLeftRadius; + } + + void setOuterTopLeftRadius(qreal v); + + [[nodiscard]] qreal outerTopRightRadius() const { + return m_outerTopRightRadius; + } + + void setOuterTopRightRadius(qreal v); + + [[nodiscard]] qreal outerBottomLeftRadius() const { + return m_outerBottomLeftRadius; + } + + void setOuterBottomLeftRadius(qreal v); + + [[nodiscard]] qreal outerBottomRightRadius() const { + return m_outerBottomRightRadius; + } + + void setOuterBottomRightRadius(qreal v); + signals: void borderLeftChanged(); void borderRightChanged(); void borderTopChanged(); void borderBottomChanged(); + void outerRadiusChanged(); + void outerTopLeftRadiusChanged(); + void outerTopRightRadiusChanged(); + void outerBottomLeftRadiusChanged(); + void outerBottomRightRadiusChanged(); protected: + void outerCornerRadii(float out[4]) const override; + void registerWithGroup() override; void unregisterFromGroup() override; @@ -55,4 +105,10 @@ class BlobInvertedRect2 : public BlobShape2 { qreal m_borderRight = 0; qreal m_borderTop = 0; qreal m_borderBottom = 0; + + qreal m_outerRadius = 0; + qreal m_outerTopLeftRadius = -1; + qreal m_outerTopRightRadius = -1; + qreal m_outerBottomLeftRadius = -1; + qreal m_outerBottomRightRadius = -1; }; diff --git a/Plugins/ZShell/Blobs/blobmaterial.cpp b/Plugins/ZShell/Blobs/blobmaterial.cpp index e3b276f..d4bd70f 100644 --- a/Plugins/ZShell/Blobs/blobmaterial.cpp +++ b/Plugins/ZShell/Blobs/blobmaterial.cpp @@ -44,22 +44,17 @@ bool BlobMaterialShader::updateUniformData( memcpy(buf->data() + 64, &opacity, 4); } - // Padded rect (offset 68) memcpy(buf->data() + 68, &mat->m_paddedX, 4); memcpy(buf->data() + 72, &mat->m_paddedY, 4); memcpy(buf->data() + 76, &mat->m_paddedW, 4); memcpy(buf->data() + 80, &mat->m_paddedH, 4); - // Smooth factor (offset 84) memcpy(buf->data() + 84, &mat->m_smoothFactor, 4); - // Rect count (offset 88) memcpy(buf->data() + 88, &mat->m_rectCount, 4); - // My index (offset 92). -2 skips the ownership test in the shader. memcpy(buf->data() + 92, &mat->m_myIndex, 4); - // Color as vec4 (offset 96, 16 bytes) const float color[4] = { mat->m_color.redF(), mat->m_color.greenF(), @@ -68,26 +63,19 @@ bool BlobMaterialShader::updateUniformData( }; memcpy(buf->data() + 96, color, 16); - // Has inverted (offset 112) memcpy(buf->data() + 112, &mat->m_hasInverted, 4); - // Inverted radius (offset 116) memcpy(buf->data() + 116, &mat->m_invertedRadius, 4); - // Padding at 120-127 (skip) - // Inverted outer (offset 128, 16 bytes) memcpy(buf->data() + 128, mat->m_invertedOuter, 16); - // Inverted inner (offset 144, 16 bytes) memcpy(buf->data() + 144, mat->m_invertedInner, 16); - // Rect data (offset 160, each rect = 5 vec4s = 80 bytes) const int count = qMin(mat->m_rectCount, 16); for (int i = 0; i < count; ++i) { const auto& r = mat->m_rects[i]; const int base = 160 + i * 80; - // Pack excludeMask into props.x via bit-cast (read in shader with floatBitsToInt) float maskAsFloat = NAN; memcpy(&maskAsFloat, &r.excludeMask, sizeof(float)); const float d0[4] = {r.cx, r.cy, r.hw, r.hh}; @@ -100,12 +88,9 @@ bool BlobMaterialShader::updateUniformData( memcpy(buf->data() + base + 64, r.radius, 16); } - // Shadow: after rectData, which ends at 1440. shadowColor is 16-byte aligned, - // so it lands at 1456. memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4); memcpy(buf->data() + 1456, mat->m_shadowColor, 16); - // Outer edge corner radii, after shadowColor, at 1472. memcpy(buf->data() + 1472, mat->m_invertedOuterRadii, 16); return true; diff --git a/Plugins/ZShell/Blobs/blobmaterial.hpp b/Plugins/ZShell/Blobs/blobmaterial.hpp index 7cc35de..82ad03b 100644 --- a/Plugins/ZShell/Blobs/blobmaterial.hpp +++ b/Plugins/ZShell/Blobs/blobmaterial.hpp @@ -8,14 +8,9 @@ struct BlobRectData { float cx = 0, cy = 0, hw = 0, hh = 0; float offsetX = 0, offsetY = 0; float minEig = 1.0f; - // Inverse of 2x2 deformation matrix, column-major for GLSL float invDeform[4] = {1, 0, 0, 1}; - // Screen-space AABB half-extents of the deformed rect float screenHalfX = 0, screenHalfY = 0; - // Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU float radius[4] = {0, 0, 0, 0}; - // Bitmask of indices in this rect's m_cachedRects that mutually exclude (or are excluded by) this rect. - // Used by the shader to skip smin between excluded pairs. int excludeMask = 0; }; @@ -32,19 +27,14 @@ class BlobMaterial : public QSGMaterial { float m_paddedH = 0; float m_smoothFactor = 32.0f; int m_rectCount = 0; - // -2 means no owner test: the single group draw writes every pixel it covers. int m_myIndex = -2; QColor m_color{0x44, 0x88, 0xff}; int m_hasInverted = 0; float m_invertedRadius = 0; float m_invertedOuter[4] = {}; float m_invertedInner[4] = {}; - // Phase 2: shadow around the material edges. Matches the uniform block, which - // puts these after rectData. float m_shadowRadius = 0.0f; float m_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f}; - // Outer-edge corner radii (tr, br, bl, tl). Matches the uniform block, which - // puts this after shadowColor. float m_invertedOuterRadii[4] = {}; BlobRectData m_rects[16] = {}; }; diff --git a/Plugins/ZShell/Blobs/blobrect.cpp b/Plugins/ZShell/Blobs/blobrect.cpp index 3100604..28f6ee2 100644 --- a/Plugins/ZShell/Blobs/blobrect.cpp +++ b/Plugins/ZShell/Blobs/blobrect.cpp @@ -7,19 +7,94 @@ namespace { constexpr float k_retireMs = 120.0f; -} +constexpr qint64 k_minEvalMs = 10; +constexpr float k_staleSampleSec = 0.1f; +constexpr float k_staleMovePx = 2.0f; +constexpr float k_moveSpeed = 5.0f; +constexpr float k_settleDelta = 0.004f; +constexpr float k_settleVel = 0.05f; +constexpr float k_identityEps = 0.0005f; +constexpr float k_stillPx = 0.5f; +constexpr int k_settleStillSamples = 3; +} // namespace -BlobRect::BlobRect(QQuickItem* parent) : BlobShape(parent) {} +BlobRect::BlobRect(QQuickItem* parent) : BlobShape(parent) { + m_stillSamples = k_settleStillSamples; +} BlobRect::~BlobRect() { if (m_group) m_group->removeShape(this); } +bool BlobRect::needsFrame() const { + return m_physicsActive || m_retiring || + m_stillSamples < k_settleStillSamples || + std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + + std::abs(m_dm11 - 1.0f) > + k_identityEps; +} + +void BlobRect::resetDeformMatrix() { + m_dm00 = 1.0f; + m_dm01 = 0.0f; + m_dm11 = 1.0f; + m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; + m_retiring = false; + m_physicsActive = false; + BlobShape::resetDeformMatrix(); +} + +void BlobRect::requestPhysics() { + if (m_group) m_group->requestPhysics(); +} + void BlobRect::updatePolish() { BlobShape::updatePolish(); + if (m_physicsActive || m_retiring) requestPhysics(); +} + +void BlobRect::finishAtIdentity() { + m_dm00 = 1.0f; + m_dm01 = 0.0f; + m_dm11 = 1.0f; + m_deformMatrix = QMatrix4x4(); + m_retiring = false; + m_physicsActive = false; + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); + + if (m_group) m_group->markDirty(); +} + +void BlobRect::beginRetire() { + if (m_retiring) return; + + m_retire00 = m_dm00; + m_retire01 = m_dm01; + m_retire11 = m_dm11; + m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; + m_retireElapsed.start(); + m_retiring = true; + requestPhysics(); +} + +void BlobRect::trySettle(float speed) { + if (m_retiring) return; if (!m_physicsActive) return; + const float totalDelta = + std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f); + const float totalVel = + std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); + + if (speed < k_moveSpeed && totalDelta < k_settleDelta && + totalVel < k_settleVel && m_stillSamples >= k_settleStillSamples) { + beginRetire(); + } +} + +void BlobRect::updatePhysics() { if (m_retiring) { const float t = std::clamp( static_cast(m_retireElapsed.elapsed()) / k_retireMs, @@ -28,100 +103,80 @@ void BlobRect::updatePolish() { const float s = t * t * (3.0f - 2.0f * t); if (t >= 1.0f) { - m_dm00 = 1.0f; - m_dm01 = 0.0f; - m_dm11 = 1.0f; - m_deformMatrix = QMatrix4x4(); - m_retiring = false; - m_physicsActive = false; - } else { - m_dm00 = m_retire00 + (1.0f - m_retire00) * s; - m_dm01 = m_retire01 - m_retire01 * s; - m_dm11 = m_retire11 + (1.0f - m_retire11) * s; - m_deformMatrix = QMatrix4x4( - m_dm00, - m_dm01, - 0, - 0, - m_dm01, - m_dm11, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 1); + finishAtIdentity(); + return; } + m_dm00 = m_retire00 + (1.0f - m_retire00) * s; + m_dm01 = m_retire01 - m_retire01 * s; + m_dm11 = m_retire11 + (1.0f - m_retire11) * s; + m_deformMatrix = QMatrix4x4( + m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); - - if (!m_physicsActive) return; - } else { - const float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + - std::abs(m_dm11 - 1.0f); - const float totalVel = - std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); - - if (totalDelta < 0.004f && totalVel < 0.05f) beginRetire(); } - QMetaObject::invokeMethod( - this, - [this]() { - if (m_physicsActive && m_group) m_group->markDirty(); - }, - Qt::QueuedConnection); -} - -void BlobRect::beginRetire() { - m_retire00 = m_dm00; - m_retire01 = m_dm01; - m_retire11 = m_dm11; - m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; - m_retireElapsed.start(); - m_retiring = true; -} - -void BlobRect::updatePhysics() { const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); + if (!m_physicsActive && !m_retiring) { + const float stuck = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + + std::abs(m_dm11 - 1.0f); + if (stuck > k_identityEps) { + beginRetire(); + return; + } + } + if (!m_hasPrevPos) { m_prevScenePos = scenePos; m_elapsed.start(); m_hasPrevPos = true; + m_stillSamples = k_settleStillSamples; return; } - const float dt = static_cast(m_elapsed.restart()) / 1000.0f; - if (dt > 0.1f || dt < 0.001f) { - m_prevScenePos = scenePos; - if (m_physicsActive) checkAtRest(0.0f); + const qint64 evalMs = m_elapsed.elapsed(); + if (evalMs < k_minEvalMs) { return; } + m_elapsed.restart(); - const float velX = - static_cast(scenePos.x() - m_prevScenePos.x()) / dt; - const float velY = - static_cast(scenePos.y() - m_prevScenePos.y()) / dt; + const float dx = static_cast(scenePos.x() - m_prevScenePos.x()); + const float dy = static_cast(scenePos.y() - m_prevScenePos.y()); m_prevScenePos = scenePos; + if (std::abs(dx) + std::abs(dy) > k_stillPx) + m_stillSamples = 0; + else + m_stillSamples += 1; + + const float dt = static_cast(evalMs) / 1000.0f; + if (dt > k_staleSampleSec) { + if (std::abs(dx) + std::abs(dy) > k_staleMovePx) { + m_physicsActive = true; + requestPhysics(); + } else { + trySettle(0.0f); + } + return; + } + + const float velX = dx / dt; + const float velY = dy / dt; + const float speed = std::sqrt(velX * velX + velY * velY); if (m_retiring) { - if (speed > 5.0f) + if (speed > k_moveSpeed) m_retiring = false; else return; } if (!m_physicsActive) { - if (speed < 5.0f) return; + if (speed < k_moveSpeed) return; m_physicsActive = true; + requestPhysics(); } const float kStretchFactor = static_cast(m_deformScale); @@ -131,7 +186,7 @@ void BlobRect::updatePhysics() { float target01 = 0.0f; float target11 = 1.0f; - if (speed > 5.0f) { + if (speed > k_moveSpeed) { const float targetStretch = 1.0f + std::min(speed * kStretchFactor, kMaxStretch); const float targetCompress = 1.0f / targetStretch; @@ -165,7 +220,7 @@ void BlobRect::updatePhysics() { emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); - checkAtRest(speed); + trySettle(speed); } void BlobRect::setTopLeftRadius(qreal r) { @@ -340,16 +395,3 @@ void BlobRect::excludeCornersRemoveLast(QQmlListProperty* prop) { if (self->m_group) self->m_group->markDirty(); emit self->excludeCornersChanged(); } - -void BlobRect::checkAtRest(float speed) { - if (m_retiring) return; - - constexpr float kEpsilon = 0.002f; - const bool atRest = - 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_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && - speed < 5.0f; - - if (atRest) beginRetire(); -} diff --git a/Plugins/ZShell/Blobs/blobrect.hpp b/Plugins/ZShell/Blobs/blobrect.hpp index 967e339..fde7f75 100644 --- a/Plugins/ZShell/Blobs/blobrect.hpp +++ b/Plugins/ZShell/Blobs/blobrect.hpp @@ -105,15 +105,20 @@ class BlobRect : public BlobShape { protected: void updatePolish() override; void updatePhysics() override; + bool needsFrame() const override; + void resetDeformMatrix() override; private: - void checkAtRest(float speed); + void trySettle(float speed); void beginRetire(); + void finishAtIdentity(); + void requestPhysics(); QPointF m_prevScenePos; QElapsedTimer m_elapsed; bool m_physicsActive = false; bool m_hasPrevPos = false; + int m_stillSamples = 0; bool m_retiring = false; QElapsedTimer m_retireElapsed; diff --git a/Plugins/ZShell/Blobs/blobrect2.cpp b/Plugins/ZShell/Blobs/blobrect2.cpp index 3fdfa28..24824ef 100644 --- a/Plugins/ZShell/Blobs/blobrect2.cpp +++ b/Plugins/ZShell/Blobs/blobrect2.cpp @@ -6,14 +6,59 @@ namespace { constexpr float k_retireMs = 120.0f; +constexpr qint64 k_minEvalMs = 10; +constexpr float k_staleSampleSec = 0.1f; +constexpr float k_staleMovePx = 2.0f; +constexpr float k_moveSpeed = 5.0f; +constexpr float k_settleDelta = 0.004f; +constexpr float k_settleVel = 0.05f; +constexpr float k_identityEps = 0.0005f; +constexpr float k_stillPx = 0.5f; +constexpr int k_settleStillSamples = 3; } // namespace -BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {} +BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) { + m_stillSamples = k_settleStillSamples; +} BlobRect2::~BlobRect2() { if (m_group) m_group->removeShape(this); } +bool BlobRect2::needsFrame() const { + return m_physicsActive || m_retiring || + m_stillSamples < k_settleStillSamples || + std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + + std::abs(m_dm11 - 1.0f) > + k_identityEps; +} + +bool BlobRect2::isMoving() const { + return m_atRestOverride || m_physicsActive || m_retiring || + m_stillSamples < k_settleStillSamples; +} + +void BlobRect2::updateMoving() { + const bool moving = isMoving(); + if (moving == m_movingEmitted) return; + m_movingEmitted = moving; + emit movingChanged(); +} + +void BlobRect2::resetDeformMatrix() { + m_dm00 = 1.0f; + m_dm01 = 0.0f; + m_dm11 = 1.0f; + m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; + m_retiring = false; + m_physicsActive = false; + BlobShape2::resetDeformMatrix(); +} + +void BlobRect2::requestPhysics() { + if (m_group) m_group->requestPhysics(); +} + void BlobRect2::finishAtIdentity() { m_dm00 = 1.0f; m_dm01 = 0.0f; @@ -23,11 +68,12 @@ void BlobRect2::finishAtIdentity() { m_physicsActive = false; emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); + + if (m_group) m_group->markDirty(); + updateMoving(); } void BlobRect2::beginRetire() { - // The settle check would otherwise re-enter this every frame and restart the - // curve timer, so the curve never reaches t = 1. if (m_retiring) return; m_retire00 = m_dm00; @@ -36,10 +82,26 @@ void BlobRect2::beginRetire() { m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; m_retireElapsed.start(); m_retiring = true; + requestPhysics(); + updateMoving(); +} + +void BlobRect2::trySettle(float speed) { + if (m_retiring) return; + if (!m_physicsActive) return; + + const float totalDelta = + std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f); + const float totalVel = + std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); + + if (speed < k_moveSpeed && totalDelta < k_settleDelta && + totalVel < k_settleVel && m_stillSamples >= k_settleStillSamples) { + beginRetire(); + } } void BlobRect2::updatePhysics() { - // Shapes are data only, so the group drives physics here. if (m_retiring) { const float t = std::clamp( static_cast(m_retireElapsed.elapsed()) / k_retireMs, @@ -63,12 +125,16 @@ void BlobRect2::updatePhysics() { if (!m_physicsActive) return; } - // No "not active" guard here: an inactive shape still has to be evaluated so it - // can see the panel start moving and become active. The frame loop is only kept - // alive while active, and it is requested before anything can return early — - // requesting it at the end lets a shape stop being evaluated with the matrix - // still off identity, which is what left the panel contents blurry. - if (m_physicsActive && m_group) m_group->markDirty(); + if (!m_physicsActive && !m_retiring) { + const float stuck = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + + std::abs(m_dm11 - 1.0f); + if (stuck > k_identityEps) { + beginRetire(); + return; + } + } + + if (m_physicsActive) requestPhysics(); const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); @@ -76,34 +142,54 @@ void BlobRect2::updatePhysics() { m_prevScenePos = scenePos; m_elapsed.start(); m_hasPrevPos = true; + m_stillSamples = k_settleStillSamples; + updateMoving(); return; } - const float dt = static_cast(m_elapsed.restart()) / 1000.0f; - if (dt > 0.1f || dt < 0.001f) { - m_prevScenePos = scenePos; - if (m_physicsActive) checkAtRest(0.0f); + const qint64 evalMs = m_elapsed.elapsed(); + if (evalMs < k_minEvalMs) { return; } + m_elapsed.restart(); - const float velX = - static_cast(scenePos.x() - m_prevScenePos.x()) / dt; - const float velY = - static_cast(scenePos.y() - m_prevScenePos.y()) / dt; + const float dx = static_cast(scenePos.x() - m_prevScenePos.x()); + const float dy = static_cast(scenePos.y() - m_prevScenePos.y()); m_prevScenePos = scenePos; + if (std::abs(dx) + std::abs(dy) > k_stillPx) + m_stillSamples = 0; + else + m_stillSamples += 1; + + const float dt = static_cast(evalMs) / 1000.0f; + if (dt > k_staleSampleSec) { + if (std::abs(dx) + std::abs(dy) > k_staleMovePx) { + m_physicsActive = true; + requestPhysics(); + updateMoving(); + } else { + trySettle(0.0f); + } + return; + } + + const float velX = dx / dt; + const float velY = dy / dt; + const float speed = std::sqrt(velX * velX + velY * velY); if (m_retiring) { - if (speed > 5.0f) + if (speed > k_moveSpeed) m_retiring = false; else return; } if (!m_physicsActive) { - if (speed < 5.0f) return; + if (speed < k_moveSpeed) return; m_physicsActive = true; + requestPhysics(); } const float kStretchFactor = static_cast(m_deformScale); @@ -113,7 +199,7 @@ void BlobRect2::updatePhysics() { float target01 = 0.0f; float target11 = 1.0f; - if (speed > 5.0f) { + if (speed > k_moveSpeed) { const float targetStretch = 1.0f + std::min(speed * kStretchFactor, kMaxStretch); const float targetCompress = 1.0f / targetStretch; @@ -147,22 +233,8 @@ void BlobRect2::updatePhysics() { emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); - // Start the retire as soon as the matrix is close to identity, as in v1. Waiting - // for every term to fall below kEpsilon keeps the matrix just off identity for - // longer, and the panel content is transformed by it, which reads as blur on - // text. - const float totalDelta = - std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f); - const float totalVel = - std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); - // The residual is already inside the rest epsilon, so the eased curve would only - // be aborted or restarted every frame by the panel creeping. Snap instead. - if (totalDelta < 0.004f && totalVel < 0.05f) { - finishAtIdentity(); - return; - } - - checkAtRest(speed); + trySettle(speed); + updateMoving(); } void BlobRect2::setTopLeftRadius(qreal r) { @@ -338,16 +410,3 @@ void BlobRect2::excludeCornersRemoveLast(QQmlListProperty* prop) { if (self->m_group) self->m_group->markDirty(); emit self->excludeCornersChanged(); } - -void BlobRect2::checkAtRest(float speed) { - if (m_retiring) return; - - constexpr float kEpsilon = 0.002f; - const bool atRest = - 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_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && - speed < 5.0f; - - if (atRest) beginRetire(); -} diff --git a/Plugins/ZShell/Blobs/blobrect2.hpp b/Plugins/ZShell/Blobs/blobrect2.hpp index 16832f0..c1194ae 100644 --- a/Plugins/ZShell/Blobs/blobrect2.hpp +++ b/Plugins/ZShell/Blobs/blobrect2.hpp @@ -25,6 +25,7 @@ class BlobRect2 : public BlobShape2 { Q_PROPERTY( bool atRestOverride READ atRestOverride WRITE setAtRestOverride NOTIFY atRestOverrideChanged) + Q_PROPERTY(bool moving READ isMoving NOTIFY movingChanged) Q_PROPERTY( qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged) @@ -78,18 +79,13 @@ class BlobRect2 : public BlobShape2 { if (m_atRestOverride == o) return; m_atRestOverride = o; emit atRestOverrideChanged(); + updateMoving(); if (m_group) m_group->markDirty(); } bool isExcluded(const BlobShape2* other) const override; bool isCornerExcluded(const BlobShape2* other) const override; - // The shape that is pushing is the one that can shape the bar edge. A shape that is - // retiring still counts, because the retire curve is still moving the surface. - // atRestOverride keeps the shape in the group's data even at rest; when it is false - // the actual spring state decides, so it is not an override of the state itself. - bool isMoving() const override { - return m_atRestOverride || m_physicsActive || m_retiring; - } + bool isMoving() const override; void cornerRadii(float out[4]) const override; [[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; } @@ -117,6 +113,7 @@ class BlobRect2 : public BlobShape2 { void excludeChanged(); void excludeCornersChanged(); void atRestOverrideChanged(); + void movingChanged(); void topLeftRadiusChanged(); void topRightRadiusChanged(); void bottomLeftRadiusChanged(); @@ -124,20 +121,24 @@ class BlobRect2 : public BlobShape2 { protected: void updatePhysics() override; + bool needsFrame() const override; + void resetDeformMatrix() override; private: - void checkAtRest(float speed); + void trySettle(float speed); void beginRetire(); void finishAtIdentity(); + void requestPhysics(); + void updateMoving(); - // Physics state QPointF m_prevScenePos; QElapsedTimer m_elapsed; bool m_physicsActive = false; bool m_hasPrevPos = false; - // Set from QML. Keeps the shape in the group's data while it is at rest; when it is - // false the spring state decides on its own. + int m_stillSamples = 0; + bool m_movingEmitted = false; + bool m_atRestOverride = false; bool m_retiring = false; @@ -146,13 +147,10 @@ class BlobRect2 : public BlobShape2 { float m_retire01 = 0.0f; float m_retire11 = 1.0f; - // Symmetric 2x2 deformation matrix components (3 independent: m00, m01, - // m11) Rest state is identity: m00=1, m01=0, m11=1 float m_dm00 = 1.0f; float m_dm01 = 0.0f; float m_dm11 = 1.0f; - // Spring velocities for each component float m_dmVel00 = 0.0f; float m_dmVel01 = 0.0f; float m_dmVel11 = 0.0f; diff --git a/Plugins/ZShell/Blobs/blobshape.cpp b/Plugins/ZShell/Blobs/blobshape.cpp index 33fd5b4..aa66401 100644 --- a/Plugins/ZShell/Blobs/blobshape.cpp +++ b/Plugins/ZShell/Blobs/blobshape.cpp @@ -8,6 +8,10 @@ #include #include +namespace { +constexpr int kMaxRects = 16; +} // namespace + static float deformPadding(const QMatrix4x4& dm, float hw, float hh) { const float dm00 = dm(0, 0), dm01 = dm(0, 1); const float dm10 = dm(1, 0), dm11 = dm(1, 1); @@ -45,6 +49,7 @@ BlobShape::BlobShape(QQuickItem* parent) : QQuickItem(parent) { void BlobShape::setGroup(BlobGroup* g) { if (m_group == g) return; if (m_group && isComponentComplete()) unregisterFromGroup(); + if (m_group) resetDeformMatrix(); m_group = g; if (m_group && isComponentComplete()) registerWithGroup(); emit groupChanged(); @@ -87,6 +92,8 @@ void BlobShape::geometryChange( std::abs(m_pendingDh) > syncThreshold) { m_pendingDx = 0; m_pendingDy = 0; + m_pendingDw = 0; + m_pendingDh = 0; m_group->markShapeDirty(this); } } @@ -105,6 +112,12 @@ void BlobShape::updateCenteredDeformMatrix() { } } +void BlobShape::resetDeformMatrix() { + m_deformMatrix = QMatrix4x4(); + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); +} + void BlobShape::cornerRadii(float out[4]) const { const auto maxR = static_cast(std::min(width(), height())) * 0.5f; const auto r = std::min(static_cast(m_radius), maxR); @@ -187,6 +200,12 @@ void BlobShape::updatePolish() { } if (include) { + if (other == this && m_cachedRects.size() >= kMaxRects) { + m_cachedRects.removeLast(); + rectShapes.removeLast(); + } + if (m_cachedRects.size() >= kMaxRects) continue; + if (other == this) m_cachedMyIndex = static_cast(m_cachedRects.size()); @@ -240,8 +259,7 @@ void BlobShape::updatePolish() { m_cachedInvertedRadius = 0; memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter)); memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner)); - memset( - m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii)); + memset(m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii)); auto* inv = m_group->invertedRect(); if (inv) { @@ -308,10 +326,11 @@ void BlobShape::updatePolish() { BlobShape* const si = rectShapes[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; + 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; for (qsizetype j = 0; cornerFill && j < rectCount; ++j) { if (j == i) continue; @@ -319,10 +338,12 @@ void BlobShape::updatePolish() { BlobShape* const sj = rectShapes[j]; if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) continue; const auto& rj = m_cachedRects[j]; - const float sdTr = cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh); - const float sdBr = cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh); - const float sdBl = cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh); - const float sdTl = cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh); + const float jcx = rj.cx + rj.offsetX; + const float jcy = rj.cy + rj.offsetY; + const float sdTr = cpuSdBox(cTrX, cTrY, jcx, jcy, rj.hw, rj.hh); + const float sdBr = cpuSdBox(cBrX, cBrY, jcx, jcy, rj.hw, rj.hh); + const float sdBl = cpuSdBox(cBlX, cBlY, jcx, jcy, rj.hw, rj.hh); + const float sdTl = cpuSdBox(cTlX, cTlY, jcx, jcy, rj.hw, rj.hh); fTr = std::min(fTr, cornerFillFactor(sdTr, smoothFactor)); fBr = std::min(fBr, cornerFillFactor(sdBr, smoothFactor)); fBl = std::min(fBl, cornerFillFactor(sdBl, smoothFactor)); @@ -360,10 +381,14 @@ void BlobShape::updatePolish() { -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); } - 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); + 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; } } @@ -428,7 +453,7 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { sizeof(m_cachedInvertedOuterRadii)); const int count = - static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + static_cast(qMin(m_cachedRects.size(), qsizetype(kMaxRects))); material->m_rectCount = count; for (int i = 0; i < count; ++i) material->m_rects[i] = m_cachedRects[i]; diff --git a/Plugins/ZShell/Blobs/blobshape.hpp b/Plugins/ZShell/Blobs/blobshape.hpp index 71b0510..b06f6ce 100644 --- a/Plugins/ZShell/Blobs/blobshape.hpp +++ b/Plugins/ZShell/Blobs/blobshape.hpp @@ -61,9 +61,6 @@ class BlobShape : public QQuickItem { virtual void cornerRadii(float out[4]) const; - // Outer-edge corner radii. Only the inverted rect draws an outer frame edge, - // so the base has no outer rounding and returns zeros. Subclasses that expose - // outer corner properties override this (order: tr, br, bl, tl). virtual void outerCornerRadii(float out[4]) const { out[0] = 0; out[1] = 0; @@ -73,16 +70,19 @@ class BlobShape : public QQuickItem { virtual void updatePhysics() {} + virtual bool needsFrame() const { return false; } + virtual void registerWithGroup(); virtual void unregisterFromGroup(); void updateCenteredDeformMatrix(); + virtual void resetDeformMatrix(); + BlobGroup* m_group = nullptr; qreal m_radius = 0; QMatrix4x4 m_deformMatrix; // identity by default QMatrix4x4 m_centeredDeformMatrix; - // Cached data from updatePolish float m_cachedPaddedX = 0; float m_cachedPaddedY = 0; float m_cachedPaddedW = 0; diff --git a/Plugins/ZShell/Blobs/blobshape2.cpp b/Plugins/ZShell/Blobs/blobshape2.cpp index 8ce1232..9bb4425 100644 --- a/Plugins/ZShell/Blobs/blobshape2.cpp +++ b/Plugins/ZShell/Blobs/blobshape2.cpp @@ -9,6 +9,7 @@ BlobShape2::BlobShape2(QQuickItem* parent) : QQuickItem(parent) {} void BlobShape2::setGroup(BlobGroup2* g) { if (m_group == g) return; if (m_group && isComponentComplete()) unregisterFromGroup(); + if (m_group) resetDeformMatrix(); m_group = g; if (m_group && isComponentComplete()) registerWithGroup(); emit groupChanged(); @@ -32,7 +33,6 @@ void BlobShape2::geometryChange( QQuickItem::geometryChange(newGeometry, oldGeometry); updateCenteredDeformMatrix(); - // One draw covers the whole group, so any shape change is a group change. if (m_group) m_group->markDirty(); } @@ -49,6 +49,12 @@ void BlobShape2::updateCenteredDeformMatrix() { } } +void BlobShape2::resetDeformMatrix() { + m_deformMatrix = QMatrix4x4(); + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); +} + void BlobShape2::cornerRadii(float out[4]) const { const auto maxR = static_cast(std::min(width(), height())) * 0.5f; const auto r = std::min(static_cast(m_radius), maxR); diff --git a/Plugins/ZShell/Blobs/blobshape2.hpp b/Plugins/ZShell/Blobs/blobshape2.hpp index 230b660..519a1b0 100644 --- a/Plugins/ZShell/Blobs/blobshape2.hpp +++ b/Plugins/ZShell/Blobs/blobshape2.hpp @@ -7,12 +7,6 @@ class BlobGroup2; -// Data-only shape. -// -// The group draws a single node for the whole group, so a shape here contributes -// only what the shader needs: position, radii, exclusion, physics. A shape does -// not create a render node, so no pixel can be evaluated by one shape and missed -// by another. class BlobShape2 : public QQuickItem { Q_OBJECT Q_PROPERTY(BlobGroup2* group READ group WRITE setGroup NOTIFY groupChanged) @@ -56,8 +50,6 @@ class BlobShape2 : public QQuickItem { virtual bool isExcluded(const BlobShape2*) const { return false; } - // Whether this shape is part of the group's data. Only a pushing shape is, so a shape - // at rest has no part in the draw. BlobRect2 also honours the QML atRestOverride. virtual bool isMoving() const { return false; } virtual bool isCornerExcluded(const BlobShape2* /*other*/) const { @@ -66,13 +58,23 @@ class BlobShape2 : public QQuickItem { virtual void cornerRadii(float out[4]) const; - // Called by the group when it rebuilds the shared data, once per frame. + virtual void outerCornerRadii(float out[4]) const { + out[0] = 0; + out[1] = 0; + out[2] = 0; + out[3] = 0; + } + virtual void updatePhysics() {} + virtual bool needsFrame() const { return false; } + virtual void registerWithGroup(); virtual void unregisterFromGroup(); void updateCenteredDeformMatrix(); + virtual void resetDeformMatrix(); + BlobGroup2* m_group = nullptr; qreal m_radius = 0; QMatrix4x4 m_deformMatrix; // identity by default diff --git a/Plugins/ZShell/Blobs/shaders/blob.frag b/Plugins/ZShell/Blobs/shaders/blob.frag index 5e6fdfc..3885251 100644 --- a/Plugins/ZShell/Blobs/shaders/blob.frag +++ b/Plugins/ZShell/Blobs/shaders/blob.frag @@ -30,7 +30,6 @@ float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) { } float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) { - // r = (topRight, bottomRight, bottomLeft, topLeft) p -= center; r.xy = (p.x > 0.0) ? r.xy : r.wz; r.x = (p.y > 0.0) ? r.y : r.x; @@ -44,7 +43,6 @@ float sdBox(vec2 p, vec2 center, vec2 halfSize) { } float smin(float a, float b, float k) { - // Cubic smooth min (C2 continuous — no curvature kinks at blend boundary) float h = max(k - abs(a - b), 0.0) / k; return min(a, b) - h * h * h * k * (1.0 / 6.0); } @@ -55,9 +53,6 @@ float smax(float a, float b, float k) { } float smaxSharpA(float a, float b, float k) { - // smax variant that keeps a's boundary sharp (no inward rounding at a = 0). - // Used for the frame inner side, so the outer edge keeps its own shape and - // always fills to the edges. float h = max(k - abs(a - b), 0.0) / k; float blend = h * h * h * k * (1.0 / 6.0); blend *= smoothstep(0.0, k * 0.5, -a); @@ -67,10 +62,6 @@ float smaxSharpA(float a, float b, float k) { void main() { vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); - // Phase 1: compute per-rect SDF, and track which rect owns the pixel for the - // per-shape path (BlobGroup). The single group draw (BlobGroup2) has no owner: - // myIndex stays -2, so the ownership test is skipped and this pass writes every - // pixel the quad covers. Excluded pairs still need the pairwise pass below. float dArr[16]; int owner = -2; float minDist = 1e10; @@ -84,12 +75,8 @@ void main() { vec4 radii = rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) - // Offset center for asymmetric deformation vec2 center = rect.xy + props.yz; - // AABB early-out: skip rects far from this pixel. The reach has to include the - // shadow, otherwise a rect that is only inside the shadow range would be - // skipped and its distance could not be used. vec2 extent = sh.xy + vec2(smoothFactor * 1.5 + shadowRadius); if (abs(pixel.x - center.x) > extent.x || abs(pixel.y - center.y) > extent.y) { @@ -97,19 +84,13 @@ void main() { continue; } - // Apply pre-computed inverse deformation to the evaluation point mat2 invDeform = mat2(invDm.xy, invDm.zw); vec2 transformedPixel = center + invDeform * (pixel - center); - // Use pre-computed effective per-corner radii float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); - // Use pre-computed minimum eigenvalue for SDF correction d *= max(props.w, 0.01); - // Scale SDF on the axis facing a nearby border to narrow the smin blend - // zone in that direction only, without reducing k (which would cause sharp - // edges). if (hasInverted != 0) { vec2 screenHalf = sh.xy; @@ -122,26 +103,21 @@ void main() { float distX1 = (invertedInner.x + invertedInner.z) - (center.x - screenHalf.x); - // 0 = far from border, 1 = at border (max compression) float yProx = 1.0 - min(smoothstep(0.0, smoothFactor, distY0), smoothstep(0.0, smoothFactor, distY1)); float xProx = 1.0 - min(smoothstep(0.0, smoothFactor, distX0), smoothstep(0.0, smoothFactor, distX1)); - // Smooth axis weights: gradient-based at corners, face-based inside. vec2 q = abs(pixel - center) - screenHalf; vec2 qp = max(q, vec2(0.0)); float cornerLen = length(qp); - // Gradient direction in corner region (smooth 90-degree rotation) float gradX = qp.x / max(cornerLen, 0.001); float gradY = qp.y / max(cornerLen, 0.001); - // Smooth face weights for inside/edge (no hard branch) float faceY = smoothstep(-4.0, 4.0, q.y - q.x); float faceX = 1.0 - faceY; - // Blend: gradient in corner region, face-based inside float t = smoothstep(0.0, 2.0, cornerLen); float xWeight = mix(faceX, gradX, t); float yWeight = mix(faceY, gradY, t); @@ -158,15 +134,11 @@ void main() { } } - // Phase 2: hard-min baseline over all rects. float mergedSdf = 1e10; for (int i = 0; i < rectCount; i++) { mergedSdf = min(mergedSdf, dArr[i]); } - // Phase 3: pair-wise smin contributions, skipping excluded pairs. Pair smin - // <= min, so taking the min over all non-excluded pair smins gives the - // smoothly-merged SDF. for (int i = 0; i < rectCount; i++) { if (dArr[i] >= 1e9) continue; @@ -176,7 +148,6 @@ void main() { continue; if ((excludeMask & (1 << j)) != 0) continue; - // smin only deviates from min within smoothFactor if (abs(dArr[i] - dArr[j]) >= smoothFactor) continue; mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor)); @@ -184,16 +155,12 @@ void main() { } if (hasInverted != 0) { - // Outer edge rounds with its own per-corner radii. With all-zero radii - // sdRoundedBox4 equals sdBox, so a frame that sets no outer radius is - // unchanged. smaxSharpA keeps this boundary authoritative for the fill. float dOuter = sdRoundedBox4(pixel, invertedOuter.xy, invertedOuter.zw, invertedOuterRadii) - 1.0; float dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); - // Border sinks: track the opposite rect edge, clamped to border thickness float innerTop = invertedInner.y - invertedInner.w; float innerBot = invertedInner.y + invertedInner.w; float innerLeft = invertedInner.x - invertedInner.z; @@ -203,62 +170,51 @@ void main() { float outerLeft = invertedOuter.x - invertedOuter.z; float outerRight = invertedOuter.x + invertedOuter.z; + float topZone = + 1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y); + float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y); + float leftZone = + 1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x); + float rightZone = + smoothstep(innerRight - smoothFactor, innerRight, pixel.x); + float sinkValue = 0.0; - for (int i = 0; i < rectCount; i++) { - vec4 rect = rectData[i * 5]; - vec4 sinkProps = rectData[i * 5 + 1]; - vec2 sinkSh = rectData[i * 5 + 3].xy; + float preOff = smoothFactor * (1.0 / 6.0); + float s = smoothFactor * 2.0; - // Screen-space center (with offset) and pre-computed AABB half-extents - vec2 ctr = rect.xy + sinkProps.yz; + if (topZone > 0.0 || botZone > 0.0 || leftZone > 0.0 || rightZone > 0.0) { + for (int i = 0; i < rectCount; i++) { + vec4 rect = rectData[i * 5]; + vec4 sinkProps = rectData[i * 5 + 1]; + vec2 sinkSh = rectData[i * 5 + 3].xy; - // Delay sink to absorb smin blend depth (cubic smin max = k/6) - float preOff = smoothFactor * (1.0 / 6.0); + vec2 ctr = rect.xy + sinkProps.yz; - // Top border: track rect's BOTTOM edge, only within border thickness - float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, - innerTop - outerTop); + float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, + innerTop - outerTop); - // Bottom border: track rect's TOP edge - float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0, - outerBot - innerBot); + float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0, + outerBot - innerBot); - // Left border: track rect's RIGHT edge - float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0, - innerLeft - outerLeft); + float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0, + innerLeft - outerLeft); - // Right border: track rect's LEFT edge - float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0, - outerRight - innerRight); + float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0, + outerRight - innerRight); - // Lateral distance from pixel to rect's extent along each edge - float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0); - float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0); + float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0); + float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0); - // Perpendicular proximity: full strength in border, fade inside inner - // area - float topZone = - 1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y); - float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y); - float leftZone = - 1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x); - float rightZone = - smoothstep(innerRight - smoothFactor, innerRight, pixel.x); - - float s = smoothFactor * 2.0; - float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone, - botPen * smoothstep(s, 0.0, hLat) * botZone), - max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, - rightPen * smoothstep(s, 0.0, vLat) * rightZone)); - sinkValue = max(sinkValue, sink); + float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone, + botPen * smoothstep(s, 0.0, hLat) * botZone), + max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, + rightPen * smoothstep(s, 0.0, vLat) * rightZone)); + sinkValue = max(sinkValue, sink); + } } dInner -= sinkValue; - // dOuter is already a true circle (sdRoundedBox4). Keep it exact whenever the - // outer arc is the active boundary; a smax term added there would pull the - // arc's sides inward (curvature/tangent mismatch), so the corner would not be - // a circle. Only blend on the inner side, where the inner boundary is active. float negInner = -dInner; float dFrame = dOuter >= negInner ? dOuter : smaxSharpA(dOuter, negInner, smoothFactor); @@ -269,37 +225,22 @@ void main() { } } - // A pixel this far from every rect cannot have a value, so it is not written. - // The one-unit margin keeps the outermost pixels of the quad out of the AA: their - // derivatives are undefined (the quad neighbour is outside the primitive), which - // is what drew a line along the coverage edge. float reach = max(smoothFactor, shadowRadius); - if (mergedSdf > reach - 1.0) + float pxWidth = max(length(vec2(dFdx(mergedSdf), dFdy(mergedSdf))), 1e-5); + if (mergedSdf > reach - max(2.0 * pxWidth, 1.0)) discard; - // Only the owner writes a pixel, which is what makes the per-shape path look - // like one surface: qt_Opacity is applied once per pixel instead of once per - // overlapping draw. The single group draw has no owner (myIndex == -2), so the - // test is skipped and the value below is the only value a pixel can have. if (myIndex >= -1 && owner != myIndex) discard; - // Fixed-width AA: fwidth is capped at one unit, so a bad derivative near the quad - // boundary cannot make a wide edge. - float fw = min(fwidth(mergedSdf), 1.0); - float materialAlpha = 1.0 - smoothstep(-fw, fw, mergedSdf); + float materialAlpha = clamp(0.5 - mergedSdf / pxWidth, 0.0, 1.0); - // Phase 2: the shadow, as a distance falloff from the merged surface. The - // quadratic reaches zero with zero slope at its outer edge, so that edge needs no - // AA of its own. Only the single group draw writes a shadow: in the per-shape - // path overlapping draws would darken the same area twice. float shadowAlpha = 0.0; if (shadowRadius > 0.0 && myIndex == -2) { float t = clamp(mergedSdf / shadowRadius, 0.0, 1.0); shadowAlpha = (1.0 - t) * (1.0 - t) * shadowColor.a; } - // Material over shadow, premultiplied. float totalAlpha = materialAlpha + shadowAlpha * (1.0 - materialAlpha); vec3 rgb = color.rgb * materialAlpha + shadowColor.rgb * shadowAlpha * (1.0 - materialAlpha); diff --git a/Plugins/ZShell/Blobs/shaders/frag.spv b/Plugins/ZShell/Blobs/shaders/frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..0c52ace5daa8bfbdfe1ad0853a1f85e7704ad6ae GIT binary patch literal 24680 zcmZvk2b^71wZ<=*Ng;#~dT1dbGzAfm-UCR21`-I;q)jrDWFVOtW+o66C6OWuVo*>7 zl&DBYijATWq^dN1AfTuOrK_lP1oHm>d(Ss)?~n8De*62rwf0)OopaB=8EF|berT;_ zx!MY~VYN+KYxQg8+MwEUpcQIE>h;9Q(0EC^ZMk&Yop<1 zo^Ado(FrrRA(MGl!!LPO2bX!)fd6lvmfD(Vbc41-YGbJT=5}^%O1~SQM$C24+v+|o zwRNedS9m?@rUdqouYHxSirSW>6nr z;ghLnSNQw^+*-Q?Jbg|_SEEg9?Pu_w&iNg4D&L=LpEr|{4XKH}j_*HuzqbC~&iUZz zEUo&Kp0*jC{U@mpuKT<0J#FnQ?4bjiJE)-@HlVpD8rmTP+Ta0g657yuydh|PbGy6y zXYbQCqrcl}$JXeZd#$%)M*m*yr2e|z=)*AZ+>`cYcg-J&y*%2C?m6`UX+Iooc3a=R zT_<$(_II=ojunYkhCh*S*{NUDAOz z^?PAz5B#Q|Kz`ivxWEW?CY=ZqsQTd1Gm~d`>iqB2J-yvM++ub93N?M% zdv2Y#)ZU`*W8>L&8QA+Vu?Dkg-oLk6Yb(Gz=J#_~H~hDOw@>J;ud(;|PHx_GzLrr5sTkJj2$a8FxrTccfT?Z75}P!m7ofB4|qEO_6H z-j0s0Njzm5{W}(|ok=>3nRI+7hHus2r&j#X%DyPPvR@28X=-n`_mS-{htD72Ewvlr zxqojK_vpu-2G8j3Wx*zQ_|UcO?P#-@+(RpVPsIM>bac*|?fSRYmcmae__Ofo-QDbthX3>Mz8P(EeDK=8q1^OyJ9=kzv`=eis!&>L z!_XS*-cnm@01vKh2JVOgc{x3!AwM&)g9(WP0ue0m8gWKlJYwU~G+Dm9Xy&Y3#*6*K| z+ROUXWAKf@C(ni9;P;;~gKI0njm51o@SIt_(x%m)EqtfKXY}{h`@8{|F--O5a12|* z8N&eI4el5e?CWHBz5jKOMw`RYW>0Rb)}yu70Y9POJ#hP!HM{`c)iEoN`Fh@q(AxLm znZpFwk4xd@UcRh}U(v*`ZQ|E8@f(`>t?=HCwi!%GJ>MPL=O5D9-ai{Yr1ms?ZX0{0 zvu)0VIX$!6s`Yywz45)$P`zJYMa#Zx;mf(dyMLgO_7P~kv!)YwaQ%I6W^dc9#*c50#7Z?%I8&36kmpO@*&uu40k(3Y>XV+zgp3fp?ar>*Z4 zYR+*q-zC%*6q@f4YKscZcL%k_h30!hU1LCr=Ul2?UueE7s4YQroqRtSNAU{Zph4^W zKCth!UfO-P4_}oPb?(QpzAe<^H9*eaSjw9CJ8yDrW8u!3nnmt>#b+GIxzb<16n%_k z-?h1pwq1qVw(7QBmAbjDKI2f=u589i*^C(aZ%OgmoPB4!tte`i)$!SedMw4uHu7!j z?0void^yT$jC3M(VlM(;ebt9|3%?T1wfkt|+yt=ij>kMVQ;($>CwXoGPo=Dj-Fr%Z zwJ~J!9x;Yv_@H7ui*2m+5nFP9hC83XKzE&tyK}|eA8Pi;7`Ink|0NYRZtQo0-3!|9 zrFJaNsqy6Qr;N{dzot0;w0{81)~vVxKWf|R7yF~&_m(YNCO7Y6l(z=0J~j3y;9lWR zfsJ8*mQp*GGTyUr?h6afDxP~~$BL^P&wRUsYqbUSe8xWcV}Nf=@J)~F+FyV3_rZP6 zh4;hD`92-)voZEt7#zzzu-^Bw+zxG#}_*+ff^Hu!qkLRnBd%g;H?|HrocdvQA3b*~lg4=)3Q>EQARmnY5mE1E` zxc&D`6>j`l1vkFuso3S7r%LX5s^p%hO73~8)ER0o~=sm z*{bB8t-_r@&sHV(Y*lj4R^hI{XRC1Yd$tNUo@c9)d$uaMXRC1Ad$uaMXRDHXt}6NB zChmEvw0o8+x#y^odxk2x=ckf;b}G4Nr;>YiD!J#Tl6z(hZY?tk0d4_}mRvpGq9#-9xdRx;|>ZqTEMGoZoHeCa`)hP`^kq=d%>;w)K7DC5kr3>G!-m zasL4>HMm-t>vg!g`!c!S z04JCBI2KfYm(1q}^&@ z+j)*jyVb$!-rKfY18iIMJQvmkt6TJQUPgoMqc+Fm`=^?@{JvQW>~mpTN^-4@rXHVl zz|KSIvo2iy)T%%0fo-Qgf*#t(Fjt!~3d|s%Jr>Ob75GU70V8^!uCF9!|O+7xFfE{1yvngCX9cWLu=iv7wH@5^hPrz}{up-i`&_rZacuANa%Zs5lkD|f;A$2xtN88;DSdZ?t699i zS||rpdu0zaV|hiJP-)81_JkP6E81R_rks7hH^i96i1u-??e$ml{JsxGUu}-xvzD4; z@Lrh&b|1*~-?Y));S_2k-IuTO!?XX3$dHA~jOcAg2frQM-mpRd|HAIf*+H0?cn7obQCIS-h<5 z(=33t%)xPB*QdPq=fKtDGZ$>T(x(fqzL0)9Pu*bKsrx;-1nqdR#jmgM zE7zhQU0a@M^T4)Mk9Gps+}a%9vD9jbJsoj6{1%#e#&RLpcIweC z0-IBt<6A_nW?b))Z-X7*Nb1bncfji5mw?^ZBdTZIVlbb9e{;}ZpTztwxH;zc(9{$2 z`(XD{VqOa7GZ0gMeexXqL1o_vAM@*BJU>tnuqsMV71A+U2<=6e{f9-rSr08cq z?x)tCG5sE_mUxeW-GA!(KT53@|382mbI^DX`Xijrz+Qg>qK|DKr`Ddfe*zmX{Lf(5 zA^b03+hiP1f%(+7px9P_eT@Aiwf3}q8tgd2{|a`_!~X^@V?P7uQ;%(1{q-^SQflqu zXQ`i~B+d)q%M1P@*uLc5>?N>1>S^IHA zSq9ff{qib~FaO5U<~#5p4+C7j18ZB3qUM;ziQNJ=m*X*qT$}UJ3U)rU`CFpgxl0`D z@?7cpGHnM}Z5Pl#V-10;`Ma9&jH_n*{08Z7f!h4{7tgF>|Lx@ouzt&f?Z36ZRjxoi zg5srp#ma7-evAYg!!=xr-%4QDruRHJEpQ*C-?ebV`N^(f}7`c9GZIObpx>N)SXxPIu!Gn zU!3^k!HMrYZU}cC)iaM9fz@xT)@>88+Qt;`fe%r8nM>QI6g6{+9h>8nt0(v7U~}j9 z=Z}EZ%HKG*fU7rtCmvdVFa0RoIe3<$z5K4*60ZGZ`r`et6; zSS{kM*KT*XYp1S{_st$)^{m|luv*q`FR+)nwCzbzGnd%0IZnBHa(^7`e1`7> zwl8;8&!LH6=TZIMN}B|>owod@urFBMd&Ir;NwAmw(e??7n)$_Lf7FwIKd@_=?~wh$ z&V_pBVhUJ2ZKi_D`8xn^8}+m~5UieeooQg>sOR1EATZ77F~1>!?ZXoAr@*fFNQ{Yh zFj_-zd@mgWx4pXKKaAST@oPJjqUQL;<@nVT_tW5^6?fks0d_tz)+52?SdWG~R{av= z7_hqc%2Cwv%;$8l{>F*_46wTXZPfCNy#w4l_L*qv`nOZdcjQ#yKAi;~Lv3DTY0qHA!;bJE6O60-|UJ@-R5SS|O%@nA3agSH-untc-+$8*&QV9$8Y z%RI2$yf2{52b;H#T7Q4vJPE9x-=jYRR63f&RIon2%i6wI8zpn}MR4Zm z46rxnHtY-1b9GwZ*mRMf_H^({$O+B%`3Rbi58JMH9A=-?! z1pQpFu}-IU9NsHm1FJj6Mbz@l(bvH%;p6!1m+|z`_cUtl`CaOKaQe9eZ4&<*=-T4< z&BE{SYJTj;1?bu`|K9@JR^5E(QL81+Mc@z0cQLxQ_OPn8p zKPcai(Y3|zCxu^`@2BY6GQOXIZL4m+8>!V2=Voy7mFss4y0-Y;TKJXo`Ezt_8Q(9! zwpBOZP1I_Mb36Eh@-0Ev7QZ_RzcSx1(Y0lKcY7)pDQR4fY-hzo+68s(b%l zxcw>LAMQg_PmW)KZKrNK-vfUQ_BmJXiQk~9=Xa9(!9KtI{mp(DN1x2q17M${`q}rp zsMXTfhrr%%>FdMb=Dz+GO+7Jx2ezGh;`|>taqRme@aDchil&~v{~nya8`Bv2*v|*4 zwI}}LVDHoLCn}zP{{i0I??0lcXO5o)+fF@m{3o#eEcfM~(bUt=zkt(E`(Ye?G9OQY zon!s%`(xB<>Fd*A@A>rgui)ms{tZn%F`oh3PCap+1t*Sk{2aWw@6V&Dr|&O-(|2PU zLm&IOlv=y^Md~eynLhp<-rUE3psDBgqJM(b7SYZaFM-t(<6q$B82?67PmKS7)%@+y z7%zj>GUivn?%Di}z^h<2`x>9u;O>|Bybe~==Q8dke>ZvqZd+|>_a@l2O1rnfYGu2( z;kMP5cK-!CcWL(ySgmaLF5I@-JP$0R_VWEt+j|r>-~Ysp-H-2!zIQGM)@JQFwS~Hs z;-!6XWw*9po)v~@r#SyhfkVO0|5KFs3`0}TIdyrk?bPj?d=SNa<`<{!3Si^soIe7r z<~iSIs_&>P!p-?CMZ0Z%2OA02<~aR(PaCR)r^*_T*X( ztj%2ht*Jb@)&Q5e)`Y8-xkkg)?<{hy1y3&R$+b3ETiyrP0jpWOy!Y&ff8(kx?bZW3 z4{0|BtY-1DU3}L^a~$y<3s$q}$M&f|<2*l%Lo=2>=JjuB)r{r4)_CxFG;ojj4A~H_ zPx&3Q5nMe!8-wkC>9Yx3eIap-yD8Xq>UoBJ2y8xW$^T)n&x!bK2G>tLKAVH>WB5nl z*A#8Gfa{~4=iEoZ&c$XHu=k>V&VheFtUcq|3fw&At1H; z@plI=CVs}d2i!R+zXvD4)#I}#*f}YE_JXS~toFg)VB4u@o<0sXpSI-R2VBmGVY;pebh4thk-K(#&iz! za}Fj`YtQ&T4Q_siI08*QK1YI^=jkXk^^EUmuL(n+sP@ye_cq)ib7UaK@xPW9k5F%RV?Byg|Wx!M-cS-UoLcvIqLX#`RpF zuW|Lsd)GX$G4)G~6TrS}#pgu0e(Ld=53WB;s_&DN;AKBP126mWS$O)Pul>*`IZg%} zQ@`Z+9N71{_tNglev3U1td`$mwW*cw#4Q!i`?GcWbw0MTpWlF=hCTP-H{ojf**|S+Y5y&- z+9}okz7Vda?*-KI_+AX|EpmSwu224r<~v}0)I9@ym)gtsY;B7v-={by;>7(P*tq7r z1T1&_w%6A-@%;ffzL$dKv0ViS%scl?mB9D`hFw0x$i$hQ`i3n zYI*wpWAGUIZVqE<*U#MAlJh5EbB6yET>c%w&*1u~yPh{wd%2$4Zlb8UMq+dLcL2A5 zouAmO<;Rm>U+awR=h&LZ_6s!i_}o_ctjZ0SzTU1z$^Ezlocq!C+U?J+)Y{VSmtg1N zp=!V130I5#uFC#!WxpG)mcJ>#2W%Vld`I33Hus(s?dj96D&6mIeeR>!r#q;%CFiff z_9^@~aQhv8KiD>T<~;!R_XG9hdJvrPYxnn=JOdtr8&jX0;U5N@SDS15J8Cc2R@-kW zYOa^~9iofXT$iQzJ_>g29-+3cKHq*1R=*A(e}{Mstmd9D&lA*M=F#>zMa|ga#Q6i* zy_8&kgg57U5=}k-2K6Vf?bO|GiEEwzJ;k5F=5-wUo6~+hMeSw3^!W=#&3=h9ezmO2 zU%}v`}jGqYv)-`yWfNPx1gut+TAzLSGL?2FTkCX z^1gTxO+D-U5?IZcj`JU2FUP6v?-Vu1Db9TV6Wl!CFQchnRjuv6z-o^Y&-qRq>&*AR z!RB$^^f!n7c!k=_e(3WbikkfpXO3S3JI5LG>+t3=zk#Nnd4CgZJN3-*Tj0#G_HvG2 zg=hFkCdGPLsF7?$(UKuOy{VB4!_ z?bV#;%#(Hc(~8Z#5H#S`bWTO7T46A{w=O;wK*^LSuJ@!3Rd$wSpTgl_Q$?z^DLTogpYx> z?Mczk^>J>tqZp(7jcNNzkI#;kPkGkf2~9oU|2u<|+xFUH+oiIZM?ar^j?p#Um15uY z$=vJ)c5ZSH>|VDy2mWt;2i4@|z8DSnT9uN0v08ywFYp=_UWIy1iu+gY3w1!D^Y~PlLUjV{M01j-og>;>11zoc=gY_wpWQ*R&f=JxhL z+&qW#(bV(&I|*!Cb)SFo^7%Ig?zJ|>=illAq10E8mdKI)l+g<$8$eYJoR-!FiTuRUvVDmZhgJ!|2dokp=QdAB|ttiQT@ z$G)qjZ)bqZzJ3vIUz6)hu(@1!eYL0Um%zpg|8m9ietH&MANBa0-Q@EXxPI#KIS1?* z66dR6{nVqK3szr5$y|L6Y=5=6?(%Zo$HTqKbsr1Q9vD~P4ZyDZhSXX2^TDor_$3uj zzrO*uUHCV_V+(!(*c|Eax4`*!bGBHW!2S^?ub}?t^c`?MwE- zcfk6qyAK?TTKc*etQP*=if6vR2VYE4kI(nPwu#TBVExqN^8>K`h|gtU{nVpf4pw(9 z?VIzemKaxn%NSR|)vhdjt_F{#9UJG`vwxT#s#?U7@eh4;i`g9#w&35UN qTKumEk1hBO;Bl0!&5dC5xi #include #include +#include #include Q_LOGGING_CATEGORY(lcQuickLayouts, "qt.quick.layouts") @@ -54,49 +55,13 @@ SnapGridLayoutItem::SnapGridLayoutItem( int column, int rowSpan, int columnSpan, - Qt::Alignment alignment, - SnapLayout* layout) - : QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment) - , m_layout(layout) {} + Qt::Alignment alignment) + : QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment) {} void SnapGridLayoutItem::setGeometry(const QRectF& rect) { QQuickLayoutAttached* info = attachedLayoutObject(m_item, false); - QRectF placed = info ? rect.marginsRemoved(info->effectiveQMargins()) - : rect; - - const QQuickWindow* win = m_item->window(); - const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0; - - if (dpr > 0.0) { - const QPointF origin = absoluteOrigin(m_layout); - const qreal left = (origin.x() + placed.x()) * dpr; - const qreal top = (origin.y() + placed.y()) * dpr; - const qreal right = (origin.x() + placed.right()) * dpr; - const qreal bottom = (origin.y() + placed.bottom()) * dpr; - - const qint64 nearX = std::llround(left); - const qint64 nearY = std::llround(top); - const qint64 farX = std::llround(right); - const qint64 farY = std::llround(bottom); - - placed = QRectF( - qreal(nearX) / dpr - origin.x(), - qreal(nearY) / dpr - origin.y(), - qreal(farX - nearX) / dpr, - qreal(farY - nearY) / dpr); - } - - const QSizeF oldSize(m_item->width(), m_item->height()); - const QSizeF newSize = placed.size(); - - m_item->setPosition(placed.topLeft()); - if (newSize == oldSize) { - if (QQuickLayout* lay = qobject_cast(m_item)) { - if (lay->invalidatedArrangement()) lay->rearrange(newSize); - } - } else { - m_item->setSize(newSize); - } + engineRect = info ? rect.marginsRemoved(info->effectiveQMargins()) : rect; + placed = true; } void SnapLayoutPrivate::mirrorChange() { @@ -119,6 +84,7 @@ SnapLayout::~SnapLayout() { void SnapLayout::componentComplete() { QQuickLayout::componentComplete(); + applySpacing(); invalidate(); ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints); @@ -128,8 +94,12 @@ void SnapLayout::componentComplete() { void SnapLayout::itemChange(ItemChange change, const ItemChangeData& value) { QQuickLayout::itemChange(change, value); - if (change == ItemDevicePixelRatioHasChanged || change == ItemSceneChange) + if (change == ItemChildRemovedChange && value.item) + d_func()->childSizes.remove(value.item); + if (change == ItemDevicePixelRatioHasChanged || change == ItemSceneChange) { + applySpacing(); invalidate(); + } } QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const { @@ -169,16 +139,33 @@ Qt::LayoutDirection SnapLayout::effectiveLayoutDirection() const { qreal SnapLayout::spacing() const { Q_D(const SnapLayout); - return d->engine.spacing(d->orientation, d->styleInfo); + if (!std::isnan(d->spacingRequest)) return d->spacingRequest; + return d->styleInfo->spacing(d->orientation); +} + +void SnapLayout::applySpacing() { + Q_D(SnapLayout); + if (!isReady()) return; + + const QQuickWindow* win = window(); + const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0; + + qreal space = d->spacingRequest; + if (std::isnan(space)) space = d->styleInfo->spacing(d->orientation); + + if (dpr > 0.0) space = qreal(std::llround(space * dpr)) / dpr; + + d->engine.setSpacing(space, Qt::Horizontal | Qt::Vertical); } void SnapLayout::setSpacing(qreal space) { Q_D(SnapLayout); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wfloat-equal" - if (std::isnan(space) || spacing() == space) return; + if (std::isnan(space) || d->spacingRequest == space) return; #pragma GCC diagnostic pop - d->engine.setSpacing(space, Qt::Horizontal | Qt::Vertical); + d->spacingRequest = space; + applySpacing(); invalidate(); emit spacingChanged(); } @@ -242,6 +229,14 @@ void SnapLayout::updateLayoutItems() { Q_D(SnapLayout); if (!isReady()) return; + const auto items = childItems(); + for (auto entry = d->childSizes.begin(); entry != d->childSizes.end();) { + if (items.contains(entry.key())) + ++entry; + else + entry = d->childSizes.erase(entry); + } + d->engine.deleteItems(); insertLayoutItems(); } @@ -286,8 +281,8 @@ void SnapLayout::insertLayoutItems() { int gridRow = 0; int gridColumn = index; if (d->orientation == Qt::Vertical) qSwap(gridRow, gridColumn); - QGridLayoutItem* layoutItem = new SnapGridLayoutItem( - child, gridRow, gridColumn, 1, 1, alignment, this); + QGridLayoutItem* layoutItem = + new SnapGridLayoutItem(child, gridRow, gridColumn, 1, 1, alignment); if (hStretch >= 0) { if (!fillWidth) @@ -315,6 +310,7 @@ void SnapLayout::removeGridItem(QGridLayoutItem* gridItem) { void SnapLayout::itemDestroyed(QQuickItem* item) { if (!isReady()) return; Q_D(SnapLayout); + d->childSizes.remove(item); if (QQuickGridLayoutItem* gridItem = d->engine.findLayoutItem(item)) { removeGridItem(gridItem); delete gridItem; @@ -381,6 +377,98 @@ void SnapLayout::clearAncestorListeners() { d->ancestorListeners.clear(); } +void SnapLayout::arrangeChildren() { + Q_D(SnapLayout); + const QQuickWindow* win = window(); + const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0; + if (dpr <= 0.0) return; + + const bool horiz = d->orientation == Qt::Horizontal; + const QPointF origin = absoluteOrigin(this); + const qint64 originXDev = std::llround(origin.x() * dpr); + const qint64 originYDev = std::llround(origin.y() * dpr); + + qreal prevEndExact = 0.0; + qint64 prevNearDev = 0; + qint64 prevSizeDev = 0; + bool started = false; + + for (int i = 0; i < d->engine.itemCount(); ++i) { + SnapGridLayoutItem* gridItem = + static_cast(d->engine.itemAt(i)); + if (!gridItem->placed) continue; + QQuickItem* child = gridItem->layoutItem(); + const QRectF& cell = gridItem->engineRect; + + const qreal mainExact = (horiz ? cell.x() : cell.y()) * dpr; + const qreal mainSizeExact = + (horiz ? cell.width() : cell.height()) * dpr; + const qreal crossExact = (horiz ? cell.y() : cell.x()) * dpr; + const qint64 mainSizeDev = std::llround(mainSizeExact); + const qint64 crossSizeDev = + std::llround((horiz ? cell.height() : cell.width()) * dpr); + const qint64 engineWidth = horiz ? mainSizeDev : crossSizeDev; + const qint64 engineHeight = horiz ? crossSizeDev : mainSizeDev; + + qint64 mainNear = 0; + if (!started) { + mainNear = std::llround(mainExact); + started = true; + } else { + mainNear = prevNearDev + prevSizeDev + + std::llround(mainExact - prevEndExact); + } + + const qint64 mainFarDev = std::llround(mainExact + mainSizeExact); + + const auto stored = d->childSizes.constFind(child); + qint64 width = engineWidth; + qint64 height = engineHeight; + if (horiz) + width = std::max(mainFarDev - mainNear, qint64(0)); + else + height = std::max(mainFarDev - mainNear, qint64(0)); + + if (stored != d->childSizes.constEnd() && + stored.value().engineWidth == engineWidth && + stored.value().engineHeight == engineHeight && + std::llround(child->width() * dpr) == stored.value().width && + std::llround(child->height() * dpr) == stored.value().height) { + width = stored.value().width; + height = stored.value().height; + } else { + d->childSizes[child] = SnapLayoutPrivate::ChildSize{ + engineWidth, engineHeight, width, height}; + } + + prevEndExact = mainExact + mainSizeExact; + prevNearDev = mainNear; + prevSizeDev = horiz ? width : height; + + const qint64 crossNear = std::llround(crossExact); + const qint64 nearX = originXDev + (horiz ? mainNear : crossNear); + const qint64 nearY = originYDev + (horiz ? crossNear : mainNear); + + const QRectF snapped( + qreal(nearX) / dpr - origin.x(), + qreal(nearY) / dpr - origin.y(), + qreal(width) / dpr, + qreal(height) / dpr); + + const QSizeF oldSize(child->width(), child->height()); + const QSizeF newSize = snapped.size(); + + child->setPosition(snapped.topLeft()); + if (newSize == oldSize) { + if (QQuickLayout* lay = qobject_cast(child)) { + if (lay->invalidatedArrangement()) lay->rearrange(newSize); + } + } else { + child->setSize(newSize); + } + } +} + void SnapLayout::rearrange(const QSizeF& size) { Q_D(SnapLayout); if (!isReady() || !size.isValid()) return; @@ -408,7 +496,11 @@ void SnapLayout::rearrange(const QSizeF& size) { } QQuickLayout::rearrange(size); + + for (int i = 0; i < d->engine.itemCount(); ++i) + static_cast(d->engine.itemAt(i))->placed = false; d->engine.setGeometries(QRectF(QPointF(0, 0), size), d->styleInfo); + arrangeChildren(); d->rearranging = false; for (auto childItem : std::as_const(d->invalidateAfterRearrange)) diff --git a/Plugins/ZShell/Components/snaplayout.hpp b/Plugins/ZShell/Components/snaplayout.hpp index 150ae02..78bccc9 100644 --- a/Plugins/ZShell/Components/snaplayout.hpp +++ b/Plugins/ZShell/Components/snaplayout.hpp @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include #include @@ -20,6 +22,20 @@ class SnapLayoutStyleInfo : public QAbstractLayoutStyleInfo { bool hasChangedCore() const override; }; +class SnapGridLayoutEngine : public QGridLayoutEngine { + public: + SnapGridLayoutEngine() : QGridLayoutEngine(Qt::AlignVCenter, false) {} + + QQuickGridLayoutItem* findLayoutItem(QQuickItem* item) const { + for (int i = int(q_items.size()) - 1; i >= 0; --i) { + QQuickGridLayoutItem* layoutItem = + static_cast(q_items.at(i)); + if (layoutItem->layoutItem() == item) return layoutItem; + } + return nullptr; + } +}; + class SnapGridLayoutItem : public QQuickGridLayoutItem { public: SnapGridLayoutItem( @@ -28,13 +44,12 @@ class SnapGridLayoutItem : public QQuickGridLayoutItem { int column, int rowSpan, int columnSpan, - Qt::Alignment alignment, - SnapLayout* layout); + Qt::Alignment alignment); void setGeometry(const QRectF& rect) override; - private: - SnapLayout* m_layout; + QRectF engineRect; + bool placed = false; }; class SnapLayout : public QQuickLayout { @@ -101,6 +116,10 @@ class SnapLayout : public QQuickLayout { void insertLayoutItems(); void removeGridItem(QGridLayoutItem* gridItem); + void arrangeChildren(); + + void applySpacing(); + void updateAncestorListeners(); void clearAncestorListeners(); }; @@ -113,10 +132,11 @@ class SnapLayoutPrivate : public QQuickLayoutPrivate { void mirrorChange() override; - QQuickGridLayoutEngine engine; + SnapGridLayoutEngine engine; SnapLayoutStyleInfo* styleInfo = nullptr; Qt::Orientation orientation = Qt::Horizontal; Qt::LayoutDirection layoutDirection = Qt::LeftToRight; + qreal spacingRequest = qQNaN(); bool rearranging = false; int recurRearrangeCounter = 0; QList invalidateAfterRearrange; @@ -124,6 +144,14 @@ class SnapLayoutPrivate : public QQuickLayoutPrivate { QVector> ancestorListeners; qint64 originResidualX = -1; qint64 originResidualY = -1; + + struct ChildSize { + qint64 engineWidth = 0; + qint64 engineHeight = 0; + qint64 width = 0; + qint64 height = 0; + }; + QHash childSizes; }; } // namespace ZShell::components