physics throttling, targeted dirty marking, proximity-based border

- Snap deformation to rest when imperceptible instead of pumping frames
- Only trigger markDirty on geometry changes exceeding 0.5px
- Only include inverted rect data when rect is near the border
This commit is contained in:
Robin Seger 2026-03-25 20:27:56 +01:00
parent 6103ce19e8
commit ebccf801f4
4 changed files with 96 additions and 20 deletions

View file

@ -66,6 +66,40 @@ void BlobGroup::markDirty() {
}
}
void BlobGroup::markShapeDirty(BlobShape* source) {
m_physicsUpdated = false;
source->polish();
source->update();
// Use cached padded rects to find spatial neighbors
const float pad = static_cast<float>(m_smoothing) * 2.0f;
const QRectF srcRect(
static_cast<double>(source->m_cachedPaddedX - pad),
static_cast<double>(source->m_cachedPaddedY - pad),
static_cast<double>(source->m_cachedPaddedW + pad * 2.0f),
static_cast<double>(source->m_cachedPaddedH + pad * 2.0f));
for (auto* shape : std::as_const(m_shapes)) {
if (shape == source)
continue;
const QRectF otherRect(
static_cast<double>(shape->m_cachedPaddedX),
static_cast<double>(shape->m_cachedPaddedY),
static_cast<double>(shape->m_cachedPaddedW),
static_cast<double>(shape->m_cachedPaddedH));
if (srcRect.intersects(otherRect)) {
shape->polish();
shape->update();
}
}
if (m_invertedRect && static_cast<BlobShape*>(m_invertedRect) != source) {
static_cast<BlobShape*>(m_invertedRect)->polish();
static_cast<BlobShape*>(m_invertedRect)->update();
}
}
void BlobGroup::ensurePhysicsUpdated() {
if (m_physicsUpdated)
return;

View file

@ -38,6 +38,7 @@ public:
BlobInvertedRect* invertedRect() const { return m_invertedRect; }
void markDirty();
void markShapeDirty(BlobShape* source);
void ensurePhysicsUpdated();
signals:

View file

@ -16,13 +16,30 @@ void BlobRect::updatePolish() {
BlobShape::updatePolish();
if (m_physicsActive) {
QMetaObject::invokeMethod(
this,
[this]() {
if (m_physicsActive && m_group)
m_group->markDirty();
},
Qt::QueuedConnection);
// Check if deformation is visually imperceptible
float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01)
+ std::abs(m_dm11 - 1.0f);
float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
if (totalDelta < 0.004f && totalVel < 0.05f) {
// Snap to rest, no visible deformation
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_deformMatrix = QMatrix4x4();
updateCenteredDeformMatrix();
m_physicsActive = false;
} else {
QMetaObject::invokeMethod(
this,
[this]() {
if (m_physicsActive && m_group)
m_group->markDirty();
},
Qt::QueuedConnection);
}
}
}

View file

@ -70,8 +70,15 @@ void BlobShape::geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry);
updateCenteredDeformMatrix();
if (m_group)
m_group->markDirty();
if (m_group) {
// Only trigger redraw if the change is visually meaningful
const auto dx = std::abs(newGeometry.x() - oldGeometry.x());
const auto dy = std::abs(newGeometry.y() - oldGeometry.y());
const auto dw = std::abs(newGeometry.width() - oldGeometry.width());
const auto dh = std::abs(newGeometry.height() - oldGeometry.height());
if (dx > 0.5 || dy > 0.5 || dw > 0.5 || dh > 0.5)
m_group->markShapeDirty(this);
}
}
void BlobShape::updateCenteredDeformMatrix() {
@ -210,9 +217,6 @@ void BlobShape::updatePolish() {
auto* inv = m_group->invertedRect();
if (inv) {
m_cachedHasInverted = true;
m_cachedInvertedRadius = static_cast<float>(inv->radius());
const QPointF invScene = inv->mapToScene(QPointF(0, 0));
const float outerCX =
static_cast<float>(invScene.x() + inv->width() / 2.0);
@ -234,15 +238,35 @@ void BlobShape::updatePolish() {
outerHH -
static_cast<float>((inv->borderTop() + inv->borderBottom()) / 2.0);
m_cachedInvertedOuter[0] = outerCX;
m_cachedInvertedOuter[1] = outerCY;
m_cachedInvertedOuter[2] = outerHW;
m_cachedInvertedOuter[3] = outerHH;
// Check if this rect is near the border (within 2x smoothing of inner edge)
bool nearBorder = isInvertedRect();
if (!nearBorder) {
const float margin = pad * 2.0f;
const float myCX = m_cachedPaddedX + m_cachedPaddedW * 0.5f;
const float myCY = m_cachedPaddedY + m_cachedPaddedH * 0.5f;
const float myHW = m_cachedPaddedW * 0.5f;
const float myHH = m_cachedPaddedH * 0.5f;
// Near border if any edge of padded rect is within margin of inner edge
nearBorder = (myCX - myHW < innerCX - innerHW + margin) ||
(myCX + myHW > innerCX + innerHW - margin) ||
(myCY - myHH < innerCY - innerHH + margin) ||
(myCY + myHH > innerCY + innerHH - margin);
}
m_cachedInvertedInner[0] = innerCX;
m_cachedInvertedInner[1] = innerCY;
m_cachedInvertedInner[2] = innerHW;
m_cachedInvertedInner[3] = innerHH;
if (nearBorder) {
m_cachedHasInverted = true;
m_cachedInvertedRadius = static_cast<float>(inv->radius());
m_cachedInvertedOuter[0] = outerCX;
m_cachedInvertedOuter[1] = outerCY;
m_cachedInvertedOuter[2] = outerHW;
m_cachedInvertedOuter[3] = outerHH;
m_cachedInvertedInner[0] = innerCX;
m_cachedInvertedInner[1] = innerCY;
m_cachedInvertedInner[2] = innerHW;
m_cachedInvertedInner[3] = innerHH;
}
}
// Pre-compute corner fill factors (moves O(N²) work from GPU to CPU)