feat: add corner fill toggle for blobs

This commit is contained in:
2 * r + 2 * t 2026-05-08 03:13:53 +10:00
parent ba2d4f03d6
commit 71386f008d
6 changed files with 95 additions and 2 deletions

View file

@ -28,6 +28,14 @@ void BlobGroup::setColor(const QColor& c) {
markDirty(); markDirty();
} }
void BlobGroup::setCornerFill(bool e) {
if (m_cornerFill == e)
return;
m_cornerFill = e;
emit cornerFillChanged();
markDirty();
}
void BlobGroup::addShape(BlobShape* shape) { void BlobGroup::addShape(BlobShape* shape) {
if (!shape || m_shapes.contains(shape)) if (!shape || m_shapes.contains(shape))
return; return;

View file

@ -13,6 +13,7 @@ class BlobGroup : public QObject {
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged) Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged)
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
Q_PROPERTY(bool cornerFill READ cornerFill WRITE setCornerFill NOTIFY cornerFillChanged)
public: public:
explicit BlobGroup(QObject* parent = nullptr); explicit BlobGroup(QObject* parent = nullptr);
@ -26,6 +27,10 @@ public:
void setColor(const QColor& c); void setColor(const QColor& c);
bool cornerFill() const { return m_cornerFill; }
void setCornerFill(bool e);
void addShape(BlobShape* shape); void addShape(BlobShape* shape);
void removeShape(BlobShape* shape); void removeShape(BlobShape* shape);
@ -43,10 +48,12 @@ public:
signals: signals:
void smoothingChanged(); void smoothingChanged();
void colorChanged(); void colorChanged();
void cornerFillChanged();
private: private:
qreal m_smoothing = 32.0; qreal m_smoothing = 32.0;
QColor m_color{ 0x44, 0x88, 0xff }; QColor m_color{ 0x44, 0x88, 0xff };
bool m_cornerFill = true;
QList<BlobShape*> m_shapes; QList<BlobShape*> m_shapes;
BlobInvertedRect* m_invertedRect = nullptr; BlobInvertedRect* m_invertedRect = nullptr;
bool m_physicsUpdated = false; bool m_physicsUpdated = false;

View file

@ -173,11 +173,24 @@ bool BlobRect::isExcluded(const BlobShape* other) const {
return false; return false;
} }
bool BlobRect::isCornerExcluded(const BlobShape* other) const {
for (const auto& ptr : m_excludeCorners) {
if (ptr == other)
return true;
}
return false;
}
QQmlListProperty<BlobRect> BlobRect::exclude() { QQmlListProperty<BlobRect> BlobRect::exclude() {
return QQmlListProperty<BlobRect>( return QQmlListProperty<BlobRect>(
this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast); this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast);
} }
QQmlListProperty<BlobRect> BlobRect::excludeCorners() {
return QQmlListProperty<BlobRect>(this, nullptr, &excludeCornersAppend, &excludeCornersCount, &excludeCornersAt,
&excludeCornersClear, &excludeCornersReplace, &excludeCornersRemoveLast);
}
void BlobRect::excludeAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect) { void BlobRect::excludeAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect) {
auto* self = static_cast<BlobRect*>(prop->object); auto* self = static_cast<BlobRect*>(prop->object);
self->m_exclude.append(rect); self->m_exclude.append(rect);
@ -224,6 +237,52 @@ void BlobRect::excludeRemoveLast(QQmlListProperty<BlobRect>* prop) {
emit self->excludeChanged(); emit self->excludeChanged();
} }
void BlobRect::excludeCornersAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect) {
auto* self = static_cast<BlobRect*>(prop->object);
self->m_excludeCorners.append(rect);
if (self->m_group)
self->m_group->markDirty();
emit self->excludeCornersChanged();
}
qsizetype BlobRect::excludeCornersCount(QQmlListProperty<BlobRect>* prop) {
auto* self = static_cast<BlobRect*>(prop->object);
return self->m_excludeCorners.size();
}
BlobRect* BlobRect::excludeCornersAt(QQmlListProperty<BlobRect>* prop, qsizetype index) {
auto* self = static_cast<BlobRect*>(prop->object);
return self->m_excludeCorners.at(index);
}
void BlobRect::excludeCornersClear(QQmlListProperty<BlobRect>* prop) {
auto* self = static_cast<BlobRect*>(prop->object);
if (self->m_excludeCorners.isEmpty())
return;
self->m_excludeCorners.clear();
if (self->m_group)
self->m_group->markDirty();
emit self->excludeCornersChanged();
}
void BlobRect::excludeCornersReplace(QQmlListProperty<BlobRect>* prop, qsizetype index, BlobRect* rect) {
auto* self = static_cast<BlobRect*>(prop->object);
self->m_excludeCorners[index] = rect;
if (self->m_group)
self->m_group->markDirty();
emit self->excludeCornersChanged();
}
void BlobRect::excludeCornersRemoveLast(QQmlListProperty<BlobRect>* prop) {
auto* self = static_cast<BlobRect*>(prop->object);
if (self->m_excludeCorners.isEmpty())
return;
self->m_excludeCorners.removeLast();
if (self->m_group)
self->m_group->markDirty();
emit self->excludeCornersChanged();
}
void BlobRect::checkAtRest(float speed) { void BlobRect::checkAtRest(float speed) {
constexpr float kEpsilon = 0.002f; constexpr float kEpsilon = 0.002f;
const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&

View file

@ -14,6 +14,7 @@ class BlobRect : public BlobShape {
Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged)
Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged) Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged)
Q_PROPERTY(QQmlListProperty<BlobRect> exclude READ exclude NOTIFY excludeChanged) Q_PROPERTY(QQmlListProperty<BlobRect> exclude READ exclude NOTIFY excludeChanged)
Q_PROPERTY(QQmlListProperty<BlobRect> excludeCorners READ excludeCorners NOTIFY excludeCornersChanged)
Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged) Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged)
Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged) Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged)
Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged) Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged)
@ -52,8 +53,10 @@ public:
} }
QQmlListProperty<BlobRect> exclude(); QQmlListProperty<BlobRect> exclude();
QQmlListProperty<BlobRect> excludeCorners();
bool isExcluded(const BlobShape* other) const override; bool isExcluded(const BlobShape* other) const override;
bool isCornerExcluded(const BlobShape* other) const override;
void cornerRadii(float out[4]) const override; void cornerRadii(float out[4]) const override;
qreal topLeftRadius() const { return m_topLeftRadius; } qreal topLeftRadius() const { return m_topLeftRadius; }
@ -77,6 +80,7 @@ signals:
void dampingChanged(); void dampingChanged();
void deformScaleChanged(); void deformScaleChanged();
void excludeChanged(); void excludeChanged();
void excludeCornersChanged();
void topLeftRadiusChanged(); void topLeftRadiusChanged();
void topRightRadiusChanged(); void topRightRadiusChanged();
void bottomLeftRadiusChanged(); void bottomLeftRadiusChanged();
@ -116,6 +120,7 @@ private:
qreal m_bottomRightRadius = -1; qreal m_bottomRightRadius = -1;
QList<QPointer<BlobRect>> m_exclude; QList<QPointer<BlobRect>> m_exclude;
QList<QPointer<BlobRect>> m_excludeCorners;
static void excludeAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect); static void excludeAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect);
static qsizetype excludeCount(QQmlListProperty<BlobRect>* prop); static qsizetype excludeCount(QQmlListProperty<BlobRect>* prop);
@ -123,4 +128,11 @@ private:
static void excludeClear(QQmlListProperty<BlobRect>* prop); static void excludeClear(QQmlListProperty<BlobRect>* prop);
static void excludeReplace(QQmlListProperty<BlobRect>* prop, qsizetype index, BlobRect* rect); static void excludeReplace(QQmlListProperty<BlobRect>* prop, qsizetype index, BlobRect* rect);
static void excludeRemoveLast(QQmlListProperty<BlobRect>* prop); static void excludeRemoveLast(QQmlListProperty<BlobRect>* prop);
static void excludeCornersAppend(QQmlListProperty<BlobRect>* prop, BlobRect* rect);
static qsizetype excludeCornersCount(QQmlListProperty<BlobRect>* prop);
static BlobRect* excludeCornersAt(QQmlListProperty<BlobRect>* prop, qsizetype index);
static void excludeCornersClear(QQmlListProperty<BlobRect>* prop);
static void excludeCornersReplace(QQmlListProperty<BlobRect>* prop, qsizetype index, BlobRect* rect);
static void excludeCornersRemoveLast(QQmlListProperty<BlobRect>* prop);
}; };

View file

@ -288,10 +288,12 @@ void BlobShape::updatePolish() {
// Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU) // Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU)
const float smoothFactor = pad; const float smoothFactor = pad;
constexpr float minR = 2.0f; constexpr float minR = 2.0f;
const bool cornerFill = m_group->cornerFill();
const auto rectCount = m_cachedRects.size(); const auto rectCount = m_cachedRects.size();
for (qsizetype i = 0; i < rectCount; ++i) { for (qsizetype i = 0; i < rectCount; ++i) {
auto& ri = m_cachedRects[i]; auto& ri = m_cachedRects[i];
const int riExcludeMask = ri.excludeMask; const int riExcludeMask = ri.excludeMask;
BlobShape* const si = rectShapes[i];
float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; 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 cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh;
@ -299,11 +301,14 @@ void BlobShape::updatePolish() {
const float cBlX = ri.cx - ri.hw, cBlY = 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 cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh;
for (qsizetype j = 0; j < rectCount; ++j) { for (qsizetype j = 0; cornerFill && j < rectCount; ++j) {
if (j == i) if (j == i)
continue; continue;
if (riExcludeMask & (1 << j)) if (riExcludeMask & (1 << j))
continue; continue;
BlobShape* const sj = rectShapes[j];
if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si))
continue;
const auto& rj = m_cachedRects[j]; const auto& rj = m_cachedRects[j];
// Skip when the corner is inside rj: it's buried, not on a visible junction, // Skip when the corner is inside rj: it's buried, not on a visible junction,
// so squaring it would only perturb interior SDF gradients. // so squaring it would only perturb interior SDF gradients.
@ -321,7 +326,7 @@ void BlobShape::updatePolish() {
fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, sdTl)); fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, sdTl));
} }
if (m_cachedHasInverted) { if (cornerFill && m_cachedHasInverted) {
const float icx = m_cachedInvertedInner[0]; const float icx = m_cachedInvertedInner[0];
const float icy = m_cachedInvertedInner[1]; const float icy = m_cachedInvertedInner[1];
const float ihw = m_cachedInvertedInner[2]; const float ihw = m_cachedInvertedInner[2];

View file

@ -49,6 +49,8 @@ protected:
virtual bool isExcluded(const BlobShape* /*other*/) const { return false; } virtual bool isExcluded(const BlobShape* /*other*/) const { return false; }
virtual bool isCornerExcluded(const BlobShape* /*other*/) const { return false; }
virtual void cornerRadii(float out[4]) const; virtual void cornerRadii(float out[4]) const;
virtual void updatePhysics() {} virtual void updatePhysics() {}