#pragma once #include "blobmaterial.hpp" #include #include #include #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 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. static constexpr int kMaxRects = 16; QVector rects; bool hasInverted = false; float invertedRadius = 0; float invertedOuter[4] = {}; float invertedInner[4] = {}; }; class BlobGroup2 : public QQuickItem { Q_OBJECT QML_ELEMENT Q_PROPERTY( qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged) Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) Q_PROPERTY( bool cornerFill READ cornerFill WRITE setCornerFill NOTIFY cornerFillChanged) Q_PROPERTY( QColor shadowColor READ shadowColor WRITE setShadowColor NOTIFY shadowColorChanged) Q_PROPERTY( qreal shadowRadius READ shadowRadius WRITE setShadowRadius NOTIFY shadowRadiusChanged) friend class BlobShape2; public: explicit BlobGroup2(QQuickItem* parent = nullptr); ~BlobGroup2() override; [[nodiscard]] qreal smoothing() const { return m_smoothing; } void setSmoothing(qreal s); [[nodiscard]] QColor color() const { return m_color; } void setColor(const QColor& c); [[nodiscard]] bool cornerFill() const { return m_cornerFill; } void setCornerFill(bool e); [[nodiscard]] QColor shadowColor() const { return m_shadowColor; } void setShadowColor(const QColor& c); [[nodiscard]] qreal shadowRadius() const { return m_shadowRadius; } void setShadowRadius(qreal r); void addShape(BlobShape2* shape); void removeShape(BlobShape2* shape); void setInvertedRect(BlobInvertedRect2* rect); void clearInvertedRect(BlobInvertedRect2* rect); [[nodiscard]] const QList& shapes() const { return m_shapes; } [[nodiscard]] BlobInvertedRect2* invertedRect() const { return m_invertedRect; } [[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(); signals: void smoothingChanged(); void colorChanged(); void cornerFillChanged(); void shadowColorChanged(); void shadowRadiusChanged(); protected: void updatePolish() override; QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; private: void ensurePhysicsUpdated(); void ensureDataBuilt(); void resolveSources(const QVector& entryShapes); void fillMaterial(BlobMaterial* material); qreal m_smoothing = 32.0; QColor m_color{0x44, 0x88, 0xff}; bool m_cornerFill = true; QColor m_shadowColor{0, 0, 0, 128}; qreal m_shadowRadius = 0.0; QList m_shapes; BlobInvertedRect2* m_invertedRect = nullptr; bool m_physicsUpdated = false; bool m_dataBuilt = false; 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; };