fix: ignore stale coordinate data for panels + convert wavyline to use new qt 6.12 canvas painter api

This commit is contained in:
2026-10-12 15:54:32 +02:00
parent 3883917228
commit 255db9e533
16 changed files with 222 additions and 185 deletions
+8 -28
View File
@@ -138,11 +138,16 @@ void BlobGroup2::ensureDataBuilt() {
const double extent = static_cast<double>(smoothFactor) * 1.5 +
static_cast<double>(m_shadowRadius);
// Group order, no pruning: the shader sees this list for every pixel.
// 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.
QVector<BlobShape2*> entryShapes;
for (auto* shape : std::as_const(m_shapes)) {
if (!shape->isMoving()) continue;
if (shape->width() <= 0 || shape->height() <= 0) continue;
if (static_cast<int>(m_data.rects.size()) >= BlobGroupData::kMaxRects)
continue;
@@ -189,9 +194,6 @@ void BlobGroup2::ensureDataBuilt() {
const int count = static_cast<int>(m_data.rects.size());
// Which shape is the source for the bar edge.
resolveSources(entryShapes);
// Exclusion gates only the smooth blend.
for (int i = 0; i < count; ++i) {
int mask = 0;
@@ -254,8 +256,8 @@ void BlobGroup2::ensureDataBuilt() {
ri.radius[3] = std::max(ri.radius[3] * fTl, minR);
}
// Inverted rect. The frame term is evaluated for every pixel in the bounds, so the
// group flag cannot be gated per shape.
// Inverted rect. The frame term is evaluated for every pixel in the bounds,
// so the group flag cannot be gated per shape.
if (!m_invertedRect) return;
auto* inv = static_cast<BlobShape2*>(m_invertedRect);
@@ -302,28 +304,6 @@ void BlobGroup2::ensureDataBuilt() {
2.0 * (static_cast<double>(outerHH) + extent)));
}
// A point on the bar edge can only have one source. A shape that is not pushing is not
// the source: it still fills its own area, but inside the border strip it must not add
// its own outline to a span another shape is already shaping. That is the case that
// matters — one panel at rest next to one that is bouncing — where the edge showed
// several steps instead of one smooth indent of the closing panel. Only one shape can be
// pushing at a time, so the flag is just that, and the shader gates the strip with it.
void BlobGroup2::resolveSources(const QVector<BlobShape2*>& entryShapes) {
const int count = static_cast<int>(m_data.rects.size());
// With nothing pushing there is no span being shaped, so a shape at rest keeps its
// own outline and nothing is gated.
bool anyPushing = false;
for (int i = 0; i < count; ++i)
if (entryShapes[i]->isMoving()) {
anyPushing = true;
break;
}
for (int i = 0; i < count; ++i)
m_data.rects[i].source = !anyPushing || entryShapes[i]->isMoving();
}
void BlobGroup2::fillMaterial(BlobMaterial* material) {
material->m_paddedX = static_cast<float>(m_bounds.x());
material->m_paddedY = static_cast<float>(m_bounds.y());
+3 -6
View File
@@ -22,11 +22,9 @@ class BlobInvertedRect2;
// 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 of 5 vec4, 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. Each entry is either a source for its span or not; the
// flag is in the third vec4 alongside the screen half-extents, so no extra data is
// needed.
// 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<BlobRectData> rects;
@@ -111,7 +109,6 @@ class BlobGroup2 : public QQuickItem {
private:
void ensurePhysicsUpdated();
void ensureDataBuilt();
void resolveSources(const QVector<BlobShape2*>& entryShapes);
void fillMaterial(BlobMaterial* material);
qreal m_smoothing = 32.0;
+1 -6
View File
@@ -92,12 +92,7 @@ bool BlobMaterialShader::updateUniformData(
memcpy(&maskAsFloat, &r.excludeMask, sizeof(float));
const float d0[4] = {r.cx, r.cy, r.hw, r.hh};
const float d1[4] = {maskAsFloat, r.offsetX, r.offsetY, r.minEig};
const float d3[4] = {
r.screenHalfX,
r.screenHalfY,
r.source ? 1.0f : 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 + 16, d1, 16);
memcpy(buf->data() + base + 32, r.invDeform, 16);
-5
View File
@@ -17,11 +17,6 @@ struct BlobRectData {
// 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;
// Whether this entry is pushing on the bar edge. Only the shape that is pushing can
// shape the edge; a shape at rest still fills its own area, but the shader gives it
// no contribution inside the border strip, so it cannot add its own outline to a span
// another shape is shaping.
bool source = true;
};
class BlobMaterial : public QSGMaterial {
+22 -1
View File
@@ -22,6 +22,9 @@ class BlobRect2 : public BlobShape2 {
Q_PROPERTY(
QQmlListProperty<BlobRect2> excludeCorners READ excludeCorners NOTIFY
excludeCornersChanged)
Q_PROPERTY(
bool atRestOverride READ atRestOverride WRITE setAtRestOverride NOTIFY
atRestOverrideChanged)
Q_PROPERTY(
qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY
topLeftRadiusChanged)
@@ -69,11 +72,24 @@ class BlobRect2 : public BlobShape2 {
QQmlListProperty<BlobRect2> exclude();
QQmlListProperty<BlobRect2> excludeCorners();
[[nodiscard]] bool atRestOverride() const { return m_atRestOverride; }
void setAtRestOverride(bool o) {
if (m_atRestOverride == o) return;
m_atRestOverride = o;
emit atRestOverrideChanged();
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.
bool isMoving() const override { return m_physicsActive || m_retiring; }
// 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;
}
void cornerRadii(float out[4]) const override;
[[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; }
@@ -100,6 +116,7 @@ class BlobRect2 : public BlobShape2 {
void deformScaleChanged();
void excludeChanged();
void excludeCornersChanged();
void atRestOverrideChanged();
void topLeftRadiusChanged();
void topRightRadiusChanged();
void bottomLeftRadiusChanged();
@@ -119,6 +136,10 @@ class BlobRect2 : public BlobShape2 {
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.
bool m_atRestOverride = false;
bool m_retiring = false;
QElapsedTimer m_retireElapsed;
float m_retire00 = 1.0f;
+2 -2
View File
@@ -56,8 +56,8 @@ class BlobShape2 : public QQuickItem {
virtual bool isExcluded(const BlobShape2*) const { return false; }
// Whether this shape is currently pushing on a border. Only the shape that is pushing
// can shape the bar edge, so this decides the source flag.
// 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 {
+6 -26
View File
@@ -65,19 +65,6 @@ float smaxSharpA(float a, float b, float k) {
void main() {
vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH);
// The frame distances are needed before the per-rect pass. In the border strip the
// boundary is the frame, displaced only by the shape that is pushing, so a shape that
// is not the source must not define the boundary there. It still fills the inner area,
// so this only gates the strip, which is the bar edge.
float dOuter = 0.0;
float dInner = 0.0;
bool inStrip = false;
if (hasInverted != 0) {
dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0;
dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius);
inStrip = dOuter < 0.0 && dInner > 0.0;
}
// 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
@@ -91,17 +78,10 @@ void main() {
vec4 props =
rectData[i * 5 + 1]; // excludeMask(int bits), offsetX, offsetY, minEig
vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix
vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, isSource, 0
vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0
vec4 radii =
rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl)
// A shape that is not pushing does not shape the bar edge, so inside the strip it
// must not add its own outline to a span another shape is already shaping.
if (inStrip && sh.z < 0.5) {
dArr[i] = 1e10;
continue;
}
// Offset center for asymmetric deformation
vec2 center = rect.xy + props.yz;
@@ -202,6 +182,10 @@ void main() {
}
if (hasInverted != 0) {
float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 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;
@@ -214,10 +198,6 @@ void main() {
float sinkValue = 0.0;
for (int i = 0; i < rectCount; i++) {
// Only a shape that is pushing can move the border.
if (rectData[i * 5 + 3].z < 0.5)
continue;
vec4 rect = rectData[i * 5];
vec4 sinkProps = rectData[i * 5 + 1];
vec2 sinkSh = rectData[i * 5 + 3].xy;
@@ -228,7 +208,7 @@ void main() {
// Delay sink to absorb smin blend depth (cubic smin max = k/6)
float preOff = smoothFactor * (1.0 / 6.0);
// Top border: track rect's BOTTOM edge, clamped to border thickness
// Top border: track rect's BOTTOM edge, only within border thickness
float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0,
innerTop - outerTop);