From bde55ad574ac84440e2cdc9c1122a344bb1cb67a Mon Sep 17 00:00:00 2001 From: Eskil Abrahamsen Blomfeldt Date: Tue, 10 Oct 2023 14:23:33 +0200 Subject: Introduce a CurveRendering backend for text This moves the internals of the curve renderer out from Qt Quick Shapes and into a more centralized location in Qt Quick, so that we can use the same code to create a new text backend for rendering large scale text without artifacts. Change-Id: I3f7e6f7961c1bbe230fcb531c0ca028e038c1afd Reviewed-by: Eirik Aavitsland --- src/quick/CMakeLists.txt | 143 +++ src/quick/items/qquicktext.cpp | 8 + src/quick/items/qquicktext_p.h | 3 +- src/quick/items/qquicktextedit.cpp | 8 + src/quick/items/qquicktextedit_p.h | 3 +- src/quick/items/qquicktextinput.cpp | 8 + src/quick/items/qquicktextinput_p.h | 3 +- src/quick/items/qquicktextutil_p.h | 2 + src/quick/items/qquickwindow.cpp | 9 +- src/quick/items/qquickwindow.h | 3 +- src/quick/items/qsginternaltextnode.cpp | 11 +- .../adaptations/software/qsgsoftwarecontext.cpp | 4 +- .../adaptations/software/qsgsoftwarecontext_p.h | 2 +- src/quick/scenegraph/qsgcontext.cpp | 10 + src/quick/scenegraph/qsgcontext_p.h | 5 +- src/quick/scenegraph/qsgcurveabstractnode_p.h | 32 + src/quick/scenegraph/qsgcurvefillnode.cpp | 60 + src/quick/scenegraph/qsgcurvefillnode_p.cpp | 345 ++++++ src/quick/scenegraph/qsgcurvefillnode_p.h | 226 ++++ src/quick/scenegraph/qsgcurvefillnode_p_p.h | 44 + src/quick/scenegraph/qsgcurveglyphatlas.cpp | 142 +++ src/quick/scenegraph/qsgcurveglyphatlas_p.h | 69 ++ src/quick/scenegraph/qsgcurveglyphnode.cpp | 165 +++ src/quick/scenegraph/qsgcurveglyphnode_p.h | 69 ++ src/quick/scenegraph/qsgcurveprocessor.cpp | 1109 ++++++++++++++++++ src/quick/scenegraph/qsgcurveprocessor_p.h | 51 + src/quick/scenegraph/qsgcurvestrokenode.cpp | 112 ++ src/quick/scenegraph/qsgcurvestrokenode_p.cpp | 88 ++ src/quick/scenegraph/qsgcurvestrokenode_p.h | 116 ++ src/quick/scenegraph/qsgcurvestrokenode_p_p.h | 73 ++ src/quick/scenegraph/qsgdefaultcontext.cpp | 7 +- src/quick/scenegraph/qsgdefaultcontext_p.h | 2 +- src/quick/scenegraph/qsgdefaultrendercontext.cpp | 19 + src/quick/scenegraph/qsgdefaultrendercontext_p.h | 2 + src/quick/scenegraph/shaders_ng/shapecurve.frag | 164 +++ src/quick/scenegraph/shaders_ng/shapecurve.vert | 81 ++ src/quick/scenegraph/shaders_ng/shapestroke.frag | 130 +++ src/quick/scenegraph/shaders_ng/shapestroke.vert | 76 ++ src/quick/scenegraph/util/qquadpath.cpp | 802 +++++++++++++ src/quick/scenegraph/util/qquadpath_p.h | 258 +++++ src/quick/scenegraph/util/qsggradientcache.cpp | 121 ++ src/quick/scenegraph/util/qsggradientcache_p.h | 72 ++ src/quick/scenegraph/util/qsgtextnode.cpp | 8 + src/quick/scenegraph/util/qsgtextnode.h | 3 +- src/quickshapes/CMakeLists.txt | 129 --- src/quickshapes/qquadpath.cpp | 800 ------------- src/quickshapes/qquadpath_p.h | 257 ----- src/quickshapes/qquickshape.cpp | 106 -- src/quickshapes/qquickshape_p_p.h | 41 - src/quickshapes/qquickshapeabstractcurvenode_p.h | 32 - src/quickshapes/qquickshapecurvenode.cpp | 63 -- src/quickshapes/qquickshapecurvenode_p.cpp | 344 ------ src/quickshapes/qquickshapecurvenode_p.h | 196 ---- src/quickshapes/qquickshapecurvenode_p_p.h | 43 - src/quickshapes/qquickshapecurverenderer.cpp | 1195 ++------------------ src/quickshapes/qquickshapecurverenderer_p.h | 16 +- src/quickshapes/qquickshapegenericrenderer.cpp | 28 +- src/quickshapes/qquickshapegenericrenderer_p.h | 5 +- src/quickshapes/qquickshapestrokenode.cpp | 112 -- src/quickshapes/qquickshapestrokenode_p.cpp | 89 -- src/quickshapes/qquickshapestrokenode_p.h | 105 -- src/quickshapes/qquickshapestrokenode_p_p.h | 72 -- src/quickshapes/shaders_ng/shapecurve.frag | 164 --- src/quickshapes/shaders_ng/shapecurve.vert | 81 -- src/quickshapes/shaders_ng/shapestroke.frag | 134 --- src/quickshapes/shaders_ng/shapestroke.vert | 76 -- tests/baseline/scenegraph/data/text/text_emoji.qml | 5 +- .../scenegraph/data/text/text_emoji_hebrew.qml | 4 +- tests/manual/painterpathquickshape/TextShape.qml | 2 +- tests/manual/textrendering/CMakeLists.txt | 40 + tests/manual/textrendering/main.cpp | 26 + tests/manual/textrendering/main.qml | 103 ++ 72 files changed, 4929 insertions(+), 4007 deletions(-) create mode 100644 src/quick/scenegraph/qsgcurveabstractnode_p.h create mode 100644 src/quick/scenegraph/qsgcurvefillnode.cpp create mode 100644 src/quick/scenegraph/qsgcurvefillnode_p.cpp create mode 100644 src/quick/scenegraph/qsgcurvefillnode_p.h create mode 100644 src/quick/scenegraph/qsgcurvefillnode_p_p.h create mode 100644 src/quick/scenegraph/qsgcurveglyphatlas.cpp create mode 100644 src/quick/scenegraph/qsgcurveglyphatlas_p.h create mode 100644 src/quick/scenegraph/qsgcurveglyphnode.cpp create mode 100644 src/quick/scenegraph/qsgcurveglyphnode_p.h create mode 100644 src/quick/scenegraph/qsgcurveprocessor.cpp create mode 100644 src/quick/scenegraph/qsgcurveprocessor_p.h create mode 100644 src/quick/scenegraph/qsgcurvestrokenode.cpp create mode 100644 src/quick/scenegraph/qsgcurvestrokenode_p.cpp create mode 100644 src/quick/scenegraph/qsgcurvestrokenode_p.h create mode 100644 src/quick/scenegraph/qsgcurvestrokenode_p_p.h create mode 100644 src/quick/scenegraph/shaders_ng/shapecurve.frag create mode 100644 src/quick/scenegraph/shaders_ng/shapecurve.vert create mode 100644 src/quick/scenegraph/shaders_ng/shapestroke.frag create mode 100644 src/quick/scenegraph/shaders_ng/shapestroke.vert create mode 100644 src/quick/scenegraph/util/qquadpath.cpp create mode 100644 src/quick/scenegraph/util/qquadpath_p.h create mode 100644 src/quick/scenegraph/util/qsggradientcache.cpp create mode 100644 src/quick/scenegraph/util/qsggradientcache_p.h delete mode 100644 src/quickshapes/qquadpath.cpp delete mode 100644 src/quickshapes/qquadpath_p.h delete mode 100644 src/quickshapes/qquickshapeabstractcurvenode_p.h delete mode 100644 src/quickshapes/qquickshapecurvenode.cpp delete mode 100644 src/quickshapes/qquickshapecurvenode_p.cpp delete mode 100644 src/quickshapes/qquickshapecurvenode_p.h delete mode 100644 src/quickshapes/qquickshapecurvenode_p_p.h delete mode 100644 src/quickshapes/qquickshapestrokenode.cpp delete mode 100644 src/quickshapes/qquickshapestrokenode_p.cpp delete mode 100644 src/quickshapes/qquickshapestrokenode_p.h delete mode 100644 src/quickshapes/qquickshapestrokenode_p_p.h delete mode 100644 src/quickshapes/shaders_ng/shapecurve.frag delete mode 100644 src/quickshapes/shaders_ng/shapecurve.vert delete mode 100644 src/quickshapes/shaders_ng/shapestroke.frag delete mode 100644 src/quickshapes/shaders_ng/shapestroke.vert create mode 100644 tests/manual/textrendering/CMakeLists.txt create mode 100644 tests/manual/textrendering/main.cpp create mode 100644 tests/manual/textrendering/main.qml diff --git a/src/quick/CMakeLists.txt b/src/quick/CMakeLists.txt index c859008fa0..2c2604edab 100644 --- a/src/quick/CMakeLists.txt +++ b/src/quick/CMakeLists.txt @@ -134,13 +134,18 @@ qt_internal_add_qml_module(Quick scenegraph/coreapi/qsgtexture.cpp scenegraph/coreapi/qsgtexture.h scenegraph/coreapi/qsgtexture_p.h scenegraph/coreapi/qsgtexture_platform.h scenegraph/qsgadaptationlayer.cpp scenegraph/qsgadaptationlayer_p.h + scenegraph/qsgcurveabstractnode_p.h scenegraph/qsgbasicglyphnode.cpp scenegraph/qsgbasicglyphnode_p.h scenegraph/qsgbasicinternalimagenode.cpp scenegraph/qsgbasicinternalimagenode_p.h scenegraph/qsgbasicinternalrectanglenode.cpp scenegraph/qsgbasicinternalrectanglenode_p.h scenegraph/qsgcontext.cpp scenegraph/qsgcontext_p.h scenegraph/qsgcontextplugin.cpp scenegraph/qsgcontextplugin_p.h + scenegraph/qsgcurvefillnode.cpp scenegraph/qsgcurvefillnode_p.cpp scenegraph/qsgcurvefillnode_p.h scenegraph/qsgcurvefillnode_p_p.h + scenegraph/qsgcurvestrokenode.cpp scenegraph/qsgcurvestrokenode_p.cpp scenegraph/qsgcurvestrokenode_p.h scenegraph/qsgcurvestrokenode_p_p.h scenegraph/qsgdefaultcontext.cpp scenegraph/qsgdefaultcontext_p.h scenegraph/qsgdefaultglyphnode.cpp scenegraph/qsgdefaultglyphnode_p.cpp scenegraph/qsgdefaultglyphnode_p.h + scenegraph/qsgcurveglyphatlas.cpp scenegraph/qsgcurveglyphatlas_p.h + scenegraph/qsgcurveglyphnode.cpp scenegraph/qsgcurveglyphnode_p.h scenegraph/qsgdefaultglyphnode_p_p.h scenegraph/qsgdefaultinternalimagenode.cpp scenegraph/qsgdefaultinternalimagenode_p.h scenegraph/qsgdefaultinternalrectanglenode.cpp scenegraph/qsgdefaultinternalrectanglenode_p.h @@ -153,6 +158,7 @@ qt_internal_add_qml_module(Quick scenegraph/qsgrhishadereffectnode.cpp scenegraph/qsgrhishadereffectnode_p.h scenegraph/qsgrhisupport.cpp scenegraph/qsgrhisupport_p.h scenegraph/qsgrhitextureglyphcache.cpp scenegraph/qsgrhitextureglyphcache_p.h + scenegraph/qsgcurveprocessor.cpp scenegraph/qsgcurveprocessor_p.h scenegraph/util/qsgareaallocator.cpp scenegraph/util/qsgareaallocator_p.h scenegraph/util/qsgdefaultimagenode.cpp scenegraph/util/qsgdefaultimagenode_p.h scenegraph/util/qsgdefaultninepatchnode.cpp scenegraph/util/qsgdefaultninepatchnode_p.h @@ -171,6 +177,8 @@ qt_internal_add_qml_module(Quick scenegraph/util/qsgtextureprovider.cpp scenegraph/util/qsgtextureprovider.h scenegraph/util/qsgtexturereader.cpp scenegraph/util/qsgtexturereader_p.h scenegraph/util/qsgvertexcolormaterial.cpp scenegraph/util/qsgvertexcolormaterial.h + scenegraph/util/qquadpath.cpp scenegraph/util/qquadpath_p.h + scenegraph/util/qsggradientcache.cpp scenegraph/util/qsggradientcache_p.h util/qminimalflatset_p.h util/qquickanimation.cpp util/qquickanimation_p.h util/qquickanimation_p_p.h @@ -310,6 +318,141 @@ qt_internal_add_shaders(Quick "scenegraph_shaders" "scenegraph/shaders_ng/visualization.vert" ) +qt_internal_add_shaders(Quick "scenegraph_curve_shaders" + SILENT + BATCHABLE + PRECOMPILE + OPTIMIZED + PREFIX + "/qt-project.org" + FILES + "scenegraph/shaders_ng/shapecurve.frag" + "scenegraph/shaders_ng/shapecurve.vert" + "scenegraph/shaders_ng/shapestroke.frag" + "scenegraph/shaders_ng/shapestroke.vert" +) + +qt_internal_add_shaders(Quick "scenegraph_curve_shaders_derivatives" + SILENT + BATCHABLE + PRECOMPILE + OPTIMIZED + DEFINES "USE_DERIVATIVES" + PREFIX + "/qt-project.org" + FILES + "scenegraph/shaders_ng/shapecurve.frag" + "scenegraph/shaders_ng/shapecurve.vert" + OUTPUTS + "scenegraph/shaders_ng/shapecurve_derivatives.frag.qsb" + "scenegraph/shaders_ng/shapecurve_derivatives.vert.qsb" +) + +qt_internal_add_shaders(Quick "scenegraph_curve_shaders_lg" + SILENT + BATCHABLE + PRECOMPILE + OPTIMIZED + DEFINES + "LINEARGRADIENT" + PREFIX + "/qt-project.org" + FILES + "scenegraph/shaders_ng/shapecurve.frag" + "scenegraph/shaders_ng/shapecurve.vert" + OUTPUTS + "scenegraph/shaders_ng/shapecurve_lg.frag.qsb" + "scenegraph/shaders_ng/shapecurve_lg.vert.qsb" +) + +qt_internal_add_shaders(Quick "scenegraph_curve_shaders_lg_derivatives" + SILENT + BATCHABLE + PRECOMPILE + OPTIMIZED + DEFINES + "LINEARGRADIENT" + "USE_DERIVATIVES" + PREFIX + "/qt-project.org" + FILES + "scenegraph/shaders_ng/shapecurve.frag" + "scenegraph/shaders_ng/shapecurve.vert" + OUTPUTS + "scenegraph/shaders_ng/shapecurve_lg_derivatives.frag.qsb" + "scenegraph/shaders_ng/shapecurve_lg_derivatives.vert.qsb" +) + +qt_internal_add_shaders(Quick "scenegraph_curve_shaders_rg" + SILENT + BATCHABLE + PRECOMPILE + OPTIMIZED + DEFINES + "RADIALGRADIENT" + PREFIX + "/qt-project.org" + FILES + "scenegraph/shaders_ng/shapecurve.frag" + "scenegraph/shaders_ng/shapecurve.vert" + OUTPUTS + "scenegraph/shaders_ng/shapecurve_rg.frag.qsb" + "scenegraph/shaders_ng/shapecurve_rg.vert.qsb" +) + +qt_internal_add_shaders(Quick "scenegraph_curve_shaders_rg_derivatives" + SILENT + BATCHABLE + PRECOMPILE + OPTIMIZED + DEFINES + "RADIALGRADIENT" + "USE_DERIVATIVES" + PREFIX + "/qt-project.org" + FILES + "scenegraph/shaders_ng/shapecurve.frag" + "scenegraph/shaders_ng/shapecurve.vert" + OUTPUTS + "scenegraph/shaders_ng/shapecurve_rg_derivatives.frag.qsb" + "scenegraph/shaders_ng/shapecurve_rg_derivatives.vert.qsb" +) + +qt_internal_add_shaders(Quick "scenegraph_curve_shaders_cg" + SILENT + BATCHABLE + PRECOMPILE + OPTIMIZED + DEFINES + "CONICALGRADIENT" + PREFIX + "/qt-project.org" + FILES + "scenegraph/shaders_ng/shapecurve.frag" + "scenegraph/shaders_ng/shapecurve.vert" + OUTPUTS + "scenegraph/shaders_ng/shapecurve_cg.frag.qsb" + "scenegraph/shaders_ng/shapecurve_cg.vert.qsb" +) + +qt_internal_add_shaders(Quick "scenegraph_curve_shaders_cg_derivatives" + SILENT + BATCHABLE + PRECOMPILE + OPTIMIZED + DEFINES + "CONICALGRADIENT" + "USE_DERIVATIVES" + PREFIX + "/qt-project.org" + FILES + "scenegraph/shaders_ng/shapecurve.frag" + "scenegraph/shaders_ng/shapecurve.vert" + OUTPUTS + "scenegraph/shaders_ng/shapecurve_cg_derivatives.frag.qsb" + "scenegraph/shaders_ng/shapecurve_cg_derivatives.vert.qsb" +) + qt_internal_extend_target(Quick CONDITION QT_FEATURE_qml_network LIBRARIES Qt::Network diff --git a/src/quick/items/qquicktext.cpp b/src/quick/items/qquicktext.cpp index e69d878409..a3e5c94761 100644 --- a/src/quick/items/qquicktext.cpp +++ b/src/quick/items/qquicktext.cpp @@ -3119,12 +3119,20 @@ void QQuickText::setRenderTypeQuality(int renderTypeQuality) \value Text.QtRendering Text is rendered using a scalable distance field for each glyph. \value Text.NativeRendering Text is rendered using a platform-specific technique. + \value Text.CurveRendering Text is rendered using a curve rasterizer running directly on the + graphics hardware. (Introduced in Qt 6.7.0.) Select \c Text.NativeRendering if you prefer text to look native on the target platform and do not require advanced features such as transformation of the text. Using such features in combination with the NativeRendering render type will lend poor and sometimes pixelated results. + Both \c Text.QtRendering and \c Text.CurveRendering are hardware-accelerated techniques. + \c QtRendering is the faster of the two, but uses more memory and will exhibit rendering + artifacts at large sizes. \c CurveRendering should be considered as an alternative in cases + where \c QtRendering does not give good visual results or where reducing graphics memory + consumption is a priority. + The default rendering type is determined by \l QQuickWindow::textRenderType(). */ QQuickText::RenderType QQuickText::renderType() const diff --git a/src/quick/items/qquicktext_p.h b/src/quick/items/qquicktext_p.h index 5304648fca..d827a0e5d7 100644 --- a/src/quick/items/qquicktext_p.h +++ b/src/quick/items/qquicktext_p.h @@ -109,7 +109,8 @@ public: Q_ENUM(WrapMode) enum RenderType { QtRendering, - NativeRendering + NativeRendering, + CurveRendering }; Q_ENUM(RenderType) diff --git a/src/quick/items/qquicktextedit.cpp b/src/quick/items/qquicktextedit.cpp index 7870e89538..e139b825f0 100644 --- a/src/quick/items/qquicktextedit.cpp +++ b/src/quick/items/qquicktextedit.cpp @@ -533,12 +533,20 @@ void QQuickTextEdit::setTextFormat(TextFormat format) \value TextEdit.QtRendering Text is rendered using a scalable distance field for each glyph. \value TextEdit.NativeRendering Text is rendered using a platform-specific technique. + \value TextEdit.CurveRendering Text is rendered using a curve rasterizer running directly on + the graphics hardware. (Introduced in Qt 6.7.0.) Select \c TextEdit.NativeRendering if you prefer text to look native on the target platform and do not require advanced features such as transformation of the text. Using such features in combination with the NativeRendering render type will lend poor and sometimes pixelated results. + Both \c TextEdit.QtRendering and \c TextEdit.CurveRendering are hardware-accelerated techniques. + \c QtRendering is the faster of the two, but uses more memory and will exhibit rendering + artifacts at large sizes. \c CurveRendering should be considered as an alternative in cases + where \c QtRendering does not give good visual results or where reducing graphics memory + consumption is a priority. + The default rendering type is determined by \l QQuickWindow::textRenderType(). */ QQuickTextEdit::RenderType QQuickTextEdit::renderType() const diff --git a/src/quick/items/qquicktextedit_p.h b/src/quick/items/qquicktextedit_p.h index f64332d508..2a3d01353c 100644 --- a/src/quick/items/qquicktextedit_p.h +++ b/src/quick/items/qquicktextedit_p.h @@ -124,7 +124,8 @@ public: Q_ENUM(SelectionMode) enum RenderType { QtRendering, - NativeRendering + NativeRendering, + CurveRendering }; Q_ENUM(RenderType) diff --git a/src/quick/items/qquicktextinput.cpp b/src/quick/items/qquicktextinput.cpp index 2cce5378fb..f2042c70bb 100644 --- a/src/quick/items/qquicktextinput.cpp +++ b/src/quick/items/qquicktextinput.cpp @@ -136,12 +136,20 @@ void QQuickTextInput::setText(const QString &s) \value TextInput.QtRendering Text is rendered using a scalable distance field for each glyph. \value TextInput.NativeRendering Text is rendered using a platform-specific technique. + \value TextInput.CurveRendering Text is rendered using a curve rasterizer running directly on + the graphics hardware. (Introduced in Qt 6.7.0.) Select \c TextInput.NativeRendering if you prefer text to look native on the target platform and do not require advanced features such as transformation of the text. Using such features in combination with the NativeRendering render type will lend poor and sometimes pixelated results. + Both \c TextInput.QtRendering and \c TextInput.CurveRendering are hardware-accelerated techniques. + \c QtRendering is the faster of the two, but uses more memory and will exhibit rendering + artifacts at large sizes. \c CurveRendering should be considered as an alternative in cases + where \c QtRendering does not give good visual results or where reducing graphics memory + consumption is a priority. + The default rendering type is determined by \l QQuickWindow::textRenderType(). */ QQuickTextInput::RenderType QQuickTextInput::renderType() const diff --git a/src/quick/items/qquicktextinput_p.h b/src/quick/items/qquicktextinput_p.h index 515c696b0c..d9c8328921 100644 --- a/src/quick/items/qquicktextinput_p.h +++ b/src/quick/items/qquicktextinput_p.h @@ -138,7 +138,8 @@ public: Q_ENUM(CursorPosition) enum RenderType { QtRendering, - NativeRendering + NativeRendering, + CurveRendering }; Q_ENUM(RenderType) diff --git a/src/quick/items/qquicktextutil_p.h b/src/quick/items/qquicktextutil_p.h index 212c488c73..9f8e2ae393 100644 --- a/src/quick/items/qquicktextutil_p.h +++ b/src/quick/items/qquicktextutil_p.h @@ -99,6 +99,8 @@ typename T::RenderType QQuickTextUtil::textRenderType() return T::QtRendering; case QQuickWindow::NativeTextRendering: return T::NativeRendering; + case QQuickWindow::CurveTextRendering: + return T::CurveRendering; } Q_UNREACHABLE_RETURN(T::QtRendering); diff --git a/src/quick/items/qquickwindow.cpp b/src/quick/items/qquickwindow.cpp index 315381e4e6..4f371b98a3 100644 --- a/src/quick/items/qquickwindow.cpp +++ b/src/quick/items/qquickwindow.cpp @@ -2656,9 +2656,16 @@ QQmlIncubationController *QQuickWindow::incubationController() const text. Using such features in combination with the NativeTextRendering render type will lend poor and sometimes pixelated results. - \value QtTextRendering Use Qt's own rasterization algorithm. + Both \c QtTextRendering and \c CurveTextRendering are hardware-accelerated techniques. + \c QtTextRendering is the faster of the two, but uses more memory and will exhibit rendering + artifacts at large sizes. \c CurveTextRendering should be considered as an alternative in cases + where \c QtTextRendering does not give good visual results or where reducing graphics memory + consumption is a priority. + \value QtTextRendering Use Qt's own rasterization algorithm. \value NativeTextRendering Use the operating system's native rasterizer for text. + \value CurveTextRendering Text is rendered using a curve rasterizer running directly on + the graphics hardware. (Introduced in Qt 6.7.0.) */ /*! diff --git a/src/quick/items/qquickwindow.h b/src/quick/items/qquickwindow.h index 1a184e7c34..d93ee71806 100644 --- a/src/quick/items/qquickwindow.h +++ b/src/quick/items/qquickwindow.h @@ -82,7 +82,8 @@ public: enum TextRenderType { QtTextRendering, - NativeTextRendering + NativeTextRendering, + CurveTextRendering }; Q_ENUM(TextRenderType) diff --git a/src/quick/items/qsginternaltextnode.cpp b/src/quick/items/qsginternaltextnode.cpp index 1e3c2b468d..c9200e1e3d 100644 --- a/src/quick/items/qsginternaltextnode.cpp +++ b/src/quick/items/qsginternaltextnode.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include @@ -60,10 +59,14 @@ QSGGlyphNode *QSGInternalTextNode::addGlyphs(const QPointF &position, const QGly } } + QSGTextNode::RenderType preferredRenderType = + preferNativeGlyphNode + ? QSGTextNode::NativeRendering + : m_renderType; + QSGGlyphNode *node = m_renderContext->sceneGraphContext()->createGlyphNode(m_renderContext, - preferNativeGlyphNode, + preferredRenderType, m_renderTypeQuality); - node->setGlyphs(position + QPointF(0, glyphs.rawFont().ascent()), glyphs); node->setStyle(style); node->setStyleColor(styleColor); @@ -85,7 +88,7 @@ QSGGlyphNode *QSGInternalTextNode::addGlyphs(const QPointF &position, const QGly if (style == QQuickText::Outline && color.alpha() > 0 && styleColor != color) { QSGGlyphNode *fillNode = m_renderContext->sceneGraphContext()->createGlyphNode(m_renderContext, - preferNativeGlyphNode, + preferredRenderType, m_renderTypeQuality); fillNode->setGlyphs(position + QPointF(0, glyphs.rawFont().ascent()), glyphs); fillNode->setStyle(QQuickText::Normal); diff --git a/src/quick/scenegraph/adaptations/software/qsgsoftwarecontext.cpp b/src/quick/scenegraph/adaptations/software/qsgsoftwarecontext.cpp index 4fd0003e54..ef43aa1012 100644 --- a/src/quick/scenegraph/adaptations/software/qsgsoftwarecontext.cpp +++ b/src/quick/scenegraph/adaptations/software/qsgsoftwarecontext.cpp @@ -75,10 +75,10 @@ QSGPainterNode *QSGSoftwareContext::createPainterNode(QQuickPaintedItem *item) return new QSGSoftwarePainterNode(item); } -QSGGlyphNode *QSGSoftwareContext::createGlyphNode(QSGRenderContext *rc, bool preferNativeGlyphNode, int renderTypeQuality) +QSGGlyphNode *QSGSoftwareContext::createGlyphNode(QSGRenderContext *rc, QSGTextNode::RenderType renderType, int renderTypeQuality) { Q_UNUSED(rc); - Q_UNUSED(preferNativeGlyphNode); + Q_UNUSED(renderType); Q_UNUSED(renderTypeQuality); return new QSGSoftwareGlyphNode(); } diff --git a/src/quick/scenegraph/adaptations/software/qsgsoftwarecontext_p.h b/src/quick/scenegraph/adaptations/software/qsgsoftwarecontext_p.h index 6b95ebb1ad..68f18b490a 100644 --- a/src/quick/scenegraph/adaptations/software/qsgsoftwarecontext_p.h +++ b/src/quick/scenegraph/adaptations/software/qsgsoftwarecontext_p.h @@ -55,7 +55,7 @@ public: QSGInternalRectangleNode *createInternalRectangleNode() override; QSGInternalImageNode *createInternalImageNode(QSGRenderContext *renderContext) override; QSGPainterNode *createPainterNode(QQuickPaintedItem *item) override; - QSGGlyphNode *createGlyphNode(QSGRenderContext *rc, bool preferNativeGlyphNode, int renderTypeQuality) override; + QSGGlyphNode *createGlyphNode(QSGRenderContext *rc, QSGTextNode::RenderType renderType, int renderTypeQuality) override; QSGLayer *createLayer(QSGRenderContext *renderContext) override; QSurfaceFormat defaultSurfaceFormat() const override; QSGRendererInterface *rendererInterface(QSGRenderContext *renderContext) override; diff --git a/src/quick/scenegraph/qsgcontext.cpp b/src/quick/scenegraph/qsgcontext.cpp index 4271c079ce..76c521dfd1 100644 --- a/src/quick/scenegraph/qsgcontext.cpp +++ b/src/quick/scenegraph/qsgcontext.cpp @@ -473,6 +473,16 @@ void QSGRenderContext::preprocess() { } +/*! + Factory function for curve atlases that can be used to provide geometry for the curve + renderer for a given font. +*/ +QSGCurveGlyphAtlas *QSGRenderContext::curveGlyphAtlas(const QRawFont &font) +{ + Q_UNUSED(font); + return nullptr; +} + /*! Factory function for scene graph backends of the distance-field glyph cache. */ diff --git a/src/quick/scenegraph/qsgcontext_p.h b/src/quick/scenegraph/qsgcontext_p.h index 0bc50a2c2f..6f124c52dd 100644 --- a/src/quick/scenegraph/qsgcontext_p.h +++ b/src/quick/scenegraph/qsgcontext_p.h @@ -27,6 +27,7 @@ #include #include +#include #include @@ -65,6 +66,7 @@ class QRhiRenderPassDescriptor; class QRhiCommandBuffer; class QQuickGraphicsConfiguration; class QQuickItem; +class QSGCurveGlyphAtlas; Q_DECLARE_LOGGING_CATEGORY(QSG_LOG_TIME_RENDERLOOP) Q_DECLARE_LOGGING_CATEGORY(QSG_LOG_TIME_COMPILATION) @@ -98,7 +100,7 @@ public: virtual QSGInternalImageNode *createInternalImageNode(QSGRenderContext *renderContext) = 0; virtual QSGInternalTextNode *createInternalTextNode(QSGRenderContext *renderContext); virtual QSGPainterNode *createPainterNode(QQuickPaintedItem *item) = 0; - virtual QSGGlyphNode *createGlyphNode(QSGRenderContext *rc, bool preferNativeGlyphNode, int renderTypeQuality) = 0; + virtual QSGGlyphNode *createGlyphNode(QSGRenderContext *rc, QSGTextNode::RenderType renderType, int renderTypeQuality) = 0; virtual QSGLayer *createLayer(QSGRenderContext *renderContext) = 0; virtual QSGGuiThreadShaderEffectManager *createGuiThreadShaderEffectManager(); virtual QSGShaderEffectNode *createShaderEffectNode(QSGRenderContext *renderContext); @@ -165,6 +167,7 @@ public: virtual void preprocess(); virtual void invalidateGlyphCaches(); virtual QSGDistanceFieldGlyphCache *distanceFieldGlyphCache(const QRawFont &font, int renderTypeQuality); + virtual QSGCurveGlyphAtlas *curveGlyphAtlas(const QRawFont &font); QSGTexture *textureForFactory(QQuickTextureFactory *factory, QQuickWindow *window); virtual QSGTexture *createTexture(const QImage &image, uint flags = CreateTexture_Alpha) const = 0; diff --git a/src/quick/scenegraph/qsgcurveabstractnode_p.h b/src/quick/scenegraph/qsgcurveabstractnode_p.h new file mode 100644 index 0000000000..aadc17d3f0 --- /dev/null +++ b/src/quick/scenegraph/qsgcurveabstractnode_p.h @@ -0,0 +1,32 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSGCURVEABSTRACTNODE_P_H +#define QSGCURVEABSTRACTNODE_P_H + +#include +#include + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of a number of Qt sources files. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +QT_BEGIN_NAMESPACE + +class QSGCurveAbstractNode : public QSGGeometryNode +{ +public: + virtual void setColor(QColor col) = 0; + virtual void cookGeometry() = 0; +}; + +QT_END_NAMESPACE + +#endif // QSGCURVEABSTRACTNODE_P_H diff --git a/src/quick/scenegraph/qsgcurvefillnode.cpp b/src/quick/scenegraph/qsgcurvefillnode.cpp new file mode 100644 index 0000000000..9fa526bb0a --- /dev/null +++ b/src/quick/scenegraph/qsgcurvefillnode.cpp @@ -0,0 +1,60 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsgcurvefillnode_p.h" +#include "qsgcurvefillnode_p_p.h" + +QT_BEGIN_NAMESPACE + +QSGCurveFillNode::QSGCurveFillNode() +{ + setFlag(OwnsGeometry, true); + setGeometry(new QSGGeometry(attributes(), 0, 0)); + + updateMaterial(); +} + +void QSGCurveFillNode::updateMaterial() +{ + m_material.reset(new QSGCurveFillMaterial(this)); + setMaterial(m_material.data()); +} + +void QSGCurveFillNode::cookGeometry() +{ + QSGGeometry *g = geometry(); + if (g->indexType() != QSGGeometry::UnsignedIntType) { + g = new QSGGeometry(attributes(), + m_uncookedVertexes.size(), + m_uncookedIndexes.size(), + QSGGeometry::UnsignedIntType); + setGeometry(g); + } else { + g->allocate(m_uncookedVertexes.size(), m_uncookedIndexes.size()); + } + + g->setDrawingMode(QSGGeometry::DrawTriangles); + memcpy(g->vertexData(), + m_uncookedVertexes.constData(), + g->vertexCount() * g->sizeOfVertex()); + memcpy(g->indexData(), + m_uncookedIndexes.constData(), + g->indexCount() * g->sizeOfIndex()); + + m_uncookedIndexes.clear(); + m_uncookedVertexes.clear(); +} + +const QSGGeometry::AttributeSet &QSGCurveFillNode::attributes() +{ + static QSGGeometry::Attribute data[] = { + QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute), + QSGGeometry::Attribute::createWithAttributeType(1, 3, QSGGeometry::FloatType, QSGGeometry::TexCoordAttribute), + QSGGeometry::Attribute::createWithAttributeType(2, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), + QSGGeometry::Attribute::createWithAttributeType(3, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), + }; + static QSGGeometry::AttributeSet attrs = { 4, sizeof(CurveNodeVertex), data }; + return attrs; +} + +QT_END_NAMESPACE diff --git a/src/quick/scenegraph/qsgcurvefillnode_p.cpp b/src/quick/scenegraph/qsgcurvefillnode_p.cpp new file mode 100644 index 0000000000..b7b9c877c1 --- /dev/null +++ b/src/quick/scenegraph/qsgcurvefillnode_p.cpp @@ -0,0 +1,345 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsgcurvefillnode_p_p.h" +#include "qsgcurvefillnode_p.h" +#include "util/qsggradientcache_p.h" + +#include + +QT_BEGIN_NAMESPACE + +namespace { + + class QSGCurveFillMaterialShader : public QSGMaterialShader + { + public: + QSGCurveFillMaterialShader(QGradient::Type gradientType, + bool includeStroke, + bool useDerivatives); + + bool updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override; + void updateSampledImage(RenderState &state, int binding, QSGTexture **texture, + QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; + }; + + QSGCurveFillMaterialShader::QSGCurveFillMaterialShader(QGradient::Type gradientType, + bool includeStroke, + bool useDerivatives) + { + QString baseName = QStringLiteral(":/qt-project.org/scenegraph/shaders_ng/shapecurve"); + + if (gradientType == QGradient::LinearGradient) { + baseName += QStringLiteral("_lg"); + } else if (gradientType == QGradient::RadialGradient) { + baseName += QStringLiteral("_rg"); + } else if (gradientType == QGradient::ConicalGradient) { + baseName += QStringLiteral("_cg"); + } + + if (includeStroke) + baseName += QStringLiteral("_stroke"); + + if (useDerivatives) + baseName += QStringLiteral("_derivatives"); + + setShaderFileName(VertexStage, baseName + QStringLiteral(".vert.qsb")); + setShaderFileName(FragmentStage, baseName + QStringLiteral(".frag.qsb")); + } + + void QSGCurveFillMaterialShader::updateSampledImage(RenderState &state, int binding, QSGTexture **texture, + QSGMaterial *newMaterial, QSGMaterial *oldMaterial) + { + Q_UNUSED(oldMaterial); + const QSGCurveFillMaterial *m = static_cast(newMaterial); + const QSGCurveFillNode *node = m->node(); + if (binding != 1 || node->gradientType() == QGradient::NoGradient) + return; + + const QSGGradientCacheKey cacheKey(node->fillGradient().stops, + node->fillGradient().spread); + QSGTexture *t = QSGGradientCache::cacheForRhi(state.rhi())->get(cacheKey); + t->commitTextureOperations(state.rhi(), state.resourceUpdateBatch()); + *texture = t; + } + + bool QSGCurveFillMaterialShader::updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) + { + bool changed = false; + QByteArray *buf = state.uniformData(); + Q_ASSERT(buf->size() >= 80); + + int offset = 0; + float matrixScale = 0.0f; + if (state.isMatrixDirty()) { + const QMatrix4x4 m = state.combinedMatrix(); + + memcpy(buf->data() + offset, m.constData(), 64); + + matrixScale = qSqrt(qAbs(state.determinant())); + memcpy(buf->data() + offset + 64, &matrixScale, 4); + + changed = true; + } + offset += 68; + + if (state.isOpacityDirty()) { + const float opacity = state.opacity(); + memcpy(buf->data() + offset, &opacity, 4); + changed = true; + } + offset += 4; + + QSGCurveFillMaterial *newMaterial = static_cast(newEffect); + QSGCurveFillMaterial *oldMaterial = static_cast(oldEffect); + + QSGCurveFillNode *newNode = newMaterial != nullptr ? newMaterial->node() : nullptr; + QSGCurveFillNode *oldNode = oldMaterial != nullptr ? oldMaterial->node() : nullptr; + + if (newNode == nullptr) + return changed; + + if (oldNode == nullptr || oldNode->debug() != newNode->debug()) { + float debug = newNode->debug(); + memcpy(buf->data() + offset, &debug, 4); + changed = true; + } + offset += 8; + + if (newNode->hasStroke()) { + Q_ASSERT(buf->size() >= offset + 32); + QVector4D newStrokeColor(newNode->strokeColor().redF(), + newNode->strokeColor().greenF(), + newNode->strokeColor().blueF(), + newNode->strokeColor().alphaF()); + QVector4D oldStrokeColor = oldNode != nullptr + ? QVector4D(oldNode->strokeColor().redF(), + oldNode->strokeColor().greenF(), + oldNode->strokeColor().blueF(), + oldNode->strokeColor().alphaF()) + : QVector4D{}; + + if (oldNode == nullptr || oldStrokeColor != newStrokeColor) { + memcpy(buf->data() + offset, &newStrokeColor, 16); + changed = true; + } + offset += 16; + + if (oldNode == nullptr + || !qFuzzyCompare(newNode->strokeWidth(), oldNode->strokeWidth()) + || (state.isMatrixDirty() && newNode->strokeWidth() > 0.0f)) { + float w = newNode->strokeWidth() * matrixScale; // matrixScale calculated earlier + memcpy(buf->data() + offset, &w, 4); + changed = true; + } + offset += 16; + } + + if (newNode->gradientType() == QGradient::NoGradient) { + Q_ASSERT(buf->size() >= offset + 16); + + QVector4D newColor = QVector4D(newNode->color().redF(), + newNode->color().greenF(), + newNode->color().blueF(), + newNode->color().alphaF()); + QVector4D oldColor = oldNode != nullptr + ? QVector4D(oldNode->color().redF(), + oldNode->color().greenF(), + oldNode->color().blueF(), + oldNode->color().alphaF()) + : QVector4D{}; + + if (oldNode == nullptr || oldColor != newColor) { + memcpy(buf->data() + offset, &newColor, 16); + changed = true; + } + + offset += 16; + } else if (newNode->gradientType() == QGradient::LinearGradient) { + Q_ASSERT(buf->size() >= offset + 8 + 8); + + QVector2D newGradientStart = QVector2D(newNode->fillGradient().a); + QVector2D oldGradientStart = oldNode != nullptr + ? QVector2D(oldNode->fillGradient().a) + : QVector2D{}; + + if (newGradientStart != oldGradientStart || oldEffect == nullptr) { + memcpy(buf->data() + offset, &newGradientStart, 8); + changed = true; + } + offset += 8; + + QVector2D newGradientEnd = QVector2D(newNode->fillGradient().b); + QVector2D oldGradientEnd = oldNode!= nullptr + ? QVector2D(oldNode->fillGradient().b) + : QVector2D{}; + + if (newGradientEnd != oldGradientEnd || oldEffect == nullptr) { + memcpy(buf->data() + offset, &newGradientEnd, 8); + changed = true; + } + + offset += 8; + } else if (newNode->gradientType() == QGradient::RadialGradient) { + Q_ASSERT(buf->size() >= offset + 8 + 8 + 4 + 4); + + QVector2D newFocalPoint = QVector2D(newNode->fillGradient().b); + QVector2D oldFocalPoint = oldNode != nullptr + ? QVector2D(oldNode->fillGradient().b) + : QVector2D{}; + if (oldNode == nullptr || newFocalPoint != oldFocalPoint) { + memcpy(buf->data() + offset, &newFocalPoint, 8); + changed = true; + } + offset += 8; + + QVector2D newCenterPoint = QVector2D(newNode->fillGradient().a); + QVector2D oldCenterPoint = oldNode != nullptr + ? QVector2D(oldNode->fillGradient().a) + : QVector2D{}; + + QVector2D newCenterToFocal = newCenterPoint - newFocalPoint; + QVector2D oldCenterToFocal = oldCenterPoint - oldFocalPoint; + if (oldNode == nullptr || newCenterToFocal != oldCenterToFocal) { + memcpy(buf->data() + offset, &newCenterToFocal, 8); + changed = true; + } + offset += 8; + + float newCenterRadius = newNode->fillGradient().v0; + float oldCenterRadius = oldNode != nullptr + ? oldNode->fillGradient().v0 + : 0.0f; + if (oldNode == nullptr || !qFuzzyCompare(newCenterRadius, oldCenterRadius)) { + memcpy(buf->data() + offset, &newCenterRadius, 4); + changed = true; + } + offset += 4; + + float newFocalRadius = newNode->fillGradient().v1; + float oldFocalRadius = oldNode != nullptr + ? oldNode->fillGradient().v1 + : 0.0f; + if (oldNode == nullptr || !qFuzzyCompare(newFocalRadius, oldFocalRadius)) { + memcpy(buf->data() + offset, &newFocalRadius, 4); + changed = true; + } + offset += 4; + + } else if (newNode->gradientType() == QGradient::ConicalGradient) { + Q_ASSERT(buf->size() >= offset + 8 + 4); + + QVector2D newFocalPoint = QVector2D(newNode->fillGradient().a); + QVector2D oldFocalPoint = oldNode != nullptr + ? QVector2D(oldNode->fillGradient().a) + : QVector2D{}; + if (oldNode == nullptr || newFocalPoint != oldFocalPoint) { + memcpy(buf->data() + offset, &newFocalPoint, 8); + changed = true; + } + offset += 8; + + float newAngle = newNode->fillGradient().v0; + float oldAngle = oldNode != nullptr + ? oldNode->fillGradient().v0 + : 0.0f; + if (oldNode == nullptr || !qFuzzyCompare(newAngle, oldAngle)) { + newAngle = -qDegreesToRadians(newAngle); + memcpy(buf->data() + offset, &newAngle, 4); + changed = true; + } + offset += 4; + } + + return changed; + } +} + +QSGCurveFillMaterial::QSGCurveFillMaterial(QSGCurveFillNode *node) + : m_node(node) +{ + setFlag(Blending, true); + setFlag(RequiresDeterminant, true); +} + +int QSGCurveFillMaterial::compare(const QSGMaterial *other) const +{ + if (other->type() != type()) + return (type() - other->type()); + + const QSGCurveFillMaterial *otherMaterial = + static_cast(other); + + QSGCurveFillNode *a = node(); + QSGCurveFillNode *b = otherMaterial->node(); + if (a == b) + return 0; + + if (int d = a->strokeColor().rgba() - b->strokeColor().rgba()) + return d; + + if (a->gradientType() == QGradient::NoGradient) { + if (int d = a->color().red() - b->color().red()) + return d; + if (int d = a->color().green() - b->color().green()) + return d; + if (int d = a->color().blue() - b->color().blue()) + return d; + if (int d = a->color().alpha() - b->color().alpha()) + return d; + } else { + const QSGGradientCache::GradientDesc &ga = a->fillGradient(); + const QSGGradientCache::GradientDesc &gb = b->fillGradient(); + + if (int d = ga.a.x() - gb.a.x()) + return d; + if (int d = ga.a.y() - gb.a.y()) + return d; + if (int d = ga.b.x() - gb.b.x()) + return d; + if (int d = ga.b.y() - gb.b.y()) + return d; + + if (int d = ga.v0 - gb.v0) + return d; + if (int d = ga.v1 - gb.v1) + return d; + + if (int d = ga.spread - gb.spread) + return d; + + if (int d = ga.stops.size() - gb.stops.size()) + return d; + + for (int i = 0; i < ga.stops.size(); ++i) { + if (int d = ga.stops[i].first - gb.stops[i].first) + return d; + if (int d = ga.stops[i].second.rgba() - gb.stops[i].second.rgba()) + return d; + } + } + + return 0; +} + +QSGMaterialType *QSGCurveFillMaterial::type() const +{ + static QSGMaterialType type[8]; + uint index = node()->gradientType(); + Q_ASSERT((index & ~3) == 0); // Only two first bits for gradient type + + if (node()->hasStroke()) + index |= 4; + + return &type[index]; +} + +QSGMaterialShader *QSGCurveFillMaterial::createShader(QSGRendererInterface::RenderMode renderMode) const +{ + return new QSGCurveFillMaterialShader(node()->gradientType(), + node()->hasStroke(), + renderMode == QSGRendererInterface::RenderMode3D); +} + + +QT_END_NAMESPACE diff --git a/src/quick/scenegraph/qsgcurvefillnode_p.h b/src/quick/scenegraph/qsgcurvefillnode_p.h new file mode 100644 index 0000000000..3ef4caa61c --- /dev/null +++ b/src/quick/scenegraph/qsgcurvefillnode_p.h @@ -0,0 +1,226 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSGCURVEFILLNODE_P_H +#define QSGCURVEFILLNODE_P_H + +#include + +#include +#include +#include + +#include "qsgcurveabstractnode_p.h" + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of a number of Qt sources files. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +QT_BEGIN_NAMESPACE + +class Q_QUICK_PRIVATE_EXPORT QSGCurveFillNode : public QSGCurveAbstractNode +{ +public: + QSGCurveFillNode(); + + void setColor(QColor col) override + { + if (m_color == col) + return; + m_color = col; + updateMaterial(); + } + + QColor color() const + { + return m_color; + } + + void setStrokeColor(QColor col) + { + const bool hadStroke = hasStroke(); + m_strokeColor = col; + if (hadStroke != hasStroke()) + updateMaterial(); + } + + QColor strokeColor() const + { + return m_strokeColor; + } + + void setStrokeWidth(float width) + { + const bool hadStroke = hasStroke(); + m_strokeWidth = width; + if (hadStroke != hasStroke()) + updateMaterial(); + } + + float strokeWidth() const + { + return m_strokeWidth; + } + + void setFillGradient(const QSGGradientCache::GradientDesc &fillGradient) + { + m_fillGradient = fillGradient; + } + + QSGGradientCache::GradientDesc fillGradient() const + { + return m_fillGradient; + } + + void setGradientType(QGradient::Type type) + { + if (m_gradientType != type) { + m_gradientType = type; + updateMaterial(); + } + } + + QGradient::Type gradientType() const + { + return m_gradientType; + } + + float debug() const + { + return m_debug; + } + + void setDebug(float newDebug) + { + m_debug = newDebug; + } + + + bool hasStroke() const + { + return m_strokeWidth > 0.0f && m_strokeColor.alpha() > 0; + } + + void appendTriangle(const std::array &v, // triangle vertices + const std::array &n, // vertex normals + std::function uvForPoint + ) + { + QVector3D uv1 = uvForPoint(v[0]); + QVector3D uv2 = uvForPoint(v[1]); + QVector3D uv3 = uvForPoint(v[2]); + + QVector2D duvdx = QVector2D(uvForPoint(v[0] + QVector2D(1, 0))) - QVector2D(uv1); + QVector2D duvdy = QVector2D(uvForPoint(v[0] + QVector2D(0, 1))) - QVector2D(uv1); + + m_uncookedIndexes.append(m_uncookedVertexes.size()); + m_uncookedVertexes.append( { v[0].x(), v[0].y(), + uv1.x(), uv1.y(), uv1.z(), + duvdx.x(), duvdx.y(), + duvdy.x(), duvdy.y(), + n[0].x(), n[0].y() + }); + + m_uncookedIndexes.append(m_uncookedVertexes.size()); + m_uncookedVertexes.append( { v[1].x(), v[1].y(), + uv2.x(), uv2.y(), uv2.z(), + duvdx.x(), duvdx.y(), + duvdy.x(), duvdy.y(), + n[1].x(), n[1].y() + }); + + m_uncookedIndexes.append(m_uncookedVertexes.size()); + m_uncookedVertexes.append( { v[2].x(), v[2].y(), + uv3.x(), uv3.y(), uv3.z(), + duvdx.x(), duvdx.y(), + duvdy.x(), duvdy.y(), + n[2].x(), n[2].y() + }); + } + + void appendTriangle(const QVector2D &v1, + const QVector2D &v2, + const QVector2D &v3, + const QVector3D &uv1, + const QVector3D &uv2, + const QVector3D &uv3, + const QVector2D &n1, + const QVector2D &n2, + const QVector2D &n3, + const QVector2D &duvdx, + const QVector2D &duvdy) + { + m_uncookedIndexes.append(m_uncookedVertexes.size()); + m_uncookedVertexes.append( { v1.x(), v1.y(), + uv1.x(), uv1.y(), uv1.z(), + duvdx.x(), duvdx.y(), + duvdy.x(), duvdy.y(), + n1.x(), n1.y() + }); + + m_uncookedIndexes.append(m_uncookedVertexes.size()); + m_uncookedVertexes.append( { v2.x(), v2.y(), + uv2.x(), uv2.y(), uv2.z(), + duvdx.x(), duvdx.y(), + duvdy.x(), duvdy.y(), + n2.x(), n2.y() + }); + + m_uncookedIndexes.append(m_uncookedVertexes.size()); + m_uncookedVertexes.append( { v3.x(), v3.y(), + uv3.x(), uv3.y(), uv3.z(), + duvdx.x(), duvdx.y(), + duvdy.x(), duvdy.y(), + n3.x(), n3.y() + }); + } + + void appendTriangle(const QVector2D &v1, + const QVector2D &v2, + const QVector2D &v3, + std::function uvForPoint) + { + appendTriangle({v1, v2, v3}, {}, uvForPoint); + } + + QVector uncookedIndexes() const + { + return m_uncookedIndexes; + } + + void cookGeometry() override; + +private: + struct CurveNodeVertex + { + float x, y, u, v, w; + float dudx, dvdx, dudy, dvdy; // Size of pixel in curve space (must be same for all vertices in triangle) + float nx, ny; // normal vector describing the direction to shift the vertex for AA + }; + + void updateMaterial(); + static const QSGGeometry::AttributeSet &attributes(); + + QColor m_color = Qt::white; + QColor m_strokeColor = Qt::transparent; + float m_strokeWidth = 0.0f; + float m_debug = 0.0f; + QSGGradientCache::GradientDesc m_fillGradient; + QGradient::Type m_gradientType = QGradient::NoGradient; + + QScopedPointer m_material; + + QVector m_uncookedVertexes; + QVector m_uncookedIndexes; +}; + +QT_END_NAMESPACE + +#endif // QSGCURVEFILLNODE_P_H diff --git a/src/quick/scenegraph/qsgcurvefillnode_p_p.h b/src/quick/scenegraph/qsgcurvefillnode_p_p.h new file mode 100644 index 0000000000..d52f227d85 --- /dev/null +++ b/src/quick/scenegraph/qsgcurvefillnode_p_p.h @@ -0,0 +1,44 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSGCURVEFILLNODE_P_P_H +#define QSGCURVEFILLNODE_P_P_H + +#include +#include + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of a number of Qt sources files. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +QT_BEGIN_NAMESPACE + +class QSGCurveFillNode; +class Q_QUICK_PRIVATE_EXPORT QSGCurveFillMaterial : public QSGMaterial +{ +public: + QSGCurveFillMaterial(QSGCurveFillNode *node); + int compare(const QSGMaterial *other) const override; + + QSGCurveFillNode *node() const + { + return m_node; + } + +private: + QSGMaterialType *type() const override; + QSGMaterialShader *createShader(QSGRendererInterface::RenderMode renderMode) const override; + + QSGCurveFillNode *m_node; +}; + +QT_END_NAMESPACE + +#endif // QSGCURVEFILLNODE_P_P_H diff --git a/src/quick/scenegraph/qsgcurveglyphatlas.cpp b/src/quick/scenegraph/qsgcurveglyphatlas.cpp new file mode 100644 index 0000000000..410ce2dd26 --- /dev/null +++ b/src/quick/scenegraph/qsgcurveglyphatlas.cpp @@ -0,0 +1,142 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsgcurveglyphatlas_p.h" +#include "qsgcurvefillnode_p.h" +#include "qsgcurvestrokenode_p.h" +#include "qsgcurveprocessor_p.h" +#include "util/qquadpath_p.h" + +#include +#include + +QT_BEGIN_NAMESPACE + +QSGCurveGlyphAtlas::QSGCurveGlyphAtlas(const QRawFont &font) + : m_font(font) +{ + // The font size used for the curve atlas currently affects the outlines, since we don't + // really support cosmetic outlines. Therefore we need to pick one which gives large enough + // triangles relative to glyph size that we can reuse the same triangles for any font size. + // When experimenting, 10 works for all font sizes we tested, so we currently default to this + // but allow overriding it. + static int curveGlyphAtlasFontSize = qEnvironmentVariableIntValue("QSGCURVEGLYPHATLAS_FONT_SIZE"); + m_font.setPixelSize(curveGlyphAtlasFontSize > 0 ? qreal(curveGlyphAtlasFontSize) : 10.0); +} + +QSGCurveGlyphAtlas::~QSGCurveGlyphAtlas() +{ +} + +void QSGCurveGlyphAtlas::populate(const QList &glyphs) +{ + for (glyph_t glyphIndex : glyphs) { + if (!m_glyphs.contains(glyphIndex)) { + QPainterPath path = m_font.pathForGlyph(glyphIndex); + QQuadPath quadPath = QQuadPath::fromPainterPath(path); + quadPath.setFillRule(Qt::WindingFill); + + Glyph glyph; + + QSGCurveProcessor::processStroke(quadPath, 2, 2, Qt::MiterJoin, Qt::FlatCap, + [&glyph](const std::array &s, + const std::array &p, + const std::array &n, + bool isLine) { + glyph.strokeVertices.append(s.at(0)); + glyph.strokeVertices.append(s.at(1)); + glyph.strokeVertices.append(s.at(2)); + + glyph.strokeUvs.append(p.at(0)); + glyph.strokeUvs.append(p.at(1)); + glyph.strokeUvs.append(p.at(2)); + + glyph.strokeNormals.append(n.at(0)); + glyph.strokeNormals.append(n.at(1)); + glyph.strokeNormals.append(n.at(2)); + + glyph.strokeElementIsLine.append(isLine); + }); + + quadPath = quadPath.subPathsClosed(); + quadPath.addCurvatureData(); // ### Since the inside of glyphs is defined by order of + // vertices, this step could be simplified + QSGCurveProcessor::solveOverlaps(quadPath); + + QSGCurveProcessor::processFill(quadPath, + Qt::WindingFill, + [&glyph](const std::array &v, + const std::array &n, + QSGCurveProcessor::uvForPointCallback uvForPoint) + { + glyph.vertices.append(v.at(0)); + glyph.vertices.append(v.at(1)); + glyph.vertices.append(v.at(2)); + + QVector3D uv1 = uvForPoint(v.at(0)); + glyph.uvs.append(uv1); + glyph.uvs.append(uvForPoint(v.at(1))); + glyph.uvs.append(uvForPoint(v.at(2))); + + glyph.normals.append(n.at(0)); + glyph.normals.append(n.at(1)); + glyph.normals.append(n.at(2)); + + glyph.duvdx.append(QVector2D(uvForPoint(v.at(0) + QVector2D(1, 0))) - QVector2D(uv1)); + glyph.duvdy.append(QVector2D(uvForPoint(v.at(0) + QVector2D(0, 1))) - QVector2D(uv1)); + }); + + m_glyphs.insert(glyphIndex, glyph); + } + } +} + +void QSGCurveGlyphAtlas::addStroke(QSGCurveStrokeNode *node, + glyph_t glyphIndex, + const QPointF &position) const +{ + const Glyph &glyph = m_glyphs[glyphIndex]; + + const QVector2D v(position); + for (qsizetype i = glyph.strokeElementIsLine.size() - 1; i >= 0; --i) { + QVector2D v1 = glyph.strokeVertices.at(i * 3 + 0) + v; + QVector2D v2 = glyph.strokeVertices.at(i * 3 + 1) + v; + QVector2D v3 = glyph.strokeVertices.at(i * 3 + 2) + v; + if (glyph.strokeElementIsLine.at(i)) { + node->appendTriangle({ v1, v2, v3 }, + std::array({ glyph.strokeUvs.at(i * 3 + 0) + v, glyph.strokeUvs.at(i * 3 + 2) + v }), + { glyph.strokeNormals.at(i * 3 + 0), glyph.strokeNormals.at(i * 3 + 1), glyph.strokeNormals.at(i * 3 + 2) }); + } else { + node->appendTriangle({ v1, v2, v3 }, + { glyph.strokeUvs.at(i * 3 + 0) + v, glyph.strokeUvs.at(i * 3 + 1) + v, glyph.strokeUvs.at(i * 3 + 2) + v }, + { glyph.strokeNormals.at(i * 3 + 0), glyph.strokeNormals.at(i * 3 + 1), glyph.strokeNormals.at(i * 3 + 2) }); + + } + } +} + +void QSGCurveGlyphAtlas::addGlyph(QSGCurveFillNode *node, + glyph_t glyphIndex, + const QPointF &position, + qreal pixelSize) const +{ + const Glyph &glyph = m_glyphs[glyphIndex]; + + const float scaleFactor = pixelSize / m_font.pixelSize(); + const QVector2D v(position); + for (qsizetype i = 0; i < glyph.vertices.size() / 3; ++i) { + node->appendTriangle(scaleFactor * glyph.vertices.at(i * 3 + 0) + v, + scaleFactor * glyph.vertices.at(i * 3 + 1) + v, + scaleFactor * glyph.vertices.at(i * 3 + 2) + v, + glyph.uvs.at(i * 3 + 0), + glyph.uvs.at(i * 3 + 1), + glyph.uvs.at(i * 3 + 2), + glyph.normals.at(i * 3 + 0), + glyph.normals.at(i * 3 + 1), + glyph.normals.at(i * 3 + 2), + glyph.duvdx.at(i) / scaleFactor, + glyph.duvdy.at(i) / scaleFactor); + } +} + +QT_END_NAMESPACE diff --git a/src/quick/scenegraph/qsgcurveglyphatlas_p.h b/src/quick/scenegraph/qsgcurveglyphatlas_p.h new file mode 100644 index 0000000000..317abe11f8 --- /dev/null +++ b/src/quick/scenegraph/qsgcurveglyphatlas_p.h @@ -0,0 +1,69 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSGCURVEGLYPHATLAS_P_H +#define QSGCURVEGLYPHATLAS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QSGCurveFillNode; +class QSGCurveStrokeNode; + +class Q_QUICK_PRIVATE_EXPORT QSGCurveGlyphAtlas +{ +public: + QSGCurveGlyphAtlas(const QRawFont &font); + virtual ~QSGCurveGlyphAtlas(); + + void populate(const QList &glyphs); + void addGlyph(QSGCurveFillNode *node, + glyph_t glyph, + const QPointF &position, + qreal pixelSize) const; + void addStroke(QSGCurveStrokeNode *node, + glyph_t glyph, + const QPointF &position) const; + + qreal fontSize() const + { + return m_font.pixelSize(); + } + +private: + struct Glyph + { + QList vertices; + QList uvs; + QList normals; + QList duvdx; + QList duvdy; + + QList strokeVertices; + QList strokeUvs; + QList strokeNormals; + QList strokeElementIsLine; + }; + + QHash m_glyphs; + QRawFont m_font; +}; + +QT_END_NAMESPACE + + +#endif // QSGCURVEGLYPHATLAS_P_H diff --git a/src/quick/scenegraph/qsgcurveglyphnode.cpp b/src/quick/scenegraph/qsgcurveglyphnode.cpp new file mode 100644 index 0000000000..4e20ce75b2 --- /dev/null +++ b/src/quick/scenegraph/qsgcurveglyphnode.cpp @@ -0,0 +1,165 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsgcurveglyphnode_p.h" +#include "qsgcurveglyphatlas_p.h" +#include "qsgcurvefillnode_p.h" +#include "qsgcurvestrokenode_p.h" + +#include +#include +#include + +#include +#include + +QT_BEGIN_NAMESPACE + +QSGCurveGlyphNode::QSGCurveGlyphNode(QSGRenderContext *context) + : m_context(context) + , m_geometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 0) + , m_dirtyGeometry(false) +{ + setFlag(UsePreprocess); + + // #### To avoid asserts: we should probably merge this with QSGCurveFillNode + setGeometry(&m_geometry); + setMaterial(new QSGTextureMaterial); +} + +QSGCurveGlyphNode::~QSGCurveGlyphNode() +{ +} + +void QSGCurveGlyphNode::setPreferredAntialiasingMode(AntialiasingMode mode) +{ + Q_UNUSED(mode); +} + +void QSGCurveGlyphNode::setColor(const QColor &color) +{ + m_color = color; + if (m_glyphNode != nullptr) + m_glyphNode->setColor(color); +} + +void QSGCurveGlyphNode::setStyleColor(const QColor &styleColor) +{ + m_styleColor = styleColor; + if (m_styleNode != nullptr) + m_styleNode->setColor(styleColor); +} + +void QSGCurveGlyphNode::setStyle(QQuickText::TextStyle style) +{ + if (m_style != style) { + m_style = style; + m_dirtyGeometry = true; + update(); + } +} + +void QSGCurveGlyphNode::setGlyphs(const QPointF &position, const QGlyphRun &glyphs) +{ + m_glyphs = glyphs; + + QRawFont font = glyphs.rawFont(); + m_fontSize = font.pixelSize(); + m_position = QPointF(position.x(), position.y() - font.ascent()); + + m_curveGlyphAtlas = m_context->curveGlyphAtlas(font); + + m_curveGlyphAtlas->populate(glyphs.glyphIndexes()); + + m_dirtyGeometry = true; + +#ifdef QSG_RUNTIME_DESCRIPTION + qsgnode_set_description(this, QString::number(glyphs.glyphIndexes().count()) + + QStringLiteral(" curve glyphs: ") + + m_glyphs.rawFont().familyName() + + QStringLiteral(" ") + + QString::number(m_glyphs.rawFont().pixelSize())); +#endif +} + +void QSGCurveGlyphNode::update() +{ + markDirty(DirtyGeometry); +} + +void QSGCurveGlyphNode::preprocess() +{ + if (m_dirtyGeometry) + updateGeometry(); +} + +void QSGCurveGlyphNode::updateGeometry() +{ + delete m_glyphNode; + m_glyphNode = nullptr; + + delete m_styleNode; + m_styleNode = nullptr; + + Q_ASSERT(m_curveGlyphAtlas != nullptr); + + m_glyphNode = new QSGCurveFillNode; + m_glyphNode->setColor(m_color); + + QPointF offset; + + float fontScale = float(m_fontSize / m_curveGlyphAtlas->fontSize()); + QSGCurveFillNode *raisedSunkenStyleNode = nullptr; + QSGCurveStrokeNode *outlineNode = nullptr; + if (m_style == QQuickText::Raised || m_style == QQuickText::Sunken) { + raisedSunkenStyleNode = new QSGCurveFillNode; + raisedSunkenStyleNode ->setColor(m_styleColor); + + offset = m_style == QQuickText::Raised ? QPointF(0.0f, 1.0f) : QPointF(0.0f, -1.0f); + m_styleNode = raisedSunkenStyleNode; + } else if (m_style == QQuickText::Outline) { + outlineNode = new QSGCurveStrokeNode; + outlineNode->setColor(m_styleColor); + outlineNode->setStrokeWidth(2 / fontScale); + outlineNode->setLocalScale(fontScale); + + m_styleNode = outlineNode; + } + + const QVector indexes = m_glyphs.glyphIndexes(); + const QVector positions = m_glyphs.positions(); + for (qsizetype i = 0; i < indexes.size(); ++i) { + if (i == 0) + m_baseLine = positions.at(i); + m_curveGlyphAtlas->addGlyph(m_glyphNode, + indexes.at(i), + m_position + positions.at(i), + m_fontSize); + if (raisedSunkenStyleNode != nullptr) { + m_curveGlyphAtlas->addGlyph(raisedSunkenStyleNode, + indexes.at(i), + m_position + positions.at(i) + offset, + m_fontSize); + } + if (outlineNode != nullptr) { + // Since the stroke node will scale everything by fontScale internally (the + // shader does not support pre-transforming the vertices), we have to also first + // do the inverse scale on the glyph position to get the correct position. + m_curveGlyphAtlas->addStroke(outlineNode, + indexes.at(i), + (m_position + positions.at(i)) / fontScale); + } + } + + if (m_styleNode != nullptr) { + m_styleNode->cookGeometry(); + appendChildNode(m_styleNode); + } + + m_glyphNode->cookGeometry(); + appendChildNode(m_glyphNode); + + m_dirtyGeometry = false; +} + +QT_END_NAMESPACE diff --git a/src/quick/scenegraph/qsgcurveglyphnode_p.h b/src/quick/scenegraph/qsgcurveglyphnode_p.h new file mode 100644 index 0000000000..862fdbf70a --- /dev/null +++ b/src/quick/scenegraph/qsgcurveglyphnode_p.h @@ -0,0 +1,69 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSGCURVEGLYPHNODE_P_H +#define QSGCURVEGLYPHNODE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QSGCurveGlyphAtlas; +class QSGCurveFillNode; +class QSGCurveAbstractNode; + +class Q_QUICK_PRIVATE_EXPORT QSGCurveGlyphNode : public QSGGlyphNode +{ +public: + QSGCurveGlyphNode(QSGRenderContext *context); + ~QSGCurveGlyphNode(); + void setGlyphs(const QPointF &position, const QGlyphRun &glyphs) override; + void update() override; + void preprocess() override; + void setPreferredAntialiasingMode(AntialiasingMode) override; + void updateGeometry(); + void setColor(const QColor &color) override; + void setStyle(QQuickText::TextStyle style) override; + + void setStyleColor(const QColor &color) override; + QPointF baseLine() const override { return m_baseLine; } + +private: + QSGRenderContext *m_context; + QSGCurveGlyphAtlas *m_curveGlyphAtlas = nullptr; + QSGGeometry m_geometry; + QColor m_color = Qt::black; + + struct GlyphInfo { + QVector indexes; + QVector positions; + }; + + uint m_dirtyGeometry: 1; + qreal m_fontSize = 0.0f; + QGlyphRun m_glyphs; + QQuickText::TextStyle m_style; + QColor m_styleColor; + QPointF m_baseLine; + QPointF m_position; + + QSGCurveFillNode *m_glyphNode = nullptr; + QSGCurveAbstractNode *m_styleNode = nullptr; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/quick/scenegraph/qsgcurveprocessor.cpp b/src/quick/scenegraph/qsgcurveprocessor.cpp new file mode 100644 index 0000000000..805cdf3a46 --- /dev/null +++ b/src/quick/scenegraph/qsgcurveprocessor.cpp @@ -0,0 +1,1109 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsgcurveprocessor_p.h" + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcSGCurveProcessor, "qt.quick.curveprocessor"); + +namespace { +// Input coordinate space is pre-mapped so that (0, 0) maps to [0, 0] in uv space. +// v1 maps to [1,0], v2 maps to [0,1]. p is the point to be mapped to uv in this space (i.e. vector from p0) +static inline QVector2D uvForPoint(QVector2D v1, QVector2D v2, QVector2D p) +{ + double divisor = v1.x() * v2.y() - v2.x() * v1.y(); + + float u = (p.x() * v2.y() - p.y() * v2.x()) / divisor; + float v = (p.y() * v1.x() - p.x() * v1.y()) / divisor; + + return {u, v}; +} + +// Find uv coordinates for the point p, for a quadratic curve from p0 to p2 with control point p1 +// also works for a line from p0 to p2, where p1 is on the inside of the path relative to the line +static inline QVector2D curveUv(QVector2D p0, QVector2D p1, QVector2D p2, QVector2D p) +{ + QVector2D v1 = 2 * (p1 - p0); + QVector2D v2 = p2 - v1 - p0; + return uvForPoint(v1, v2, p - p0); +} + +static QVector3D elementUvForPoint(const QQuadPath::Element& e, QVector2D p) +{ + auto uv = curveUv(e.startPoint(), e.controlPoint(), e.endPoint(), p); + if (e.isLine()) + return { uv.x(), uv.y(), 0.0f }; + else + return { uv.x(), uv.y(), e.isConvex() ? -1.0f : 1.0f }; +} + +static inline QVector2D calcNormalVector(QVector2D a, QVector2D b) +{ + auto v = b - a; + return {v.y(), -v.x()}; +} + +// The sign of the return value indicates which side of the line defined by a and n the point p falls +static inline float testSideOfLineByNormal(QVector2D a, QVector2D n, QVector2D p) +{ + float dot = QVector2D::dotProduct(p - a, n); + return dot; +}; + +template +void iteratePath(const QQuadPath &path, int index, Func &&lambda) +{ + const auto &element = path.elementAt(index); + if (element.childCount() == 0) { + lambda(element, index); + } else { + for (int i = 0; i < element.childCount(); ++i) + iteratePath(path, element.indexOfChild(i), lambda); + } +} + +static inline float determinant(const QVector2D &p1, const QVector2D &p2, const QVector2D &p3) +{ + return p1.x() * (p2.y() - p3.y()) + + p2.x() * (p3.y() - p1.y()) + + p3.x() * (p1.y() - p2.y()); +} + +/* + Clever triangle overlap algorithm. Stack Overflow says: + + You can prove that the two triangles do not collide by finding an edge (out of the total 6 + edges that make up the two triangles) that acts as a separating line where all the vertices + of one triangle lie on one side and the vertices of the other triangle lie on the other side. + If you can find such an edge then it means that the triangles do not intersect otherwise the + triangles are colliding. +*/ +using TrianglePoints = std::array; +using LinePoints = std::array; + +// The sign of the determinant tells the winding order: positive means counter-clockwise + +static inline double determinant(const TrianglePoints &p) +{ + return determinant(p[0], p[1], p[2]); +} + +// Fix the triangle so that the determinant is positive +static void fixWinding(TrianglePoints &p) +{ + double det = determinant(p); + if (det < 0.0) { + qSwap(p[0], p[1]); + } +} + +// Return true if the determinant is negative, i.e. if the winding order is opposite of the triangle p1,p2,p3. +// This means that p is strictly on the other side of p1-p2 relative to p3 [where p1,p2,p3 is a triangle with +// a positive determinant]. +bool checkEdge(QVector2D &p1, QVector2D &p2, QVector2D &p, float epsilon) +{ + return determinant(p1, p2, p) <= epsilon; +} + + +bool checkTriangleOverlap(TrianglePoints &triangle1, TrianglePoints &triangle2, float epsilon = 1.0/32) +{ + // See if there is an edge of triangle1 such that all vertices in triangle2 are on the opposite side + fixWinding(triangle1); + for (int i = 0; i < 3; i++) { + int ni = (i + 1) % 3; + if (checkEdge(triangle1[i], triangle1[ni], triangle2[0], epsilon) && + checkEdge(triangle1[i], triangle1[ni], triangle2[1], epsilon) && + checkEdge(triangle1[i], triangle1[ni], triangle2[2], epsilon)) + return false; + } + + // See if there is an edge of triangle2 such that all vertices in triangle1 are on the opposite side + fixWinding(triangle2); + for (int i = 0; i < 3; i++) { + int ni = (i + 1) % 3; + + if (checkEdge(triangle2[i], triangle2[ni], triangle1[0], epsilon) && + checkEdge(triangle2[i], triangle2[ni], triangle1[1], epsilon) && + checkEdge(triangle2[i], triangle2[ni], triangle1[2], epsilon)) + return false; + } + + return true; +} + +bool checkLineTriangleOverlap(TrianglePoints &triangle, LinePoints &line, float epsilon = 1.0/32) +{ + // See if all vertices of the triangle are on the same side of the line + bool s1 = determinant(line[0], line[1], triangle[0]) < 0; + auto s2 = determinant(line[0], line[1], triangle[1]) < 0; + auto s3 = determinant(line[0], line[1], triangle[2]) < 0; + // If all determinants have the same sign, then there is no overlap + if (s1 == s2 && s2 == s3) { + return false; + } + // See if there is an edge of triangle1 such that both vertices in line are on the opposite side + fixWinding(triangle); + for (int i = 0; i < 3; i++) { + int ni = (i + 1) % 3; + if (checkEdge(triangle[i], triangle[ni], line[0], epsilon) && + checkEdge(triangle[i], triangle[ni], line[1], epsilon)) + return false; + } + + return true; +} + +// We could slightly optimize this if we did fixWinding in advance +bool checkTriangleContains (QVector2D pt, QVector2D v1, QVector2D v2, QVector2D v3, float epsilon = 1.0/32) +{ + float d1, d2, d3; + d1 = determinant(pt, v1, v2); + d2 = determinant(pt, v2, v3); + d3 = determinant(pt, v3, v1); + + bool allNegative = d1 < -epsilon && d2 < -epsilon && d3 < -epsilon; + bool allPositive = d1 > epsilon && d2 > epsilon && d3 > epsilon; + + return allNegative || allPositive; +} + +// e1 is always a concave curve. e2 can be curve or line +static bool isOverlap(const QQuadPath &path, int e1, int e2) +{ + const QQuadPath::Element &element1 = path.elementAt(e1); + const QQuadPath::Element &element2 = path.elementAt(e2); + + TrianglePoints t1{ element1.startPoint(), element1.controlPoint(), element1.endPoint() }; + + if (element2.isLine()) { + LinePoints line{ element2.startPoint(), element2.endPoint() }; + return checkLineTriangleOverlap(t1, line); + } else { + TrianglePoints t2{ element2.startPoint(), element2.controlPoint(), element2.endPoint() }; + return checkTriangleOverlap(t1, t2); + } + + return false; +} + +static bool isOverlap(const QQuadPath &path, int index, const QVector2D &vertex) +{ + const QQuadPath::Element &elem = path.elementAt(index); + return checkTriangleContains(vertex, elem.startPoint(), elem.controlPoint(), elem.endPoint()); +} + +struct TriangleData +{ + TrianglePoints points; + int pathElementIndex; + TrianglePoints normals; +}; + +// Returns a vector that is normal to baseLine, pointing to the right +QVector2D normalVector(QVector2D baseLine) +{ + QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized(); + return normal; +} + +// Returns a vector that is normal to the path and pointing to the right. If endSide is false +// the vector is normal to the start point, otherwise to the end point +QVector2D normalVector(const QQuadPath::Element &element, bool endSide = false) +{ + if (element.isLine()) + return normalVector(element.endPoint() - element.startPoint()); + else if (!endSide) + return normalVector(element.controlPoint() - element.startPoint()); + else + return normalVector(element.endPoint() - element.controlPoint()); +} + +// Returns a vector that is parallel to the path. If endSide is false +// the vector starts at the start point and points forward, +// otherwise it starts at the end point and points backward +QVector2D tangentVector(const QQuadPath::Element &element, bool endSide = false) +{ + if (element.isLine()) { + if (!endSide) + return element.endPoint() - element.startPoint(); + else + return element.startPoint() - element.endPoint(); + } else { + if (!endSide) + return element.controlPoint() - element.startPoint(); + else + return element.controlPoint() - element.endPoint(); + } +} + +// Really simplistic O(n^2) triangulator - only intended for five points +QList simplePointTriangulator(const QList &pts, const QList &normals, int elementIndex) +{ + int count = pts.size(); + Q_ASSERT(count >= 3); + Q_ASSERT(normals.size() == count); + + // First we find the convex hull: it's always in positive determinant winding order + QList hull; + float det1 = determinant(pts[0], pts[1], pts[2]); + if (det1 > 0) + hull << 0 << 1 << 2; + else + hull << 2 << 1 << 0; + auto connectableInHull = [&](int idx) -> QList { + QList r; + const int n = hull.size(); + const auto &pt = pts[idx]; + for (int i = 0; i < n; ++i) { + const auto &i1 = hull.at(i); + const auto &i2 = hull.at((i+1) % n); + if (determinant(pts[i1], pts[i2], pt) < 0.0f) + r << i; + } + return r; + }; + for (int i = 3; i < count; ++i) { + auto visible = connectableInHull(i); + if (visible.isEmpty()) + continue; + int visCount = visible.count(); + int hullCount = hull.count(); + // Find where the visible part of the hull starts. (This is the part we need to triangulate to, + // and the part we're going to replace. "visible" contains the start point of the line segments that are visible from p. + int boundaryStart = visible[0]; + for (int j = 0; j < visCount - 1; ++j) { + if ((visible[j] + 1) % hullCount != visible[j+1]) { + boundaryStart = visible[j + 1]; + break; + } + } + // Finally replace the points that are now inside the hull + // We insert the new point after boundaryStart, and before boundaryStart + visCount (modulo...) + // and remove the points in between + int pointsToKeep = hullCount - visCount + 1; + QList newHull; + newHull << i; + for (int j = 0; j < pointsToKeep; ++j) { + newHull << hull.at((j + boundaryStart + visCount) % hullCount); + } + hull = newHull; + } + + // Now that we have a convex hull, we can trivially triangulate it + QList ret; + for (int i = 1; i < hull.size() - 1; ++i) { + int i0 = hull[0]; + int i1 = hull[i]; + int i2 = hull[i+1]; + ret.append({{pts[i0], pts[i1], pts[i2]}, elementIndex, {normals[i0], normals[i1], normals[i2]}}); + } + return ret; +} + +static bool needsSplit(const QQuadPath::Element &el) +{ + const auto v1 = el.controlPoint() - el.startPoint(); + const auto v2 = el.endPoint() - el.controlPoint(); + float cos = QVector2D::dotProduct(v1, v2) / (v1.length() * v2.length()); + return cos < 0.9; +} +static void splitElementIfNecessary(QQuadPath &path, int index) +{ + auto &e = path.elementAt(index); + if (e.isLine()) + return; + if (e.childCount() == 0) { + if (needsSplit(e)) + path.splitElementAt(index); + } else { + for (int i = 0; i < e.childCount(); ++i) + splitElementIfNecessary(path, e.indexOfChild(i)); + } +} + +static QQuadPath subdivide(const QQuadPath &path, int subdivisions) +{ + QQuadPath newPath = path; + + for (int i = 0; i < subdivisions; ++i) + for (int j = 0; j < newPath.elementCount(); j++) + splitElementIfNecessary(newPath, j); + return newPath; +} + +static QList customTriangulator2(const QQuadPath &path, float penWidth, Qt::PenJoinStyle joinStyle, Qt::PenCapStyle capStyle, float miterLimit) +{ + const bool bevelJoin = joinStyle == Qt::BevelJoin; + const bool roundJoin = joinStyle == Qt::RoundJoin; + const bool miterJoin = !bevelJoin && !roundJoin; + + const bool roundCap = capStyle == Qt::RoundCap; + const bool squareCap = capStyle == Qt::SquareCap; + // We can't use the simple miter for miter joins, since the shader currently only supports round joins + const bool simpleMiter = joinStyle == Qt::RoundJoin; + + Q_ASSERT(miterLimit > 0 || !miterJoin); + float inverseMiterLimit = miterJoin ? 1.0f / miterLimit : 1.0; + + const float penFactor = penWidth / 2; + + // Returns {inner1, inner2, outer1, outer2, outerMiter} + // where foo1 is for the end of element1 and foo2 is for the start of element2 + // and inner1 == inner2 unless we had to give up finding a decent point + auto calculateJoin = [&](const QQuadPath::Element *element1, const QQuadPath::Element *element2, + bool &outerBisectorWithinMiterLimit, bool &innerIsRight, bool &giveUp) -> std::array + { + outerBisectorWithinMiterLimit = true; + innerIsRight = true; + giveUp = false; + if (!element1) { + Q_ASSERT(element2); + QVector2D n = normalVector(*element2).normalized(); + return {n, n, -n, -n, -n}; + } + if (!element2) { + Q_ASSERT(element1); + QVector2D n = normalVector(*element1, true).normalized(); + return {n, n, -n, -n, -n}; + } + + Q_ASSERT(element1->endPoint() == element2->startPoint()); + + const auto p1 = element1->isLine() ? element1->startPoint() : element1->controlPoint(); + const auto p2 = element1->endPoint(); + const auto p3 = element2->isLine() ? element2->endPoint() : element2->controlPoint(); + + const auto v1 = (p1 - p2).normalized(); + const auto v2 = (p3 - p2).normalized(); + const auto b = (v1 + v2); + + constexpr float epsilon = 1.0f / 32.0f; + bool smoothJoin = qAbs(b.x()) < epsilon && qAbs(b.y()) < epsilon; + + if (smoothJoin) { + // v1 and v2 are almost parallel and pointing in opposite directions + // angle bisector formula will give an almost null vector: use normal of bisector of normals instead + QVector2D n1(-v1.y(), v1.x()); + QVector2D n2(-v2.y(), v2.x()); + QVector2D n = (n2 - n1).normalized(); + return {n, n, -n, -n, -n}; + } + // Calculate the length of the bisector, so it will cover the entire miter. + // Using the identity sin(x/2) == sqrt((1 - cos(x)) / 2), and the fact that the + // dot product of two unit vectors is the cosine of the angle between them + // The length of the miter is w/sin(x/2) where x is the angle between the two elements + + const auto bisector = b.normalized(); + float cos2x = QVector2D::dotProduct(v1, v2); + cos2x = qMin(1.0f, cos2x); // Allow for float inaccuracy + float sine = sqrt((1.0f - cos2x) / 2); + innerIsRight = determinant(p1, p2, p3) > 0; + sine = qMax(sine, 0.01f); // Avoid divide by zero + float length = penFactor / sine; + + // Check if bisector is longer than one of the lines it's trying to bisect + + auto tooLong = [](QVector2D p1, QVector2D p2, QVector2D n, float length, float margin) -> bool { + auto v = p2 - p1; + // It's too long if the projection onto the bisector is longer than the bisector + // and the projection onto the normal to the bisector is shorter + // than the pen margin (that projection is just v - proj) + // (we're adding a 10% safety margin to make room for AA -- not exact) + auto projLen = QVector2D::dotProduct(v, n); + return projLen * 0.9f < length && (v - n * projLen).length() * 0.9 < margin; + }; + + + // The angle bisector of the tangent lines is not correct for curved lines. We could fix this by calculating + // the exact intersection point, but for now just give up and use the normals. + + giveUp = !element1->isLine() || !element2->isLine() + || tooLong(p1, p2, bisector, length, penFactor) + || tooLong(p3, p2, bisector, length, penFactor); + outerBisectorWithinMiterLimit = sine >= inverseMiterLimit / 2.0f; + bool simpleJoin = simpleMiter && outerBisectorWithinMiterLimit && !giveUp; + const QVector2D bn = bisector / sine; + + if (simpleJoin) + return {bn, bn, -bn, -bn, -bn}; // We only have one inner and one outer point TODO: change inner point when conflict/curve + const QVector2D n1 = normalVector(*element1, true).normalized(); + const QVector2D n2 = normalVector(*element2).normalized(); + if (giveUp) { + if (innerIsRight) + return {n1, n2, -n1, -n2, -bn}; + else + return {-n1, -n2, n1, n2, -bn}; + + } else { + if (innerIsRight) + return {bn, bn, -n1, -n2, -bn}; + else + return {bn, bn, n1, n2, -bn}; + } + }; + + QList ret; + + auto triangulateCurve = [&](int idx, const QVector2D &p1, const QVector2D &p2, const QVector2D &p3, const QVector2D &p4, + const QVector2D &n1, const QVector2D &n2, const QVector2D &n3, const QVector2D &n4) + { + const auto &element = path.elementAt(idx); + const auto &s = element.startPoint(); + const auto &c = element.controlPoint(); + const auto &e = element.endPoint(); + // TODO: Don't flatten the path in addCurveStrokeNodes, but iterate over the children here instead + bool controlPointOnRight = determinant(s, c, e) > 0; + QVector2D startNormal = normalVector(element).normalized(); + QVector2D endNormal = normalVector(element, true).normalized(); + QVector2D controlPointNormal = (startNormal + endNormal).normalized(); + if (controlPointOnRight) + controlPointNormal = -controlPointNormal; + QVector2D p5 = c + controlPointNormal * penFactor; // This is too simplistic + TrianglePoints t1{p1, p2, p5}; + TrianglePoints t2{p3, p4, p5}; + bool simpleCase = !checkTriangleOverlap(t1, t2); + + if (simpleCase) { + ret.append({{p1, p2, p5}, idx, {n1, n2, controlPointNormal}}); + ret.append({{p3, p4, p5}, idx, {n3, n4, controlPointNormal}}); + if (controlPointOnRight) { + ret.append({{p1, p3, p5}, idx, {n1, n3, controlPointNormal}}); + } else { + ret.append({{p2, p4, p5}, idx, {n2, n4, controlPointNormal}}); + } + } else { + ret.append(simplePointTriangulator({p1, p2, p5, p3, p4}, {n1, n2, controlPointNormal, n3, n4}, idx)); + } + }; + + // Each element is calculated independently, so we don't have to special-case closed sub-paths. + // Take care so the end points of one element are precisely equal to the start points of the next. + // Any additional triangles needed for joining are added at the end of the current element. + + int count = path.elementCount(); + int subStart = 0; + while (subStart < count) { + int subEnd = subStart; + for (int i = subStart + 1; i < count; ++i) { + const auto &e = path.elementAt(i); + if (e.isSubpathStart()) { + subEnd = i - 1; + break; + } + if (i == count - 1) { + subEnd = i; + break; + } + } + bool closed = path.elementAt(subStart).startPoint() == path.elementAt(subEnd).endPoint(); + const int subCount = subEnd - subStart + 1; + + auto addIdx = [&](int idx, int delta) -> int { + int subIdx = idx - subStart; + if (closed) + subIdx = (subIdx + subCount + delta) % subCount; + else + subIdx += delta; + return subStart + subIdx; + }; + auto elementAt = [&](int idx, int delta) -> const QQuadPath::Element * { + int subIdx = idx - subStart; + if (closed) { + subIdx = (subIdx + subCount + delta) % subCount; + return &path.elementAt(subStart + subIdx); + } + subIdx += delta; + if (subIdx >= 0 && subIdx < subCount) + return &path.elementAt(subStart + subIdx); + return nullptr; + }; + + for (int i = subStart; i <= subEnd; ++i) { + const auto &element = path.elementAt(i); + const auto *nextElement = elementAt(i, +1); + const auto *prevElement = elementAt(i, -1); + + const auto &s = element.startPoint(); + const auto &e = element.endPoint(); + + bool startInnerIsRight; + bool startBisectorWithinMiterLimit; // Not used + bool giveUpOnStartJoin; // Not used + auto startJoin = calculateJoin(prevElement, &element, + startBisectorWithinMiterLimit, startInnerIsRight, + giveUpOnStartJoin); + const QVector2D &startInner = startJoin[1]; + const QVector2D &startOuter = startJoin[3]; + + bool endInnerIsRight; + bool endBisectorWithinMiterLimit; + bool giveUpOnEndJoin; + auto endJoin = calculateJoin(&element, nextElement, + endBisectorWithinMiterLimit, endInnerIsRight, + giveUpOnEndJoin); + QVector2D endInner = endJoin[0]; + QVector2D endOuter = endJoin[2]; + QVector2D nextOuter = endJoin[3]; + QVector2D outerB = endJoin[4]; + + QVector2D p1, p2, p3, p4; + QVector2D n1, n2, n3, n4; + + if (startInnerIsRight) { + n1 = startInner; + n2 = startOuter; + } else { + n1 = startOuter; + n2 = startInner; + } + + p1 = s + n1 * penFactor; + p2 = s + n2 * penFactor; + + // repeat logic above for the other end: + if (endInnerIsRight) { + n3 = endInner; + n4 = endOuter; + } else { + n3 = endOuter; + n4 = endInner; + } + + p3 = e + n3 * penFactor; + p4 = e + n4 * penFactor; + + // End caps + + if (!prevElement) { + QVector2D capSpace = tangentVector(element).normalized() * -penFactor; + if (roundCap) { + p1 += capSpace; + p2 += capSpace; + } else if (squareCap) { + QVector2D c1 = p1 + capSpace; + QVector2D c2 = p2 + capSpace; + ret.append({{p1, s, c1}, -1, {}}); + ret.append({{c1, s, c2}, -1, {}}); + ret.append({{p2, s, c2}, -1, {}}); + } + } + if (!nextElement) { + QVector2D capSpace = tangentVector(element, true).normalized() * -penFactor; + if (roundCap) { + p3 += capSpace; + p4 += capSpace; + } else if (squareCap) { + QVector2D c3 = p3 + capSpace; + QVector2D c4 = p4 + capSpace; + ret.append({{p3, e, c3}, -1, {}}); + ret.append({{c3, e, c4}, -1, {}}); + ret.append({{p4, e, c4}, -1, {}}); + } + } + + if (element.isLine()) { + ret.append({{p1, p2, p3}, i, {n1, n2, n3}}); + ret.append({{p2, p3, p4}, i, {n2, n3, n4}}); + } else { + triangulateCurve(i, p1, p2, p3, p4, n1, n2, n3, n4); + } + + bool trivialJoin = simpleMiter && endBisectorWithinMiterLimit && !giveUpOnEndJoin; + if (!trivialJoin && nextElement) { + // inside of join (opposite of bevel) is defined by + // triangle s, e, next.e + bool innerOnRight = endInnerIsRight; + + const auto outer1 = e + endOuter * penFactor; + const auto outer2 = e + nextOuter * penFactor; + //const auto inner = e + endInner * penFactor; + + if (bevelJoin || (miterJoin && !endBisectorWithinMiterLimit)) { + ret.append({{outer1, e, outer2}, -1, {}}); + } else if (roundJoin) { + ret.append({{outer1, e, outer2}, i, {}}); + QVector2D nn = calcNormalVector(outer1, outer2).normalized() * penFactor; + if (!innerOnRight) + nn = -nn; + ret.append({{outer1, outer1 + nn, outer2}, i, {}}); + ret.append({{outer1 + nn, outer2, outer2 + nn}, i, {}}); + + } else if (miterJoin) { + QVector2D outer = e + outerB * penFactor; + ret.append({{outer1, e, outer}, -2, {}}); + ret.append({{outer, e, outer2}, -2, {}}); + } + + if (!giveUpOnEndJoin) { + QVector2D inner = e + endInner * penFactor; + ret.append({{inner, e, outer1}, i, {endInner, {}, endOuter}}); + // The remaining triangle ought to be done by nextElement, but we don't have start join logic there (yet) + int nextIdx = addIdx(i, +1); + ret.append({{inner, e, outer2}, nextIdx, {endInner, {}, nextOuter}}); + } + } + } + subStart = subEnd + 1; + } + return ret; +} + +// TODO: we could optimize by preprocessing e1, since we call this function multiple times on the same +// elements +static void handleOverlap(QQuadPath &path, int e1, int e2, int recursionLevel = 0) +{ + if (!isOverlap(path, e1, e2)) { + return; + } + + if (recursionLevel > 8) { + qCDebug(lcSGCurveProcessor) << "Triangle overlap: recursion level" << recursionLevel << "aborting!"; + return; + } + + if (path.elementAt(e1).childCount() > 1) { + auto e11 = path.indexOfChildAt(e1, 0); + auto e12 = path.indexOfChildAt(e1, 1); + handleOverlap(path, e11, e2, recursionLevel + 1); + handleOverlap(path, e12, e2, recursionLevel + 1); + } else if (path.elementAt(e2).childCount() > 1) { + auto e21 = path.indexOfChildAt(e2, 0); + auto e22 = path.indexOfChildAt(e2, 1); + handleOverlap(path, e1, e21, recursionLevel + 1); + handleOverlap(path, e1, e22, recursionLevel + 1); + } else { + path.splitElementAt(e1); + auto e11 = path.indexOfChildAt(e1, 0); + auto e12 = path.indexOfChildAt(e1, 1); + bool overlap1 = isOverlap(path, e11, e2); + bool overlap2 = isOverlap(path, e12, e2); + if (!overlap1 && !overlap2) + return; // no more overlap: success! + + // We need to split more: + if (path.elementAt(e2).isLine()) { + // Splitting a line won't help, so we just split e1 further + if (overlap1) + handleOverlap(path, e11, e2, recursionLevel + 1); + if (overlap2) + handleOverlap(path, e12, e2, recursionLevel + 1); + } else { + // See if splitting e2 works: + path.splitElementAt(e2); + auto e21 = path.indexOfChildAt(e2, 0); + auto e22 = path.indexOfChildAt(e2, 1); + if (overlap1) { + handleOverlap(path, e11, e21, recursionLevel + 1); + handleOverlap(path, e11, e22, recursionLevel + 1); + } + if (overlap2) { + handleOverlap(path, e12, e21, recursionLevel + 1); + handleOverlap(path, e12, e22, recursionLevel + 1); + } + } + } +} + +// Test if element contains a start point of another element +static void handleOverlap(QQuadPath &path, int e1, const QVector2D vertex, int recursionLevel = 0) +{ + // First of all: Ignore the next element: it trivially overlaps (maybe not necessary: we do check for strict containment) + if (vertex == path.elementAt(e1).endPoint() || !isOverlap(path, e1, vertex)) + return; + if (recursionLevel > 8) { + qDebug() << "Vertex overlap: recursion level" << recursionLevel << "aborting!"; + return; + } + + // Don't split if we're already split + if (path.elementAt(e1).childCount() == 0) + path.splitElementAt(e1); + + handleOverlap(path, path.indexOfChildAt(e1, 0), vertex, recursionLevel + 1); + handleOverlap(path, path.indexOfChildAt(e1, 1), vertex, recursionLevel + 1); +} + +} + +void QSGCurveProcessor::solveOverlaps(QQuadPath &path) +{ + for (int i = 0; i < path.elementCount(); i++) { + auto &element = path.elementAt(i); + // only concave curve overlap is problematic, as long as we don't allow self-intersecting curves + if (element.isLine() || element.isConvex()) + continue; + + for (int j = 0; j < path.elementCount(); j++) { + if (i == j) + continue; // Would be silly to test overlap with self + auto &other = path.elementAt(j); + if (!other.isConvex() && !other.isLine() && j < i) + continue; // We have already tested this combination, so no need to test again + handleOverlap(path, i, j); + } + } + + static const int handleConcaveJoint = qEnvironmentVariableIntValue("QT_QUICKSHAPES_WIP_CONCAVE_JOINT"); + if (handleConcaveJoint) { + // Note that the joint between two non-concave elements can also be concave, so we have to + // test all convex elements to see if there is a vertex in any of them. We could do it the other way + // by identifying concave joints, but then we would have to know which side is the inside + // TODO: optimization potential! Maybe do that at the same time as we identify concave curves? + + // We do this in a separate loop, since the triangle/triangle test above is more expensive, and + // if we did this first, there would be more triangles to test + for (int i = 0; i < path.elementCount(); i++) { + auto &element = path.elementAt(i); + if (!element.isConvex()) + continue; + + for (int j = 0; j < path.elementCount(); j++) { + // We only need to check one point per element, since all subpaths are closed + // Could do smartness to skip elements that cannot overlap, but let's do it the easy way first + if (i == j) + continue; + const auto &other = path.elementAt(j); + handleOverlap(path, i, other.startPoint()); + } + } + } +} + +void QSGCurveProcessor::processStroke(const QQuadPath &strokePath, + float miterLimit, + float penWidth, + Qt::PenJoinStyle joinStyle, + Qt::PenCapStyle capStyle, + addStrokeTriangleCallback addTriangle, + int subdivisions) +{ + auto thePath = subdivide(strokePath, subdivisions).flattened(); // TODO: don't flatten, but handle it in the triangulator + auto triangles = customTriangulator2(thePath, penWidth, joinStyle, capStyle, miterLimit); + + auto addCurveTriangle = [&](const QQuadPath::Element &element, const TriangleData &t){ + addTriangle(t.points, + { element.startPoint(), element.controlPoint(), element.endPoint() }, + t.normals, + element.isLine()); + }; + + auto addBevelTriangle = [&](const TrianglePoints &p) + { + QVector2D fp1 = p[0]; + QVector2D fp2 = p[2]; + + // That describes a path that passes through those points. We want the stroke + // edge, so we need to shift everything down by the stroke offset + + QVector2D nn = calcNormalVector(p[0], p[2]); + if (determinant(p) < 0) + nn = -nn; + float delta = penWidth / 2; + + QVector2D offset = nn.normalized() * delta; + fp1 += offset; + fp2 += offset; + + TrianglePoints n; + // p1 is inside, so n[1] is {0,0} + n[0] = (p[0] - p[1]).normalized(); + n[2] = (p[2] - p[1]).normalized(); + + addTriangle(p, { fp1, QVector2D(0.0f, 0.0f), fp2 }, n, true); + }; + + for (const auto &triangle : triangles) { + if (triangle.pathElementIndex < 0) { + addBevelTriangle(triangle.points); + continue; + } + const auto &element = thePath.elementAt(triangle.pathElementIndex); + addCurveTriangle(element, triangle); + } +} + +void QSGCurveProcessor::processFill(const QQuadPath &fillPath, + Qt::FillRule fillRule, + addTriangleCallback addTriangle) +{ + QPainterPath internalHull; + internalHull.setFillRule(fillRule); + + QHash, int> linePointHash; + QHash, int> concaveControlPointHash; + QHash, int> convexPointHash; + + auto toRoundedPair = [](const QPointF &p) -> QPair { + return qMakePair(qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f); + }; + + auto toRoundedVec2D = [](const QPointF &p) -> QVector2D { + return { qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f }; + }; + + auto roundVec2D = [](const QVector2D &p) -> QVector2D { + return { qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f }; + }; + + auto addCurveTriangle = [&](const QQuadPath::Element &element, + const QVector2D &sp, + const QVector2D &ep, + const QVector2D &cp) { + addTriangle({ sp, cp, ep }, + {}, + [&element](QVector2D v) { return elementUvForPoint(element, v); }); + }; + + auto addCurveTriangleWithNormals = [&](const QQuadPath::Element &element, + const std::array &v, + const std::array &n) { + addTriangle(v, n, [&element](QVector2D v) { return elementUvForPoint(element, v); }); + }; + + auto outsideNormal = [](const QVector2D &startPoint, + const QVector2D &endPoint, + const QVector2D &insidePoint) { + + QVector2D baseLine = endPoint - startPoint; + QVector2D insideVector = insidePoint - startPoint; + QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized(); + + bool swap = QVector2D::dotProduct(insideVector, normal) < 0; + + return swap ? normal : -normal; + }; + + auto addTriangleForLine = [&](const QQuadPath::Element &element, + const QVector2D &sp, + const QVector2D &ep, + const QVector2D &cp) { + addCurveTriangle(element, sp, ep, cp); + + // Add triangles on the outer side to make room for AA + const QVector2D normal = outsideNormal(sp, ep, cp); + constexpr QVector2D null; + addCurveTriangleWithNormals(element, {sp, sp, ep}, {null, normal, null}); + addCurveTriangleWithNormals(element, {sp, ep, ep}, {normal, normal, null}); + }; + + auto addTriangleForConcave = [&](const QQuadPath::Element &element, + const QVector2D &sp, + const QVector2D &ep, + const QVector2D &cp) { + addTriangleForLine(element, sp, ep, cp); + }; + + auto addTriangleForConvex = [&](const QQuadPath::Element &element, + const QVector2D &sp, + const QVector2D &ep, + const QVector2D &cp) { + addCurveTriangle(element, sp, ep, cp); + // Add two triangles on the outer side to get some more AA + + constexpr QVector2D null; + // First triangle on the line sp-cp, replacing ep + { + const QVector2D normal = outsideNormal(sp, cp, ep); + addCurveTriangleWithNormals(element, {sp, sp, cp}, {null, normal, null}); + } + + // Second triangle on the line ep-cp, replacing sp + { + const QVector2D normal = outsideNormal(ep, cp, sp); + addCurveTriangleWithNormals(element, {ep, ep, cp}, {null, normal, null}); + } + }; + + auto addFillTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3) { + constexpr QVector3D uv(0.0, 1.0, -1.0); + addTriangle({ p1, p2, p3 }, + {}, + [&uv](QVector2D) { return uv; }); + }; + + for (int i = 0; i < fillPath.elementCount(); ++i) { + iteratePath(fillPath, i, [&](const QQuadPath::Element &element, int index) { + QPointF sp(element.startPoint().toPointF()); //### to much conversion to and from pointF + QPointF cp(element.controlPoint().toPointF()); + QPointF ep(element.endPoint().toPointF()); + if (element.isSubpathStart()) + internalHull.moveTo(sp); + if (element.isLine()) { + internalHull.lineTo(ep); + linePointHash.insert(toRoundedPair(sp), index); + } else { + if (element.isConvex()) { + internalHull.lineTo(ep); + addTriangleForConvex(element, toRoundedVec2D(sp), toRoundedVec2D(ep), toRoundedVec2D(cp)); + convexPointHash.insert(toRoundedPair(sp), index); + } else { + internalHull.lineTo(cp); + internalHull.lineTo(ep); + addTriangleForConcave(element, toRoundedVec2D(sp), toRoundedVec2D(ep), toRoundedVec2D(cp)); + concaveControlPointHash.insert(toRoundedPair(cp), index); + } + } + }); + } + + auto makeHashable = [](const QVector2D &p) -> QPair { + return qMakePair(qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f); + }; + // Points in p are already rounded do 1/32 + // Returns false if the triangle needs to be split. Adds the triangle to the graphics buffers and returns true otherwise. + // (Does not handle ambiguous vertices that are on multiple unrelated lines/curves) + auto onSameSideOfLine = [](const QVector2D &p1, + const QVector2D &p2, + const QVector2D &linePoint, + const QVector2D &lineNormal) { + float side1 = testSideOfLineByNormal(linePoint, lineNormal, p1); + float side2 = testSideOfLineByNormal(linePoint, lineNormal, p2); + return side1 * side2 >= 0; + }; + + auto pointInSafeSpace = [&](const QVector2D &p, const QQuadPath::Element &element) -> bool { + const QVector2D a = element.startPoint(); + const QVector2D b = element.endPoint(); + const QVector2D c = element.controlPoint(); + // There are "safe" areas of the curve also across the baseline: the curve can never cross: + // line1: the line through A and B' + // line2: the line through B and A' + // Where A' = A "mirrored" through C and B' = B "mirrored" through C + const QVector2D n1 = calcNormalVector(a, c + (c - b)); + const QVector2D n2 = calcNormalVector(b, c + (c - a)); + bool safeSideOf1 = onSameSideOfLine(p, c, a, n1); + bool safeSideOf2 = onSameSideOfLine(p, c, b, n2); + return safeSideOf1 && safeSideOf2; + }; + + auto handleTriangle = [&](const QVector2D (&p)[3]) -> bool { + int lineElementIndex = -1; + int concaveElementIndex = -1; + int convexElementIndex = -1; + + bool foundElement = false; + int si = -1; + int ei = -1; + for (int i = 0; i < 3; ++i) { + if (auto found = linePointHash.constFind(makeHashable(p[i])); found != linePointHash.constEnd()) { + // check if this triangle is on a line, i.e. if one point is the sp and another is the ep of the same path element + const auto &element = fillPath.elementAt(*found); + for (int j = 0; j < 3; ++j) { + if (i != j && roundVec2D(element.endPoint()) == p[j]) { + if (foundElement) + return false; // More than one edge on path: must split + lineElementIndex = *found; + si = i; + ei = j; + foundElement = true; + } + } + } else if (auto found = concaveControlPointHash.constFind(makeHashable(p[i])); found != concaveControlPointHash.constEnd()) { + // check if this triangle is on the tangent line of a concave curve, + // i.e if one point is the cp, and the other is sp or ep + // TODO: clean up duplicated code (almost the same as the lineElement path above) + const auto &element = fillPath.elementAt(*found); + for (int j = 0; j < 3; ++j) { + if (i == j) + continue; + if (roundVec2D(element.startPoint()) == p[j] || roundVec2D(element.endPoint()) == p[j]) { + if (foundElement) + return false; // More than one edge on path: must split + concaveElementIndex = *found; + // The tangent line is p[i] - p[j] + si = i; + ei = j; + foundElement = true; + } + } + } else if (auto found = convexPointHash.constFind(makeHashable(p[i])); found != convexPointHash.constEnd()) { + // check if this triangle is on a curve, i.e. if one point is the sp and another is the ep of the same path element + const auto &element = fillPath.elementAt(*found); + for (int j = 0; j < 3; ++j) { + if (i != j && roundVec2D(element.endPoint()) == p[j]) { + if (foundElement) + return false; // More than one edge on path: must split + convexElementIndex = *found; + si = i; + ei = j; + foundElement = true; + } + } + } + } + if (lineElementIndex != -1) { + int ci = (6 - si - ei) % 3; // 1+2+3 is 6, so missing number is 6-n1-n2 + addTriangleForLine(fillPath.elementAt(lineElementIndex), p[si], p[ei], p[ci]); + } else if (concaveElementIndex != -1) { + addCurveTriangle(fillPath.elementAt(concaveElementIndex), p[0], p[1], p[2]); + } else if (convexElementIndex != -1) { + int oi = (6 - si - ei) % 3; + const auto &otherPoint = p[oi]; + const auto &element = fillPath.elementAt(convexElementIndex); + // We have to test whether the triangle can cross the line + // TODO: use the toplevel element's safe space + bool safeSpace = pointInSafeSpace(otherPoint, element); + if (safeSpace) { + addCurveTriangle(element, p[0], p[1], p[2]); + } else { + // Find a point inside the triangle that's also in the safe space + QVector2D newPoint = (p[0] + p[1] + p[2]) / 3; + // We should calculate the point directly, but just do a lazy implementation for now: + for (int i = 0; i < 7; ++i) { + safeSpace = pointInSafeSpace(newPoint, element); + if (safeSpace) + break; + newPoint = (p[si] + p[ei] + newPoint) / 3; + } + if (safeSpace) { + // Split triangle. We know the original triangle is only on one path element, so the other triangles are both fill. + // Curve triangle is (sp, ep, np) + addCurveTriangle(element, p[si], p[ei], newPoint); + // The other two are (sp, op, np) and (ep, op, np) + addFillTriangle(p[si], p[oi], newPoint); + addFillTriangle(p[ei], p[oi], newPoint); + } else { + // fallback to fill if we can't find a point in safe space + addFillTriangle(p[0], p[1], p[2]); + } + } + + } else { + addFillTriangle(p[0], p[1], p[2]); + } + return true; + }; + + QTriangleSet triangles = qTriangulate(internalHull); + + const quint32 *idxTable = static_cast(triangles.indices.data()); + for (int triangle = 0; triangle < triangles.indices.size() / 3; ++triangle) { + const quint32 *idx = &idxTable[triangle * 3]; + + QVector2D p[3]; + for (int i = 0; i < 3; ++i) { + p[i] = toRoundedVec2D(QPointF(triangles.vertices.at(idx[i] * 2), + triangles.vertices.at(idx[i] * 2 + 1))); + } + if (qFuzzyIsNull(determinant(p[0], p[1], p[2]))) + continue; // Skip degenerate triangles + bool needsSplit = !handleTriangle(p); + if (needsSplit) { + QVector2D c = (p[0] + p[1] + p[2]) / 3; + for (int i = 0; i < 3; ++i) { + qSwap(c, p[i]); + handleTriangle(p); + qSwap(c, p[i]); + } + } + } +} + + +QT_END_NAMESPACE diff --git a/src/quick/scenegraph/qsgcurveprocessor_p.h b/src/quick/scenegraph/qsgcurveprocessor_p.h new file mode 100644 index 0000000000..271f15207c --- /dev/null +++ b/src/quick/scenegraph/qsgcurveprocessor_p.h @@ -0,0 +1,51 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSGCURVEPROCESSOR_P_H +#define QSGCURVEPROCESSOR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include "util/qquadpath_p.h" + +QT_BEGIN_NAMESPACE + +class Q_QUICK_PRIVATE_EXPORT QSGCurveProcessor +{ +public: + typedef std::function uvForPointCallback; + typedef std::function &, + const std::array &, + uvForPointCallback)> addTriangleCallback; + typedef std::function &, + const std::array &, + const std::array &, + bool)> addStrokeTriangleCallback; + + + static void processFill(const QQuadPath &path, + Qt::FillRule fillRule, + addTriangleCallback addTriangle); + static void processStroke(const QQuadPath &strokePath, + float miterLimit, + float penWidth, + Qt::PenJoinStyle joinStyle, + Qt::PenCapStyle capStyle, + addStrokeTriangleCallback addTriangle, + int subdivisions = 3); + static void solveOverlaps(QQuadPath &path); +}; + +QT_END_NAMESPACE + +#endif // QSGCURVEPROCESSOR_P_H diff --git a/src/quick/scenegraph/qsgcurvestrokenode.cpp b/src/quick/scenegraph/qsgcurvestrokenode.cpp new file mode 100644 index 0000000000..2bfaff1b4e --- /dev/null +++ b/src/quick/scenegraph/qsgcurvestrokenode.cpp @@ -0,0 +1,112 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsgcurvestrokenode_p.h" +#include "qsgcurvestrokenode_p_p.h" + +QT_BEGIN_NAMESPACE + +QSGCurveStrokeNode::QSGCurveStrokeNode() +{ + setFlag(OwnsGeometry, true); + setGeometry(new QSGGeometry(attributes(), 0, 0)); + updateMaterial(); +} + +void QSGCurveStrokeNode::QSGCurveStrokeNode::updateMaterial() +{ + m_material.reset(new QSGCurveStrokeMaterial(this)); + setMaterial(m_material.data()); +} + +// Take the start, control and end point of a curve and return the points A, B, C +// representing the curve as Q(s) = A*s*s + B*s + C +std::array QSGCurveStrokeNode::curveABC(const std::array &p) +{ + QVector2D a = p[0] - 2*p[1] + p[2]; + QVector2D b = 2*p[1] - 2*p[0]; + QVector2D c = p[0]; + + return {a, b, c}; +} + +// Curve from p[0] to p[2] with control point p[1] +void QSGCurveStrokeNode::appendTriangle(const std::array &v, + const std::array &p, + const std::array &n) +{ + auto abc = curveABC(p); + + int currentVertex = m_uncookedVertexes.count(); + + for (int i = 0; i < 3; ++i) { + m_uncookedVertexes.append( { v[i].x(), v[i].y(), + abc[0].x(), abc[0].y(), abc[1].x(), abc[1].y(), abc[2].x(), abc[2].y(), + n[i].x(), n[i].y() } ); + } + m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2; +} + +// Straight line from p0 to p1 +void QSGCurveStrokeNode::appendTriangle(const std::array &v, + const std::array &p, + const std::array &n) +{ + // We could reduce this to a linear equation by setting A to (0,0). + // However, then we cannot use the cubic solution and need an additional + // code path in the shader. The following formulation looks more complicated + // but allows to always use the cubic solution. + auto A = p[1] - p[0]; + auto B = QVector2D(0., 0.); + auto C = p[0]; + + int currentVertex = m_uncookedVertexes.count(); + +// for (auto v : QList>({{v0, n0}, {v1, n1}, {v2, n2}})) { + for (int i = 0; i < 3; ++i) { + m_uncookedVertexes.append( { v[i].x(), v[i].y(), + A.x(), A.y(), B.x(), B.y(), C.x(), C.y(), + n[i].x(), n[i].y() } ); + } + m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2; +} + +void QSGCurveStrokeNode::cookGeometry() +{ + QSGGeometry *g = geometry(); + if (g->indexType() != QSGGeometry::UnsignedIntType) { + g = new QSGGeometry(attributes(), + m_uncookedVertexes.size(), + m_uncookedIndexes.size(), + QSGGeometry::UnsignedIntType); + setGeometry(g); + } else { + g->allocate(m_uncookedVertexes.size(), m_uncookedIndexes.size()); + } + + g->setDrawingMode(QSGGeometry::DrawTriangles); + memcpy(g->vertexData(), + m_uncookedVertexes.constData(), + g->vertexCount() * g->sizeOfVertex()); + memcpy(g->indexData(), + m_uncookedIndexes.constData(), + g->indexCount() * g->sizeOfIndex()); + + m_uncookedIndexes.clear(); + m_uncookedVertexes.clear(); +} + +const QSGGeometry::AttributeSet &QSGCurveStrokeNode::attributes() +{ + static QSGGeometry::Attribute data[] = { + QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute), //vertexCoord + QSGGeometry::Attribute::createWithAttributeType(1, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //A + QSGGeometry::Attribute::createWithAttributeType(2, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //B + QSGGeometry::Attribute::createWithAttributeType(3, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //C + QSGGeometry::Attribute::createWithAttributeType(4, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //normalVector + }; + static QSGGeometry::AttributeSet attrs = { 5, sizeof(StrokeVertex), data }; + return attrs; +} + +QT_END_NAMESPACE diff --git a/src/quick/scenegraph/qsgcurvestrokenode_p.cpp b/src/quick/scenegraph/qsgcurvestrokenode_p.cpp new file mode 100644 index 0000000000..c53a2c054b --- /dev/null +++ b/src/quick/scenegraph/qsgcurvestrokenode_p.cpp @@ -0,0 +1,88 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsgcurvestrokenode_p_p.h" +#include "qsgcurvestrokenode_p.h" + +QT_BEGIN_NAMESPACE + +bool QSGCurveStrokeMaterialShader::updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) +{ + bool changed = false; + QByteArray *buf = state.uniformData(); + Q_ASSERT(buf->size() >= 64); + + auto *newMaterial = static_cast(newEffect); + auto *oldMaterial = static_cast(oldEffect); + + auto *newNode = newMaterial != nullptr ? newMaterial->node() : nullptr; + auto *oldNode = oldMaterial != nullptr ? oldMaterial->node() : nullptr; + + if (state.isMatrixDirty()) { + QMatrix4x4 m = state.combinedMatrix(); + float localScale = newNode != nullptr ? newNode->localScale() : 1.0f; + m.scale(localScale); + memcpy(buf->data(), m.constData(), 64); + + float matrixScale = qSqrt(qAbs(state.determinant())) * state.devicePixelRatio() * localScale; + memcpy(buf->data()+64, &matrixScale, 4); + changed = true; + } + + if (state.isOpacityDirty()) { + const float opacity = state.opacity(); + memcpy(buf->data() + 64 + 4, &opacity, 4); + changed = true; + } + + int offset = 64+16; + if (newNode == nullptr) + return changed; + + QVector4D newStrokeColor(newNode->color().redF(), + newNode->color().greenF(), + newNode->color().blueF(), + newNode->color().alphaF()); + QVector4D oldStrokeColor = oldNode != nullptr + ? QVector4D(oldNode->color().redF(), + oldNode->color().greenF(), + oldNode->color().blueF(), + oldNode->color().alphaF()) + : QVector4D{}; + + if (oldNode == nullptr || oldStrokeColor != newStrokeColor) { + memcpy(buf->data() + offset, &newStrokeColor, 16); + changed = true; + } + offset += 16; + + if (oldNode == nullptr || newNode->strokeWidth() != oldNode->strokeWidth()) { + float w = newNode->strokeWidth(); + memcpy(buf->data() + offset, &w, 4); + changed = true; + } + offset += 4; + if (oldNode == nullptr || newNode->debug() != oldNode->debug()) { + float w = newNode->debug(); + memcpy(buf->data() + offset, &w, 4); + changed = true; + } +// offset += 4; + + return changed; +} + +int QSGCurveStrokeMaterial::compare(const QSGMaterial *other) const +{ + int typeDif = type() - other->type(); + if (!typeDif) { + auto *othernode = static_cast(other)->node(); + if (node()->color() != othernode->color()) + return node()->color().rgb() < othernode->color().rgb() ? -1 : 1; + if (node()->strokeWidth() != othernode->strokeWidth()) + return node()->strokeWidth() < othernode->strokeWidth() ? -1 : 1; + } + return typeDif; +} + +QT_END_NAMESPACE diff --git a/src/quick/scenegraph/qsgcurvestrokenode_p.h b/src/quick/scenegraph/qsgcurvestrokenode_p.h new file mode 100644 index 0000000000..d765d2258c --- /dev/null +++ b/src/quick/scenegraph/qsgcurvestrokenode_p.h @@ -0,0 +1,116 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSGCURVESTROKENODE_P_H +#define QSGCURVESTROKENODE_P_H + +#include +#include + +#include "qsgcurveabstractnode_p.h" +#include "qsgcurvestrokenode_p_p.h" + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of a number of Qt sources files. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +QT_BEGIN_NAMESPACE + +class Q_QUICK_PRIVATE_EXPORT QSGCurveStrokeNode : public QSGCurveAbstractNode +{ +public: + QSGCurveStrokeNode(); + + void setColor(QColor col) override + { + m_color = col; + } + + QColor color() const + { + return m_color; + } + + void setStrokeWidth(float width) + { + m_strokeWidth = width; + } + + float strokeWidth() const + { + return m_strokeWidth; + } + + void appendTriangle(const std::array &v, // triangle vertices + const std::array &p, // curve points + const std::array &n); // vertex normals + void appendTriangle(const std::array &v, // triangle vertices + const std::array &p, // line points + const std::array &n); // vertex normals + + void cookGeometry() override; + + static const QSGGeometry::AttributeSet &attributes(); + + QVector uncookedIndexes() const + { + return m_uncookedIndexes; + } + + float debug() const + { + return m_debug; + } + + void setDebug(float newDebug) + { + m_debug = newDebug; + } + + void setLocalScale(float scale) + { + m_localScale = scale; + } + + float localScale() const + { + return m_localScale; + } + +private: + + struct StrokeVertex + { + float x, y; + float ax, ay; + float bx, by; + float cx, cy; + float nx, ny; //normal vector: direction to move vertext to account for AA + }; + + void updateMaterial(); + + static std::array curveABC(const std::array &p); + + QColor m_color; + float m_strokeWidth = 0.0f; + float m_debug = 0.0f; + float m_localScale = 1.0f; + +protected: + QScopedPointer m_material; + + QVector m_uncookedVertexes; + QVector m_uncookedIndexes; +}; + +QT_END_NAMESPACE + +#endif // QSGCURVESTROKENODE_P_H diff --git a/src/quick/scenegraph/qsgcurvestrokenode_p_p.h b/src/quick/scenegraph/qsgcurvestrokenode_p_p.h new file mode 100644 index 0000000000..9597a6eb48 --- /dev/null +++ b/src/quick/scenegraph/qsgcurvestrokenode_p_p.h @@ -0,0 +1,73 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSGCURVESTROKENODE_P_P_H +#define QSGCURVESTROKENODE_P_P_H + +#include +#include + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of a number of Qt sources files. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +QT_BEGIN_NAMESPACE + +class QSGCurveStrokeNode; +class QSGCurveStrokeMaterial; + +class Q_QUICK_PRIVATE_EXPORT QSGCurveStrokeMaterialShader : public QSGMaterialShader +{ +public: + QSGCurveStrokeMaterialShader() + { + setShaderFileName(VertexStage, + QStringLiteral(":/qt-project.org/scenegraph/shaders_ng/shapestroke.vert.qsb")); + setShaderFileName(FragmentStage, + QStringLiteral(":/qt-project.org/scenegraph/shaders_ng/shapestroke.frag.qsb")); + } + + bool updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override; +}; + + +class Q_QUICK_PRIVATE_EXPORT QSGCurveStrokeMaterial : public QSGMaterial +{ +public: + QSGCurveStrokeMaterial(QSGCurveStrokeNode *node) + : m_node(node) + { + setFlag(Blending, true); + } + + int compare(const QSGMaterial *other) const override; + + QSGCurveStrokeNode *node() const + { + return m_node; + } + +protected: + QSGMaterialType *type() const override + { + static QSGMaterialType t; + return &t; + } + QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override + { + return new QSGCurveStrokeMaterialShader; + } + + QSGCurveStrokeNode *m_node; +}; + +QT_END_NAMESPACE + +#endif // QSGCURVESTROKENODE_P_P_H diff --git a/src/quick/scenegraph/qsgdefaultcontext.cpp b/src/quick/scenegraph/qsgdefaultcontext.cpp index f37e0661b7..379debfe82 100644 --- a/src/quick/scenegraph/qsgdefaultcontext.cpp +++ b/src/quick/scenegraph/qsgdefaultcontext.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -146,10 +147,12 @@ QSGPainterNode *QSGDefaultContext::createPainterNode(QQuickPaintedItem *item) } QSGGlyphNode *QSGDefaultContext::createGlyphNode(QSGRenderContext *rc, - bool preferNativeGlyphNode, + QSGTextNode::RenderType renderType, int renderTypeQuality) { - if (m_distanceFieldDisabled || preferNativeGlyphNode) { + if (renderType == QSGTextNode::CurveRendering) { + return new QSGCurveGlyphNode(rc); + } else if (m_distanceFieldDisabled || renderType == QSGTextNode::NativeRendering) { return new QSGDefaultGlyphNode(rc); } else { QSGDistanceFieldGlyphNode *node = new QSGDistanceFieldGlyphNode(rc); diff --git a/src/quick/scenegraph/qsgdefaultcontext_p.h b/src/quick/scenegraph/qsgdefaultcontext_p.h index 8dd507bd9a..9250224f50 100644 --- a/src/quick/scenegraph/qsgdefaultcontext_p.h +++ b/src/quick/scenegraph/qsgdefaultcontext_p.h @@ -33,7 +33,7 @@ public: QSGInternalRectangleNode *createInternalRectangleNode() override; QSGInternalImageNode *createInternalImageNode(QSGRenderContext *renderContext) override; QSGPainterNode *createPainterNode(QQuickPaintedItem *item) override; - QSGGlyphNode *createGlyphNode(QSGRenderContext *rc, bool preferNativeGlyphNode, int renderTypeQuality) override; + QSGGlyphNode *createGlyphNode(QSGRenderContext *rc, QSGTextNode::RenderType renderType, int renderTypeQuality) override; QSGLayer *createLayer(QSGRenderContext *renderContext) override; QSurfaceFormat defaultSurfaceFormat() const override; QSGRendererInterface *rendererInterface(QSGRenderContext *renderContext) override; diff --git a/src/quick/scenegraph/qsgdefaultrendercontext.cpp b/src/quick/scenegraph/qsgdefaultrendercontext.cpp index 73f4bec27f..dc3262f2da 100644 --- a/src/quick/scenegraph/qsgdefaultrendercontext.cpp +++ b/src/quick/scenegraph/qsgdefaultrendercontext.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qsgdefaultrendercontext_p.h" +#include "qsgcurveglyphatlas_p.h" #include @@ -71,6 +72,9 @@ void QSGDefaultRenderContext::invalidateGlyphCaches() } } + qDeleteAll(m_curveGlyphAtlases); + m_curveGlyphAtlases.clear(); + { auto it = m_fontEnginesToClean.begin(); while (it != m_fontEnginesToClean.end()) { @@ -131,6 +135,9 @@ void QSGDefaultRenderContext::invalidate() } m_fontEnginesToClean.clear(); + qDeleteAll(m_curveGlyphAtlases); + m_curveGlyphAtlases.clear(); + qDeleteAll(m_glyphCaches); m_glyphCaches.clear(); @@ -254,6 +261,18 @@ void QSGDefaultRenderContext::preprocess() } } +QSGCurveGlyphAtlas *QSGDefaultRenderContext::curveGlyphAtlas(const QRawFont &font) +{ + QString key = fontKey(font, 0); + QSGCurveGlyphAtlas *atlas = m_curveGlyphAtlases.value(key, nullptr); + if (atlas == nullptr) { + atlas = new QSGCurveGlyphAtlas(font); + m_curveGlyphAtlases.insert(key, atlas); + } + + return atlas; +} + QSGDistanceFieldGlyphCache *QSGDefaultRenderContext::distanceFieldGlyphCache(const QRawFont &font, int renderTypeQuality) { QString key = fontKey(font, renderTypeQuality); diff --git a/src/quick/scenegraph/qsgdefaultrendercontext_p.h b/src/quick/scenegraph/qsgdefaultrendercontext_p.h index 06c6adfc64..1743ea35b7 100644 --- a/src/quick/scenegraph/qsgdefaultrendercontext_p.h +++ b/src/quick/scenegraph/qsgdefaultrendercontext_p.h @@ -70,6 +70,7 @@ public: void preprocess() override; void invalidateGlyphCaches() override; QSGDistanceFieldGlyphCache *distanceFieldGlyphCache(const QRawFont &font, int renderTypeQuality) override; + QSGCurveGlyphAtlas *curveGlyphAtlas(const QRawFont &font) override; QSGTexture *createTexture(const QImage &image, uint flags) const override; QSGRenderer *createRenderer(QSGRendererInterface::RenderMode renderMode = QSGRendererInterface::RenderMode2D) override; @@ -119,6 +120,7 @@ protected: bool m_useDepthBufferFor2D; QRhiResourceUpdateBatch *m_glyphCacheResourceUpdates; QSet m_pendingGlyphCacheTextures; + QHash m_curveGlyphAtlases; }; QT_END_NAMESPACE diff --git a/src/quick/scenegraph/shaders_ng/shapecurve.frag b/src/quick/scenegraph/shaders_ng/shapecurve.frag new file mode 100644 index 0000000000..2badca31a7 --- /dev/null +++ b/src/quick/scenegraph/shaders_ng/shapecurve.frag @@ -0,0 +1,164 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#version 440 + +layout(location = 0) in vec4 qt_TexCoord; +layout(location = 1) in vec4 gradient; + +#if defined(LINEARGRADIENT) +layout(location = 2) in float gradTabIndex; +#elif defined(RADIALGRADIENT) || defined(CONICALGRADIENT) +layout(location = 2) in vec2 coord; +#endif + + +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float matrixScale; + float opacity; + float debug; + float reserved3; + +#if defined(STROKE) + vec4 strokeColor; + float strokeWidth; + float reserved4; + float reserved5; + float reserved6; +#endif + +#if defined(LINEARGRADIENT) + vec2 gradientStart; + vec2 gradientEnd; +#elif defined(RADIALGRADIENT) + vec2 translationPoint; + vec2 focalToCenter; + float centerRadius; + float focalRadius; +#elif defined(CONICALGRADIENT) + vec2 translationPoint; + float angle; +#else + vec4 color; +#endif +} ubuf; + +#define INVERSE_2PI 0.1591549430918953358 + +#if defined(LINEARGRADIENT) || defined(RADIALGRADIENT) || defined(CONICALGRADIENT) +layout(binding = 1) uniform sampler2D gradTabTexture; +#endif + +vec4 baseColor() +{ +#if defined(LINEARGRADIENT) + return texture(gradTabTexture, vec2(gradTabIndex, 0.5)); +#elif defined(RADIALGRADIENT) + float rd = ubuf.centerRadius - ubuf.focalRadius; + float b = 2.0 * (rd * ubuf.focalRadius + dot(coord, ubuf.focalToCenter)); + float fmp2_m_radius2 = -ubuf.focalToCenter.x * ubuf.focalToCenter.x - ubuf.focalToCenter.y * ubuf.focalToCenter.y + rd * rd; + float inverse_2_fmp2_m_radius2 = 1.0 / (2.0 * fmp2_m_radius2); + float det = b * b - 4.0 * fmp2_m_radius2 * ((ubuf.focalRadius * ubuf.focalRadius) - dot(coord, coord)); + vec4 result = vec4(0.0); + if (det >= 0.0) { + float detSqrt = sqrt(det); + float w = max((-b - detSqrt) * inverse_2_fmp2_m_radius2, (-b + detSqrt) * inverse_2_fmp2_m_radius2); + if (ubuf.focalRadius + w * (ubuf.centerRadius - ubuf.focalRadius) >= 0.0) + result = texture(gradTabTexture, vec2(w, 0.5)); + } + + return result; +#elif defined(CONICALGRADIENT) + float t; + if (abs(coord.y) == abs(coord.x)) + t = (atan(-coord.y + 0.002, coord.x) + ubuf.angle) * INVERSE_2PI; + else + t = (atan(-coord.y, coord.x) + ubuf.angle) * INVERSE_2PI; + return texture(gradTabTexture, vec2(t - floor(t), 0.5)); +#else + return vec4(ubuf.color.rgb, 1.0) * ubuf.color.a; +#endif +} + +void main() +{ + float f = qt_TexCoord.z * (qt_TexCoord.x * qt_TexCoord.x - qt_TexCoord.y) // curve + + (1.0 - abs(qt_TexCoord.z)) * (qt_TexCoord.x - qt_TexCoord.y); // line + +#if defined(USE_DERIVATIVES) + float _ddx = dFdx(f); + float _ddy = dFdy(f); + float df = length(vec2(_ddx, _ddy)); +#else + // We calculate the partial derivatives for f'(x, y) based on knowing the partial derivatives + // for the texture coordinates (u, v). + // So for curves: + // f(x,y) = u(x, y) * u(x, y) - v(x, y) + // f(x,y) = p(u(x,y)) - v(x, y) where p(u) = u^2 + // So f'(x, y) = p'(u(x, y)) * u'(x, y) - v'(x, y) + // (by chain rule and sum rule) + // f'(x, y) = 2 * u(x, y) * u'(x, y) - v'(x, y) + // And so: + // df/dx = 2 * u(x, y) * du/dx - dv/dx + // df/dy = 2 * u(x, y) * du/dy - dv/dy + // + // and similarly for straight lines: + // f(x, y) = u(x, y) - v(x, y) + // f'(x, y) = dudx - dvdx + + float dudx = gradient.x; + float dvdx = gradient.y; + float dudy = gradient.z; + float dvdy = gradient.w; + + // Test with analytic derivatives +// dudx = dFdx(qt_TexCoord.x); +// dvdx = dFdx(qt_TexCoord.y); +// dudy = dFdy(qt_TexCoord.x); +// dvdy = dFdy(qt_TexCoord.y); + + float dfx_curve = 2.0f * qt_TexCoord.x * dudx - dvdx; + float dfy_curve = 2.0f * qt_TexCoord.x * dudy - dvdy; + + float dfx_line = dudx - dvdx; + float dfy_line = dudy - dvdy; + + float dfx = qt_TexCoord.z * dfx_curve + (1.0 - abs(qt_TexCoord.z)) * dfx_line; + float dfy = qt_TexCoord.z * dfy_curve + (1.0 - abs(qt_TexCoord.z)) * dfy_line; + float df = length(vec2(dfx, dfy)); +#endif + + float isLine = 1.0 - abs(qt_TexCoord.z); + float isCurve = 1.0 - isLine; + float debugR = isCurve * min(1.0, 1.0 - qt_TexCoord.z); + float debugG = isLine; + float debugB = isCurve * min(1.0, 1.0 - qt_TexCoord.z * -1.0) + debugG; + vec3 debugColor = vec3(debugR, debugG, debugB); + +#if defined(STROKE) + float distance = (f / df); // distance from centre of fragment to line + + float halfStrokeWidth = ubuf.strokeWidth / 2.0; + + // calculate stroke + float strokeCoverage = 1.0 - clamp(0.5 + abs(distance) - halfStrokeWidth, 0.0, 1.0); + vec4 stroke = ubuf.strokeColor * strokeCoverage; + + float fillCoverage = clamp(0.5 + f / df, 0.0, 1.0); + vec4 fill = baseColor() * fillCoverage; + + vec4 combined = fill * (1.0 - stroke.a) + stroke * stroke.a; + + // finally mix in debug + fragColor = mix(combined, vec4(debugColor, 1.0), ubuf.debug) * ubuf.opacity; +#else + // Special case: mask out concave curve in "negative space". + int specialCaseMask = 1 - int(qt_TexCoord.w != 0.0) * (int(qt_TexCoord.x < 0.0) + int(qt_TexCoord.x > 1.0)); + float fillCoverage = clamp(0.5 + f / df, 0.0, 1.0) * float(specialCaseMask); + + fragColor = mix(baseColor() * fillCoverage, vec4(debugColor, 1.0), ubuf.debug) * ubuf.opacity; +#endif +} diff --git a/src/quick/scenegraph/shaders_ng/shapecurve.vert b/src/quick/scenegraph/shaders_ng/shapecurve.vert new file mode 100644 index 0000000000..64e32b5517 --- /dev/null +++ b/src/quick/scenegraph/shaders_ng/shapecurve.vert @@ -0,0 +1,81 @@ +#version 440 + +layout(location = 0) in vec4 vertexCoord; +layout(location = 1) in vec4 vertexTexCoord; +layout(location = 2) in vec4 vertexGradient; +layout(location = 3) in vec2 normalVector; + +layout(location = 0) out vec4 qt_TexCoord; +layout(location = 1) out vec4 gradient; + +#if defined(LINEARGRADIENT) +layout(location = 2) out float gradTabIndex; +#elif defined(RADIALGRADIENT) || defined(CONICALGRADIENT) +layout(location = 2) out vec2 coord; +#endif + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float matrixScale; + float opacity; + float debug; + float reserved3; + +#if defined(STROKE) + vec4 strokeColor; + float strokeWidth; + float reserved4; + float reserved5; + float reserved6; +#endif + +#if defined(LINEARGRADIENT) + vec2 gradientStart; + vec2 gradientEnd; +#elif defined(RADIALGRADIENT) + vec2 translationPoint; + vec2 focalToCenter; + float centerRadius; + float focalRadius; +#elif defined(CONICALGRADIENT) + vec2 translationPoint; + float angle; +#else + vec4 color; +#endif +} ubuf; + +out gl_PerVertex { vec4 gl_Position; }; + +#define SQRT2 1.41421356237 + +vec4 addOffset(vec4 texCoord, vec2 offset, vec4 duvdxy) +{ + float dudx = duvdxy.x; + float dvdx = duvdxy.y; + float dudy = duvdxy.z; + float dvdy = duvdxy.w; + float u = offset.x * dudx + offset.y * dudy; + float v = offset.x * dvdx + offset.y * dvdy; + // special case external triangles for concave curves + int specialCase = int(texCoord.z > 0) * (int(offset.x != 0) + int(offset.y != 0)); + return vec4(texCoord.x + u, texCoord.y + v, texCoord.z, float(specialCase)); +} + +void main() +{ + vec2 offset = normalVector * SQRT2/ubuf.matrixScale; + + qt_TexCoord = addOffset(vertexTexCoord, offset, vertexGradient); + + gradient = vertexGradient / ubuf.matrixScale; + +#if defined(LINEARGRADIENT) + vec2 gradVec = ubuf.gradientEnd - ubuf.gradientStart; + gradTabIndex = dot(gradVec, vertexCoord.xy - ubuf.gradientStart.xy) / dot(gradVec, gradVec); +#elif defined(RADIALGRADIENT) || defined(CONICALGRADIENT) + coord = vertexCoord.xy - ubuf.translationPoint; +#endif + + gl_Position = ubuf.qt_Matrix * (vertexCoord + vec4(offset, 0, 0)); +} diff --git a/src/quick/scenegraph/shaders_ng/shapestroke.frag b/src/quick/scenegraph/shaders_ng/shapestroke.frag new file mode 100644 index 0000000000..84435ba6f9 --- /dev/null +++ b/src/quick/scenegraph/shaders_ng/shapestroke.frag @@ -0,0 +1,130 @@ +#version 440 + +layout(location = 0) in vec4 P; +layout(location = 1) in vec2 A; +layout(location = 2) in vec2 B; +layout(location = 3) in vec2 C; +layout(location = 4) in vec2 HG; +layout(location = 5) in float offset; + +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + + float matrixScale; + float opacity; + float reserved2; + float reserved3; + + vec4 strokeColor; + + float strokeWidth; + float debug; + float reserved5; + float reserved6; +} ubuf; + +float cuberoot(float x) +{ + return sign(x) * pow(abs(x), 1 / 3.); +} + +#define PI 3.1415926538 + +vec3 solveDepressedCubic(float p, float q) +{ + float D = q * q / 4. + p * p * p / 27.; + + float u1 = cuberoot(-q / 2. - sqrt(D)); + float u2 = cuberoot(-q / 2. + sqrt(D)); + vec3 rootsD1 = vec3(u1 - p / (3. * u1), u2 - p / (3. * u2), 0); + + float v = 2.*sqrt(-p / 3.); + float t = acos(3. * q / p / v) / 3.; + float k = 2. * PI / 3.; + vec3 rootsD2 = vec3(v * cos(t), v * cos(t - k), v * cos(t - 2. * k)); + + return D > 0 ? rootsD1 : rootsD2; +} + +mat2 qInverse(mat2 matrix) { + float a = matrix[0][0], b = matrix[0][1]; + float c = matrix[1][0], d = matrix[1][1]; + + float determinant = a * d - b * c; + float invDet = 1.0 / determinant; + + mat2 inverseMatrix; + inverseMatrix[0][0] = d * invDet; + inverseMatrix[0][1] = -b * invDet; + inverseMatrix[1][0] = -c * invDet; + inverseMatrix[1][1] = a * invDet; + + return inverseMatrix; +} + +void main() +{ + vec3 s = solveDepressedCubic(HG.x, HG.y) - vec3(offset, offset, offset); + + vec2 Qmin = vec2(1e10, 1e10); + float dmin = 1e4; + for (int i = 0; i < 3; i++) { + float t = clamp(s[i], 0., 1.); + vec2 Q = A * t * t + B * t + C; + float d = length(Q - P.xy); + float foundNewMin = step(d, dmin); + dmin = min(d, dmin); + Qmin = foundNewMin * Q + (1. - foundNewMin) * Qmin; + } + vec2 n = (P.xy - Qmin) / dmin; + vec2 Q1 = (Qmin + ubuf.strokeWidth / 2. * n); + vec2 Q2 = (Qmin - ubuf.strokeWidth / 2. * n); + + // Converting to screen coordinates: +#if defined(USE_DERIVATIVES) + mat2 T = mat2(dFdx(P.x), dFdy(P.x), dFdx(P.y), dFdy(P.y)); + mat2 Tinv = qInverse(T); + vec2 Q1_s = Tinv * Q1; + vec2 Q2_s = Tinv * Q2; + vec2 P_s = Tinv * P.xy; + vec2 n_s = Tinv * n; + n_s = n_s / length(n_s); +#else + vec2 Q1_s = ubuf.matrixScale * Q1; + vec2 Q2_s = ubuf.matrixScale * Q2; + vec2 P_s = ubuf.matrixScale * P.xy; + vec2 n_s = n; +#endif + + // Geometric solution for anti aliasing using the known distances + // to the edges of the path in the screen coordinate system. + float dist1 = dot(P_s - Q1_s, n_s); + float dist2 = dot(P_s - Q2_s, n_s); + + // Calculate the fill coverage if the line is crossing the square cell + // normally (vertically or horizontally). + // float fillCoverageLin = clamp(0.5-dist1, 0., 1.) - clamp(0.5-dist2, 0., 1.); + + // Calculate the fill coverage if the line is crossing the square cell + // diagonally. + float fillCoverageDia = clamp(step(0., -dist1) + sign(dist1) * pow(max(0., sqrt(2.) / 2. - abs(dist1)), 2.), 0., 1.) - + clamp(step(0., -dist2) + sign(dist2) * pow(max(0., sqrt(2.) / 2. - abs(dist2)), 2.), 0., 1.); + + // Merge the normal and the diagonal solution. The merge factor is periodic + // in 90 degrees and 0/1 at 0 and 45 degree. The simple equation was + // estimated after numerical experiments. + // float mergeFactor = 2 * abs(n_s.x) * abs(n_s.y); + // float fillCoverage = mergeFactor * fillCoverageDia + (1-mergeFactor) * fillCoverageLin; + + // It seems to be sufficient to use the equation for the diagonal. + float fillCoverage = fillCoverageDia; + + // The center line is sometimes not filled because of numerical issues. This fixes this. + float centerline = step(ubuf.strokeWidth * 0.01, dmin); + fillCoverage = fillCoverage * centerline + min(1., ubuf.strokeWidth * ubuf.matrixScale) * (1. - centerline); + + fragColor = vec4(ubuf.strokeColor.rgb, 1.0) *ubuf.strokeColor.a * fillCoverage * ubuf.opacity + + ubuf.debug * vec4(0.0, 0.5, 1.0, 1.0) * (1.0 - fillCoverage) * ubuf.opacity; +} diff --git a/src/quick/scenegraph/shaders_ng/shapestroke.vert b/src/quick/scenegraph/shaders_ng/shapestroke.vert new file mode 100644 index 0000000000..9b22eb4eeb --- /dev/null +++ b/src/quick/scenegraph/shaders_ng/shapestroke.vert @@ -0,0 +1,76 @@ +#version 440 + +layout(location = 0) in vec4 vertexCoord; +layout(location = 1) in vec2 inA; +layout(location = 2) in vec2 inB; +layout(location = 3) in vec2 inC; +layout(location = 4) in vec2 normalVector; + +layout(location = 0) out vec4 P; +layout(location = 1) out vec2 A; +layout(location = 2) out vec2 B; +layout(location = 3) out vec2 C; +layout(location = 4) out vec2 HG; +layout(location = 5) out float offset; + + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + + float matrixScale; + float opacity; + float reserved2; + float reserved3; + + vec4 strokeColor; + + float strokeWidth; + float debug; + float reserved5; + float reserved6; +} ubuf; + +out gl_PerVertex { vec4 gl_Position; }; + +#define SQRT2 1.41421356237 + +float qdot(vec2 a, vec2 b) +{ + return a.x * b.x + a.y * b.y; +} + +void main() +{ + P = vertexCoord + vec4(normalVector, 0.0, 0.0) * SQRT2/ubuf.matrixScale; + + A = inA; + B = inB; + C = inC; + + // Find the parameters H, G for the depressed cubic + // t^2+H*t+G=0 + // that results from the equation + // Q'(s).(p-Q(s)) = 0 + // The last parameter is the static offset between s and t: + // s = t - b/(3a) + // use it to get back the parameter t + + // this is a constant for the curve + float a = -2. * qdot(A, A); + // this is a constant for the curve + float b = -3. * qdot(A, B); + //this is linear in p so it can be put into the shader with vertex data + float c = 2. * qdot(A, P.xy) - qdot(B, B) - 2. * qdot(A, C); + //this is linear in p so it can be put into the shader with vertex data + float d = qdot(B, P.xy) - qdot(B, C); + // convert to depressed cubic. + // both functions are linear in c and d and thus linear in p + float H = (3. * a * c - b * b) / (3. * a * a); + float G = (2. * b * b * b - 9. * a * b * c + 27. * a * a * d) / (27. * a * a * a); + HG = vec2(H, G); + offset = b/(3*a); + + + + gl_Position = ubuf.qt_Matrix * P; +} diff --git a/src/quick/scenegraph/util/qquadpath.cpp b/src/quick/scenegraph/util/qquadpath.cpp new file mode 100644 index 0000000000..c78c650db9 --- /dev/null +++ b/src/quick/scenegraph/util/qquadpath.cpp @@ -0,0 +1,802 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquadpath_p.h" +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcSGCurveProcessor); + +static qreal qt_scoreQuadratic(const QBezier &b, QPointF qcp) +{ + static bool init = false; + const int numSteps = 21; + Q_STATIC_ASSERT(numSteps % 2 == 1); // numTries must be odd + static qreal t2s[numSteps]; + static qreal tmts[numSteps]; + if (!init) { + // Precompute bezier factors + qreal t = 0.20; + const qreal step = (1 - (2 * t)) / (numSteps - 1); + for (int i = 0; i < numSteps; i++) { + t2s[i] = t * t; + tmts[i] = 2 * t * (1 - t); + t += step; + } + init = true; + } + + const QPointF midPoint = b.midPoint(); + auto distForIndex = [&](int i) -> qreal { + QPointF qp = (t2s[numSteps - 1 - i] * b.pt1()) + (tmts[i] * qcp) + (t2s[i] * b.pt4()); + QPointF d = midPoint - qp; + return QPointF::dotProduct(d, d); + }; + + const int halfSteps = (numSteps - 1) / 2; + bool foundIt = false; + const qreal centerDist = distForIndex(halfSteps); + qreal minDist = centerDist; + // Search for the minimum in right half + for (int i = 0; i < halfSteps; i++) { + qreal tDist = distForIndex(halfSteps + 1 + i); + if (tDist < minDist) { + minDist = tDist; + } else { + foundIt = (i > 0); + break; + } + } + if (!foundIt) { + // Search in left half + minDist = centerDist; + for (int i = 0; i < halfSteps; i++) { + qreal tDist = distForIndex(halfSteps - 1 - i); + if (tDist < minDist) { + minDist = tDist; + } else { + foundIt = (i > 0); + break; + } + } + } + return foundIt ? minDist : centerDist; +} + +static QPointF qt_quadraticForCubic(const QBezier &b) +{ + const QLineF st = b.startTangent(); + const QLineF et = b.endTangent(); + const QPointF midPoint = b.midPoint(); + bool valid = true; + QPointF quadControlPoint; + if (st.intersects(et, &quadControlPoint) == QLineF::NoIntersection) { + valid = false; + } else { + // Check if intersection is on wrong side + const QPointF bl = b.pt4() - b.pt1(); + const QPointF ml = midPoint - b.pt1(); + const QPointF ql = quadControlPoint - b.pt1(); + qreal cx1 = (ml.x() * bl.y()) - (ml.y() * bl.x()); + qreal cx2 = (ql.x() * bl.y()) - (ql.y() * bl.x()); + valid = (std::signbit(cx1) == std::signbit(cx2)); + } + return valid ? quadControlPoint : midPoint; +} + +static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints) +{ + auto isValidRoot = [](qreal r) { + return qIsFinite(r) && (r > 0) && (!qFuzzyIsNull(float(r))) && (r < 1) + && (!qFuzzyIsNull(float(r - 1))); + }; + + // normalize so pt1.x,pt1.y,pt4.y == 0 + QTransform xf; + const QLineF l(orig.pt1(), orig.pt4()); + xf.rotate(l.angle()); + xf.translate(-orig.pt1().x(), -orig.pt1().y()); + const QBezier n = orig.mapBy(xf); + Q_ASSERT(n.pt1() == QPoint() && qFuzzyIsNull(float(n.pt4().y()))); + + const qreal x2 = n.pt2().x(); + const qreal x3 = n.pt3().x(); + const qreal x4 = n.pt4().x(); + const qreal y2 = n.pt2().y(); + const qreal y3 = n.pt3().y(); + + const qreal p = x3 * y2; + const qreal q = x4 * y2; + const qreal r = x2 * y3; + const qreal s = x4 * y3; + + const qreal a = 18 * ((-3 * p) + (2 * q) + (3 * r) - s); + if (qFuzzyIsNull(float(a))) { + if (std::signbit(y2) != std::signbit(y3) && qFuzzyCompare(float(x4 - x3), float(x2))) { + tpoints[0] = 0.5; // approx + return 1; + } else if (!a) { + return 0; + } + } + const qreal b = 18 * (((3 * p) - q) - (3 * r)); + const qreal c = 18 * (r - p); + const qreal rad = (b * b) - (4 * a * c); + if (rad < 0) + return 0; + const qreal sqr = qSqrt(rad); + const qreal root1 = (-b + sqr) / (2 * a); + const qreal root2 = (-b - sqr) / (2 * a); + + int res = 0; + if (isValidRoot(root1)) + tpoints[res++] = root1; + if (root2 != root1 && isValidRoot(root2)) + tpoints[res++] = root2; + + if (res == 2 && tpoints[0] > tpoints[1]) + qSwap(tpoints[0], tpoints[1]); + + return res; +} + +static void qt_addToQuadratics(const QBezier &b, QPolygonF *p, int maxSplits, qreal maxDiff) +{ + QPointF qcp = qt_quadraticForCubic(b); + if (maxSplits <= 0 || qt_scoreQuadratic(b, qcp) < maxDiff) { + p->append(qcp); + p->append(b.pt4()); + } else { + QBezier rhs = b; + QBezier lhs; + rhs.parameterSplitLeft(0.5, &lhs); + qt_addToQuadratics(lhs, p, maxSplits - 1, maxDiff); + qt_addToQuadratics(rhs, p, maxSplits - 1, maxDiff); + } +} + +static void qt_toQuadratics(const QBezier &b, QPolygonF *out, qreal errorLimit = 0.01) +{ + out->resize(0); + out->append(b.pt1()); + + { + // Shortcut if the cubic is really a quadratic + const qreal f = 3.0 / 2.0; + const QPointF c1 = b.pt1() + f * (b.pt2() - b.pt1()); + const QPointF c2 = b.pt4() + f * (b.pt3() - b.pt4()); + if (c1 == c2) { + out->append(c1); + out->append(b.pt4()); + return; + } + } + + const QRectF cpr = b.bounds(); + const QPointF dim = cpr.bottomRight() - cpr.topLeft(); + qreal maxDiff = QPointF::dotProduct(dim, dim) * errorLimit * errorLimit; // maxdistance^2 + + qreal infPoints[2]; + int numInfPoints = qt_getInflectionPoints(b, infPoints); + const int maxSubSplits = numInfPoints > 0 ? 2 : 3; + qreal t0 = 0; + // number of main segments == #inflectionpoints + 1 + for (int i = 0; i < numInfPoints + 1; i++) { + qreal t1 = (i < numInfPoints) ? infPoints[i] : 1; + QBezier segment = b.bezierOnInterval(t0, t1); + qt_addToQuadratics(segment, out, maxSubSplits, maxDiff); + t0 = t1; + } +} + +QVector2D QQuadPath::Element::pointAtFraction(float t) const +{ + if (isLine()) { + return sp + t * (ep - sp); + } else { + const float r = 1 - t; + return (r * r * sp) + (2 * t * r * cp) + (t * t * ep); + } +} + +float QQuadPath::Element::extent() const +{ + // TBD: cache this value if we start using it a lot + QVector2D min(qMin(sp.x(), ep.x()), qMin(sp.y(), ep.y())); + QVector2D max(qMax(sp.x(), ep.x()), qMax(sp.y(), ep.y())); + if (!isLine()) { + min = QVector2D(qMin(min.x(), cp.x()), qMin(min.y(), cp.y())); + max = QVector2D(qMax(max.x(), cp.x()), qMax(max.y(), cp.y())); + } + return (max - min).length(); +} + +// Returns the number of intersections between element and a horizontal line at y. +// The t values of max 2 intersection(s) are stored in the fractions array +int QQuadPath::Element::intersectionsAtY(float y, float *fractions) const +{ + const float y0 = startPoint().y() - y; + const float y1 = controlPoint().y() - y; + const float y2 = endPoint().y() - y; + + int numRoots = 0; + const float a = y0 - (2 * y1) + y2; + if (a) { + const float b = (y1 * y1) - (y0 * y2); + if (b >= 0) { + const float sqr = qSqrt(b); + const float root1 = -(-y0 + y1 + sqr) / a; + if (qIsFinite(root1) && root1 >= 0 && root1 <= 1) + fractions[numRoots++] = root1; + const float root2 = (y0 - y1 + sqr) / a; + if (qIsFinite(root2) && root2 != root1 && root2 >= 0 && root2 <= 1) + fractions[numRoots++] = root2; + } + } else if (y1 != y2) { + const float root1 = (y2 - (2 * y1)) / (2 * (y2 - y1)); + if (qIsFinite(root1) && root1 >= 0 && root1 <= 1) + fractions[numRoots++] = root1; + } + + return numRoots; +} + +static float crossProduct(const QVector2D &sp, const QVector2D &p, const QVector2D &ep) +{ + QVector2D v1 = ep - sp; + QVector2D v2 = p - sp; + return (v2.x() * v1.y()) - (v2.y() * v1.x()); +} + +bool QQuadPath::isPointOnLeft(const QVector2D &p, const QVector2D &sp, const QVector2D &ep) +{ + // Use cross product to compare directions of base vector and vector from start to p + return crossProduct(sp, p, ep) >= 0.0f; +} + +bool QQuadPath::isPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep) +{ + return qFuzzyIsNull(crossProduct(sp, p, ep)); +} + +// Assumes sp != ep +bool QQuadPath::isPointNearLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep) +{ + // epsilon is max length of p-to-baseline relative to length of baseline. So 0.01 means that + // the distance from p to the baseline must be less than 1% of the length of the baseline. + constexpr float epsilon = 0.01f; + QVector2D bv = ep - sp; + float bl2 = QVector2D::dotProduct(bv, bv); + float t = QVector2D::dotProduct(p - sp, bv) / bl2; + QVector2D pv = p - (sp + t * bv); + return (QVector2D::dotProduct(pv, pv) / bl2) < (epsilon * epsilon); +} + +QVector2D QQuadPath::closestPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep) +{ + QVector2D line = ep - sp; + float t = QVector2D::dotProduct(p - sp, line) / QVector2D::dotProduct(line, line); + return sp + qBound(0.0f, t, 1.0f) * line; +} + +// NOTE: it is assumed that subpaths are closed +bool QQuadPath::contains(const QVector2D &point) const +{ + // if (!controlPointRect().contains(pt) : good opt when we add cpr caching + // return false; + + int winding_number = 0; + for (const Element &e : m_elements) { + int dir = 1; + float y1 = e.startPoint().y(); + float y2 = e.endPoint().y(); + if (y2 < y1) { + qSwap(y1, y2); + dir = -1; + } + if (e.m_isLine) { + if (point.y() < y1 || point.y() >= y2 || y1 == y2) + continue; + const float t = (point.y() - e.startPoint().y()) / (e.endPoint().y() - e.startPoint().y()); + const float x = e.startPoint().x() + t * (e.endPoint().x() - e.startPoint().x()); + if (x <= point.x()) + winding_number += dir; + } else { + y1 = qMin(y1, e.controlPoint().y()); + y2 = qMax(y2, e.controlPoint().y()); + if (point.y() < y1 || point.y() >= y2) + continue; + float ts[2]; + const int numRoots = e.intersectionsAtY(point.y(), ts); + // Count if there is exactly one intersection to the left + bool oneHit = false; + float tForHit = -1; + for (int i = 0; i < numRoots; i++) { + if (e.pointAtFraction(ts[i]).x() <= point.x()) { + oneHit = !oneHit; + tForHit = ts[i]; + } + } + if (oneHit) { + dir = e.tangentAtFraction(tForHit).y() < 0 ? -1 : 1; + winding_number += dir; + } + } + }; + + return (fillRule() == Qt::WindingFill ? (winding_number != 0) : ((winding_number % 2) != 0)); +} + +void QQuadPath::addElement(const QVector2D &control, const QVector2D &endPoint, bool isLine) +{ + if (qFuzzyCompare(currentPoint, endPoint)) + return; // 0 length element, skip + + isLine = isLine || isPointNearLine(control, currentPoint, endPoint); // Turn flat quad into line + + m_elements.resize(m_elements.size() + 1); + Element &elem = m_elements.last(); + elem.sp = currentPoint; + elem.cp = isLine ? (0.5f * (currentPoint + endPoint)) : control; + elem.ep = endPoint; + elem.m_isLine = isLine; + elem.m_isSubpathStart = subPathToStart; + subPathToStart = false; + currentPoint = endPoint; +} + +#if !defined(QQUADPATH_CONVEX_CHECK_ERROR_MARGIN) +# define QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN (1.0f / 32.0f) +#endif + +QQuadPath::Element::CurvatureFlags QQuadPath::coordinateOrderOfElement(const QQuadPath::Element &element) const +{ + QVector2D baseLine = element.endPoint() - element.startPoint(); + QVector2D midPoint = element.midPoint(); + // At the midpoint, the tangent of a quad is parallel to the baseline + QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized(); + float delta = qMin(element.extent() / 100, QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN); + QVector2D justRightOfMid = midPoint + (normal * delta); + bool pathContainsPoint = contains(justRightOfMid); + return pathContainsPoint ? Element::FillOnRight : Element::CurvatureFlags(0); +} + +QQuadPath QQuadPath::fromPainterPath(const QPainterPath &path) +{ + QQuadPath res; + res.reserve(path.elementCount()); + res.setFillRule(path.fillRule()); + + QPolygonF quads; + QPointF sp; + for (int i = 0; i < path.elementCount(); ++i) { + QPainterPath::Element element = path.elementAt(i); + + QPointF ep(element); + switch (element.type) { + case QPainterPath::MoveToElement: + res.moveTo(QVector2D(ep)); + break; + case QPainterPath::LineToElement: + res.lineTo(QVector2D(ep)); + break; + case QPainterPath::CurveToElement: { + QPointF cp1 = ep; + QPointF cp2(path.elementAt(++i)); + ep = path.elementAt(++i); + QBezier b = QBezier::fromPoints(sp, cp1, cp2, ep); + qt_toQuadratics(b, &quads); + for (int i = 1; i < quads.size(); i += 2) { + QVector2D cp(quads[i]); + QVector2D ep(quads[i + 1]); + res.quadTo(cp, ep); + } + break; + } + default: + Q_UNREACHABLE(); + break; + } + sp = ep; + } + + return res; +} + +void QQuadPath::addCurvatureData() +{ + // We use the convention that the inside of a curve is on the *right* side of the + // direction of the baseline.Thus, as long as this is true: if the control point is + // on the left side of the baseline, the curve is convex and otherwise it is + // concave. The paths we get can be arbitrary order, but each subpath will have a + // consistent order. Therefore, for the first curve element in a subpath, we can + // determine if the direction already follows the convention or not, and then we + // can easily detect curvature of all subsequent elements in the subpath. + + static bool checkAnomaly = qEnvironmentVariableIntValue("QT_QUICKSHAPES_CHECK_ALL_CURVATURE") != 0; + + Element::CurvatureFlags flags = Element::CurvatureUndetermined; + for (QQuadPath::Element &element : m_elements) { + Q_ASSERT(element.childCount() == 0); + if (element.isSubpathStart()) { + flags = coordinateOrderOfElement(element); + } else if (checkAnomaly) { + Element::CurvatureFlags newFlags = coordinateOrderOfElement(element); + if (flags != newFlags) { + qDebug() << "Curvature anomaly detected:" << element + << "Subpath fill on right:" << (flags & Element::FillOnRight) + << "Element fill on right:" << (newFlags & Element::FillOnRight); + flags = newFlags; + } + } + + if (element.isLine()) { + element.m_curvatureFlags = flags; + // Set the control point to an arbitrary point on the inside side of the line + // (doesn't need to actually be inside the shape: it just makes our calculations + // easier later if it is at the same side as the fill). + const QVector2D &sp = element.sp; + const QVector2D &ep = element.ep; + QVector2D v = ep - sp; + element.cp = flags & Element::FillOnRight ? sp + QVector2D(-v.y(), v.x()) : sp + QVector2D(v.y(), -v.x()); + } else { + bool controlPointOnLeft = element.isControlPointOnLeft(); + bool isFillOnRight = flags & Element::FillOnRight; + bool isConvex = controlPointOnLeft == isFillOnRight; + + if (isConvex) + element.m_curvatureFlags = Element::CurvatureFlags(flags | Element::Convex); + else + element.m_curvatureFlags = flags; + } + } +} + +QRectF QQuadPath::controlPointRect() const +{ + QRectF res; + if (elementCount()) { + QVector2D min, max; + min = max = m_elements.constFirst().sp; + // No need to recurse, as split curve's controlpoints are within the parent curve's + for (const QQuadPath::Element &e : std::as_const(m_elements)) { + min.setX(std::min({ min.x(), e.sp.x(), e.cp.x(), e.ep.x() })); + min.setY(std::min({ min.y(), e.sp.y(), e.cp.y(), e.ep.y() })); + max.setX(std::max({ max.x(), e.sp.x(), e.cp.x(), e.ep.x() })); + max.setY(std::max({ max.y(), e.sp.y(), e.cp.y(), e.ep.y() })); + } + res = QRectF(min.toPointF(), max.toPointF()); + } + return res; +} + +// Count leaf elements +int QQuadPath::elementCountRecursive() const +{ + int count = 0; + iterateElements([&](const QQuadPath::Element &) { count++; }); + return count; +} + +QPainterPath QQuadPath::toPainterPath() const +{ + // Currently only converts the main, unsplit path; no need for the split path identified + QPainterPath res; + res.reserve(elementCount()); + res.setFillRule(fillRule()); + for (const Element &element : m_elements) { + if (element.m_isSubpathStart) + res.moveTo(element.startPoint().toPointF()); + if (element.m_isLine) + res.lineTo(element.endPoint().toPointF()); + else + res.quadTo(element.controlPoint().toPointF(), element.endPoint().toPointF()); + }; + return res; +} + +// Returns a new path since doing it inline would probably be less efficient +// (technically changing it from O(n) to O(n^2)) +// Note that this function should be called before splitting any elements, +// so we can assume that the structure is a list and not a tree +QQuadPath QQuadPath::subPathsClosed() const +{ + Q_ASSERT(m_childElements.isEmpty()); + + QQuadPath res = *this; + res.m_elements = {}; + res.m_elements.reserve(elementCount()); + int subStart = -1; + int prevElement = -1; + for (int i = 0; i < elementCount(); i++) { + const auto &element = m_elements.at(i); + if (element.m_isSubpathStart) { + if (subStart >= 0 && m_elements[i - 1].ep != m_elements[subStart].sp) { + res.currentPoint = m_elements[i - 1].ep; + res.lineTo(m_elements[subStart].sp); + auto &endElement = res.m_elements.last(); + endElement.m_isSubpathEnd = true; + // lineTo() can bail out if the points are too close. + // In that case, just change the end point to be equal to the start + // (No need to test because the assignment is a no-op otherwise). + endElement.ep = m_elements[subStart].sp; + } else if (prevElement >= 0) { + res.m_elements[prevElement].m_isSubpathEnd = true; + } + subStart = i; + } + res.m_elements.append(element); + prevElement = res.m_elements.size() - 1; + } + + if (subStart >= 0 && m_elements.last().ep != m_elements[subStart].sp) { + res.currentPoint = m_elements.last().ep; + res.lineTo(m_elements[subStart].sp); + } + if (!res.m_elements.isEmpty()) { + auto &endElement = res.m_elements.last(); + endElement.m_isSubpathEnd = true; + endElement.ep = m_elements[subStart].sp; + } + + // ### Workaround for triangulator issue: Avoid 3-element paths + if (res.elementCount() == 3) { + res.splitElementAt(2); + res = res.flattened(); + Q_ASSERT(res.elementCount() == 4); + } + + return res; +} + +QQuadPath QQuadPath::flattened() const +{ + QQuadPath res; + res.reserve(elementCountRecursive()); + iterateElements([&](const QQuadPath::Element &element) { res.m_elements.append(element); }); + return res; +} + +class ElementCutter +{ +public: + ElementCutter(const QQuadPath::Element &element) + : m_element(element) + { + m_currentPoint = m_element.startPoint(); + if (m_element.isLine()) + m_lineLength = (m_element.endPoint() - m_element.startPoint()).length(); + else + fillLUT(); + } + + bool consume(float length) + { + m_lastT = m_currentT; + m_lastPoint = m_currentPoint; + float nextCut = m_consumed + length; + float cutT = m_element.isLine() ? nextCut / m_lineLength : tForLength(nextCut); + if (cutT < 1) { + m_currentT = cutT; + m_currentPoint = m_element.pointAtFraction(m_currentT); + m_consumed = nextCut; + return true; + } else { + m_currentT = 1; + m_currentPoint = m_element.endPoint(); + return false; + } + } + + QVector2D currentCutPoint() + { + return m_currentPoint; + } + + QVector2D currentControlPoint() + { + Q_ASSERT(!m_element.isLine()); + // Split curve right at lastT, yields { lastPoint, rcp, endPoint } quad segment + QVector2D rcp = (1 - m_lastT) * m_element.controlPoint() + m_lastT * m_element.endPoint(); + // Split that left at currentT, yields { lastPoint, lcp, currentPoint } quad segment + float segmentT = (m_currentT - m_lastT) / (1 - m_lastT); + QVector2D lcp = (1 - segmentT) * m_lastPoint + segmentT * rcp; + return lcp; + } + + float lastLength() + { + float elemLength = m_element.isLine() ? m_lineLength : m_lut.last(); + return elemLength - m_consumed; + } + +private: + void fillLUT() + { + Q_ASSERT(!m_element.isLine()); + QVector2D ap = m_element.startPoint() - 2 * m_element.controlPoint() + m_element.endPoint(); + QVector2D bp = 2 * m_element.controlPoint() - 2 * m_element.startPoint(); + float A = 4 * QVector2D::dotProduct(ap, ap); + float B = 4 * QVector2D::dotProduct(ap, bp); + float C = QVector2D::dotProduct(bp, bp); + float b = B / (2 * A); + float c = C / A; + float k = c - (b * b); + float l2 = b * std::sqrt(b * b + k); + float lnom = b + std::sqrt(b * b + k); + float l0 = 0.5f * std::sqrt(A); + + m_lut.resize(LUTSize, 0); + for (int i = 1; i < LUTSize; i++) { + float t = float(i) / (LUTSize - 1); + float u = t + b; + float w = std::sqrt(u * u + k); + float l1 = u * w; + float lden = u + w; + float l3 = k * std::log(std::fabs(lden / lnom)); + float res = l0 * (l1 - l2 + l3); + m_lut[i] = res; + } + } + + float tForLength(float length) + { + Q_ASSERT(!m_element.isLine()); + Q_ASSERT(!m_lut.isEmpty()); + + float res = 2; // I.e. invalid, outside [0, 1] range + auto it = std::upper_bound(m_lut.cbegin(), m_lut.cend(), length); + if (it != m_lut.cend()) { + float nextLength = *it--; + float prevLength = *it; + int prevIndex = std::distance(m_lut.cbegin(), it); + float fraction = (length - prevLength) / (nextLength - prevLength); + res = (prevIndex + fraction) / (LUTSize - 1); + } + return res; + } + + const QQuadPath::Element &m_element; + float m_lastT = 0; + float m_currentT = 0; + QVector2D m_lastPoint; + QVector2D m_currentPoint; + float m_consumed = 0; + // For line elements: + float m_lineLength; + // For quadratic curve elements: + static constexpr int LUTSize = 21; + QVarLengthArray m_lut; +}; + +QQuadPath QQuadPath::dashed(qreal lineWidth, const QList &dashPattern, qreal dashOffset) const +{ + QVarLengthArray pattern; + float patternLength = 0; + for (int i = 0; i < 2 * (dashPattern.length() / 2); i++) { + float dashLength = qMax(lineWidth * dashPattern[i], qreal(0)); + pattern.append(dashLength); + patternLength += dashLength; + } + if (patternLength == 0) + return {}; + + int startIndex = 0; + float startOffset = std::fmod(lineWidth * dashOffset, patternLength); + if (startOffset < 0) + startOffset += patternLength; + for (float dashLength : pattern) { + if (dashLength > startOffset) + break; + startIndex++; + startOffset -= dashLength; + } + + int dashIndex = startIndex; + float offset = startOffset; + QQuadPath res; + for (int i = 0; i < elementCount(); i++) { + const Element &element = elementAt(i); + if (element.isSubpathStart()) { + res.moveTo(element.startPoint()); + dashIndex = startIndex; + offset = startOffset; + } + ElementCutter cutter(element); + while (true) { + bool gotAll = cutter.consume(pattern.at(dashIndex) - offset); + QVector2D nextPoint = cutter.currentCutPoint(); + if (dashIndex & 1) + res.moveTo(nextPoint); // gap + else if (element.isLine()) + res.lineTo(nextPoint); // dash in line + else + res.quadTo(cutter.currentControlPoint(), nextPoint); // dash in curve + if (gotAll) { + offset = 0; + dashIndex = (dashIndex + 1) % pattern.size(); + } else { + offset += cutter.lastLength(); + break; + } + } + } + return res; +} + +void QQuadPath::splitElementAt(int index) +{ + const int newChildIndex = m_childElements.size(); + m_childElements.resize(newChildIndex + 2); + Element &parent = elementAt(index); + parent.m_numChildren = 2; + parent.m_firstChildIndex = newChildIndex; + + Element &quad1 = m_childElements[newChildIndex]; + const QVector2D mp = parent.midPoint(); + quad1.sp = parent.sp; + quad1.cp = 0.5f * (parent.sp + parent.cp); + quad1.ep = mp; + quad1.m_isSubpathStart = parent.m_isSubpathStart; + quad1.m_isSubpathEnd = false; + quad1.m_curvatureFlags = parent.m_curvatureFlags; + quad1.m_isLine = parent.m_isLine; //### || isPointNearLine(quad1.cp, quad1.sp, quad1.ep); + + Element &quad2 = m_childElements[newChildIndex + 1]; + quad2.sp = mp; + quad2.cp = 0.5f * (parent.ep + parent.cp); + quad2.ep = parent.ep; + quad2.m_isSubpathStart = false; + quad2.m_isSubpathEnd = parent.m_isSubpathEnd; + quad2.m_curvatureFlags = parent.m_curvatureFlags; + quad2.m_isLine = parent.m_isLine; //### || isPointNearLine(quad2.cp, quad2.sp, quad2.ep); + +#ifndef QT_NO_DEBUG + if (qFuzzyCompare(quad1.sp, quad1.ep) || qFuzzyCompare(quad2.sp, quad2.ep)) + qCDebug(lcSGCurveProcessor) << "Splitting has resulted in ~null quad"; +#endif +} + +static void printElement(QDebug stream, const QQuadPath::Element &element) +{ + auto printPoint = [&](QVector2D p) { stream << "(" << p.x() << ", " << p.y() << ") "; }; + stream << "{ "; + printPoint(element.startPoint()); + printPoint(element.controlPoint()); + printPoint(element.endPoint()); + stream << "} " << (element.isLine() ? "L " : "C ") << (element.isConvex() ? "X " : "O ") + << (element.isSubpathStart() ? "S" : element.isSubpathEnd() ? "E" : ""); +} + +QDebug operator<<(QDebug stream, const QQuadPath::Element &element) +{ + QDebugStateSaver saver(stream); + stream.nospace(); + stream << "QuadPath::Element( "; + printElement(stream, element); + stream << " )"; + return stream; +} + +QDebug operator<<(QDebug stream, const QQuadPath &path) +{ + QDebugStateSaver saver(stream); + stream.nospace(); + stream << "QuadPath(" << path.elementCount() << " main elements, " + << path.elementCountRecursive() << " leaf elements, " + << (path.fillRule() == Qt::OddEvenFill ? "OddEven" : "Winding") << Qt::endl; + int count = 0; + path.iterateElements([&](const QQuadPath::Element &e) { + stream << " " << count++ << (e.isSubpathStart() ? " >" : " "); + printElement(stream, e); + stream << Qt::endl; + }); + stream << ")"; + return stream; +} + +QT_END_NAMESPACE diff --git a/src/quick/scenegraph/util/qquadpath_p.h b/src/quick/scenegraph/util/qquadpath_p.h new file mode 100644 index 0000000000..74589e3ab9 --- /dev/null +++ b/src/quick/scenegraph/util/qquadpath_p.h @@ -0,0 +1,258 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQUADPATH_P_H +#define QQUADPATH_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of a number of Qt sources files. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class Q_QUICK_PRIVATE_EXPORT QQuadPath +{ +public: + class Element + { + public: + Element () + : m_isSubpathStart(false), m_isSubpathEnd(false), m_isLine(false) + { + } + + bool isSubpathStart() const + { + return m_isSubpathStart; + } + + bool isSubpathEnd() const + { + return m_isSubpathEnd; + } + + bool isLine() const + { + return m_isLine; + } + + bool isConvex() const + { + return m_curvatureFlags & Convex; + } + + QVector2D startPoint() const + { + return sp; + } + + QVector2D controlPoint() const + { + return cp; + } + + QVector2D endPoint() const + { + return ep; + } + + QVector2D midPoint() const + { + return isLine() ? 0.5f * (sp + ep) : (0.25f * sp) + (0.5f * cp) + (0.25 * ep); + } + + int childCount() const { return m_numChildren; } + + int indexOfChild(int childNumber) const + { + Q_ASSERT(childNumber >= 0 && childNumber < childCount()); + return -(m_firstChildIndex + 1 + childNumber); + } + + QVector2D pointAtFraction(float t) const; + + QVector2D tangentAtFraction(float t) const + { + return isLine() ? (ep - sp) : ((1 - t) * 2 * (cp - sp)) + (t * 2 * (ep - cp)); + } + + QVector2D normalAtFraction(float t) const + { + const QVector2D tan = tangentAtFraction(t); + return QVector2D(-tan.y(), tan.x()); + } + + float extent() const; + + void setAsConvex(bool isConvex) + { + if (isConvex) + m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags | Element::Convex); + else + m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags & ~Element::Convex); + } + + bool isControlPointOnLeft() const + { + return isPointOnLeft(cp, sp, ep); + } + + private: + int intersectionsAtY(float y, float *fractions) const; + + enum CurvatureFlags : quint8 { + CurvatureUndetermined = 0, + FillOnRight = 1, + Convex = 2 + }; + + QVector2D sp; + QVector2D cp; + QVector2D ep; + int m_firstChildIndex = 0; + quint8 m_numChildren = 0; + CurvatureFlags m_curvatureFlags = CurvatureUndetermined; + quint8 m_isSubpathStart : 1; + quint8 m_isSubpathEnd : 1; + quint8 m_isLine : 1; + friend class QQuadPath; + friend QDebug operator<<(QDebug, const QQuadPath::Element &); + }; + + void moveTo(const QVector2D &to) + { + subPathToStart = true; + currentPoint = to; + } + + void lineTo(const QVector2D &to) + { + addElement({}, to, true); + } + + void quadTo(const QVector2D &control, const QVector2D &to) + { + addElement(control, to); + } + + Element &elementAt(int i) + { + return i < 0 ? m_childElements[-(i + 1)] : m_elements[i]; + } + + const Element &elementAt(int i) const + { + return i < 0 ? m_childElements[-(i + 1)] : m_elements[i]; + } + + int indexOfChildAt(int i, int childNumber) const + { + return elementAt(i).indexOfChild(childNumber); + } + + QRectF controlPointRect() const; + + Qt::FillRule fillRule() const { return m_fillRule; } + void setFillRule(Qt::FillRule rule) { m_fillRule = rule; } + + void reserve(int size) { m_elements.reserve(size); } + int elementCount() const { return m_elements.size(); } + bool isEmpty() const { return m_elements.size() == 0; } + int elementCountRecursive() const; + + static QQuadPath fromPainterPath(const QPainterPath &path); + QPainterPath toPainterPath() const; + + QQuadPath subPathsClosed() const; + void addCurvatureData(); + QQuadPath flattened() const; + QQuadPath dashed(qreal lineWidth, const QList &dashPattern, qreal dashOffset = 0) const; + void splitElementAt(int index); + bool contains(const QVector2D &point) const; + + template + void iterateChildrenOf(Element &e, Func &&lambda) + { + const int lastChildIndex = e.m_firstChildIndex + e.childCount() - 1; + for (int i = e.m_firstChildIndex; i <= lastChildIndex; i++) { + Element &c = m_childElements[i]; + if (c.childCount() > 0) + iterateChildrenOf(c, lambda); + else + lambda(c); + } + } + + template + void iterateChildrenOf(const Element &e, Func &&lambda) const + { + const int lastChildIndex = e.m_firstChildIndex + e.childCount() - 1; + for (int i = e.m_firstChildIndex; i <= lastChildIndex; i++) { + const Element &c = m_childElements[i]; + if (c.childCount() > 0) + iterateChildrenOf(c, lambda); + else + lambda(c); + } + } + + template + void iterateElements(Func &&lambda) + { + for (auto &e : m_elements) { + if (e.childCount() > 0) + iterateChildrenOf(e, lambda); + else + lambda(e); + } + } + + template + void iterateElements(Func &&lambda) const + { + for (auto &e : m_elements) { + if (e.childCount() > 0) + iterateChildrenOf(e, lambda); + else + lambda(e); + } + } + + static QVector2D closestPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep); + static bool isPointOnLeft(const QVector2D &p, const QVector2D &sp, const QVector2D &ep); + static bool isPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep); + static bool isPointNearLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep); + +private: + void addElement(const QVector2D &control, const QVector2D &to, bool isLine = false); + Element::CurvatureFlags coordinateOrderOfElement(const Element &element) const; + + friend QDebug operator<<(QDebug, const QQuadPath &); + + bool subPathToStart = true; + Qt::FillRule m_fillRule = Qt::OddEvenFill; + QVector2D currentPoint; + QList m_elements; + QList m_childElements; +}; + +QDebug operator<<(QDebug, const QQuadPath::Element &); +QDebug operator<<(QDebug, const QQuadPath &); + +QT_END_NAMESPACE + +#endif diff --git a/src/quick/scenegraph/util/qsggradientcache.cpp b/src/quick/scenegraph/util/qsggradientcache.cpp new file mode 100644 index 0000000000..ae88cd2e3e --- /dev/null +++ b/src/quick/scenegraph/util/qsggradientcache.cpp @@ -0,0 +1,121 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsggradientcache_p.h" + +#include +#include + +#include +#include + +QT_BEGIN_NAMESPACE + +static void generateGradientColorTable(const QSGGradientCacheKey &gradient, + uint *colorTable, int size, float opacity) +{ + int pos = 0; + const QGradientStops &s = gradient.stops; + Q_ASSERT(!s.isEmpty()); + const bool colorInterpolation = true; + + uint alpha = qRound(opacity * 256); + uint current_color = ARGB_COMBINE_ALPHA(s[0].second.rgba(), alpha); + qreal incr = 1.0 / qreal(size); + qreal fpos = 1.5 * incr; + colorTable[pos++] = ARGB2RGBA(qPremultiply(current_color)); + + while (fpos <= s.first().first) { + colorTable[pos] = colorTable[pos - 1]; + pos++; + fpos += incr; + } + + if (colorInterpolation) + current_color = qPremultiply(current_color); + + const int sLast = s.size() - 1; + for (int i = 0; i < sLast; ++i) { + qreal delta = 1/(s[i+1].first - s[i].first); + uint next_color = ARGB_COMBINE_ALPHA(s[i + 1].second.rgba(), alpha); + if (colorInterpolation) + next_color = qPremultiply(next_color); + + while (fpos < s[i+1].first && pos < size) { + int dist = int(256 * ((fpos - s[i].first) * delta)); + int idist = 256 - dist; + if (colorInterpolation) + colorTable[pos] = ARGB2RGBA(INTERPOLATE_PIXEL_256(current_color, idist, next_color, dist)); + else + colorTable[pos] = ARGB2RGBA(qPremultiply(INTERPOLATE_PIXEL_256(current_color, idist, next_color, dist))); + ++pos; + fpos += incr; + } + current_color = next_color; + } + + uint last_color = ARGB2RGBA(qPremultiply(ARGB_COMBINE_ALPHA(s[sLast].second.rgba(), alpha))); + for ( ; pos < size; ++pos) + colorTable[pos] = last_color; + + colorTable[size-1] = last_color; +} + +QSGGradientCache::~QSGGradientCache() +{ + qDeleteAll(m_textures); +} + +QSGGradientCache *QSGGradientCache::cacheForRhi(QRhi *rhi) +{ + static QHash caches; + auto it = caches.constFind(rhi); + if (it != caches.constEnd()) + return *it; + + QSGGradientCache *cache = new QSGGradientCache; + rhi->addCleanupCallback([cache](QRhi *rhi) { + caches.remove(rhi); + delete cache; + }); + caches.insert(rhi, cache); + return cache; +} + +QSGTexture *QSGGradientCache::get(const QSGGradientCacheKey &grad) +{ + QSGPlainTexture *tx = m_textures[grad]; + if (!tx) { + static const int W = 1024; // texture size is 1024x1 + QImage gradTab(W, 1, QImage::Format_RGBA8888_Premultiplied); + if (!grad.stops.isEmpty()) + generateGradientColorTable(grad, reinterpret_cast(gradTab.bits()), W, 1.0f); + else + gradTab.fill(Qt::black); + tx = new QSGPlainTexture; + tx->setImage(gradTab); + switch (grad.spread) { + case QGradient::PadSpread: + tx->setHorizontalWrapMode(QSGTexture::ClampToEdge); + tx->setVerticalWrapMode(QSGTexture::ClampToEdge); + break; + case QGradient::RepeatSpread: + tx->setHorizontalWrapMode(QSGTexture::Repeat); + tx->setVerticalWrapMode(QSGTexture::Repeat); + break; + case QGradient::ReflectSpread: + tx->setHorizontalWrapMode(QSGTexture::MirroredRepeat); + tx->setVerticalWrapMode(QSGTexture::MirroredRepeat); + break; + default: + qWarning("Unknown gradient spread mode %d", grad.spread); + break; + } + tx->setFiltering(QSGTexture::Linear); + m_textures[grad] = tx; + } + return tx; +} + + +QT_END_NAMESPACE diff --git a/src/quick/scenegraph/util/qsggradientcache_p.h b/src/quick/scenegraph/util/qsggradientcache_p.h new file mode 100644 index 0000000000..ade19fcf85 --- /dev/null +++ b/src/quick/scenegraph/util/qsggradientcache_p.h @@ -0,0 +1,72 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSGGRADIENTCACHE_P_H +#define QSGGRADIENTCACHE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of a number of Qt sources files. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +class QSGTexture; +class QSGPlainTexture; +class QRhi; + +struct Q_QUICK_PRIVATE_EXPORT QSGGradientCacheKey +{ + QSGGradientCacheKey(const QGradientStops &stops, QGradient::Spread spread) + : stops(stops), spread(spread) + { } + QGradientStops stops; + QGradient::Spread spread; + bool operator==(const QSGGradientCacheKey &other) const + { + return spread == other.spread && stops == other.stops; + } +}; + +inline size_t qHash(const QSGGradientCacheKey &v, size_t seed = 0) +{ + size_t h = seed + v.spread; + for (int i = 0; i < 3 && i < v.stops.size(); ++i) + h += v.stops[i].second.rgba(); + return h; +} + +class Q_QUICK_PRIVATE_EXPORT QSGGradientCache +{ +public: + struct GradientDesc { // can fully describe a linear/radial/conical gradient + QGradientStops stops; + QGradient::Spread spread = QGradient::PadSpread; + QPointF a; // start (L) or center point (R/C) + QPointF b; // end (L) or focal point (R) + qreal v0; // center radius (R) or start angle (C) + qreal v1; // focal radius (R) + }; + + ~QSGGradientCache(); + static QSGGradientCache *cacheForRhi(QRhi *rhi); + QSGTexture *get(const QSGGradientCacheKey &grad); + +private: + QHash m_textures; +}; + +QT_END_NAMESPACE + +#endif // QSGGRADIENTCACHE_P_H diff --git a/src/quick/scenegraph/util/qsgtextnode.cpp b/src/quick/scenegraph/util/qsgtextnode.cpp index c04666cf0d..3de7f8a703 100644 --- a/src/quick/scenegraph/util/qsgtextnode.cpp +++ b/src/quick/scenegraph/util/qsgtextnode.cpp @@ -44,12 +44,20 @@ QT_BEGIN_NAMESPACE \value QtRendering Text is rendered using a scalable distance field for each glyph. \value NativeRendering Text is rendered using a platform-specific technique. + \value CurveRendering Text is rendered using a curve rasterizer running directly on the + graphics hardware. Select \c NativeRendering if you prefer text to look native on the target platform and do not require advanced features such as transformation of the text. Using such features in combination with the NativeRendering render type will lend poor and sometimes pixelated results. + Both \c Text.QtRendering and \c Text.CurveRendering are hardware-accelerated techniques. + \c QtRendering is the faster of the two, but uses more memory and will exhibit rendering + artifacts at large sizes. \c CurveRendering should be considered as an alternative in cases + where \c QtRendering does not give good visual results or where reducing graphics memory + consumption is a priority. + \sa setRenderType(), setRenderTypeQuality() */ diff --git a/src/quick/scenegraph/util/qsgtextnode.h b/src/quick/scenegraph/util/qsgtextnode.h index 4df98f0af2..92f8385ce4 100644 --- a/src/quick/scenegraph/util/qsgtextnode.h +++ b/src/quick/scenegraph/util/qsgtextnode.h @@ -25,7 +25,8 @@ public: enum RenderType { QtRendering, - NativeRendering + NativeRendering, + CurveRendering }; ~QSGTextNode() override = default; diff --git a/src/quickshapes/CMakeLists.txt b/src/quickshapes/CMakeLists.txt index c1f7886c93..27bf3d4e5b 100644 --- a/src/quickshapes/CMakeLists.txt +++ b/src/quickshapes/CMakeLists.txt @@ -21,10 +21,6 @@ qt_internal_add_qml_module(QuickShapesPrivate qquickshapegenericrenderer.cpp qquickshapegenericrenderer_p.h qquickshapesglobal.h qquickshapesglobal_p.h qquickshapecurverenderer.cpp qquickshapecurverenderer_p.h qquickshapecurverenderer_p_p.h - qquickshapecurvenode.cpp qquickshapecurvenode_p.h qquickshapecurvenode_p_p.h qquickshapecurvenode_p.cpp - qquickshapeabstractcurvenode_p.h - qquickshapestrokenode.cpp qquickshapestrokenode_p.h qquickshapestrokenode_p_p.h qquickshapestrokenode_p.cpp - qquadpath_p.h qquadpath.cpp qquickshapesoftwarerenderer.cpp qquickshapesoftwarerenderer_p.h PUBLIC_LIBRARIES Qt::Core @@ -56,132 +52,7 @@ qt_internal_add_shaders(QuickShapesPrivate "qtquickshapes_shaders" "shaders_ng/radialgradient.frag" "shaders_ng/conicalgradient.vert" "shaders_ng/conicalgradient.frag" - "shaders_ng/shapecurve.frag" - "shaders_ng/shapecurve.vert" - "shaders_ng/shapestroke.frag" - "shaders_ng/shapestroke.vert" "shaders_ng/wireframe.frag" "shaders_ng/wireframe.vert" ) -qt_internal_add_shaders(QuickShapesPrivate "qtquickshapes_shaders_derivatives" - SILENT - BATCHABLE - PRECOMPILE - OPTIMIZED - DEFINES "USE_DERIVATIVES" - PREFIX - "/qt-project.org/shapes" - FILES - "shaders_ng/shapecurve.frag" - "shaders_ng/shapecurve.vert" - OUTPUTS - "shaders_ng/shapecurve_derivatives.frag.qsb" - "shaders_ng/shapecurve_derivatives.vert.qsb" -) - -qt_internal_add_shaders(QuickShapesPrivate "qtquickshapes_shaders_lg" - SILENT - BATCHABLE - PRECOMPILE - OPTIMIZED - DEFINES - "LINEARGRADIENT" - PREFIX - "/qt-project.org/shapes" - FILES - "shaders_ng/shapecurve.frag" - "shaders_ng/shapecurve.vert" - OUTPUTS - "shaders_ng/shapecurve_lg.frag.qsb" - "shaders_ng/shapecurve_lg.vert.qsb" -) - -qt_internal_add_shaders(QuickShapesPrivate "qtquickshapes_shaders_lg_derivatives" - SILENT - BATCHABLE - PRECOMPILE - OPTIMIZED - DEFINES - "LINEARGRADIENT" - "USE_DERIVATIVES" - PREFIX - "/qt-project.org/shapes" - FILES - "shaders_ng/shapecurve.frag" - "shaders_ng/shapecurve.vert" - OUTPUTS - "shaders_ng/shapecurve_lg_derivatives.frag.qsb" - "shaders_ng/shapecurve_lg_derivatives.vert.qsb" -) - -qt_internal_add_shaders(QuickShapesPrivate "qtquickshapes_shaders_rg" - SILENT - BATCHABLE - PRECOMPILE - OPTIMIZED - DEFINES - "RADIALGRADIENT" - PREFIX - "/qt-project.org/shapes" - FILES - "shaders_ng/shapecurve.frag" - "shaders_ng/shapecurve.vert" - OUTPUTS - "shaders_ng/shapecurve_rg.frag.qsb" - "shaders_ng/shapecurve_rg.vert.qsb" -) - -qt_internal_add_shaders(QuickShapesPrivate "qtquickshapes_shaders_rg_derivatives" - SILENT - BATCHABLE - PRECOMPILE - OPTIMIZED - DEFINES - "RADIALGRADIENT" - "USE_DERIVATIVES" - PREFIX - "/qt-project.org/shapes" - FILES - "shaders_ng/shapecurve.frag" - "shaders_ng/shapecurve.vert" - OUTPUTS - "shaders_ng/shapecurve_rg_derivatives.frag.qsb" - "shaders_ng/shapecurve_rg_derivatives.vert.qsb" -) - -qt_internal_add_shaders(QuickShapesPrivate "qtquickshapes_shaders_cg" - SILENT - BATCHABLE - PRECOMPILE - OPTIMIZED - DEFINES - "CONICALGRADIENT" - PREFIX - "/qt-project.org/shapes" - FILES - "shaders_ng/shapecurve.frag" - "shaders_ng/shapecurve.vert" - OUTPUTS - "shaders_ng/shapecurve_cg.frag.qsb" - "shaders_ng/shapecurve_cg.vert.qsb" -) - -qt_internal_add_shaders(QuickShapesPrivate "qtquickshapes_shaders_cg_derivatives" - SILENT - BATCHABLE - PRECOMPILE - OPTIMIZED - DEFINES - "CONICALGRADIENT" - "USE_DERIVATIVES" - PREFIX - "/qt-project.org/shapes" - FILES - "shaders_ng/shapecurve.frag" - "shaders_ng/shapecurve.vert" - OUTPUTS - "shaders_ng/shapecurve_cg_derivatives.frag.qsb" - "shaders_ng/shapecurve_cg_derivatives.vert.qsb" -) - diff --git a/src/quickshapes/qquadpath.cpp b/src/quickshapes/qquadpath.cpp deleted file mode 100644 index 7c3be8469d..0000000000 --- a/src/quickshapes/qquadpath.cpp +++ /dev/null @@ -1,800 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#include "qquadpath_p.h" -#include "qquickshapecurverenderer_p_p.h" -#include -#include -#include - -QT_BEGIN_NAMESPACE - -static qreal qt_scoreQuadratic(const QBezier &b, QPointF qcp) -{ - static bool init = false; - const int numSteps = 21; - Q_STATIC_ASSERT(numSteps % 2 == 1); // numTries must be odd - static qreal t2s[numSteps]; - static qreal tmts[numSteps]; - if (!init) { - // Precompute bezier factors - qreal t = 0.20; - const qreal step = (1 - (2 * t)) / (numSteps - 1); - for (int i = 0; i < numSteps; i++) { - t2s[i] = t * t; - tmts[i] = 2 * t * (1 - t); - t += step; - } - init = true; - } - - const QPointF midPoint = b.midPoint(); - auto distForIndex = [&](int i) -> qreal { - QPointF qp = (t2s[numSteps - 1 - i] * b.pt1()) + (tmts[i] * qcp) + (t2s[i] * b.pt4()); - QPointF d = midPoint - qp; - return QPointF::dotProduct(d, d); - }; - - const int halfSteps = (numSteps - 1) / 2; - bool foundIt = false; - const qreal centerDist = distForIndex(halfSteps); - qreal minDist = centerDist; - // Search for the minimum in right half - for (int i = 0; i < halfSteps; i++) { - qreal tDist = distForIndex(halfSteps + 1 + i); - if (tDist < minDist) { - minDist = tDist; - } else { - foundIt = (i > 0); - break; - } - } - if (!foundIt) { - // Search in left half - minDist = centerDist; - for (int i = 0; i < halfSteps; i++) { - qreal tDist = distForIndex(halfSteps - 1 - i); - if (tDist < minDist) { - minDist = tDist; - } else { - foundIt = (i > 0); - break; - } - } - } - return foundIt ? minDist : centerDist; -} - -static QPointF qt_quadraticForCubic(const QBezier &b) -{ - const QLineF st = b.startTangent(); - const QLineF et = b.endTangent(); - const QPointF midPoint = b.midPoint(); - bool valid = true; - QPointF quadControlPoint; - if (st.intersects(et, &quadControlPoint) == QLineF::NoIntersection) { - valid = false; - } else { - // Check if intersection is on wrong side - const QPointF bl = b.pt4() - b.pt1(); - const QPointF ml = midPoint - b.pt1(); - const QPointF ql = quadControlPoint - b.pt1(); - qreal cx1 = (ml.x() * bl.y()) - (ml.y() * bl.x()); - qreal cx2 = (ql.x() * bl.y()) - (ql.y() * bl.x()); - valid = (std::signbit(cx1) == std::signbit(cx2)); - } - return valid ? quadControlPoint : midPoint; -} - -static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints) -{ - auto isValidRoot = [](qreal r) { - return qIsFinite(r) && (r > 0) && (!qFuzzyIsNull(float(r))) && (r < 1) - && (!qFuzzyIsNull(float(r - 1))); - }; - - // normalize so pt1.x,pt1.y,pt4.y == 0 - QTransform xf; - const QLineF l(orig.pt1(), orig.pt4()); - xf.rotate(l.angle()); - xf.translate(-orig.pt1().x(), -orig.pt1().y()); - const QBezier n = orig.mapBy(xf); - Q_ASSERT(n.pt1() == QPoint() && qFuzzyIsNull(float(n.pt4().y()))); - - const qreal x2 = n.pt2().x(); - const qreal x3 = n.pt3().x(); - const qreal x4 = n.pt4().x(); - const qreal y2 = n.pt2().y(); - const qreal y3 = n.pt3().y(); - - const qreal p = x3 * y2; - const qreal q = x4 * y2; - const qreal r = x2 * y3; - const qreal s = x4 * y3; - - const qreal a = 18 * ((-3 * p) + (2 * q) + (3 * r) - s); - if (qFuzzyIsNull(float(a))) { - if (std::signbit(y2) != std::signbit(y3) && qFuzzyCompare(float(x4 - x3), float(x2))) { - tpoints[0] = 0.5; // approx - return 1; - } else if (!a) { - return 0; - } - } - const qreal b = 18 * (((3 * p) - q) - (3 * r)); - const qreal c = 18 * (r - p); - const qreal rad = (b * b) - (4 * a * c); - if (rad < 0) - return 0; - const qreal sqr = qSqrt(rad); - const qreal root1 = (-b + sqr) / (2 * a); - const qreal root2 = (-b - sqr) / (2 * a); - - int res = 0; - if (isValidRoot(root1)) - tpoints[res++] = root1; - if (root2 != root1 && isValidRoot(root2)) - tpoints[res++] = root2; - - if (res == 2 && tpoints[0] > tpoints[1]) - qSwap(tpoints[0], tpoints[1]); - - return res; -} - -static void qt_addToQuadratics(const QBezier &b, QPolygonF *p, int maxSplits, qreal maxDiff) -{ - QPointF qcp = qt_quadraticForCubic(b); - if (maxSplits <= 0 || qt_scoreQuadratic(b, qcp) < maxDiff) { - p->append(qcp); - p->append(b.pt4()); - } else { - QBezier rhs = b; - QBezier lhs; - rhs.parameterSplitLeft(0.5, &lhs); - qt_addToQuadratics(lhs, p, maxSplits - 1, maxDiff); - qt_addToQuadratics(rhs, p, maxSplits - 1, maxDiff); - } -} - -static void qt_toQuadratics(const QBezier &b, QPolygonF *out, qreal errorLimit = 0.01) -{ - out->resize(0); - out->append(b.pt1()); - - { - // Shortcut if the cubic is really a quadratic - const qreal f = 3.0 / 2.0; - const QPointF c1 = b.pt1() + f * (b.pt2() - b.pt1()); - const QPointF c2 = b.pt4() + f * (b.pt3() - b.pt4()); - if (c1 == c2) { - out->append(c1); - out->append(b.pt4()); - return; - } - } - - const QRectF cpr = b.bounds(); - const QPointF dim = cpr.bottomRight() - cpr.topLeft(); - qreal maxDiff = QPointF::dotProduct(dim, dim) * errorLimit * errorLimit; // maxdistance^2 - - qreal infPoints[2]; - int numInfPoints = qt_getInflectionPoints(b, infPoints); - const int maxSubSplits = numInfPoints > 0 ? 2 : 3; - qreal t0 = 0; - // number of main segments == #inflectionpoints + 1 - for (int i = 0; i < numInfPoints + 1; i++) { - qreal t1 = (i < numInfPoints) ? infPoints[i] : 1; - QBezier segment = b.bezierOnInterval(t0, t1); - qt_addToQuadratics(segment, out, maxSubSplits, maxDiff); - t0 = t1; - } -} - -QVector2D QQuadPath::Element::pointAtFraction(float t) const -{ - if (isLine()) { - return sp + t * (ep - sp); - } else { - const float r = 1 - t; - return (r * r * sp) + (2 * t * r * cp) + (t * t * ep); - } -} - -float QQuadPath::Element::extent() const -{ - // TBD: cache this value if we start using it a lot - QVector2D min(qMin(sp.x(), ep.x()), qMin(sp.y(), ep.y())); - QVector2D max(qMax(sp.x(), ep.x()), qMax(sp.y(), ep.y())); - if (!isLine()) { - min = QVector2D(qMin(min.x(), cp.x()), qMin(min.y(), cp.y())); - max = QVector2D(qMax(max.x(), cp.x()), qMax(max.y(), cp.y())); - } - return (max - min).length(); -} - -// Returns the number of intersections between element and a horizontal line at y. -// The t values of max 2 intersection(s) are stored in the fractions array -int QQuadPath::Element::intersectionsAtY(float y, float *fractions) const -{ - const float y0 = startPoint().y() - y; - const float y1 = controlPoint().y() - y; - const float y2 = endPoint().y() - y; - - int numRoots = 0; - const float a = y0 - (2 * y1) + y2; - if (a) { - const float b = (y1 * y1) - (y0 * y2); - if (b >= 0) { - const float sqr = qSqrt(b); - const float root1 = -(-y0 + y1 + sqr) / a; - if (qIsFinite(root1) && root1 >= 0 && root1 <= 1) - fractions[numRoots++] = root1; - const float root2 = (y0 - y1 + sqr) / a; - if (qIsFinite(root2) && root2 != root1 && root2 >= 0 && root2 <= 1) - fractions[numRoots++] = root2; - } - } else if (y1 != y2) { - const float root1 = (y2 - (2 * y1)) / (2 * (y2 - y1)); - if (qIsFinite(root1) && root1 >= 0 && root1 <= 1) - fractions[numRoots++] = root1; - } - - return numRoots; -} - -static float crossProduct(const QVector2D &sp, const QVector2D &p, const QVector2D &ep) -{ - QVector2D v1 = ep - sp; - QVector2D v2 = p - sp; - return (v2.x() * v1.y()) - (v2.y() * v1.x()); -} - -bool QQuadPath::isPointOnLeft(const QVector2D &p, const QVector2D &sp, const QVector2D &ep) -{ - // Use cross product to compare directions of base vector and vector from start to p - return crossProduct(sp, p, ep) >= 0.0f; -} - -bool QQuadPath::isPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep) -{ - return qFuzzyIsNull(crossProduct(sp, p, ep)); -} - -// Assumes sp != ep -bool QQuadPath::isPointNearLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep) -{ - // epsilon is max length of p-to-baseline relative to length of baseline. So 0.01 means that - // the distance from p to the baseline must be less than 1% of the length of the baseline. - constexpr float epsilon = 0.01f; - QVector2D bv = ep - sp; - float bl2 = QVector2D::dotProduct(bv, bv); - float t = QVector2D::dotProduct(p - sp, bv) / bl2; - QVector2D pv = p - (sp + t * bv); - return (QVector2D::dotProduct(pv, pv) / bl2) < (epsilon * epsilon); -} - -QVector2D QQuadPath::closestPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep) -{ - QVector2D line = ep - sp; - float t = QVector2D::dotProduct(p - sp, line) / QVector2D::dotProduct(line, line); - return sp + qBound(0.0f, t, 1.0f) * line; -} - -// NOTE: it is assumed that subpaths are closed -bool QQuadPath::contains(const QVector2D &point) const -{ - // if (!controlPointRect().contains(pt) : good opt when we add cpr caching - // return false; - - int winding_number = 0; - for (const Element &e : m_elements) { - int dir = 1; - float y1 = e.startPoint().y(); - float y2 = e.endPoint().y(); - if (y2 < y1) { - qSwap(y1, y2); - dir = -1; - } - if (e.m_isLine) { - if (point.y() < y1 || point.y() >= y2 || y1 == y2) - continue; - const float t = (point.y() - e.startPoint().y()) / (e.endPoint().y() - e.startPoint().y()); - const float x = e.startPoint().x() + t * (e.endPoint().x() - e.startPoint().x()); - if (x <= point.x()) - winding_number += dir; - } else { - y1 = qMin(y1, e.controlPoint().y()); - y2 = qMax(y2, e.controlPoint().y()); - if (point.y() < y1 || point.y() >= y2) - continue; - float ts[2]; - const int numRoots = e.intersectionsAtY(point.y(), ts); - // Count if there is exactly one intersection to the left - bool oneHit = false; - float tForHit = -1; - for (int i = 0; i < numRoots; i++) { - if (e.pointAtFraction(ts[i]).x() <= point.x()) { - oneHit = !oneHit; - tForHit = ts[i]; - } - } - if (oneHit) { - dir = e.tangentAtFraction(tForHit).y() < 0 ? -1 : 1; - winding_number += dir; - } - } - }; - - return (fillRule() == Qt::WindingFill ? (winding_number != 0) : ((winding_number % 2) != 0)); -} - -void QQuadPath::addElement(const QVector2D &control, const QVector2D &endPoint, bool isLine) -{ - if (qFuzzyCompare(currentPoint, endPoint)) - return; // 0 length element, skip - - isLine = isLine || isPointNearLine(control, currentPoint, endPoint); // Turn flat quad into line - - m_elements.resize(m_elements.size() + 1); - Element &elem = m_elements.last(); - elem.sp = currentPoint; - elem.cp = isLine ? (0.5f * (currentPoint + endPoint)) : control; - elem.ep = endPoint; - elem.m_isLine = isLine; - elem.m_isSubpathStart = subPathToStart; - subPathToStart = false; - currentPoint = endPoint; -} - -#if !defined(QQUADPATH_CONVEX_CHECK_ERROR_MARGIN) -# define QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN (1.0f / 32.0f) -#endif - -QQuadPath::Element::CurvatureFlags QQuadPath::coordinateOrderOfElement(const QQuadPath::Element &element) const -{ - QVector2D baseLine = element.endPoint() - element.startPoint(); - QVector2D midPoint = element.midPoint(); - // At the midpoint, the tangent of a quad is parallel to the baseline - QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized(); - float delta = qMin(element.extent() / 100, QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN); - QVector2D justRightOfMid = midPoint + (normal * delta); - bool pathContainsPoint = contains(justRightOfMid); - return pathContainsPoint ? Element::FillOnRight : Element::CurvatureFlags(0); -} - -QQuadPath QQuadPath::fromPainterPath(const QPainterPath &path) -{ - QQuadPath res; - res.reserve(path.elementCount()); - res.setFillRule(path.fillRule()); - - QPolygonF quads; - QPointF sp; - for (int i = 0; i < path.elementCount(); ++i) { - QPainterPath::Element element = path.elementAt(i); - - QPointF ep(element); - switch (element.type) { - case QPainterPath::MoveToElement: - res.moveTo(QVector2D(ep)); - break; - case QPainterPath::LineToElement: - res.lineTo(QVector2D(ep)); - break; - case QPainterPath::CurveToElement: { - QPointF cp1 = ep; - QPointF cp2(path.elementAt(++i)); - ep = path.elementAt(++i); - QBezier b = QBezier::fromPoints(sp, cp1, cp2, ep); - qt_toQuadratics(b, &quads); - for (int i = 1; i < quads.size(); i += 2) { - QVector2D cp(quads[i]); - QVector2D ep(quads[i + 1]); - res.quadTo(cp, ep); - } - break; - } - default: - Q_UNREACHABLE(); - break; - } - sp = ep; - } - - return res; -} - -void QQuadPath::addCurvatureData() -{ - // We use the convention that the inside of a curve is on the *right* side of the - // direction of the baseline.Thus, as long as this is true: if the control point is - // on the left side of the baseline, the curve is convex and otherwise it is - // concave. The paths we get can be arbitrary order, but each subpath will have a - // consistent order. Therefore, for the first curve element in a subpath, we can - // determine if the direction already follows the convention or not, and then we - // can easily detect curvature of all subsequent elements in the subpath. - - static bool checkAnomaly = qEnvironmentVariableIntValue("QT_QUICKSHAPES_CHECK_ALL_CURVATURE") != 0; - - Element::CurvatureFlags flags = Element::CurvatureUndetermined; - for (QQuadPath::Element &element : m_elements) { - Q_ASSERT(element.childCount() == 0); - if (element.isSubpathStart()) { - flags = coordinateOrderOfElement(element); - } else if (checkAnomaly) { - Element::CurvatureFlags newFlags = coordinateOrderOfElement(element); - if (flags != newFlags) { - qDebug() << "Curvature anomaly detected:" << element - << "Subpath fill on right:" << (flags & Element::FillOnRight) - << "Element fill on right:" << (newFlags & Element::FillOnRight); - flags = newFlags; - } - } - - if (element.isLine()) { - element.m_curvatureFlags = flags; - // Set the control point to an arbitrary point on the inside side of the line - // (doesn't need to actually be inside the shape: it just makes our calculations - // easier later if it is at the same side as the fill). - const QVector2D &sp = element.sp; - const QVector2D &ep = element.ep; - QVector2D v = ep - sp; - element.cp = flags & Element::FillOnRight ? sp + QVector2D(-v.y(), v.x()) : sp + QVector2D(v.y(), -v.x()); - } else { - bool controlPointOnLeft = element.isControlPointOnLeft(); - bool isFillOnRight = flags & Element::FillOnRight; - bool isConvex = controlPointOnLeft == isFillOnRight; - - if (isConvex) - element.m_curvatureFlags = Element::CurvatureFlags(flags | Element::Convex); - else - element.m_curvatureFlags = flags; - } - } -} - -QRectF QQuadPath::controlPointRect() const -{ - QRectF res; - if (elementCount()) { - QVector2D min, max; - min = max = m_elements.constFirst().sp; - // No need to recurse, as split curve's controlpoints are within the parent curve's - for (const QQuadPath::Element &e : std::as_const(m_elements)) { - min.setX(std::min({ min.x(), e.sp.x(), e.cp.x(), e.ep.x() })); - min.setY(std::min({ min.y(), e.sp.y(), e.cp.y(), e.ep.y() })); - max.setX(std::max({ max.x(), e.sp.x(), e.cp.x(), e.ep.x() })); - max.setY(std::max({ max.y(), e.sp.y(), e.cp.y(), e.ep.y() })); - } - res = QRectF(min.toPointF(), max.toPointF()); - } - return res; -} - -// Count leaf elements -int QQuadPath::elementCountRecursive() const -{ - int count = 0; - iterateElements([&](const QQuadPath::Element &) { count++; }); - return count; -} - -QPainterPath QQuadPath::toPainterPath() const -{ - // Currently only converts the main, unsplit path; no need for the split path identified - QPainterPath res; - res.reserve(elementCount()); - res.setFillRule(fillRule()); - for (const Element &element : m_elements) { - if (element.m_isSubpathStart) - res.moveTo(element.startPoint().toPointF()); - if (element.m_isLine) - res.lineTo(element.endPoint().toPointF()); - else - res.quadTo(element.controlPoint().toPointF(), element.endPoint().toPointF()); - }; - return res; -} - -// Returns a new path since doing it inline would probably be less efficient -// (technically changing it from O(n) to O(n^2)) -// Note that this function should be called before splitting any elements, -// so we can assume that the structure is a list and not a tree -QQuadPath QQuadPath::subPathsClosed() const -{ - Q_ASSERT(m_childElements.isEmpty()); - - QQuadPath res = *this; - res.m_elements = {}; - res.m_elements.reserve(elementCount()); - int subStart = -1; - int prevElement = -1; - for (int i = 0; i < elementCount(); i++) { - const auto &element = m_elements.at(i); - if (element.m_isSubpathStart) { - if (subStart >= 0 && m_elements[i - 1].ep != m_elements[subStart].sp) { - res.currentPoint = m_elements[i - 1].ep; - res.lineTo(m_elements[subStart].sp); - auto &endElement = res.m_elements.last(); - endElement.m_isSubpathEnd = true; - // lineTo() can bail out if the points are too close. - // In that case, just change the end point to be equal to the start - // (No need to test because the assignment is a no-op otherwise). - endElement.ep = m_elements[subStart].sp; - } else if (prevElement >= 0) { - res.m_elements[prevElement].m_isSubpathEnd = true; - } - subStart = i; - } - res.m_elements.append(element); - prevElement = res.m_elements.size() - 1; - } - - if (subStart >= 0 && m_elements.last().ep != m_elements[subStart].sp) { - res.currentPoint = m_elements.last().ep; - res.lineTo(m_elements[subStart].sp); - } - if (!res.m_elements.isEmpty()) { - auto &endElement = res.m_elements.last(); - endElement.m_isSubpathEnd = true; - endElement.ep = m_elements[subStart].sp; - } - - // ### Workaround for triangulator issue: Avoid 3-element paths - if (res.elementCount() == 3) { - res.splitElementAt(2); - res = res.flattened(); - Q_ASSERT(res.elementCount() == 4); - } - - return res; -} - -QQuadPath QQuadPath::flattened() const -{ - QQuadPath res; - res.reserve(elementCountRecursive()); - iterateElements([&](const QQuadPath::Element &element) { res.m_elements.append(element); }); - return res; -} - -class ElementCutter -{ -public: - ElementCutter(const QQuadPath::Element &element) - : m_element(element) - { - m_currentPoint = m_element.startPoint(); - if (m_element.isLine()) - m_lineLength = (m_element.endPoint() - m_element.startPoint()).length(); - else - fillLUT(); - } - - bool consume(float length) - { - m_lastT = m_currentT; - m_lastPoint = m_currentPoint; - float nextCut = m_consumed + length; - float cutT = m_element.isLine() ? nextCut / m_lineLength : tForLength(nextCut); - if (cutT < 1) { - m_currentT = cutT; - m_currentPoint = m_element.pointAtFraction(m_currentT); - m_consumed = nextCut; - return true; - } else { - m_currentT = 1; - m_currentPoint = m_element.endPoint(); - return false; - } - } - - QVector2D currentCutPoint() - { - return m_currentPoint; - } - - QVector2D currentControlPoint() - { - Q_ASSERT(!m_element.isLine()); - // Split curve right at lastT, yields { lastPoint, rcp, endPoint } quad segment - QVector2D rcp = (1 - m_lastT) * m_element.controlPoint() + m_lastT * m_element.endPoint(); - // Split that left at currentT, yields { lastPoint, lcp, currentPoint } quad segment - float segmentT = (m_currentT - m_lastT) / (1 - m_lastT); - QVector2D lcp = (1 - segmentT) * m_lastPoint + segmentT * rcp; - return lcp; - } - - float lastLength() - { - float elemLength = m_element.isLine() ? m_lineLength : m_lut.last(); - return elemLength - m_consumed; - } - -private: - void fillLUT() - { - Q_ASSERT(!m_element.isLine()); - QVector2D ap = m_element.startPoint() - 2 * m_element.controlPoint() + m_element.endPoint(); - QVector2D bp = 2 * m_element.controlPoint() - 2 * m_element.startPoint(); - float A = 4 * QVector2D::dotProduct(ap, ap); - float B = 4 * QVector2D::dotProduct(ap, bp); - float C = QVector2D::dotProduct(bp, bp); - float b = B / (2 * A); - float c = C / A; - float k = c - (b * b); - float l2 = b * std::sqrt(b * b + k); - float lnom = b + std::sqrt(b * b + k); - float l0 = 0.5f * std::sqrt(A); - - m_lut.resize(LUTSize, 0); - for (int i = 1; i < LUTSize; i++) { - float t = float(i) / (LUTSize - 1); - float u = t + b; - float w = std::sqrt(u * u + k); - float l1 = u * w; - float lden = u + w; - float l3 = k * std::log(std::fabs(lden / lnom)); - float res = l0 * (l1 - l2 + l3); - m_lut[i] = res; - } - } - - float tForLength(float length) - { - Q_ASSERT(!m_element.isLine()); - Q_ASSERT(!m_lut.isEmpty()); - - float res = 2; // I.e. invalid, outside [0, 1] range - auto it = std::upper_bound(m_lut.cbegin(), m_lut.cend(), length); - if (it != m_lut.cend()) { - float nextLength = *it--; - float prevLength = *it; - int prevIndex = std::distance(m_lut.cbegin(), it); - float fraction = (length - prevLength) / (nextLength - prevLength); - res = (prevIndex + fraction) / (LUTSize - 1); - } - return res; - } - - const QQuadPath::Element &m_element; - float m_lastT = 0; - float m_currentT = 0; - QVector2D m_lastPoint; - QVector2D m_currentPoint; - float m_consumed = 0; - // For line elements: - float m_lineLength; - // For quadratic curve elements: - static constexpr int LUTSize = 21; - QVarLengthArray m_lut; -}; - -QQuadPath QQuadPath::dashed(qreal lineWidth, const QList &dashPattern, qreal dashOffset) const -{ - QVarLengthArray pattern; - float patternLength = 0; - for (int i = 0; i < 2 * (dashPattern.length() / 2); i++) { - float dashLength = qMax(lineWidth * dashPattern[i], qreal(0)); - pattern.append(dashLength); - patternLength += dashLength; - } - if (patternLength == 0) - return {}; - - int startIndex = 0; - float startOffset = std::fmod(lineWidth * dashOffset, patternLength); - if (startOffset < 0) - startOffset += patternLength; - for (float dashLength : pattern) { - if (dashLength > startOffset) - break; - startIndex++; - startOffset -= dashLength; - } - - int dashIndex = startIndex; - float offset = startOffset; - QQuadPath res; - for (int i = 0; i < elementCount(); i++) { - const Element &element = elementAt(i); - if (element.isSubpathStart()) { - res.moveTo(element.startPoint()); - dashIndex = startIndex; - offset = startOffset; - } - ElementCutter cutter(element); - while (true) { - bool gotAll = cutter.consume(pattern.at(dashIndex) - offset); - QVector2D nextPoint = cutter.currentCutPoint(); - if (dashIndex & 1) - res.moveTo(nextPoint); // gap - else if (element.isLine()) - res.lineTo(nextPoint); // dash in line - else - res.quadTo(cutter.currentControlPoint(), nextPoint); // dash in curve - if (gotAll) { - offset = 0; - dashIndex = (dashIndex + 1) % pattern.size(); - } else { - offset += cutter.lastLength(); - break; - } - } - } - return res; -} - -void QQuadPath::splitElementAt(int index) -{ - const int newChildIndex = m_childElements.size(); - m_childElements.resize(newChildIndex + 2); - Element &parent = elementAt(index); - parent.m_numChildren = 2; - parent.m_firstChildIndex = newChildIndex; - - Element &quad1 = m_childElements[newChildIndex]; - const QVector2D mp = parent.midPoint(); - quad1.sp = parent.sp; - quad1.cp = 0.5f * (parent.sp + parent.cp); - quad1.ep = mp; - quad1.m_isSubpathStart = parent.m_isSubpathStart; - quad1.m_isSubpathEnd = false; - quad1.m_curvatureFlags = parent.m_curvatureFlags; - quad1.m_isLine = parent.m_isLine; //### || isPointNearLine(quad1.cp, quad1.sp, quad1.ep); - - Element &quad2 = m_childElements[newChildIndex + 1]; - quad2.sp = mp; - quad2.cp = 0.5f * (parent.ep + parent.cp); - quad2.ep = parent.ep; - quad2.m_isSubpathStart = false; - quad2.m_isSubpathEnd = parent.m_isSubpathEnd; - quad2.m_curvatureFlags = parent.m_curvatureFlags; - quad2.m_isLine = parent.m_isLine; //### || isPointNearLine(quad2.cp, quad2.sp, quad2.ep); - -#ifndef QT_NO_DEBUG - if (qFuzzyCompare(quad1.sp, quad1.ep) || qFuzzyCompare(quad2.sp, quad2.ep)) - qCDebug(lcShapeCurveRenderer) << "Splitting has resulted in ~null quad"; -#endif -} - -static void printElement(QDebug stream, const QQuadPath::Element &element) -{ - auto printPoint = [&](QVector2D p) { stream << "(" << p.x() << ", " << p.y() << ") "; }; - stream << "{ "; - printPoint(element.startPoint()); - printPoint(element.controlPoint()); - printPoint(element.endPoint()); - stream << "} " << (element.isLine() ? "L " : "C ") << (element.isConvex() ? "X " : "O ") - << (element.isSubpathStart() ? "S" : element.isSubpathEnd() ? "E" : ""); -} - -QDebug operator<<(QDebug stream, const QQuadPath::Element &element) -{ - QDebugStateSaver saver(stream); - stream.nospace(); - stream << "QuadPath::Element( "; - printElement(stream, element); - stream << " )"; - return stream; -} - -QDebug operator<<(QDebug stream, const QQuadPath &path) -{ - QDebugStateSaver saver(stream); - stream.nospace(); - stream << "QuadPath(" << path.elementCount() << " main elements, " - << path.elementCountRecursive() << " leaf elements, " - << (path.fillRule() == Qt::OddEvenFill ? "OddEven" : "Winding") << Qt::endl; - int count = 0; - path.iterateElements([&](const QQuadPath::Element &e) { - stream << " " << count++ << (e.isSubpathStart() ? " >" : " "); - printElement(stream, e); - stream << Qt::endl; - }); - stream << ")"; - return stream; -} - -QT_END_NAMESPACE diff --git a/src/quickshapes/qquadpath_p.h b/src/quickshapes/qquadpath_p.h deleted file mode 100644 index e6b038c09c..0000000000 --- a/src/quickshapes/qquadpath_p.h +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef QQUADPATH_P_H -#define QQUADPATH_P_H - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists for the convenience -// of a number of Qt sources files. This header file may change from -// version to version without notice, or even be removed. -// -// We mean it. -// - -#include -#include -#include -#include -#include - -QT_BEGIN_NAMESPACE - -class QQuadPath -{ -public: - class Element - { - public: - Element () - : m_isSubpathStart(false), m_isSubpathEnd(false), m_isLine(false) - { - } - - bool isSubpathStart() const - { - return m_isSubpathStart; - } - - bool isSubpathEnd() const - { - return m_isSubpathEnd; - } - - bool isLine() const - { - return m_isLine; - } - - bool isConvex() const - { - return m_curvatureFlags & Convex; - } - - QVector2D startPoint() const - { - return sp; - } - - QVector2D controlPoint() const - { - return cp; - } - - QVector2D endPoint() const - { - return ep; - } - - QVector2D midPoint() const - { - return isLine() ? 0.5f * (sp + ep) : (0.25f * sp) + (0.5f * cp) + (0.25 * ep); - } - - int childCount() const { return m_numChildren; } - - int indexOfChild(int childNumber) const - { - Q_ASSERT(childNumber >= 0 && childNumber < childCount()); - return -(m_firstChildIndex + 1 + childNumber); - } - - QVector2D pointAtFraction(float t) const; - - QVector2D tangentAtFraction(float t) const - { - return isLine() ? (ep - sp) : ((1 - t) * 2 * (cp - sp)) + (t * 2 * (ep - cp)); - } - - QVector2D normalAtFraction(float t) const - { - const QVector2D tan = tangentAtFraction(t); - return QVector2D(-tan.y(), tan.x()); - } - - float extent() const; - - void setAsConvex(bool isConvex) - { - if (isConvex) - m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags | Element::Convex); - else - m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags & ~Element::Convex); - } - - bool isControlPointOnLeft() const - { - return isPointOnLeft(cp, sp, ep); - } - - private: - int intersectionsAtY(float y, float *fractions) const; - - enum CurvatureFlags : quint8 { - CurvatureUndetermined = 0, - FillOnRight = 1, - Convex = 2 - }; - - QVector2D sp; - QVector2D cp; - QVector2D ep; - int m_firstChildIndex = 0; - quint8 m_numChildren = 0; - CurvatureFlags m_curvatureFlags = CurvatureUndetermined; - quint8 m_isSubpathStart : 1; - quint8 m_isSubpathEnd : 1; - quint8 m_isLine : 1; - friend class QQuadPath; - friend QDebug operator<<(QDebug, const QQuadPath::Element &); - }; - - void moveTo(const QVector2D &to) - { - subPathToStart = true; - currentPoint = to; - } - - void lineTo(const QVector2D &to) - { - addElement({}, to, true); - } - - void quadTo(const QVector2D &control, const QVector2D &to) - { - addElement(control, to); - } - - Element &elementAt(int i) - { - return i < 0 ? m_childElements[-(i + 1)] : m_elements[i]; - } - - const Element &elementAt(int i) const - { - return i < 0 ? m_childElements[-(i + 1)] : m_elements[i]; - } - - int indexOfChildAt(int i, int childNumber) const - { - return elementAt(i).indexOfChild(childNumber); - } - - QRectF controlPointRect() const; - - Qt::FillRule fillRule() const { return m_fillRule; } - void setFillRule(Qt::FillRule rule) { m_fillRule = rule; } - - void reserve(int size) { m_elements.reserve(size); } - int elementCount() const { return m_elements.size(); } - bool isEmpty() const { return m_elements.size() == 0; } - int elementCountRecursive() const; - - static QQuadPath fromPainterPath(const QPainterPath &path); - QPainterPath toPainterPath() const; - - QQuadPath subPathsClosed() const; - void addCurvatureData(); - QQuadPath flattened() const; - QQuadPath dashed(qreal lineWidth, const QList &dashPattern, qreal dashOffset = 0) const; - void splitElementAt(int index); - bool contains(const QVector2D &point) const; - - template - void iterateChildrenOf(Element &e, Func &&lambda) - { - const int lastChildIndex = e.m_firstChildIndex + e.childCount() - 1; - for (int i = e.m_firstChildIndex; i <= lastChildIndex; i++) { - Element &c = m_childElements[i]; - if (c.childCount() > 0) - iterateChildrenOf(c, lambda); - else - lambda(c); - } - } - - template - void iterateChildrenOf(const Element &e, Func &&lambda) const - { - const int lastChildIndex = e.m_firstChildIndex + e.childCount() - 1; - for (int i = e.m_firstChildIndex; i <= lastChildIndex; i++) { - const Element &c = m_childElements[i]; - if (c.childCount() > 0) - iterateChildrenOf(c, lambda); - else - lambda(c); - } - } - - template - void iterateElements(Func &&lambda) - { - for (auto &e : m_elements) { - if (e.childCount() > 0) - iterateChildrenOf(e, lambda); - else - lambda(e); - } - } - - template - void iterateElements(Func &&lambda) const - { - for (auto &e : m_elements) { - if (e.childCount() > 0) - iterateChildrenOf(e, lambda); - else - lambda(e); - } - } - - static QVector2D closestPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep); - static bool isPointOnLeft(const QVector2D &p, const QVector2D &sp, const QVector2D &ep); - static bool isPointOnLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep); - static bool isPointNearLine(const QVector2D &p, const QVector2D &sp, const QVector2D &ep); - -private: - void addElement(const QVector2D &control, const QVector2D &to, bool isLine = false); - Element::CurvatureFlags coordinateOrderOfElement(const Element &element) const; - - friend QDebug operator<<(QDebug, const QQuadPath &); - - bool subPathToStart = true; - Qt::FillRule m_fillRule = Qt::OddEvenFill; - QVector2D currentPoint; - QList m_elements; - QList m_childElements; -}; - -QDebug operator<<(QDebug, const QQuadPath::Element &); -QDebug operator<<(QDebug, const QQuadPath &); - -QT_END_NAMESPACE - -#endif diff --git a/src/quickshapes/qquickshape.cpp b/src/quickshapes/qquickshape.cpp index 67029bba03..431b9b5593 100644 --- a/src/quickshapes/qquickshape.cpp +++ b/src/quickshapes/qquickshape.cpp @@ -1727,112 +1727,6 @@ void QQuickShapeConicalGradient::setAngle(qreal v) } } -static void generateGradientColorTable(const QQuickShapeGradientCacheKey &gradient, - uint *colorTable, int size, float opacity) -{ - int pos = 0; - const QGradientStops &s = gradient.stops; - Q_ASSERT(!s.isEmpty()); - const bool colorInterpolation = true; - - uint alpha = qRound(opacity * 256); - uint current_color = ARGB_COMBINE_ALPHA(s[0].second.rgba(), alpha); - qreal incr = 1.0 / qreal(size); - qreal fpos = 1.5 * incr; - colorTable[pos++] = ARGB2RGBA(qPremultiply(current_color)); - - while (fpos <= s.first().first) { - colorTable[pos] = colorTable[pos - 1]; - pos++; - fpos += incr; - } - - if (colorInterpolation) - current_color = qPremultiply(current_color); - - const int sLast = s.size() - 1; - for (int i = 0; i < sLast; ++i) { - qreal delta = 1/(s[i+1].first - s[i].first); - uint next_color = ARGB_COMBINE_ALPHA(s[i + 1].second.rgba(), alpha); - if (colorInterpolation) - next_color = qPremultiply(next_color); - - while (fpos < s[i+1].first && pos < size) { - int dist = int(256 * ((fpos - s[i].first) * delta)); - int idist = 256 - dist; - if (colorInterpolation) - colorTable[pos] = ARGB2RGBA(INTERPOLATE_PIXEL_256(current_color, idist, next_color, dist)); - else - colorTable[pos] = ARGB2RGBA(qPremultiply(INTERPOLATE_PIXEL_256(current_color, idist, next_color, dist))); - ++pos; - fpos += incr; - } - current_color = next_color; - } - - uint last_color = ARGB2RGBA(qPremultiply(ARGB_COMBINE_ALPHA(s[sLast].second.rgba(), alpha))); - for ( ; pos < size; ++pos) - colorTable[pos] = last_color; - - colorTable[size-1] = last_color; -} - -QQuickShapeGradientCache::~QQuickShapeGradientCache() -{ - qDeleteAll(m_textures); -} - -QQuickShapeGradientCache *QQuickShapeGradientCache::cacheForRhi(QRhi *rhi) -{ - static QHash caches; - auto it = caches.constFind(rhi); - if (it != caches.constEnd()) - return *it; - - QQuickShapeGradientCache *cache = new QQuickShapeGradientCache; - rhi->addCleanupCallback([cache](QRhi *rhi) { - caches.remove(rhi); - delete cache; - }); - caches.insert(rhi, cache); - return cache; -} - -QSGTexture *QQuickShapeGradientCache::get(const QQuickShapeGradientCacheKey &grad) -{ - QSGPlainTexture *tx = m_textures[grad]; - if (!tx) { - static const int W = 1024; // texture size is 1024x1 - QImage gradTab(W, 1, QImage::Format_RGBA8888_Premultiplied); - if (!grad.stops.isEmpty()) - generateGradientColorTable(grad, reinterpret_cast(gradTab.bits()), W, 1.0f); - else - gradTab.fill(Qt::black); - tx = new QSGPlainTexture; - tx->setImage(gradTab); - switch (grad.spread) { - case QQuickShapeGradient::PadSpread: - tx->setHorizontalWrapMode(QSGTexture::ClampToEdge); - tx->setVerticalWrapMode(QSGTexture::ClampToEdge); - break; - case QQuickShapeGradient::RepeatSpread: - tx->setHorizontalWrapMode(QSGTexture::Repeat); - tx->setVerticalWrapMode(QSGTexture::Repeat); - break; - case QQuickShapeGradient::ReflectSpread: - tx->setHorizontalWrapMode(QSGTexture::MirroredRepeat); - tx->setVerticalWrapMode(QSGTexture::MirroredRepeat); - break; - default: - qWarning("Unknown gradient spread mode %d", grad.spread); - break; - } - tx->setFiltering(QSGTexture::Linear); - m_textures[grad] = tx; - } - return tx; -} - QT_END_NAMESPACE #include "moc_qquickshape_p.cpp" diff --git a/src/quickshapes/qquickshape_p_p.h b/src/quickshapes/qquickshape_p_p.h index 0cdfbb19a7..2e9dd6510c 100644 --- a/src/quickshapes/qquickshape_p_p.h +++ b/src/quickshapes/qquickshape_p_p.h @@ -37,16 +37,7 @@ public: SupportsAsync = 0x01 }; Q_DECLARE_FLAGS(Flags, Flag) - enum FillGradientType { NoGradient = 0, LinearGradient, RadialGradient, ConicalGradient }; - struct GradientDesc { // can fully describe a linear/radial/conical gradient - QGradientStops stops; - QQuickShapeGradient::SpreadMode spread = QQuickShapeGradient::PadSpread; - QPointF a; // start (L) or center point (R/C) - QPointF b; // end (L) or focal point (R) - qreal v0; // center radius (R) or start angle (C) - qreal v1; // focal radius (R) - }; virtual ~QQuickAbstractPathRenderer() { } @@ -164,38 +155,6 @@ public: qreal triangulationScale = 1.0; }; -struct QQuickShapeGradientCacheKey -{ - QQuickShapeGradientCacheKey(const QGradientStops &stops, QQuickShapeGradient::SpreadMode spread) - : stops(stops), spread(spread) - { } - QGradientStops stops; - QQuickShapeGradient::SpreadMode spread; - bool operator==(const QQuickShapeGradientCacheKey &other) const - { - return spread == other.spread && stops == other.stops; - } -}; - -inline size_t qHash(const QQuickShapeGradientCacheKey &v, size_t seed = 0) -{ - size_t h = seed + v.spread; - for (int i = 0; i < 3 && i < v.stops.size(); ++i) - h += v.stops[i].second.rgba(); - return h; -} - -class QQuickShapeGradientCache -{ -public: - ~QQuickShapeGradientCache(); - static QQuickShapeGradientCache *cacheForRhi(QRhi *rhi); - QSGTexture *get(const QQuickShapeGradientCacheKey &grad); - -private: - QHash m_textures; -}; - QT_END_NAMESPACE #endif diff --git a/src/quickshapes/qquickshapeabstractcurvenode_p.h b/src/quickshapes/qquickshapeabstractcurvenode_p.h deleted file mode 100644 index 68a7c052e1..0000000000 --- a/src/quickshapes/qquickshapeabstractcurvenode_p.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef QQUICKSHAPEABSTRACTCURVENODE_P_H -#define QQUICKSHAPEABSTRACTCURVENODE_P_H - -#include - -#include "qquickshapegenericrenderer_p.h" - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists for the convenience -// of a number of Qt sources files. This header file may change from -// version to version without notice, or even be removed. -// -// We mean it. -// - -QT_BEGIN_NAMESPACE - -class QQuickShapeAbstractCurveNode : public QSGGeometryNode -{ -public: - virtual void setColor(QColor col) = 0; -}; - -QT_END_NAMESPACE - -#endif // QQUICKSHAPEABSTRACTCURVENODE_P_H diff --git a/src/quickshapes/qquickshapecurvenode.cpp b/src/quickshapes/qquickshapecurvenode.cpp deleted file mode 100644 index 11b5ecbaf4..0000000000 --- a/src/quickshapes/qquickshapecurvenode.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#include "qquickshapecurvenode_p.h" -#include "qquickshapecurvenode_p_p.h" - -QT_BEGIN_NAMESPACE - -namespace { -} - -QQuickShapeCurveNode::QQuickShapeCurveNode() -{ - setFlag(OwnsGeometry, true); - setGeometry(new QSGGeometry(attributes(), 0, 0)); - - updateMaterial(); -} - -void QQuickShapeCurveNode::updateMaterial() -{ - m_material.reset(new QQuickShapeCurveMaterial(this)); - setMaterial(m_material.data()); -} - -void QQuickShapeCurveNode::cookGeometry() -{ - QSGGeometry *g = geometry(); - if (g->indexType() != QSGGeometry::UnsignedIntType) { - g = new QSGGeometry(attributes(), - m_uncookedVertexes.size(), - m_uncookedIndexes.size(), - QSGGeometry::UnsignedIntType); - setGeometry(g); - } else { - g->allocate(m_uncookedVertexes.size(), m_uncookedIndexes.size()); - } - - g->setDrawingMode(QSGGeometry::DrawTriangles); - memcpy(g->vertexData(), - m_uncookedVertexes.constData(), - g->vertexCount() * g->sizeOfVertex()); - memcpy(g->indexData(), - m_uncookedIndexes.constData(), - g->indexCount() * g->sizeOfIndex()); - - m_uncookedIndexes.clear(); - m_uncookedVertexes.clear(); -} - -const QSGGeometry::AttributeSet &QQuickShapeCurveNode::attributes() -{ - static QSGGeometry::Attribute data[] = { - QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute), - QSGGeometry::Attribute::createWithAttributeType(1, 3, QSGGeometry::FloatType, QSGGeometry::TexCoordAttribute), - QSGGeometry::Attribute::createWithAttributeType(2, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), - QSGGeometry::Attribute::createWithAttributeType(3, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), - }; - static QSGGeometry::AttributeSet attrs = { 4, sizeof(CurveNodeVertex), data }; - return attrs; -} - -QT_END_NAMESPACE diff --git a/src/quickshapes/qquickshapecurvenode_p.cpp b/src/quickshapes/qquickshapecurvenode_p.cpp deleted file mode 100644 index c19672d1eb..0000000000 --- a/src/quickshapes/qquickshapecurvenode_p.cpp +++ /dev/null @@ -1,344 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#include "qquickshapecurvenode_p_p.h" -#include "qquickshapecurvenode_p.h" - -#include "qquickshapegenericrenderer_p.h" - -QT_BEGIN_NAMESPACE - -namespace { - - class QQuickShapeCurveMaterialShader : public QSGMaterialShader - { - public: - QQuickShapeCurveMaterialShader(QQuickAbstractPathRenderer::FillGradientType gradientType, - bool includeStroke, - bool useDerivatives); - - bool updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override; - void updateSampledImage(RenderState &state, int binding, QSGTexture **texture, - QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override; - }; - - QQuickShapeCurveMaterialShader::QQuickShapeCurveMaterialShader(QQuickAbstractPathRenderer::FillGradientType gradientType, - bool includeStroke, - bool useDerivatives) - { - QString baseName = QStringLiteral(":/qt-project.org/shapes/shaders_ng/shapecurve"); - - if (gradientType == QQuickAbstractPathRenderer::LinearGradient) { - baseName += QStringLiteral("_lg"); - } else if (gradientType == QQuickAbstractPathRenderer::RadialGradient) { - baseName += QStringLiteral("_rg"); - } else if (gradientType == QQuickAbstractPathRenderer::ConicalGradient) { - baseName += QStringLiteral("_cg"); - } - - if (includeStroke) - baseName += QStringLiteral("_stroke"); - - if (useDerivatives) - baseName += QStringLiteral("_derivatives"); - - setShaderFileName(VertexStage, baseName + QStringLiteral(".vert.qsb")); - setShaderFileName(FragmentStage, baseName + QStringLiteral(".frag.qsb")); - } - - void QQuickShapeCurveMaterialShader::updateSampledImage(RenderState &state, int binding, QSGTexture **texture, - QSGMaterial *newMaterial, QSGMaterial *oldMaterial) - { - Q_UNUSED(oldMaterial); - const QQuickShapeCurveMaterial *m = static_cast(newMaterial); - const QQuickShapeCurveNode *node = m->node(); - if (binding != 1 || node->gradientType() == QQuickAbstractPathRenderer::NoGradient) - return; - - const QQuickShapeGradientCacheKey cacheKey(node->fillGradient().stops, - node->fillGradient().spread); - QSGTexture *t = QQuickShapeGradientCache::cacheForRhi(state.rhi())->get(cacheKey); - t->commitTextureOperations(state.rhi(), state.resourceUpdateBatch()); - *texture = t; - } - - bool QQuickShapeCurveMaterialShader::updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) - { - bool changed = false; - QByteArray *buf = state.uniformData(); - Q_ASSERT(buf->size() >= 80); - - int offset = 0; - float matrixScale = 0.0f; - if (state.isMatrixDirty()) { - const QMatrix4x4 m = state.combinedMatrix(); - - memcpy(buf->data() + offset, m.constData(), 64); - - matrixScale = qSqrt(qAbs(state.determinant())); - memcpy(buf->data() + offset + 64, &matrixScale, 4); - - changed = true; - } - offset += 68; - - if (state.isOpacityDirty()) { - const float opacity = state.opacity(); - memcpy(buf->data() + offset, &opacity, 4); - changed = true; - } - offset += 4; - - QQuickShapeCurveMaterial *newMaterial = static_cast(newEffect); - QQuickShapeCurveMaterial *oldMaterial = static_cast(oldEffect); - - QQuickShapeCurveNode *newNode = newMaterial != nullptr ? newMaterial->node() : nullptr; - QQuickShapeCurveNode *oldNode = oldMaterial != nullptr ? oldMaterial->node() : nullptr; - - if (newNode == nullptr) - return changed; - - if (oldNode == nullptr || oldNode->debug() != newNode->debug()) { - float debug = newNode->debug(); - memcpy(buf->data() + offset, &debug, 4); - changed = true; - } - offset += 8; - - if (newNode->hasStroke()) { - Q_ASSERT(buf->size() >= offset + 32); - QVector4D newStrokeColor(newNode->strokeColor().redF(), - newNode->strokeColor().greenF(), - newNode->strokeColor().blueF(), - newNode->strokeColor().alphaF()); - QVector4D oldStrokeColor = oldNode != nullptr - ? QVector4D(oldNode->strokeColor().redF(), - oldNode->strokeColor().greenF(), - oldNode->strokeColor().blueF(), - oldNode->strokeColor().alphaF()) - : QVector4D{}; - - if (oldNode == nullptr || oldStrokeColor != newStrokeColor) { - memcpy(buf->data() + offset, &newStrokeColor, 16); - changed = true; - } - offset += 16; - - if (oldNode == nullptr - || !qFuzzyCompare(newNode->strokeWidth(), oldNode->strokeWidth()) - || (state.isMatrixDirty() && newNode->strokeWidth() > 0.0f)) { - float w = newNode->strokeWidth() * matrixScale; // matrixScale calculated earlier - memcpy(buf->data() + offset, &w, 4); - changed = true; - } - offset += 16; - } - - if (newNode->gradientType() == QQuickAbstractPathRenderer::NoGradient) { - Q_ASSERT(buf->size() >= offset + 16); - - QVector4D newColor = QVector4D(newNode->color().redF(), - newNode->color().greenF(), - newNode->color().blueF(), - newNode->color().alphaF()); - QVector4D oldColor = oldNode != nullptr - ? QVector4D(oldNode->color().redF(), - oldNode->color().greenF(), - oldNode->color().blueF(), - oldNode->color().alphaF()) - : QVector4D{}; - - if (oldNode == nullptr || oldColor != newColor) { - memcpy(buf->data() + offset, &newColor, 16); - changed = true; - } - - offset += 16; - } else if (newNode->gradientType() == QQuickAbstractPathRenderer::LinearGradient) { - Q_ASSERT(buf->size() >= offset + 8 + 8); - - QVector2D newGradientStart = QVector2D(newNode->fillGradient().a); - QVector2D oldGradientStart = oldNode != nullptr - ? QVector2D(oldNode->fillGradient().a) - : QVector2D{}; - - if (newGradientStart != oldGradientStart || oldEffect == nullptr) { - memcpy(buf->data() + offset, &newGradientStart, 8); - changed = true; - } - offset += 8; - - QVector2D newGradientEnd = QVector2D(newNode->fillGradient().b); - QVector2D oldGradientEnd = oldNode!= nullptr - ? QVector2D(oldNode->fillGradient().b) - : QVector2D{}; - - if (newGradientEnd != oldGradientEnd || oldEffect == nullptr) { - memcpy(buf->data() + offset, &newGradientEnd, 8); - changed = true; - } - - offset += 8; - } else if (newNode->gradientType() == QQuickAbstractPathRenderer::RadialGradient) { - Q_ASSERT(buf->size() >= offset + 8 + 8 + 4 + 4); - - QVector2D newFocalPoint = QVector2D(newNode->fillGradient().b); - QVector2D oldFocalPoint = oldNode != nullptr - ? QVector2D(oldNode->fillGradient().b) - : QVector2D{}; - if (oldNode == nullptr || newFocalPoint != oldFocalPoint) { - memcpy(buf->data() + offset, &newFocalPoint, 8); - changed = true; - } - offset += 8; - - QVector2D newCenterPoint = QVector2D(newNode->fillGradient().a); - QVector2D oldCenterPoint = oldNode != nullptr - ? QVector2D(oldNode->fillGradient().a) - : QVector2D{}; - - QVector2D newCenterToFocal = newCenterPoint - newFocalPoint; - QVector2D oldCenterToFocal = oldCenterPoint - oldFocalPoint; - if (oldNode == nullptr || newCenterToFocal != oldCenterToFocal) { - memcpy(buf->data() + offset, &newCenterToFocal, 8); - changed = true; - } - offset += 8; - - float newCenterRadius = newNode->fillGradient().v0; - float oldCenterRadius = oldNode != nullptr - ? oldNode->fillGradient().v0 - : 0.0f; - if (oldNode == nullptr || !qFuzzyCompare(newCenterRadius, oldCenterRadius)) { - memcpy(buf->data() + offset, &newCenterRadius, 4); - changed = true; - } - offset += 4; - - float newFocalRadius = newNode->fillGradient().v1; - float oldFocalRadius = oldNode != nullptr - ? oldNode->fillGradient().v1 - : 0.0f; - if (oldNode == nullptr || !qFuzzyCompare(newFocalRadius, oldFocalRadius)) { - memcpy(buf->data() + offset, &newFocalRadius, 4); - changed = true; - } - offset += 4; - - } else if (newNode->gradientType() == QQuickAbstractPathRenderer::ConicalGradient) { - Q_ASSERT(buf->size() >= offset + 8 + 4); - - QVector2D newFocalPoint = QVector2D(newNode->fillGradient().a); - QVector2D oldFocalPoint = oldNode != nullptr - ? QVector2D(oldNode->fillGradient().a) - : QVector2D{}; - if (oldNode == nullptr || newFocalPoint != oldFocalPoint) { - memcpy(buf->data() + offset, &newFocalPoint, 8); - changed = true; - } - offset += 8; - - float newAngle = newNode->fillGradient().v0; - float oldAngle = oldNode != nullptr - ? oldNode->fillGradient().v0 - : 0.0f; - if (oldNode == nullptr || !qFuzzyCompare(newAngle, oldAngle)) { - newAngle = -qDegreesToRadians(newAngle); - memcpy(buf->data() + offset, &newAngle, 4); - changed = true; - } - offset += 4; - } - - return changed; - } -} - -QQuickShapeCurveMaterial::QQuickShapeCurveMaterial(QQuickShapeCurveNode *node) - : m_node(node) -{ - setFlag(Blending, true); - setFlag(RequiresDeterminant, true); -} - -int QQuickShapeCurveMaterial::compare(const QSGMaterial *other) const -{ - if (other->type() != type()) - return (type() - other->type()); - - const QQuickShapeCurveMaterial *otherMaterial = - static_cast(other); - - QQuickShapeCurveNode *a = node(); - QQuickShapeCurveNode *b = otherMaterial->node(); - if (a == b) - return 0; - - if (int d = a->strokeColor().rgba() - b->strokeColor().rgba()) - return d; - - if (a->gradientType() == QQuickAbstractPathRenderer::NoGradient) { - if (int d = a->color().red() - b->color().red()) - return d; - if (int d = a->color().green() - b->color().green()) - return d; - if (int d = a->color().blue() - b->color().blue()) - return d; - if (int d = a->color().alpha() - b->color().alpha()) - return d; - } else { - const QQuickAbstractPathRenderer::GradientDesc &ga = a->fillGradient(); - const QQuickAbstractPathRenderer::GradientDesc &gb = b->fillGradient(); - - if (int d = ga.a.x() - gb.a.x()) - return d; - if (int d = ga.a.y() - gb.a.y()) - return d; - if (int d = ga.b.x() - gb.b.x()) - return d; - if (int d = ga.b.y() - gb.b.y()) - return d; - - if (int d = ga.v0 - gb.v0) - return d; - if (int d = ga.v1 - gb.v1) - return d; - - if (int d = ga.spread - gb.spread) - return d; - - if (int d = ga.stops.size() - gb.stops.size()) - return d; - - for (int i = 0; i < ga.stops.size(); ++i) { - if (int d = ga.stops[i].first - gb.stops[i].first) - return d; - if (int d = ga.stops[i].second.rgba() - gb.stops[i].second.rgba()) - return d; - } - } - - return 0; -} - -QSGMaterialType *QQuickShapeCurveMaterial::type() const -{ - static QSGMaterialType type[8]; - uint index = node()->gradientType(); - Q_ASSERT((index & ~3) == 0); // Only two first bits for gradient type - - if (node()->hasStroke()) - index |= 4; - - return &type[index]; -} - -QSGMaterialShader *QQuickShapeCurveMaterial::createShader(QSGRendererInterface::RenderMode renderMode) const -{ - return new QQuickShapeCurveMaterialShader(node()->gradientType(), - node()->hasStroke(), - renderMode == QSGRendererInterface::RenderMode3D); -} - - -QT_END_NAMESPACE diff --git a/src/quickshapes/qquickshapecurvenode_p.h b/src/quickshapes/qquickshapecurvenode_p.h deleted file mode 100644 index 9a79e6d314..0000000000 --- a/src/quickshapes/qquickshapecurvenode_p.h +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef QQUICKSHAPECURVENODE_P_H -#define QQUICKSHAPECURVENODE_P_H - -#include - -#include "qquickshapeabstractcurvenode_p.h" -#include "qquickshapegenericrenderer_p.h" - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists for the convenience -// of a number of Qt sources files. This header file may change from -// version to version without notice, or even be removed. -// -// We mean it. -// - -QT_BEGIN_NAMESPACE - -class QQuickShapeCurveNode : public QQuickShapeAbstractCurveNode -{ -public: - QQuickShapeCurveNode(); - - void setColor(QColor col) override - { - if (m_color == col) - return; - m_color = col; - updateMaterial(); - } - - QColor color() const - { - return m_color; - } - - void setStrokeColor(QColor col) - { - const bool hadStroke = hasStroke(); - m_strokeColor = col; - if (hadStroke != hasStroke()) - updateMaterial(); - } - - QColor strokeColor() const - { - return m_strokeColor; - } - - void setStrokeWidth(float width) - { - const bool hadStroke = hasStroke(); - m_strokeWidth = width; - if (hadStroke != hasStroke()) - updateMaterial(); - } - - float strokeWidth() const - { - return m_strokeWidth; - } - - void setFillGradient(const QQuickAbstractPathRenderer::GradientDesc &fillGradient) - { - m_fillGradient = fillGradient; - } - - QQuickAbstractPathRenderer::GradientDesc fillGradient() const - { - return m_fillGradient; - } - - void setGradientType(QQuickAbstractPathRenderer::FillGradientType type) - { - if (m_gradientType != type) { - m_gradientType = type; - updateMaterial(); - } - } - - float debug() const - { - return m_debug; - } - - void setDebug(float newDebug) - { - m_debug = newDebug; - } - - QQuickAbstractPathRenderer::FillGradientType gradientType() const - { - return m_gradientType; - } - - bool hasStroke() const - { - return m_strokeWidth > 0.0f && m_strokeColor.alpha() > 0; - } - - void appendTriangle(const std::array &v, // triangle vertices - const std::array &n, // vertex normals - std::function uvForPoint - ) - { - QVector3D uv1 = uvForPoint(v[0]); - QVector3D uv2 = uvForPoint(v[1]); - QVector3D uv3 = uvForPoint(v[2]); - - QVector2D duvdx = QVector2D(uvForPoint(v[0] + QVector2D(1, 0))) - QVector2D(uv1); - QVector2D duvdy = QVector2D(uvForPoint(v[0] + QVector2D(0, 1))) - QVector2D(uv1); - - m_uncookedIndexes.append(m_uncookedVertexes.size()); - m_uncookedVertexes.append( { v[0].x(), v[0].y(), - uv1.x(), uv1.y(), uv1.z(), - duvdx.x(), duvdx.y(), - duvdy.x(), duvdy.y(), - n[0].x(), n[0].y() - }); - - m_uncookedIndexes.append(m_uncookedVertexes.size()); - m_uncookedVertexes.append( { v[1].x(), v[1].y(), - uv2.x(), uv2.y(), uv2.z(), - duvdx.x(), duvdx.y(), - duvdy.x(), duvdy.y(), - n[1].x(), n[1].y() - }); - - m_uncookedIndexes.append(m_uncookedVertexes.size()); - m_uncookedVertexes.append( { v[2].x(), v[2].y(), - uv3.x(), uv3.y(), uv3.z(), - duvdx.x(), duvdx.y(), - duvdy.x(), duvdy.y(), - n[2].x(), n[2].y() - }); - - } - - void appendTriangle(const QVector2D &v1, - const QVector2D &v2, - const QVector2D &v3, - std::function uvForPoint) - { - appendTriangle({v1, v2, v3}, {}, uvForPoint); - } - - void appendIndex(quint32 index) - { - m_uncookedIndexes.append(index); - } - - void appendIndexes(QVector indexes) - { - m_uncookedIndexes.append(indexes); - } - - QVector uncookedIndexes() const - { - return m_uncookedIndexes; - } - - void cookGeometry(); - -private: - struct CurveNodeVertex - { - float x, y, u, v, w; - float dudx, dvdx, dudy, dvdy; // Size of pixel in curve space (must be same for all vertices in triangle) - float nx, ny; // normal vector describing the direction to shift the vertex for AA - }; - - void updateMaterial(); - static const QSGGeometry::AttributeSet &attributes(); - - QColor m_color = Qt::white; - QColor m_strokeColor = Qt::transparent; - float m_strokeWidth = 0.0f; - float m_debug = 0.0f; - QQuickAbstractPathRenderer::GradientDesc m_fillGradient; - QQuickAbstractPathRenderer::FillGradientType m_gradientType = QQuickAbstractPathRenderer::NoGradient; - - QScopedPointer m_material; - - QVector m_uncookedVertexes; - QVector m_uncookedIndexes; -}; - -QT_END_NAMESPACE - -#endif // QQUICKSHAPECURVENODE_P_H diff --git a/src/quickshapes/qquickshapecurvenode_p_p.h b/src/quickshapes/qquickshapecurvenode_p_p.h deleted file mode 100644 index 5b04785e31..0000000000 --- a/src/quickshapes/qquickshapecurvenode_p_p.h +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef QQUICKSHAPECURVENODE_P_P_H -#define QQUICKSHAPECURVENODE_P_P_H - -#include - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists for the convenience -// of a number of Qt sources files. This header file may change from -// version to version without notice, or even be removed. -// -// We mean it. -// - -QT_BEGIN_NAMESPACE - -class QQuickShapeCurveNode; -class QQuickShapeCurveMaterial : public QSGMaterial -{ -public: - QQuickShapeCurveMaterial(QQuickShapeCurveNode *node); - int compare(const QSGMaterial *other) const override; - - QQuickShapeCurveNode *node() const - { - return m_node; - } - -private: - QSGMaterialType *type() const override; - QSGMaterialShader *createShader(QSGRendererInterface::RenderMode renderMode) const override; - - QQuickShapeCurveNode *m_node; -}; - -QT_END_NAMESPACE - -#endif // QQUICKSHAPECURVENODE_P_P_H diff --git a/src/quickshapes/qquickshapecurverenderer.cpp b/src/quickshapes/qquickshapecurverenderer.cpp index aefee42c23..436a6713c0 100644 --- a/src/quickshapes/qquickshapecurverenderer.cpp +++ b/src/quickshapes/qquickshapecurverenderer.cpp @@ -3,8 +3,6 @@ #include "qquickshapecurverenderer_p.h" #include "qquickshapecurverenderer_p_p.h" -#include "qquickshapecurvenode_p.h" -#include "qquickshapestrokenode_p.h" #include #include @@ -12,6 +10,10 @@ #include #include +#include +#include +#include +#include #include #include @@ -76,7 +78,7 @@ protected: }; -class QQuickShapeWireFrameNode : public QQuickShapeAbstractCurveNode +class QQuickShapeWireFrameNode : public QSGCurveAbstractNode { public: struct WireFrameVertex @@ -112,6 +114,11 @@ public: return attrs; } + void cookGeometry() override + { + // Intentionally empty + } + protected: QScopedPointer m_material; }; @@ -209,21 +216,21 @@ void QQuickShapeCurveRenderer::setStrokeStyle(int index, void QQuickShapeCurveRenderer::setFillGradient(int index, QQuickShapeGradient *gradient) { PathData &pd(m_paths[index]); - pd.gradientType = NoGradient; + pd.gradientType = QGradient::NoGradient; if (QQuickShapeLinearGradient *g = qobject_cast(gradient)) { - pd.gradientType = LinearGradient; + pd.gradientType = QGradient::LinearGradient; pd.gradient.stops = gradient->gradientStops(); - pd.gradient.spread = gradient->spread(); + pd.gradient.spread = QGradient::Spread(gradient->spread()); pd.gradient.a = QPointF(g->x1(), g->y1()); pd.gradient.b = QPointF(g->x2(), g->y2()); } else if (QQuickShapeRadialGradient *g = qobject_cast(gradient)) { - pd.gradientType = RadialGradient; + pd.gradientType = QGradient::RadialGradient; pd.gradient.a = QPointF(g->centerX(), g->centerY()); pd.gradient.b = QPointF(g->focalX(), g->focalY()); pd.gradient.v0 = g->centerRadius(); pd.gradient.v1 = g->focalRadius(); } else if (QQuickShapeConicalGradient *g = qobject_cast(gradient)) { - pd.gradientType = ConicalGradient; + pd.gradientType = QGradient::ConicalGradient; pd.gradient.a = QPointF(g->centerX(), g->centerY()); pd.gradient.v0 = g->angle(); } else @@ -235,9 +242,9 @@ void QQuickShapeCurveRenderer::setFillGradient(int index, QQuickShapeGradient *g } } - if (pd.gradientType != NoGradient) { + if (pd.gradientType != QGradient::NoGradient) { pd.gradient.stops = gradient->gradientStops(); - pd.gradient.spread = gradient->spread(); + pd.gradient.spread = QGradient::Spread(gradient->spread()); } pd.m_dirty |= FillDirty; @@ -284,7 +291,7 @@ void QQuickShapeCurveRenderer::updateNode() pathData.fillPath = pathData.path.subPathsClosed(); pathData.fillPath.addCurvatureData(); if (doOverlapSolving) - solveOverlaps(pathData.fillPath); + QSGCurveProcessor::solveOverlaps(pathData.fillPath); } pathData.fillNodes = addFillNodes(pathData, &pathData.fillDebugNodes); dirtyFlags |= StrokeDirty; @@ -323,74 +330,10 @@ void QQuickShapeCurveRenderer::updateNode() } } -namespace { -// Input coordinate space is pre-mapped so that (0, 0) maps to [0, 0] in uv space. -// v1 maps to [1,0], v2 maps to [0,1]. p is the point to be mapped to uv in this space (i.e. vector from p0) -static inline QVector2D uvForPoint(QVector2D v1, QVector2D v2, QVector2D p) -{ - double divisor = v1.x() * v2.y() - v2.x() * v1.y(); - - float u = (p.x() * v2.y() - p.y() * v2.x()) / divisor; - float v = (p.y() * v1.x() - p.x() * v1.y()) / divisor; - - return {u, v}; -} - -// Find uv coordinates for the point p, for a quadratic curve from p0 to p2 with control point p1 -// also works for a line from p0 to p2, where p1 is on the inside of the path relative to the line -static inline QVector2D curveUv(QVector2D p0, QVector2D p1, QVector2D p2, QVector2D p) -{ - QVector2D v1 = 2 * (p1 - p0); - QVector2D v2 = p2 - v1 - p0; - return uvForPoint(v1, v2, p - p0); -} - -static QVector3D elementUvForPoint(const QQuadPath::Element& e, QVector2D p) -{ - auto uv = curveUv(e.startPoint(), e.controlPoint(), e.endPoint(), p); - if (e.isLine()) - return { uv.x(), uv.y(), 0.0f }; - else - return { uv.x(), uv.y(), e.isConvex() ? -1.0f : 1.0f }; -} - -static inline QVector2D calcNormalVector(QVector2D a, QVector2D b) -{ - auto v = b - a; - return {v.y(), -v.x()}; -} - -// The sign of the return value indicates which side of the line defined by a and n the point p falls -static inline float testSideOfLineByNormal(QVector2D a, QVector2D n, QVector2D p) -{ - float dot = QVector2D::dotProduct(p - a, n); - return dot; -}; - -template -void iteratePath(const QQuadPath &path, int index, Func &&lambda) -{ - const auto &element = path.elementAt(index); - if (element.childCount() == 0) { - lambda(element, index); - } else { - for (int i = 0; i < element.childCount(); ++i) - iteratePath(path, element.indexOfChild(i), lambda); - } -} - -static inline float determinant(const QVector2D &p1, const QVector2D &p2, const QVector2D &p3) -{ - return p1.x() * (p2.y() - p3.y()) - + p2.x() * (p3.y() - p1.y()) - + p3.x() * (p1.y() - p2.y()); -} -} - QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addFillNodes(const PathData &pathData, NodeList *debugNodes) { - auto *node = new QQuickShapeCurveNode; + auto *node = new QSGCurveFillNode; node->setGradientType(pathData.gradientType); NodeList ret; @@ -401,284 +344,21 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addFillNodes(const bool visualizeDebug = debugVisualization() & DebugCurves; const float dbg = visualizeDebug ? 0.5f : 0.0f; node->setDebug(dbg); - QVector wfVertices; - - QHash, int> linePointHash; - QHash, int> concaveControlPointHash; - QHash, int> convexPointHash; - - auto toRoundedPair = [](const QPointF &p) -> QPair { - return qMakePair(qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f); - }; - - auto toRoundedVec2D = [](const QPointF &p) -> QVector2D { - return { qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f }; - }; - - auto roundVec2D = [](const QVector2D &p) -> QVector2D { - return { qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f }; - }; - - auto addCurveTriangle = [&](const QQuadPath::Element &element, - const QVector2D &sp, - const QVector2D &ep, - const QVector2D &cp) { - node->appendTriangle(sp, cp, ep, - [&element](QVector2D v) { return elementUvForPoint(element, v); }); - - wfVertices.append({sp.x(), sp.y(), 1.0f, 0.0f, 0.0f}); // 0 - wfVertices.append({cp.x(), cp.y(), 0.0f, 1.0f, 0.0f}); // 1 - wfVertices.append({ep.x(), ep.y(), 0.0f, 0.0f, 1.0f}); // 2 - }; - - auto addCurveTriangleWithNormals = [&](const QQuadPath::Element &element, - const std::array &v, - const std::array &n) { - node->appendTriangle(v, n, [&element](QVector2D v) { return elementUvForPoint(element, v); }); - wfVertices.append({v[0].x(), v[0].y(), 1.0f, 0.0f, 0.0f}); // 0 - wfVertices.append({v[1].x(), v[1].y(), 0.0f, 1.0f, 0.0f}); // 1 - wfVertices.append({v[2].x(), v[2].y(), 0.0f, 0.0f, 1.0f}); // 2 - }; - - auto outsideNormal = [](const QVector2D &startPoint, - const QVector2D &endPoint, - const QVector2D &insidePoint) { - - QVector2D baseLine = endPoint - startPoint; - QVector2D insideVector = insidePoint - startPoint; - QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized(); - - bool swap = QVector2D::dotProduct(insideVector, normal) < 0; - - return swap ? normal : -normal; - }; - - auto addTriangleForLine = [&](const QQuadPath::Element &element, - const QVector2D &sp, - const QVector2D &ep, - const QVector2D &cp) { - addCurveTriangle(element, sp, ep, cp); - - // Add triangles on the outer side to make room for AA - const QVector2D normal = outsideNormal(sp, ep, cp); - constexpr QVector2D null; - addCurveTriangleWithNormals(element, {sp, sp, ep}, {null, normal, null}); - addCurveTriangleWithNormals(element, {sp, ep, ep}, {normal, normal, null}); - }; - - auto addTriangleForConcave = [&](const QQuadPath::Element &element, - const QVector2D &sp, - const QVector2D &ep, - const QVector2D &cp) { - addTriangleForLine(element, sp, ep, cp); - }; - - auto addTriangleForConvex = [&](const QQuadPath::Element &element, - const QVector2D &sp, - const QVector2D &ep, - const QVector2D &cp) { - addCurveTriangle(element, sp, ep, cp); - // Add two triangles on the outer side to get some more AA - - constexpr QVector2D null; - // First triangle on the line sp-cp, replacing ep - { - const QVector2D normal = outsideNormal(sp, cp, ep); - addCurveTriangleWithNormals(element, {sp, sp, cp}, {null, normal, null}); - } - - // Second triangle on the line ep-cp, replacing sp - { - const QVector2D normal = outsideNormal(ep, cp, sp); - addCurveTriangleWithNormals(element, {ep, ep, cp}, {null, normal, null}); - } - }; - - auto addFillTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3) { - constexpr QVector3D uv(0.0, 1.0, -1.0); - node->appendTriangle(p1, p2, p3, [&uv](QVector2D) { return uv; } ); - - wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f}); // 0 - wfVertices.append({p3.x(), p3.y(), 0.0f, 1.0f, 0.0f}); // 1 - wfVertices.append({p2.x(), p2.y(), 0.0f, 0.0f, 1.0f}); // 2 - }; - - for (int i = 0; i < pathData.fillPath.elementCount(); ++i) { - iteratePath(pathData.fillPath, i, [&](const QQuadPath::Element &element, int index) { - QPointF sp(element.startPoint().toPointF()); //### to much conversion to and from pointF - QPointF cp(element.controlPoint().toPointF()); - QPointF ep(element.endPoint().toPointF()); - if (element.isSubpathStart()) - internalHull.moveTo(sp); - if (element.isLine()) { - internalHull.lineTo(ep); - linePointHash.insert(toRoundedPair(sp), index); - } else { - if (element.isConvex()) { - internalHull.lineTo(ep); - addTriangleForConvex(element, toRoundedVec2D(sp), toRoundedVec2D(ep), toRoundedVec2D(cp)); - convexPointHash.insert(toRoundedPair(sp), index); - } else { - internalHull.lineTo(cp); - internalHull.lineTo(ep); - addTriangleForConcave(element, toRoundedVec2D(sp), toRoundedVec2D(ep), toRoundedVec2D(cp)); - concaveControlPointHash.insert(toRoundedPair(cp), index); - } - } - }); - } - - auto makeHashable = [](const QVector2D &p) -> QPair { - return qMakePair(qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f); - }; - // Points in p are already rounded do 1/32 - // Returns false if the triangle needs to be split. Adds the triangle to the graphics buffers and returns true otherwise. - // (Does not handle ambiguous vertices that are on multiple unrelated lines/curves) - auto onSameSideOfLine = [](const QVector2D &p1, - const QVector2D &p2, - const QVector2D &linePoint, - const QVector2D &lineNormal) { - float side1 = testSideOfLineByNormal(linePoint, lineNormal, p1); - float side2 = testSideOfLineByNormal(linePoint, lineNormal, p2); - return side1 * side2 >= 0; - }; - auto pointInSafeSpace = [&](const QVector2D &p, const QQuadPath::Element &element) -> bool { - const QVector2D a = element.startPoint(); - const QVector2D b = element.endPoint(); - const QVector2D c = element.controlPoint(); - // There are "safe" areas of the curve also across the baseline: the curve can never cross: - // line1: the line through A and B' - // line2: the line through B and A' - // Where A' = A "mirrored" through C and B' = B "mirrored" through C - const QVector2D n1 = calcNormalVector(a, c + (c - b)); - const QVector2D n2 = calcNormalVector(b, c + (c - a)); - bool safeSideOf1 = onSameSideOfLine(p, c, a, n1); - bool safeSideOf2 = onSameSideOfLine(p, c, b, n2); - return safeSideOf1 && safeSideOf2; - }; - - auto handleTriangle = [&](const QVector2D (&p)[3]) -> bool { - int lineElementIndex = -1; - int concaveElementIndex = -1; - int convexElementIndex = -1; - - bool foundElement = false; - int si = -1; - int ei = -1; - for (int i = 0; i < 3; ++i) { - if (auto found = linePointHash.constFind(makeHashable(p[i])); found != linePointHash.constEnd()) { - // check if this triangle is on a line, i.e. if one point is the sp and another is the ep of the same path element - const auto &element = pathData.fillPath.elementAt(*found); - for (int j = 0; j < 3; ++j) { - if (i != j && roundVec2D(element.endPoint()) == p[j]) { - if (foundElement) - return false; // More than one edge on path: must split - lineElementIndex = *found; - si = i; - ei = j; - foundElement = true; - } - } - } else if (auto found = concaveControlPointHash.constFind(makeHashable(p[i])); found != concaveControlPointHash.constEnd()) { - // check if this triangle is on the tangent line of a concave curve, - // i.e if one point is the cp, and the other is sp or ep - // TODO: clean up duplicated code (almost the same as the lineElement path above) - const auto &element = pathData.fillPath.elementAt(*found); - for (int j = 0; j < 3; ++j) { - if (i == j) - continue; - if (roundVec2D(element.startPoint()) == p[j] || roundVec2D(element.endPoint()) == p[j]) { - if (foundElement) - return false; // More than one edge on path: must split - concaveElementIndex = *found; - // The tangent line is p[i] - p[j] - si = i; - ei = j; - foundElement = true; - } - } - } else if (auto found = convexPointHash.constFind(makeHashable(p[i])); found != convexPointHash.constEnd()) { - // check if this triangle is on a curve, i.e. if one point is the sp and another is the ep of the same path element - const auto &element = pathData.fillPath.elementAt(*found); - for (int j = 0; j < 3; ++j) { - if (i != j && roundVec2D(element.endPoint()) == p[j]) { - if (foundElement) - return false; // More than one edge on path: must split - convexElementIndex = *found; - si = i; - ei = j; - foundElement = true; - } - } - } - } - if (lineElementIndex != -1) { - int ci = (6 - si - ei) % 3; // 1+2+3 is 6, so missing number is 6-n1-n2 - addTriangleForLine(pathData.fillPath.elementAt(lineElementIndex), p[si], p[ei], p[ci]); - } else if (concaveElementIndex != -1) { - addCurveTriangle(pathData.fillPath.elementAt(concaveElementIndex), p[0], p[1], p[2]); - } else if (convexElementIndex != -1) { - int oi = (6 - si - ei) % 3; - const auto &otherPoint = p[oi]; - const auto &element = pathData.fillPath.elementAt(convexElementIndex); - // We have to test whether the triangle can cross the line - // TODO: use the toplevel element's safe space - bool safeSpace = pointInSafeSpace(otherPoint, element); - if (safeSpace) { - addCurveTriangle(element, p[0], p[1], p[2]); - } else { - // Find a point inside the triangle that's also in the safe space - QVector2D newPoint = (p[0] + p[1] + p[2]) / 3; - // We should calculate the point directly, but just do a lazy implementation for now: - for (int i = 0; i < 7; ++i) { - safeSpace = pointInSafeSpace(newPoint, element); - if (safeSpace) - break; - newPoint = (p[si] + p[ei] + newPoint) / 3; - } - if (safeSpace) { - // Split triangle. We know the original triangle is only on one path element, so the other triangles are both fill. - // Curve triangle is (sp, ep, np) - addCurveTriangle(element, p[si], p[ei], newPoint); - // The other two are (sp, op, np) and (ep, op, np) - addFillTriangle(p[si], p[oi], newPoint); - addFillTriangle(p[ei], p[oi], newPoint); - } else { - // fallback to fill if we can't find a point in safe space - addFillTriangle(p[0], p[1], p[2]); - } - } - - } else { - addFillTriangle(p[0], p[1], p[2]); - } - return true; - }; - - QTriangleSet triangles = qTriangulate(internalHull); + QVector wfVertices; - const quint32 *idxTable = static_cast(triangles.indices.data()); - for (int triangle = 0; triangle < triangles.indices.size() / 3; ++triangle) { - const quint32 *idx = &idxTable[triangle * 3]; + QSGCurveProcessor::processFill(pathData.fillPath, + pathData.fillRule, + [&wfVertices, &node](const std::array &v, + const std::array &n, + QSGCurveProcessor::uvForPointCallback uvForPoint) + { + node->appendTriangle(v, n, uvForPoint); - QVector2D p[3]; - for (int i = 0; i < 3; ++i) { - p[i] = toRoundedVec2D(QPointF(triangles.vertices.at(idx[i] * 2), - triangles.vertices.at(idx[i] * 2 + 1))); - } - if (qFuzzyIsNull(determinant(p[0], p[1], p[2]))) - continue; // Skip degenerate triangles - bool needsSplit = !handleTriangle(p); - if (needsSplit) { - QVector2D c = (p[0] + p[1] + p[2]) / 3; - for (int i = 0; i < 3; ++i) { - qSwap(c, p[i]); - handleTriangle(p); - qSwap(c, p[i]); - } - } - } + wfVertices.append({v.at(0).x(), v.at(0).y(), 1.0f, 0.0f, 0.0f}); // 0 + wfVertices.append({v.at(1).x(), v.at(1).y(), 0.0f, 1.0f, 0.0f}); // 1 + wfVertices.append({v.at(2).x(), v.at(2).y(), 0.0f, 0.0f, 1.0f}); // 2 + }); QVector indices = node->uncookedIndexes(); if (indices.size() > 0) { @@ -727,9 +407,28 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addTriangulatingStr QPen pen = pathData.pen; stroker.process(vp, pen, {}, {}); - auto *node = new QQuickShapeCurveNode; + auto *node = new QSGCurveFillNode; node->setGradientType(pathData.gradientType); + auto uvForPoint = [](QVector2D v1, QVector2D v2, QVector2D p) + { + double divisor = v1.x() * v2.y() - v2.x() * v1.y(); + + float u = (p.x() * v2.y() - p.y() * v2.x()) / divisor; + float v = (p.y() * v1.x() - p.x() * v1.y()) / divisor; + + return QVector2D(u, v); + }; + + // Find uv coordinates for the point p, for a quadratic curve from p0 to p2 with control point p1 + // also works for a line from p0 to p2, where p1 is on the inside of the path relative to the line + auto curveUv = [uvForPoint](QVector2D p0, QVector2D p1, QVector2D p2, QVector2D p) + { + QVector2D v1 = 2 * (p1 - p0); + QVector2D v2 = p2 - v1 - p0; + return uvForPoint(v1, v2, p - p0); + }; + auto findPointOtherSide = [](const QVector2D &startPoint, const QVector2D &endPoint, const QVector2D &referencePoint){ QVector2D baseLine = endPoint - startPoint; @@ -748,7 +447,7 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addTriangulatingStr return; } - auto uvForPoint = [&p1, &p2, &p3](QVector2D p) { + auto uvForPoint = [&p1, &p2, &p3, curveUv](QVector2D p) { auto uv = curveUv(p1, p2, p3, p); return QVector3D(uv.x(), uv.y(), 0.0f); // Line }; @@ -843,663 +542,44 @@ void QQuickShapeCurveRenderer::deleteAndClear(NodeList *nodeList) nodeList->clear(); } -namespace { - -/* - Clever triangle overlap algorithm. Stack Overflow says: - - You can prove that the two triangles do not collide by finding an edge (out of the total 6 - edges that make up the two triangles) that acts as a separating line where all the vertices - of one triangle lie on one side and the vertices of the other triangle lie on the other side. - If you can find such an edge then it means that the triangles do not intersect otherwise the - triangles are colliding. -*/ -using TrianglePoints = std::array; -using LinePoints = std::array; - -// The sign of the determinant tells the winding order: positive means counter-clockwise - -static inline double determinant(const TrianglePoints &p) -{ - return determinant(p[0], p[1], p[2]); -} - -// Fix the triangle so that the determinant is positive -static void fixWinding(TrianglePoints &p) -{ - double det = determinant(p); - if (det < 0.0) { - qSwap(p[0], p[1]); - } -} - -// Return true if the determinant is negative, i.e. if the winding order is opposite of the triangle p1,p2,p3. -// This means that p is strictly on the other side of p1-p2 relative to p3 [where p1,p2,p3 is a triangle with -// a positive determinant]. -bool checkEdge(QVector2D &p1, QVector2D &p2, QVector2D &p, float epsilon) -{ - return determinant(p1, p2, p) <= epsilon; -} - - -bool checkTriangleOverlap(TrianglePoints &triangle1, TrianglePoints &triangle2, float epsilon = 1.0/32) -{ - // See if there is an edge of triangle1 such that all vertices in triangle2 are on the opposite side - fixWinding(triangle1); - for (int i = 0; i < 3; i++) { - int ni = (i + 1) % 3; - if (checkEdge(triangle1[i], triangle1[ni], triangle2[0], epsilon) && - checkEdge(triangle1[i], triangle1[ni], triangle2[1], epsilon) && - checkEdge(triangle1[i], triangle1[ni], triangle2[2], epsilon)) - return false; - } - - // See if there is an edge of triangle2 such that all vertices in triangle1 are on the opposite side - fixWinding(triangle2); - for (int i = 0; i < 3; i++) { - int ni = (i + 1) % 3; - - if (checkEdge(triangle2[i], triangle2[ni], triangle1[0], epsilon) && - checkEdge(triangle2[i], triangle2[ni], triangle1[1], epsilon) && - checkEdge(triangle2[i], triangle2[ni], triangle1[2], epsilon)) - return false; - } - - return true; -} - -bool checkLineTriangleOverlap(TrianglePoints &triangle, LinePoints &line, float epsilon = 1.0/32) -{ - // See if all vertices of the triangle are on the same side of the line - bool s1 = determinant(line[0], line[1], triangle[0]) < 0; - auto s2 = determinant(line[0], line[1], triangle[1]) < 0; - auto s3 = determinant(line[0], line[1], triangle[2]) < 0; - // If all determinants have the same sign, then there is no overlap - if (s1 == s2 && s2 == s3) { - return false; - } - // See if there is an edge of triangle1 such that both vertices in line are on the opposite side - fixWinding(triangle); - for (int i = 0; i < 3; i++) { - int ni = (i + 1) % 3; - if (checkEdge(triangle[i], triangle[ni], line[0], epsilon) && - checkEdge(triangle[i], triangle[ni], line[1], epsilon)) - return false; - } - - return true; -} - -// We could slightly optimize this if we did fixWinding in advance -bool checkTriangleContains (QVector2D pt, QVector2D v1, QVector2D v2, QVector2D v3, float epsilon = 1.0/32) -{ - float d1, d2, d3; - d1 = determinant(pt, v1, v2); - d2 = determinant(pt, v2, v3); - d3 = determinant(pt, v3, v1); - - bool allNegative = d1 < -epsilon && d2 < -epsilon && d3 < -epsilon; - bool allPositive = d1 > epsilon && d2 > epsilon && d3 > epsilon; - - return allNegative || allPositive; -} - -// e1 is always a concave curve. e2 can be curve or line -static bool isOverlap(const QQuadPath &path, int e1, int e2) -{ - const QQuadPath::Element &element1 = path.elementAt(e1); - const QQuadPath::Element &element2 = path.elementAt(e2); - - TrianglePoints t1{ element1.startPoint(), element1.controlPoint(), element1.endPoint() }; - - if (element2.isLine()) { - LinePoints line{ element2.startPoint(), element2.endPoint() }; - return checkLineTriangleOverlap(t1, line); - } else { - TrianglePoints t2{ element2.startPoint(), element2.controlPoint(), element2.endPoint() }; - return checkTriangleOverlap(t1, t2); - } - - return false; -} - -static bool isOverlap(const QQuadPath &path, int index, const QVector2D &vertex) -{ - const QQuadPath::Element &elem = path.elementAt(index); - return checkTriangleContains(vertex, elem.startPoint(), elem.controlPoint(), elem.endPoint()); -} - -struct TriangleData -{ - TrianglePoints points; - int pathElementIndex; - TrianglePoints normals; -}; - -// Returns a vector that is normal to baseLine, pointing to the right -QVector2D normalVector(QVector2D baseLine) -{ - QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized(); - return normal; -} - -// Returns a vector that is normal to the path and pointing to the right. If endSide is false -// the vector is normal to the start point, otherwise to the end point -QVector2D normalVector(const QQuadPath::Element &element, bool endSide = false) -{ - if (element.isLine()) - return normalVector(element.endPoint() - element.startPoint()); - else if (!endSide) - return normalVector(element.controlPoint() - element.startPoint()); - else - return normalVector(element.endPoint() - element.controlPoint()); -} - -// Returns a vector that is parallel to the path. If endSide is false -// the vector starts at the start point and points forward, -// otherwise it starts at the end point and points backward -QVector2D tangentVector(const QQuadPath::Element &element, bool endSide = false) -{ - if (element.isLine()) { - if (!endSide) - return element.endPoint() - element.startPoint(); - else - return element.startPoint() - element.endPoint(); - } else { - if (!endSide) - return element.controlPoint() - element.startPoint(); - else - return element.controlPoint() - element.endPoint(); - } -} - -// Really simplistic O(n^2) triangulator - only intended for five points -QList simplePointTriangulator(const QList &pts, const QList &normals, int elementIndex) -{ - int count = pts.size(); - Q_ASSERT(count >= 3); - Q_ASSERT(normals.size() == count); - - // First we find the convex hull: it's always in positive determinant winding order - QList hull; - float det1 = determinant(pts[0], pts[1], pts[2]); - if (det1 > 0) - hull << 0 << 1 << 2; - else - hull << 2 << 1 << 0; - auto connectableInHull = [&](int idx) -> QList { - QList r; - const int n = hull.size(); - const auto &pt = pts[idx]; - for (int i = 0; i < n; ++i) { - const auto &i1 = hull.at(i); - const auto &i2 = hull.at((i+1) % n); - if (determinant(pts[i1], pts[i2], pt) < 0.0f) - r << i; - } - return r; - }; - for (int i = 3; i < count; ++i) { - auto visible = connectableInHull(i); - if (visible.isEmpty()) - continue; - int visCount = visible.count(); - int hullCount = hull.count(); - // Find where the visible part of the hull starts. (This is the part we need to triangulate to, - // and the part we're going to replace. "visible" contains the start point of the line segments that are visible from p. - int boundaryStart = visible[0]; - for (int j = 0; j < visCount - 1; ++j) { - if ((visible[j] + 1) % hullCount != visible[j+1]) { - boundaryStart = visible[j + 1]; - break; - } - } - // Finally replace the points that are now inside the hull - // We insert the new point after boundaryStart, and before boundaryStart + visCount (modulo...) - // and remove the points in between - int pointsToKeep = hullCount - visCount + 1; - QList newHull; - newHull << i; - for (int j = 0; j < pointsToKeep; ++j) { - newHull << hull.at((j + boundaryStart + visCount) % hullCount); - } - hull = newHull; - } - - // Now that we have a convex hull, we can trivially triangulate it - QList ret; - for (int i = 1; i < hull.size() - 1; ++i) { - int i0 = hull[0]; - int i1 = hull[i]; - int i2 = hull[i+1]; - ret.append({{pts[i0], pts[i1], pts[i2]}, elementIndex, {normals[i0], normals[i1], normals[i2]}}); - } - return ret; -} - -static bool needsSplit(const QQuadPath::Element &el, float penWidth) -{ - Q_UNUSED(penWidth) - const auto v1 = el.controlPoint() - el.startPoint(); - const auto v2 = el.endPoint() - el.controlPoint(); - float cos = QVector2D::dotProduct(v1, v2) / (v1.length() * v2.length()); - return cos < 0.9; -} -static void splitElementIfNecessary(QQuadPath &path, int index, float penWidth) -{ - auto &e = path.elementAt(index); - if (e.isLine()) - return; - if (e.childCount() == 0) { - if (needsSplit(e, penWidth)) - path.splitElementAt(index); - } else { - for (int i = 0; i < e.childCount(); ++i) - splitElementIfNecessary(path, e.indexOfChild(i), penWidth); - } -} - -static QQuadPath subdivide(const QQuadPath &path, int subdivisions, float penWidth) -{ - QQuadPath newPath = path; - - for (int i = 0; i < subdivisions; ++i) - for (int j = 0; j < newPath.elementCount(); j++) - splitElementIfNecessary(newPath, j, penWidth); - return newPath; -} - -static QList customTriangulator2(const QQuadPath &path, float penWidth, Qt::PenJoinStyle joinStyle, Qt::PenCapStyle capStyle, float miterLimit) -{ - const bool bevelJoin = joinStyle == Qt::BevelJoin; - const bool roundJoin = joinStyle == Qt::RoundJoin; - const bool miterJoin = !bevelJoin && !roundJoin; - - const bool roundCap = capStyle == Qt::RoundCap; - const bool squareCap = capStyle == Qt::SquareCap; - // We can't use the simple miter for miter joins, since the shader currently only supports round joins - const bool simpleMiter = joinStyle == Qt::RoundJoin; - - Q_ASSERT(miterLimit > 0 || !miterJoin); - float inverseMiterLimit = miterJoin ? 1.0f / miterLimit : 1.0; - - const float penFactor = penWidth / 2; - - // Returns {inner1, inner2, outer1, outer2, outerMiter} - // where foo1 is for the end of element1 and foo2 is for the start of element2 - // and inner1 == inner2 unless we had to give up finding a decent point - auto calculateJoin = [&](const QQuadPath::Element *element1, const QQuadPath::Element *element2, - bool &outerBisectorWithinMiterLimit, bool &innerIsRight, bool &giveUp) -> std::array - { - outerBisectorWithinMiterLimit = true; - innerIsRight = true; - giveUp = false; - if (!element1) { - Q_ASSERT(element2); - QVector2D n = normalVector(*element2).normalized(); - return {n, n, -n, -n, -n}; - } - if (!element2) { - Q_ASSERT(element1); - QVector2D n = normalVector(*element1, true).normalized(); - return {n, n, -n, -n, -n}; - } - - Q_ASSERT(element1->endPoint() == element2->startPoint()); - - const auto p1 = element1->isLine() ? element1->startPoint() : element1->controlPoint(); - const auto p2 = element1->endPoint(); - const auto p3 = element2->isLine() ? element2->endPoint() : element2->controlPoint(); - - const auto v1 = (p1 - p2).normalized(); - const auto v2 = (p3 - p2).normalized(); - const auto b = (v1 + v2); - - constexpr float epsilon = 1.0f / 32.0f; - bool smoothJoin = qAbs(b.x()) < epsilon && qAbs(b.y()) < epsilon; - - if (smoothJoin) { - // v1 and v2 are almost parallel and pointing in opposite directions - // angle bisector formula will give an almost null vector: use normal of bisector of normals instead - QVector2D n1(-v1.y(), v1.x()); - QVector2D n2(-v2.y(), v2.x()); - QVector2D n = (n2 - n1).normalized(); - return {n, n, -n, -n, -n}; - } - // Calculate the length of the bisector, so it will cover the entire miter. - // Using the identity sin(x/2) == sqrt((1 - cos(x)) / 2), and the fact that the - // dot product of two unit vectors is the cosine of the angle between them - // The length of the miter is w/sin(x/2) where x is the angle between the two elements - - const auto bisector = b.normalized(); - float cos2x = QVector2D::dotProduct(v1, v2); - cos2x = qMin(1.0f, cos2x); // Allow for float inaccuracy - float sine = sqrt((1.0f - cos2x) / 2); - innerIsRight = determinant(p1, p2, p3) > 0; - sine = qMax(sine, 0.01f); // Avoid divide by zero - float length = penFactor / sine; - - // Check if bisector is longer than one of the lines it's trying to bisect - - auto tooLong = [](QVector2D p1, QVector2D p2, QVector2D n, float length, float margin) -> bool { - auto v = p2 - p1; - // It's too long if the projection onto the bisector is longer than the bisector - // and the projection onto the normal to the bisector is shorter - // than the pen margin (that projection is just v - proj) - // (we're adding a 10% safety margin to make room for AA -- not exact) - auto projLen = QVector2D::dotProduct(v, n); - return projLen * 0.9f < length && (v - n * projLen).length() * 0.9 < margin; - }; - - - // The angle bisector of the tangent lines is not correct for curved lines. We could fix this by calculating - // the exact intersection point, but for now just give up and use the normals. - - giveUp = !element1->isLine() || !element2->isLine() - || tooLong(p1, p2, bisector, length, penFactor) - || tooLong(p3, p2, bisector, length, penFactor); - outerBisectorWithinMiterLimit = sine >= inverseMiterLimit / 2.0f; - bool simpleJoin = simpleMiter && outerBisectorWithinMiterLimit && !giveUp; - const QVector2D bn = bisector / sine; - - if (simpleJoin) - return {bn, bn, -bn, -bn, -bn}; // We only have one inner and one outer point TODO: change inner point when conflict/curve - const QVector2D n1 = normalVector(*element1, true).normalized(); - const QVector2D n2 = normalVector(*element2).normalized(); - if (giveUp) { - if (innerIsRight) - return {n1, n2, -n1, -n2, -bn}; - else - return {-n1, -n2, n1, n2, -bn}; - - } else { - if (innerIsRight) - return {bn, bn, -n1, -n2, -bn}; - else - return {bn, bn, n1, n2, -bn}; - } - }; - - QList ret; - - auto triangulateCurve = [&](int idx, const QVector2D &p1, const QVector2D &p2, const QVector2D &p3, const QVector2D &p4, - const QVector2D &n1, const QVector2D &n2, const QVector2D &n3, const QVector2D &n4) - { - const auto &element = path.elementAt(idx); - const auto &s = element.startPoint(); - const auto &c = element.controlPoint(); - const auto &e = element.endPoint(); - // TODO: Don't flatten the path in addCurveStrokeNodes, but iterate over the children here instead - bool controlPointOnRight = determinant(s, c, e) > 0; - QVector2D startNormal = normalVector(element).normalized(); - QVector2D endNormal = normalVector(element, true).normalized(); - QVector2D controlPointNormal = (startNormal + endNormal).normalized(); - if (controlPointOnRight) - controlPointNormal = -controlPointNormal; - QVector2D p5 = c + controlPointNormal * penFactor; // This is too simplistic - TrianglePoints t1{p1, p2, p5}; - TrianglePoints t2{p3, p4, p5}; - bool simpleCase = !checkTriangleOverlap(t1, t2); - - if (simpleCase) { - ret.append({{p1, p2, p5}, idx, {n1, n2, controlPointNormal}}); - ret.append({{p3, p4, p5}, idx, {n3, n4, controlPointNormal}}); - if (controlPointOnRight) { - ret.append({{p1, p3, p5}, idx, {n1, n3, controlPointNormal}}); - } else { - ret.append({{p2, p4, p5}, idx, {n2, n4, controlPointNormal}}); - } - } else { - ret.append(simplePointTriangulator({p1, p2, p5, p3, p4}, {n1, n2, controlPointNormal, n3, n4}, idx)); - } - }; - - // Each element is calculated independently, so we don't have to special-case closed sub-paths. - // Take care so the end points of one element are precisely equal to the start points of the next. - // Any additional triangles needed for joining are added at the end of the current element. - - int count = path.elementCount(); - int subStart = 0; - while (subStart < count) { - int subEnd = subStart; - for (int i = subStart + 1; i < count; ++i) { - const auto &e = path.elementAt(i); - if (e.isSubpathStart()) { - subEnd = i - 1; - break; - } - if (i == count - 1) { - subEnd = i; - break; - } - } - bool closed = path.elementAt(subStart).startPoint() == path.elementAt(subEnd).endPoint(); - const int subCount = subEnd - subStart + 1; - - auto addIdx = [&](int idx, int delta) -> int { - int subIdx = idx - subStart; - if (closed) - subIdx = (subIdx + subCount + delta) % subCount; - else - subIdx += delta; - return subStart + subIdx; - }; - auto elementAt = [&](int idx, int delta) -> const QQuadPath::Element * { - int subIdx = idx - subStart; - if (closed) { - subIdx = (subIdx + subCount + delta) % subCount; - return &path.elementAt(subStart + subIdx); - } - subIdx += delta; - if (subIdx >= 0 && subIdx < subCount) - return &path.elementAt(subStart + subIdx); - return nullptr; - }; - - for (int i = subStart; i <= subEnd; ++i) { - const auto &element = path.elementAt(i); - const auto *nextElement = elementAt(i, +1); - const auto *prevElement = elementAt(i, -1); - - const auto &s = element.startPoint(); - const auto &e = element.endPoint(); - - bool startInnerIsRight; - bool startBisectorWithinMiterLimit; // Not used - bool giveUpOnStartJoin; // Not used - auto startJoin = calculateJoin(prevElement, &element, - startBisectorWithinMiterLimit, startInnerIsRight, - giveUpOnStartJoin); - const QVector2D &startInner = startJoin[1]; - const QVector2D &startOuter = startJoin[3]; - - bool endInnerIsRight; - bool endBisectorWithinMiterLimit; - bool giveUpOnEndJoin; - auto endJoin = calculateJoin(&element, nextElement, - endBisectorWithinMiterLimit, endInnerIsRight, - giveUpOnEndJoin); - QVector2D endInner = endJoin[0]; - QVector2D endOuter = endJoin[2]; - QVector2D nextOuter = endJoin[3]; - QVector2D outerB = endJoin[4]; - - QVector2D p1, p2, p3, p4; - QVector2D n1, n2, n3, n4; - - if (startInnerIsRight) { - n1 = startInner; - n2 = startOuter; - } else { - n1 = startOuter; - n2 = startInner; - } - - p1 = s + n1 * penFactor; - p2 = s + n2 * penFactor; - - // repeat logic above for the other end: - if (endInnerIsRight) { - n3 = endInner; - n4 = endOuter; - } else { - n3 = endOuter; - n4 = endInner; - } - - p3 = e + n3 * penFactor; - p4 = e + n4 * penFactor; - - // End caps - - if (!prevElement) { - QVector2D capSpace = tangentVector(element).normalized() * -penFactor; - if (roundCap) { - p1 += capSpace; - p2 += capSpace; - } else if (squareCap) { - QVector2D c1 = p1 + capSpace; - QVector2D c2 = p2 + capSpace; - ret.append({{p1, s, c1}, -1, {}}); - ret.append({{c1, s, c2}, -1, {}}); - ret.append({{p2, s, c2}, -1, {}}); - } - } - if (!nextElement) { - QVector2D capSpace = tangentVector(element, true).normalized() * -penFactor; - if (roundCap) { - p3 += capSpace; - p4 += capSpace; - } else if (squareCap) { - QVector2D c3 = p3 + capSpace; - QVector2D c4 = p4 + capSpace; - ret.append({{p3, e, c3}, -1, {}}); - ret.append({{c3, e, c4}, -1, {}}); - ret.append({{p4, e, c4}, -1, {}}); - } - } - - if (element.isLine()) { - ret.append({{p1, p2, p3}, i, {n1, n2, n3}}); - ret.append({{p2, p3, p4}, i, {n2, n3, n4}}); - } else { - triangulateCurve(i, p1, p2, p3, p4, n1, n2, n3, n4); - } - - bool trivialJoin = simpleMiter && endBisectorWithinMiterLimit && !giveUpOnEndJoin; - if (!trivialJoin && nextElement) { - // inside of join (opposite of bevel) is defined by - // triangle s, e, next.e - bool innerOnRight = endInnerIsRight; - - const auto outer1 = e + endOuter * penFactor; - const auto outer2 = e + nextOuter * penFactor; - //const auto inner = e + endInner * penFactor; - - if (bevelJoin || (miterJoin && !endBisectorWithinMiterLimit)) { - ret.append({{outer1, e, outer2}, -1, {}}); - } else if (roundJoin) { - ret.append({{outer1, e, outer2}, i, {}}); - QVector2D nn = calcNormalVector(outer1, outer2).normalized() * penFactor; - if (!innerOnRight) - nn = -nn; - ret.append({{outer1, outer1 + nn, outer2}, i, {}}); - ret.append({{outer1 + nn, outer2, outer2 + nn}, i, {}}); - - } else if (miterJoin) { - QVector2D outer = e + outerB * penFactor; - ret.append({{outer1, e, outer}, -2, {}}); - ret.append({{outer, e, outer2}, -2, {}}); - } - - if (!giveUpOnEndJoin) { - QVector2D inner = e + endInner * penFactor; - ret.append({{inner, e, outer1}, i, {endInner, {}, endOuter}}); - // The remaining triangle ought to be done by nextElement, but we don't have start join logic there (yet) - int nextIdx = addIdx(i, +1); - ret.append({{inner, e, outer2}, nextIdx, {endInner, {}, nextOuter}}); - } - } - } - subStart = subEnd + 1; - } - return ret; -} - -}; - QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addCurveStrokeNodes(const PathData &pathData, NodeList *debugNodes) { NodeList ret; const QColor &color = pathData.pen.color(); const bool debug = debugVisualization() & DebugCurves; - auto *node = new QQuickShapeStrokeNode; + auto *node = new QSGCurveStrokeNode; node->setDebug(0.2f * debug); QVector wfVertices; - float miterLimit = pathData.pen.miterLimit(); - float penWidth = pathData.pen.widthF(); + const float miterLimit = pathData.pen.miterLimit(); + const float penWidth = pathData.pen.widthF(); static const int subdivisions = qEnvironmentVariable("QT_QUICKSHAPES_STROKE_SUBDIVISIONS", QStringLiteral("3")).toInt(); - auto thePath = subdivide(pathData.strokePath, subdivisions, penWidth).flattened(); // TODO: don't flatten, but handle it in the triangulator - auto triangles = customTriangulator2(thePath, penWidth, pathData.pen.joinStyle(), pathData.pen.capStyle(), miterLimit); - - auto addCurveTriangle = [&](const QQuadPath::Element &element, const TriangleData &t){ - const QVector2D &p0 = t.points[0]; - const QVector2D &p1 = t.points[1]; - const QVector2D &p2 = t.points[2]; - if (element.isLine()) { - node->appendTriangle(t.points, - LinePoints{element.startPoint(), element.endPoint()}, - t.normals); - } else { - node->appendTriangle(t.points, - { element.startPoint(), element.controlPoint(), element.endPoint()}, - t.normals); - } - wfVertices.append({p0.x(), p0.y(), 1.0f, 0.0f, 0.0f}); - wfVertices.append({p1.x(), p1.y(), 0.0f, 1.0f, 0.0f}); - wfVertices.append({p2.x(), p2.y(), 0.0f, 0.0f, 1.0f}); - }; - - auto addBevelTriangle = [&](const TrianglePoints &p) - { - QVector2D fp1 = p[0]; - QVector2D fp2 = p[2]; - - // That describes a path that passes through those points. We want the stroke - // edge, so we need to shift everything down by the stroke offset - - QVector2D nn = calcNormalVector(p[0], p[2]); - if (determinant(p) < 0) - nn = -nn; - float delta = penWidth / 2; - - QVector2D offset = nn.normalized() * delta; - fp1 += offset; - fp2 += offset; - - TrianglePoints n; - // p1 is inside, so n[1] is {0,0} - n[0] = (p[0] - p[1]).normalized(); - n[2] = (p[2] - p[1]).normalized(); - node->appendTriangle(p, LinePoints{fp1, fp2}, n); - - wfVertices.append({p[0].x(), p[0].y(), 1.0f, 0.0f, 0.0f}); - wfVertices.append({p[1].x(), p[1].y(), 0.0f, 1.0f, 0.0f}); - wfVertices.append({p[2].x(), p[2].y(), 0.0f, 0.0f, 1.0f}); - }; - - for (const auto &triangle : triangles) { - if (triangle.pathElementIndex < 0) { - addBevelTriangle(triangle.points); - continue; - } - const auto &element = thePath.elementAt(triangle.pathElementIndex); - addCurveTriangle(element, triangle); - } + QSGCurveProcessor::processStroke(pathData.strokePath, + miterLimit, + penWidth, + pathData.pen.joinStyle(), + pathData.pen.capStyle(), + [&wfVertices, &node](const std::array &s, + const std::array &p, + const std::array &n, + bool isLine) + { + const QVector2D &p0 = s.at(0); + const QVector2D &p1 = s.at(1); + const QVector2D &p2 = s.at(2); + if (isLine) + node->appendTriangle(s, std::array{p.at(0), p.at(2)}, n); + else + node->appendTriangle(s, p, n); + + wfVertices.append({p0.x(), p0.y(), 1.0f, 0.0f, 0.0f}); + wfVertices.append({p1.x(), p1.y(), 0.0f, 1.0f, 0.0f}); + wfVertices.append({p2.x(), p2.y(), 0.0f, 0.0f, 1.0f}); + }, + subdivisions); auto indexCopy = node->uncookedIndexes(); // uncookedIndexes get delete on cooking @@ -1534,123 +614,4 @@ QQuickShapeCurveRenderer::NodeList QQuickShapeCurveRenderer::addCurveStrokeNodes return ret; } -// TODO: we could optimize by preprocessing e1, since we call this function multiple times on the same -// elements -static void handleOverlap(QQuadPath &path, int e1, int e2, int recursionLevel = 0) -{ - if (!isOverlap(path, e1, e2)) { - return; - } - - if (recursionLevel > 8) { - qCDebug(lcShapeCurveRenderer) << "Triangle overlap: recursion level" << recursionLevel << "aborting!"; - return; - } - - if (path.elementAt(e1).childCount() > 1) { - auto e11 = path.indexOfChildAt(e1, 0); - auto e12 = path.indexOfChildAt(e1, 1); - handleOverlap(path, e11, e2, recursionLevel + 1); - handleOverlap(path, e12, e2, recursionLevel + 1); - } else if (path.elementAt(e2).childCount() > 1) { - auto e21 = path.indexOfChildAt(e2, 0); - auto e22 = path.indexOfChildAt(e2, 1); - handleOverlap(path, e1, e21, recursionLevel + 1); - handleOverlap(path, e1, e22, recursionLevel + 1); - } else { - path.splitElementAt(e1); - auto e11 = path.indexOfChildAt(e1, 0); - auto e12 = path.indexOfChildAt(e1, 1); - bool overlap1 = isOverlap(path, e11, e2); - bool overlap2 = isOverlap(path, e12, e2); - if (!overlap1 && !overlap2) - return; // no more overlap: success! - - // We need to split more: - if (path.elementAt(e2).isLine()) { - // Splitting a line won't help, so we just split e1 further - if (overlap1) - handleOverlap(path, e11, e2, recursionLevel + 1); - if (overlap2) - handleOverlap(path, e12, e2, recursionLevel + 1); - } else { - // See if splitting e2 works: - path.splitElementAt(e2); - auto e21 = path.indexOfChildAt(e2, 0); - auto e22 = path.indexOfChildAt(e2, 1); - if (overlap1) { - handleOverlap(path, e11, e21, recursionLevel + 1); - handleOverlap(path, e11, e22, recursionLevel + 1); - } - if (overlap2) { - handleOverlap(path, e12, e21, recursionLevel + 1); - handleOverlap(path, e12, e22, recursionLevel + 1); - } - } - } -} - -// Test if element contains a start point of another element -static void handleOverlap(QQuadPath &path, int e1, const QVector2D vertex, int recursionLevel = 0) -{ - // First of all: Ignore the next element: it trivially overlaps (maybe not necessary: we do check for strict containment) - if (vertex == path.elementAt(e1).endPoint() || !isOverlap(path, e1, vertex)) - return; - if (recursionLevel > 8) { - qCDebug(lcShapeCurveRenderer) << "Vertex overlap: recursion level" << recursionLevel << "aborting!"; - return; - } - - // Don't split if we're already split - if (path.elementAt(e1).childCount() == 0) - path.splitElementAt(e1); - - handleOverlap(path, path.indexOfChildAt(e1, 0), vertex, recursionLevel + 1); - handleOverlap(path, path.indexOfChildAt(e1, 1), vertex, recursionLevel + 1); -} - -void QQuickShapeCurveRenderer::solveOverlaps(QQuadPath &path) -{ - for (int i = 0; i < path.elementCount(); i++) { - auto &element = path.elementAt(i); - // only concave curve overlap is problematic, as long as we don't allow self-intersecting curves - if (element.isLine() || element.isConvex()) - continue; - - for (int j = 0; j < path.elementCount(); j++) { - if (i == j) - continue; // Would be silly to test overlap with self - auto &other = path.elementAt(j); - if (!other.isConvex() && !other.isLine() && j < i) - continue; // We have already tested this combination, so no need to test again - handleOverlap(path, i, j); - } - } - - static const int handleConcaveJoint = qEnvironmentVariableIntValue("QT_QUICKSHAPES_WIP_CONCAVE_JOINT"); - if (handleConcaveJoint) { - // Note that the joint between two non-concave elements can also be concave, so we have to - // test all convex elements to see if there is a vertex in any of them. We could do it the other way - // by identifying concave joints, but then we would have to know which side is the inside - // TODO: optimization potential! Maybe do that at the same time as we identify concave curves? - - // We do this in a separate loop, since the triangle/triangle test above is more expensive, and - // if we did this first, there would be more triangles to test - for (int i = 0; i < path.elementCount(); i++) { - auto &element = path.elementAt(i); - if (!element.isConvex()) - continue; - - for (int j = 0; j < path.elementCount(); j++) { - // We only need to check one point per element, since all subpaths are closed - // Could do smartness to skip elements that cannot overlap, but let's do it the easy way first - if (i == j) - continue; - const auto &other = path.elementAt(j); - handleOverlap(path, i, other.startPoint()); - } - } - } -} - QT_END_NAMESPACE diff --git a/src/quickshapes/qquickshapecurverenderer_p.h b/src/quickshapes/qquickshapecurverenderer_p.h index ae3689d5d6..1b8e7065ab 100644 --- a/src/quickshapes/qquickshapecurverenderer_p.h +++ b/src/quickshapes/qquickshapecurverenderer_p.h @@ -17,8 +17,9 @@ #include #include -#include -#include +#include +#include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include +#include QT_BEGIN_NAMESPACE @@ -57,7 +59,7 @@ public: void setRootNode(QSGNode *node); - using NodeList = QVector; + using NodeList = QVector; enum DirtyFlag { @@ -79,15 +81,15 @@ public: private: struct PathData { - bool isFillVisible() const { return fillColor.alpha() > 0 || gradientType != NoGradient; } + bool isFillVisible() const { return fillColor.alpha() > 0 || gradientType != QGradient::NoGradient; } bool isStrokeVisible() const { return validPenWidth && pen.color().alpha() > 0 && pen.style() != Qt::NoPen; } - FillGradientType gradientType = NoGradient; - GradientDesc gradient; + QGradient::Type gradientType = QGradient::NoGradient; + QSGGradientCache::GradientDesc gradient; QPainterPath originalPath; QQuadPath path; QQuadPath fillPath; @@ -111,8 +113,6 @@ private: NodeList addTriangulatingStrokerNodes(const PathData &pathData, NodeList *debugNodes); NodeList addCurveStrokeNodes(const PathData &pathData, NodeList *debugNodes); - void solveOverlaps(QQuadPath &path); - QSGNode *m_rootNode; QVector m_paths; static int debugVisualizationFlags; diff --git a/src/quickshapes/qquickshapegenericrenderer.cpp b/src/quickshapes/qquickshapegenericrenderer.cpp index a7f830ad01..896d85a623 100644 --- a/src/quickshapes/qquickshapegenericrenderer.cpp +++ b/src/quickshapes/qquickshapegenericrenderer.cpp @@ -7,6 +7,8 @@ #include #include +#include + #if QT_CONFIG(thread) #include #endif @@ -176,7 +178,7 @@ void QQuickShapeGenericRenderer::setFillGradient(int index, QQuickShapeGradient ShapePathData &d(m_sp[index]); if (gradient) { d.fillGradient.stops = gradient->gradientStops(); // sorted - d.fillGradient.spread = gradient->spread(); + d.fillGradient.spread = QGradient::Spread(gradient->spread()); if (QQuickShapeLinearGradient *g = qobject_cast(gradient)) { d.fillGradientActive = LinearGradient; d.fillGradient.a = QPointF(g->x1(), g->y1()); @@ -754,8 +756,8 @@ void QQuickShapeLinearGradientRhiShader::updateSampledImage(RenderState &state, QQuickShapeLinearGradientMaterial *m = static_cast(newMaterial); QQuickShapeGenericStrokeFillNode *node = m->node(); - const QQuickShapeGradientCacheKey cacheKey(node->m_fillGradient.stops, node->m_fillGradient.spread); - QSGTexture *t = QQuickShapeGradientCache::cacheForRhi(state.rhi())->get(cacheKey); + const QSGGradientCacheKey cacheKey(node->m_fillGradient.stops, QGradient::Spread(node->m_fillGradient.spread)); + QSGTexture *t = QSGGradientCache::cacheForRhi(state.rhi())->get(cacheKey); t->commitTextureOperations(state.rhi(), state.resourceUpdateBatch()); *texture = t; } @@ -777,8 +779,8 @@ int QQuickShapeLinearGradientMaterial::compare(const QSGMaterial *other) const if (a == b) return 0; - const QQuickAbstractPathRenderer::GradientDesc *ga = &a->m_fillGradient; - const QQuickAbstractPathRenderer::GradientDesc *gb = &b->m_fillGradient; + const QSGGradientCache::GradientDesc *ga = &a->m_fillGradient; + const QSGGradientCache::GradientDesc *gb = &b->m_fillGradient; if (int d = ga->spread - gb->spread) return d; @@ -883,8 +885,8 @@ void QQuickShapeRadialGradientRhiShader::updateSampledImage(RenderState &state, QQuickShapeRadialGradientMaterial *m = static_cast(newMaterial); QQuickShapeGenericStrokeFillNode *node = m->node(); - const QQuickShapeGradientCacheKey cacheKey(node->m_fillGradient.stops, node->m_fillGradient.spread); - QSGTexture *t = QQuickShapeGradientCache::cacheForRhi(state.rhi())->get(cacheKey); + const QSGGradientCacheKey cacheKey(node->m_fillGradient.stops, QGradient::Spread(node->m_fillGradient.spread)); + QSGTexture *t = QSGGradientCache::cacheForRhi(state.rhi())->get(cacheKey); t->commitTextureOperations(state.rhi(), state.resourceUpdateBatch()); *texture = t; } @@ -906,8 +908,8 @@ int QQuickShapeRadialGradientMaterial::compare(const QSGMaterial *other) const if (a == b) return 0; - const QQuickAbstractPathRenderer::GradientDesc *ga = &a->m_fillGradient; - const QQuickAbstractPathRenderer::GradientDesc *gb = &b->m_fillGradient; + const QSGGradientCache::GradientDesc *ga = &a->m_fillGradient; + const QSGGradientCache::GradientDesc *gb = &b->m_fillGradient; if (int d = ga->spread - gb->spread) return d; @@ -1001,8 +1003,8 @@ void QQuickShapeConicalGradientRhiShader::updateSampledImage(RenderState &state, QQuickShapeConicalGradientMaterial *m = static_cast(newMaterial); QQuickShapeGenericStrokeFillNode *node = m->node(); - const QQuickShapeGradientCacheKey cacheKey(node->m_fillGradient.stops, node->m_fillGradient.spread); - QSGTexture *t = QQuickShapeGradientCache::cacheForRhi(state.rhi())->get(cacheKey); + const QSGGradientCacheKey cacheKey(node->m_fillGradient.stops, QGradient::Spread(node->m_fillGradient.spread)); + QSGTexture *t = QSGGradientCache::cacheForRhi(state.rhi())->get(cacheKey); t->commitTextureOperations(state.rhi(), state.resourceUpdateBatch()); *texture = t; } @@ -1024,8 +1026,8 @@ int QQuickShapeConicalGradientMaterial::compare(const QSGMaterial *other) const if (a == b) return 0; - const QQuickAbstractPathRenderer::GradientDesc *ga = &a->m_fillGradient; - const QQuickAbstractPathRenderer::GradientDesc *gb = &b->m_fillGradient; + const QSGGradientCache::GradientDesc *ga = &a->m_fillGradient; + const QSGGradientCache::GradientDesc *gb = &b->m_fillGradient; if (int d = ga->a.x() - gb->a.x()) return d; diff --git a/src/quickshapes/qquickshapegenericrenderer_p.h b/src/quickshapes/qquickshapegenericrenderer_p.h index a5ec01329d..1455c02424 100644 --- a/src/quickshapes/qquickshapegenericrenderer_p.h +++ b/src/quickshapes/qquickshapegenericrenderer_p.h @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -101,7 +102,7 @@ private: Qt::FillRule fillRule; QPainterPath path; FillGradientType fillGradientActive; - GradientDesc fillGradient; + QSGGradientCache::GradientDesc fillGradient; VertexContainerType fillVertices; IndexContainerType fillIndices; QSGGeometry::Type indexType; @@ -188,7 +189,7 @@ public: void activateMaterial(QQuickWindow *window, Material m); // shadow data for custom materials - QQuickAbstractPathRenderer::GradientDesc m_fillGradient; + QSGGradientCache::GradientDesc m_fillGradient; private: QScopedPointer m_material; diff --git a/src/quickshapes/qquickshapestrokenode.cpp b/src/quickshapes/qquickshapestrokenode.cpp deleted file mode 100644 index 2b07cd5bca..0000000000 --- a/src/quickshapes/qquickshapestrokenode.cpp +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#include "qquickshapestrokenode_p.h" -#include "qquickshapestrokenode_p_p.h" - -QT_BEGIN_NAMESPACE - -QQuickShapeStrokeNode::QQuickShapeStrokeNode() -{ - setFlag(OwnsGeometry, true); - setGeometry(new QSGGeometry(attributes(), 0, 0)); - updateMaterial(); -} - -void QQuickShapeStrokeNode::QQuickShapeStrokeNode::updateMaterial() -{ - m_material.reset(new QQuickShapeStrokeMaterial(this)); - setMaterial(m_material.data()); -} - -// Take the start, control and end point of a curve and return the points A, B, C -// representing the curve as Q(s) = A*s*s + B*s + C -std::array QQuickShapeStrokeNode::curveABC(const std::array &p) -{ - QVector2D a = p[0] - 2*p[1] + p[2]; - QVector2D b = 2*p[1] - 2*p[0]; - QVector2D c = p[0]; - - return {a, b, c}; -} - -// Curve from p[0] to p[2] with control point p[1] -void QQuickShapeStrokeNode::appendTriangle(const std::array &v, - const std::array &p, - const std::array &n) -{ - auto abc = curveABC(p); - - int currentVertex = m_uncookedVertexes.count(); - - for (int i = 0; i < 3; ++i) { - m_uncookedVertexes.append( { v[i].x(), v[i].y(), - abc[0].x(), abc[0].y(), abc[1].x(), abc[1].y(), abc[2].x(), abc[2].y(), - n[i].x(), n[i].y() } ); - } - m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2; -} - -// Straight line from p0 to p1 -void QQuickShapeStrokeNode::appendTriangle(const std::array &v, - const std::array &p, - const std::array &n) -{ - // We could reduce this to a linear equation by setting A to (0,0). - // However, then we cannot use the cubic solution and need an additional - // code path in the shader. The following formulation looks more complicated - // but allows to always use the cubic solution. - auto A = p[1] - p[0]; - auto B = QVector2D(0., 0.); - auto C = p[0]; - - int currentVertex = m_uncookedVertexes.count(); - -// for (auto v : QList>({{v0, n0}, {v1, n1}, {v2, n2}})) { - for (int i = 0; i < 3; ++i) { - m_uncookedVertexes.append( { v[i].x(), v[i].y(), - A.x(), A.y(), B.x(), B.y(), C.x(), C.y(), - n[i].x(), n[i].y() } ); - } - m_uncookedIndexes << currentVertex << currentVertex + 1 << currentVertex + 2; -} - -void QQuickShapeStrokeNode::cookGeometry() -{ - QSGGeometry *g = geometry(); - if (g->indexType() != QSGGeometry::UnsignedIntType) { - g = new QSGGeometry(attributes(), - m_uncookedVertexes.size(), - m_uncookedIndexes.size(), - QSGGeometry::UnsignedIntType); - setGeometry(g); - } else { - g->allocate(m_uncookedVertexes.size(), m_uncookedIndexes.size()); - } - - g->setDrawingMode(QSGGeometry::DrawTriangles); - memcpy(g->vertexData(), - m_uncookedVertexes.constData(), - g->vertexCount() * g->sizeOfVertex()); - memcpy(g->indexData(), - m_uncookedIndexes.constData(), - g->indexCount() * g->sizeOfIndex()); - - m_uncookedIndexes.clear(); - m_uncookedVertexes.clear(); -} - -const QSGGeometry::AttributeSet &QQuickShapeStrokeNode::attributes() -{ - static QSGGeometry::Attribute data[] = { - QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute), //vertexCoord - QSGGeometry::Attribute::createWithAttributeType(1, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //A - QSGGeometry::Attribute::createWithAttributeType(2, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //B - QSGGeometry::Attribute::createWithAttributeType(3, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //C - QSGGeometry::Attribute::createWithAttributeType(4, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), //normalVector - }; - static QSGGeometry::AttributeSet attrs = { 5, sizeof(StrokeVertex), data }; - return attrs; -} - -QT_END_NAMESPACE diff --git a/src/quickshapes/qquickshapestrokenode_p.cpp b/src/quickshapes/qquickshapestrokenode_p.cpp deleted file mode 100644 index d5eb901c66..0000000000 --- a/src/quickshapes/qquickshapestrokenode_p.cpp +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#include "qquickshapestrokenode_p_p.h" -#include "qquickshapestrokenode_p.h" - -#include "qquickshapegenericrenderer_p.h" - -QT_BEGIN_NAMESPACE - -bool QQuickShapeStrokeMaterialShader::updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) -{ - bool changed = false; - QByteArray *buf = state.uniformData(); - Q_ASSERT(buf->size() >= 64); - - if (state.isMatrixDirty()) { - const QMatrix4x4 m = state.combinedMatrix(); - memcpy(buf->data(), m.constData(), 64); - - float matrixScale = qSqrt(qAbs(state.determinant())) * state.devicePixelRatio(); - memcpy(buf->data()+64, &matrixScale, 4); - changed = true; - } - - if (state.isOpacityDirty()) { - const float opacity = state.opacity(); - memcpy(buf->data() + 64 + 4, &opacity, 4); - changed = true; - } - - int offset = 64+16; - - auto *newMaterial = static_cast(newEffect); - auto *oldMaterial = static_cast(oldEffect); - - auto *newNode = newMaterial != nullptr ? newMaterial->node() : nullptr; - auto *oldNode = oldMaterial != nullptr ? oldMaterial->node() : nullptr; - - if (newNode == nullptr) - return changed; - - QVector4D newStrokeColor(newNode->color().redF(), - newNode->color().greenF(), - newNode->color().blueF(), - newNode->color().alphaF()); - QVector4D oldStrokeColor = oldNode != nullptr - ? QVector4D(oldNode->color().redF(), - oldNode->color().greenF(), - oldNode->color().blueF(), - oldNode->color().alphaF()) - : QVector4D{}; - - if (oldNode == nullptr || oldStrokeColor != newStrokeColor) { - memcpy(buf->data() + offset, &newStrokeColor, 16); - changed = true; - } - offset += 16; - - if (oldNode == nullptr || newNode->strokeWidth() != oldNode->strokeWidth()) { - float w = newNode->strokeWidth(); - memcpy(buf->data() + offset, &w, 4); - changed = true; - } - offset += 4; - if (oldNode == nullptr || newNode->debug() != oldNode->debug()) { - float w = newNode->debug(); - memcpy(buf->data() + offset, &w, 4); - changed = true; - } -// offset += 4; - - return changed; -} - -int QQuickShapeStrokeMaterial::compare(const QSGMaterial *other) const -{ - int typeDif = type() - other->type(); - if (!typeDif) { - auto *othernode = static_cast(other)->node(); - if (node()->color() != othernode->color()) - return node()->color().rgb() < othernode->color().rgb() ? -1 : 1; - if (node()->strokeWidth() != othernode->strokeWidth()) - return node()->strokeWidth() < othernode->strokeWidth() ? -1 : 1; - } - return typeDif; -} - -QT_END_NAMESPACE diff --git a/src/quickshapes/qquickshapestrokenode_p.h b/src/quickshapes/qquickshapestrokenode_p.h deleted file mode 100644 index 5c2713f052..0000000000 --- a/src/quickshapes/qquickshapestrokenode_p.h +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef QQUICKSHAPESTROKENODE_P_H -#define QQUICKSHAPESTROKENODE_P_H - -#include - -#include "qquickshapeabstractcurvenode_p.h" -#include "qquickshapegenericrenderer_p.h" -#include "qquickshapestrokenode_p_p.h" - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists for the convenience -// of a number of Qt sources files. This header file may change from -// version to version without notice, or even be removed. -// -// We mean it. -// - -QT_BEGIN_NAMESPACE - -class QQuickShapeStrokeNode : public QQuickShapeAbstractCurveNode -{ -public: - QQuickShapeStrokeNode(); - - void setColor(QColor col) override - { - m_color = col; - } - - QColor color() const - { - return m_color; - } - - void setStrokeWidth(float width) - { - m_strokeWidth = width; - } - - float strokeWidth() const - { - return m_strokeWidth; - } - - void appendTriangle(const std::array &v, // triangle vertices - const std::array &p, // curve points - const std::array &n); // vertex normals - void appendTriangle(const std::array &v, // triangle vertices - const std::array &p, // line points - const std::array &n); // vertex normals - - void cookGeometry(); - - static const QSGGeometry::AttributeSet &attributes(); - - QVector uncookedIndexes() const - { - return m_uncookedIndexes; - } - - float debug() const - { - return m_debug; - } - - void setDebug(float newDebug) - { - m_debug = newDebug; - } - -private: - - struct StrokeVertex - { - float x, y; - float ax, ay; - float bx, by; - float cx, cy; - float nx, ny; //normal vector: direction to move vertext to account for AA - }; - - void updateMaterial(); - - static std::array curveABC(const std::array &p); - - QColor m_color; - float m_strokeWidth = 0.0f; - float m_debug = 0.0f; - -protected: - QScopedPointer m_material; - - QVector m_uncookedVertexes; - QVector m_uncookedIndexes; -}; - -QT_END_NAMESPACE - -#endif // QQUICKSHAPESTROKENODE_P_H diff --git a/src/quickshapes/qquickshapestrokenode_p_p.h b/src/quickshapes/qquickshapestrokenode_p_p.h deleted file mode 100644 index 97fcad3cd8..0000000000 --- a/src/quickshapes/qquickshapestrokenode_p_p.h +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef QQUICKSHAPESTROKENODE_P_P_H -#define QQUICKSHAPESTROKENODE_P_P_H - -#include - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Qt API. It exists for the convenience -// of a number of Qt sources files. This header file may change from -// version to version without notice, or even be removed. -// -// We mean it. -// - -QT_BEGIN_NAMESPACE - -class QQuickShapeStrokeNode; -class QQuickShapeStrokeMaterial; - -class QQuickShapeStrokeMaterialShader : public QSGMaterialShader -{ -public: - QQuickShapeStrokeMaterialShader() - { - setShaderFileName(VertexStage, - QStringLiteral(":/qt-project.org/shapes/shaders_ng/shapestroke.vert.qsb")); - setShaderFileName(FragmentStage, - QStringLiteral(":/qt-project.org/shapes/shaders_ng/shapestroke.frag.qsb")); - } - - bool updateUniformData(RenderState &state, QSGMaterial *newEffect, QSGMaterial *oldEffect) override; -}; - - -class QQuickShapeStrokeMaterial : public QSGMaterial -{ -public: - QQuickShapeStrokeMaterial(QQuickShapeStrokeNode *node) - : m_node(node) - { - setFlag(Blending, true); - } - - int compare(const QSGMaterial *other) const override; - - QQuickShapeStrokeNode *node() const - { - return m_node; - } - -protected: - QSGMaterialType *type() const override - { - static QSGMaterialType t; - return &t; - } - QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override - { - return new QQuickShapeStrokeMaterialShader; - } - - QQuickShapeStrokeNode *m_node; -}; - -QT_END_NAMESPACE - -#endif // QQUICKSHAPESTROKENODE_P_P_H diff --git a/src/quickshapes/shaders_ng/shapecurve.frag b/src/quickshapes/shaders_ng/shapecurve.frag deleted file mode 100644 index 2badca31a7..0000000000 --- a/src/quickshapes/shaders_ng/shapecurve.frag +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#version 440 - -layout(location = 0) in vec4 qt_TexCoord; -layout(location = 1) in vec4 gradient; - -#if defined(LINEARGRADIENT) -layout(location = 2) in float gradTabIndex; -#elif defined(RADIALGRADIENT) || defined(CONICALGRADIENT) -layout(location = 2) in vec2 coord; -#endif - - -layout(location = 0) out vec4 fragColor; - -layout(std140, binding = 0) uniform buf { - mat4 qt_Matrix; - float matrixScale; - float opacity; - float debug; - float reserved3; - -#if defined(STROKE) - vec4 strokeColor; - float strokeWidth; - float reserved4; - float reserved5; - float reserved6; -#endif - -#if defined(LINEARGRADIENT) - vec2 gradientStart; - vec2 gradientEnd; -#elif defined(RADIALGRADIENT) - vec2 translationPoint; - vec2 focalToCenter; - float centerRadius; - float focalRadius; -#elif defined(CONICALGRADIENT) - vec2 translationPoint; - float angle; -#else - vec4 color; -#endif -} ubuf; - -#define INVERSE_2PI 0.1591549430918953358 - -#if defined(LINEARGRADIENT) || defined(RADIALGRADIENT) || defined(CONICALGRADIENT) -layout(binding = 1) uniform sampler2D gradTabTexture; -#endif - -vec4 baseColor() -{ -#if defined(LINEARGRADIENT) - return texture(gradTabTexture, vec2(gradTabIndex, 0.5)); -#elif defined(RADIALGRADIENT) - float rd = ubuf.centerRadius - ubuf.focalRadius; - float b = 2.0 * (rd * ubuf.focalRadius + dot(coord, ubuf.focalToCenter)); - float fmp2_m_radius2 = -ubuf.focalToCenter.x * ubuf.focalToCenter.x - ubuf.focalToCenter.y * ubuf.focalToCenter.y + rd * rd; - float inverse_2_fmp2_m_radius2 = 1.0 / (2.0 * fmp2_m_radius2); - float det = b * b - 4.0 * fmp2_m_radius2 * ((ubuf.focalRadius * ubuf.focalRadius) - dot(coord, coord)); - vec4 result = vec4(0.0); - if (det >= 0.0) { - float detSqrt = sqrt(det); - float w = max((-b - detSqrt) * inverse_2_fmp2_m_radius2, (-b + detSqrt) * inverse_2_fmp2_m_radius2); - if (ubuf.focalRadius + w * (ubuf.centerRadius - ubuf.focalRadius) >= 0.0) - result = texture(gradTabTexture, vec2(w, 0.5)); - } - - return result; -#elif defined(CONICALGRADIENT) - float t; - if (abs(coord.y) == abs(coord.x)) - t = (atan(-coord.y + 0.002, coord.x) + ubuf.angle) * INVERSE_2PI; - else - t = (atan(-coord.y, coord.x) + ubuf.angle) * INVERSE_2PI; - return texture(gradTabTexture, vec2(t - floor(t), 0.5)); -#else - return vec4(ubuf.color.rgb, 1.0) * ubuf.color.a; -#endif -} - -void main() -{ - float f = qt_TexCoord.z * (qt_TexCoord.x * qt_TexCoord.x - qt_TexCoord.y) // curve - + (1.0 - abs(qt_TexCoord.z)) * (qt_TexCoord.x - qt_TexCoord.y); // line - -#if defined(USE_DERIVATIVES) - float _ddx = dFdx(f); - float _ddy = dFdy(f); - float df = length(vec2(_ddx, _ddy)); -#else - // We calculate the partial derivatives for f'(x, y) based on knowing the partial derivatives - // for the texture coordinates (u, v). - // So for curves: - // f(x,y) = u(x, y) * u(x, y) - v(x, y) - // f(x,y) = p(u(x,y)) - v(x, y) where p(u) = u^2 - // So f'(x, y) = p'(u(x, y)) * u'(x, y) - v'(x, y) - // (by chain rule and sum rule) - // f'(x, y) = 2 * u(x, y) * u'(x, y) - v'(x, y) - // And so: - // df/dx = 2 * u(x, y) * du/dx - dv/dx - // df/dy = 2 * u(x, y) * du/dy - dv/dy - // - // and similarly for straight lines: - // f(x, y) = u(x, y) - v(x, y) - // f'(x, y) = dudx - dvdx - - float dudx = gradient.x; - float dvdx = gradient.y; - float dudy = gradient.z; - float dvdy = gradient.w; - - // Test with analytic derivatives -// dudx = dFdx(qt_TexCoord.x); -// dvdx = dFdx(qt_TexCoord.y); -// dudy = dFdy(qt_TexCoord.x); -// dvdy = dFdy(qt_TexCoord.y); - - float dfx_curve = 2.0f * qt_TexCoord.x * dudx - dvdx; - float dfy_curve = 2.0f * qt_TexCoord.x * dudy - dvdy; - - float dfx_line = dudx - dvdx; - float dfy_line = dudy - dvdy; - - float dfx = qt_TexCoord.z * dfx_curve + (1.0 - abs(qt_TexCoord.z)) * dfx_line; - float dfy = qt_TexCoord.z * dfy_curve + (1.0 - abs(qt_TexCoord.z)) * dfy_line; - float df = length(vec2(dfx, dfy)); -#endif - - float isLine = 1.0 - abs(qt_TexCoord.z); - float isCurve = 1.0 - isLine; - float debugR = isCurve * min(1.0, 1.0 - qt_TexCoord.z); - float debugG = isLine; - float debugB = isCurve * min(1.0, 1.0 - qt_TexCoord.z * -1.0) + debugG; - vec3 debugColor = vec3(debugR, debugG, debugB); - -#if defined(STROKE) - float distance = (f / df); // distance from centre of fragment to line - - float halfStrokeWidth = ubuf.strokeWidth / 2.0; - - // calculate stroke - float strokeCoverage = 1.0 - clamp(0.5 + abs(distance) - halfStrokeWidth, 0.0, 1.0); - vec4 stroke = ubuf.strokeColor * strokeCoverage; - - float fillCoverage = clamp(0.5 + f / df, 0.0, 1.0); - vec4 fill = baseColor() * fillCoverage; - - vec4 combined = fill * (1.0 - stroke.a) + stroke * stroke.a; - - // finally mix in debug - fragColor = mix(combined, vec4(debugColor, 1.0), ubuf.debug) * ubuf.opacity; -#else - // Special case: mask out concave curve in "negative space". - int specialCaseMask = 1 - int(qt_TexCoord.w != 0.0) * (int(qt_TexCoord.x < 0.0) + int(qt_TexCoord.x > 1.0)); - float fillCoverage = clamp(0.5 + f / df, 0.0, 1.0) * float(specialCaseMask); - - fragColor = mix(baseColor() * fillCoverage, vec4(debugColor, 1.0), ubuf.debug) * ubuf.opacity; -#endif -} diff --git a/src/quickshapes/shaders_ng/shapecurve.vert b/src/quickshapes/shaders_ng/shapecurve.vert deleted file mode 100644 index 64e32b5517..0000000000 --- a/src/quickshapes/shaders_ng/shapecurve.vert +++ /dev/null @@ -1,81 +0,0 @@ -#version 440 - -layout(location = 0) in vec4 vertexCoord; -layout(location = 1) in vec4 vertexTexCoord; -layout(location = 2) in vec4 vertexGradient; -layout(location = 3) in vec2 normalVector; - -layout(location = 0) out vec4 qt_TexCoord; -layout(location = 1) out vec4 gradient; - -#if defined(LINEARGRADIENT) -layout(location = 2) out float gradTabIndex; -#elif defined(RADIALGRADIENT) || defined(CONICALGRADIENT) -layout(location = 2) out vec2 coord; -#endif - -layout(std140, binding = 0) uniform buf { - mat4 qt_Matrix; - float matrixScale; - float opacity; - float debug; - float reserved3; - -#if defined(STROKE) - vec4 strokeColor; - float strokeWidth; - float reserved4; - float reserved5; - float reserved6; -#endif - -#if defined(LINEARGRADIENT) - vec2 gradientStart; - vec2 gradientEnd; -#elif defined(RADIALGRADIENT) - vec2 translationPoint; - vec2 focalToCenter; - float centerRadius; - float focalRadius; -#elif defined(CONICALGRADIENT) - vec2 translationPoint; - float angle; -#else - vec4 color; -#endif -} ubuf; - -out gl_PerVertex { vec4 gl_Position; }; - -#define SQRT2 1.41421356237 - -vec4 addOffset(vec4 texCoord, vec2 offset, vec4 duvdxy) -{ - float dudx = duvdxy.x; - float dvdx = duvdxy.y; - float dudy = duvdxy.z; - float dvdy = duvdxy.w; - float u = offset.x * dudx + offset.y * dudy; - float v = offset.x * dvdx + offset.y * dvdy; - // special case external triangles for concave curves - int specialCase = int(texCoord.z > 0) * (int(offset.x != 0) + int(offset.y != 0)); - return vec4(texCoord.x + u, texCoord.y + v, texCoord.z, float(specialCase)); -} - -void main() -{ - vec2 offset = normalVector * SQRT2/ubuf.matrixScale; - - qt_TexCoord = addOffset(vertexTexCoord, offset, vertexGradient); - - gradient = vertexGradient / ubuf.matrixScale; - -#if defined(LINEARGRADIENT) - vec2 gradVec = ubuf.gradientEnd - ubuf.gradientStart; - gradTabIndex = dot(gradVec, vertexCoord.xy - ubuf.gradientStart.xy) / dot(gradVec, gradVec); -#elif defined(RADIALGRADIENT) || defined(CONICALGRADIENT) - coord = vertexCoord.xy - ubuf.translationPoint; -#endif - - gl_Position = ubuf.qt_Matrix * (vertexCoord + vec4(offset, 0, 0)); -} diff --git a/src/quickshapes/shaders_ng/shapestroke.frag b/src/quickshapes/shaders_ng/shapestroke.frag deleted file mode 100644 index 846500c44c..0000000000 --- a/src/quickshapes/shaders_ng/shapestroke.frag +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#version 440 - -layout(location = 0) in vec4 P; -layout(location = 1) in vec2 A; -layout(location = 2) in vec2 B; -layout(location = 3) in vec2 C; -layout(location = 4) in vec2 HG; -layout(location = 5) in float offset; - -layout(location = 0) out vec4 fragColor; - -layout(std140, binding = 0) uniform buf { - mat4 qt_Matrix; - - float matrixScale; - float opacity; - float reserved2; - float reserved3; - - vec4 strokeColor; - - float strokeWidth; - float debug; - float reserved5; - float reserved6; -} ubuf; - -float cuberoot(float x) -{ - return sign(x) * pow(abs(x), 1 / 3.); -} - -#define PI 3.1415926538 - -vec3 solveDepressedCubic(float p, float q) -{ - float D = q * q / 4. + p * p * p / 27.; - - float u1 = cuberoot(-q / 2. - sqrt(D)); - float u2 = cuberoot(-q / 2. + sqrt(D)); - vec3 rootsD1 = vec3(u1 - p / (3. * u1), u2 - p / (3. * u2), 0); - - float v = 2.*sqrt(-p / 3.); - float t = acos(3. * q / p / v) / 3.; - float k = 2. * PI / 3.; - vec3 rootsD2 = vec3(v * cos(t), v * cos(t - k), v * cos(t - 2. * k)); - - return D > 0 ? rootsD1 : rootsD2; -} - -mat2 qInverse(mat2 matrix) { - float a = matrix[0][0], b = matrix[0][1]; - float c = matrix[1][0], d = matrix[1][1]; - - float determinant = a * d - b * c; - float invDet = 1.0 / determinant; - - mat2 inverseMatrix; - inverseMatrix[0][0] = d * invDet; - inverseMatrix[0][1] = -b * invDet; - inverseMatrix[1][0] = -c * invDet; - inverseMatrix[1][1] = a * invDet; - - return inverseMatrix; -} - -void main() -{ - vec3 s = solveDepressedCubic(HG.x, HG.y) - vec3(offset, offset, offset); - - vec2 Qmin = vec2(1e10, 1e10); - float dmin = 1e4; - for (int i = 0; i < 3; i++) { - float t = clamp(s[i], 0., 1.); - vec2 Q = A * t * t + B * t + C; - float d = length(Q - P.xy); - float foundNewMin = step(d, dmin); - dmin = min(d, dmin); - Qmin = foundNewMin * Q + (1. - foundNewMin) * Qmin; - } - vec2 n = (P.xy - Qmin) / dmin; - vec2 Q1 = (Qmin + ubuf.strokeWidth / 2. * n); - vec2 Q2 = (Qmin - ubuf.strokeWidth / 2. * n); - - // Converting to screen coordinates: -#if defined(USE_DERIVATIVES) - mat2 T = mat2(dFdx(P.x), dFdy(P.x), dFdx(P.y), dFdy(P.y)); - mat2 Tinv = qInverse(T); - vec2 Q1_s = Tinv * Q1; - vec2 Q2_s = Tinv * Q2; - vec2 P_s = Tinv * P.xy; - vec2 n_s = Tinv * n; - n_s = n_s / length(n_s); -#else - vec2 Q1_s = ubuf.matrixScale * Q1; - vec2 Q2_s = ubuf.matrixScale * Q2; - vec2 P_s = ubuf.matrixScale * P.xy; - vec2 n_s = n; -#endif - - // Geometric solution for anti aliasing using the known distances - // to the edges of the path in the screen coordinate system. - float dist1 = dot(P_s - Q1_s, n_s); - float dist2 = dot(P_s - Q2_s, n_s); - - // Calculate the fill coverage if the line is crossing the square cell - // normally (vertically or horizontally). - // float fillCoverageLin = clamp(0.5-dist1, 0., 1.) - clamp(0.5-dist2, 0., 1.); - - // Calculate the fill coverage if the line is crossing the square cell - // diagonally. - float fillCoverageDia = clamp(step(0., -dist1) + sign(dist1) * pow(max(0., sqrt(2.) / 2. - abs(dist1)), 2.), 0., 1.) - - clamp(step(0., -dist2) + sign(dist2) * pow(max(0., sqrt(2.) / 2. - abs(dist2)), 2.), 0., 1.); - - // Merge the normal and the diagonal solution. The merge factor is periodic - // in 90 degrees and 0/1 at 0 and 45 degree. The simple equation was - // estimated after numerical experiments. - // float mergeFactor = 2 * abs(n_s.x) * abs(n_s.y); - // float fillCoverage = mergeFactor * fillCoverageDia + (1-mergeFactor) * fillCoverageLin; - - // It seems to be sufficient to use the equation for the diagonal. - float fillCoverage = fillCoverageDia; - - // The center line is sometimes not filled because of numerical issues. This fixes this. - float centerline = step(ubuf.strokeWidth * 0.01, dmin); - fillCoverage = fillCoverage * centerline + min(1., ubuf.strokeWidth * ubuf.matrixScale) * (1. - centerline); - - fragColor = vec4(ubuf.strokeColor.rgb, 1.0) *ubuf.strokeColor.a * fillCoverage * ubuf.opacity - + ubuf.debug * vec4(0.0, 0.5, 1.0, 1.0) * (1.0 - fillCoverage) * ubuf.opacity; - -} diff --git a/src/quickshapes/shaders_ng/shapestroke.vert b/src/quickshapes/shaders_ng/shapestroke.vert deleted file mode 100644 index 9b22eb4eeb..0000000000 --- a/src/quickshapes/shaders_ng/shapestroke.vert +++ /dev/null @@ -1,76 +0,0 @@ -#version 440 - -layout(location = 0) in vec4 vertexCoord; -layout(location = 1) in vec2 inA; -layout(location = 2) in vec2 inB; -layout(location = 3) in vec2 inC; -layout(location = 4) in vec2 normalVector; - -layout(location = 0) out vec4 P; -layout(location = 1) out vec2 A; -layout(location = 2) out vec2 B; -layout(location = 3) out vec2 C; -layout(location = 4) out vec2 HG; -layout(location = 5) out float offset; - - -layout(std140, binding = 0) uniform buf { - mat4 qt_Matrix; - - float matrixScale; - float opacity; - float reserved2; - float reserved3; - - vec4 strokeColor; - - float strokeWidth; - float debug; - float reserved5; - float reserved6; -} ubuf; - -out gl_PerVertex { vec4 gl_Position; }; - -#define SQRT2 1.41421356237 - -float qdot(vec2 a, vec2 b) -{ - return a.x * b.x + a.y * b.y; -} - -void main() -{ - P = vertexCoord + vec4(normalVector, 0.0, 0.0) * SQRT2/ubuf.matrixScale; - - A = inA; - B = inB; - C = inC; - - // Find the parameters H, G for the depressed cubic - // t^2+H*t+G=0 - // that results from the equation - // Q'(s).(p-Q(s)) = 0 - // The last parameter is the static offset between s and t: - // s = t - b/(3a) - // use it to get back the parameter t - - // this is a constant for the curve - float a = -2. * qdot(A, A); - // this is a constant for the curve - float b = -3. * qdot(A, B); - //this is linear in p so it can be put into the shader with vertex data - float c = 2. * qdot(A, P.xy) - qdot(B, B) - 2. * qdot(A, C); - //this is linear in p so it can be put into the shader with vertex data - float d = qdot(B, P.xy) - qdot(B, C); - // convert to depressed cubic. - // both functions are linear in c and d and thus linear in p - float H = (3. * a * c - b * b) / (3. * a * a); - float G = (2. * b * b * b - 9. * a * b * c + 27. * a * a * d) / (27. * a * a * a); - HG = vec2(H, G); - offset = b/(3*a); - - - - gl_Position = ubuf.qt_Matrix * P; -} diff --git a/tests/baseline/scenegraph/data/text/text_emoji.qml b/tests/baseline/scenegraph/data/text/text_emoji.qml index d50f6d6715..c9b9a33715 100644 --- a/tests/baseline/scenegraph/data/text/text_emoji.qml +++ b/tests/baseline/scenegraph/data/text/text_emoji.qml @@ -2,14 +2,13 @@ import QtQuick 2.0 Item { width: 320 - height: 480 - + height: 680 Component { id: component Column { property variant listModel: model Repeater { - model: [Text.NativeRendering, Text.QtRendering] + model: [Text.NativeRendering, Text.QtRendering, Text.CurveRendering] Rectangle { width: text.implicitWidth height: text.implicitHeight diff --git a/tests/baseline/scenegraph/data/text/text_emoji_hebrew.qml b/tests/baseline/scenegraph/data/text/text_emoji_hebrew.qml index b67d584a12..4062690cd0 100644 --- a/tests/baseline/scenegraph/data/text/text_emoji_hebrew.qml +++ b/tests/baseline/scenegraph/data/text/text_emoji_hebrew.qml @@ -2,14 +2,14 @@ import QtQuick 2.0 Item { width: 320 - height: 480 + height: 680 Component { id: component Column { property variant listModel: model Repeater { - model: [Text.NativeRendering, Text.QtRendering] + model: [Text.NativeRendering, Text.QtRendering, Text.CurveRendering] Rectangle { width: text.implicitWidth height: text.implicitHeight diff --git a/tests/manual/painterpathquickshape/TextShape.qml b/tests/manual/painterpathquickshape/TextShape.qml index fa595d3390..f9cabf1ecd 100644 --- a/tests/manual/painterpathquickshape/TextShape.qml +++ b/tests/manual/painterpathquickshape/TextShape.qml @@ -7,7 +7,7 @@ import QtQuick.Controls import QtQuick.Dialogs ControlledShape { - fillRule: ShapePath.OddEvenFill + fillRule: ShapePath.WindingFill delegate: [ PathText { text: "foobar" diff --git a/tests/manual/textrendering/CMakeLists.txt b/tests/manual/textrendering/CMakeLists.txt new file mode 100644 index 0000000000..9987010192 --- /dev/null +++ b/tests/manual/textrendering/CMakeLists.txt @@ -0,0 +1,40 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) + +if (NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + project(textrendering LANGUAGES C CXX ASM) + find_package(Qt6BuildInternals COMPONENTS STANDALONE_TEST) +endif() + +find_package(Qt6 COMPONENTS ShaderTools) + +qt_internal_add_manual_test(textrendering + GUI + SOURCES + main.cpp + LIBRARIES + Qt::Gui + Qt::Qml + Qt::Quick + Qt::QuickPrivate +) + + +set(qml_resource_files + "main.qml" +) + +qt_internal_add_resource(textrendering "qml" + PREFIX + "/" + FILES + ${qml_resource_files} +) + +qt_add_qml_module(textrendering + VERSION 1.0 + URI TextRendering + RESOURCE_PREFIX / +) diff --git a/tests/manual/textrendering/main.cpp b/tests/manual/textrendering/main.cpp new file mode 100644 index 0000000000..0b2490f5df --- /dev/null +++ b/tests/manual/textrendering/main.cpp @@ -0,0 +1,26 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include +#include +#include + +int main(int argc, char *argv[]) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +#endif + QGuiApplication app(argc, argv); + app.setOrganizationName("QtProject"); + + QQmlApplicationEngine engine; + const QUrl url(QStringLiteral("qrc:/main.qml")); + QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, + &app, [url](QObject *obj, const QUrl &objUrl) { + if (!obj && url == objUrl) + QCoreApplication::exit(-1); + }, Qt::QueuedConnection); + engine.load(url); + + return app.exec(); +} diff --git a/tests/manual/textrendering/main.qml b/tests/manual/textrendering/main.qml new file mode 100644 index 0000000000..5d3a6489dd --- /dev/null +++ b/tests/manual/textrendering/main.qml @@ -0,0 +1,103 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes + +Window { + id: theWindow + width: 1024 + height: 768 + visible: true + title: qsTr("Text Rendering") + color: "white" + + Text { + id: dummyTextRendering + scale: scaleSlider.value + anchors.centerIn: parent + text: dummyText.text + font.pixelSize: fontSize.value + renderType: renderTypeCombo.currentIndex + style: styleCombo.currentIndex + styleColor: "indianred" + color: "black" + visible: renderTypeCombo.currentIndex <= 2 + } + + Shape { + id: dummyShapeRendering + anchors.centerIn: parent + scale: scaleSlider.value + visible: !dummyTextRendering.visible + width: boundingRect.width + height: boundingRect.height + preferredRendererType: shapesRendererCombo.currentIndex === 0 ? Shape.GeometryRenderer : Shape.CurveRenderer + + ShapePath { + id: shapePath + fillColor: "black" + strokeColor: styleCombo.currentIndex === 1 ? "indianred" : "transparent" + strokeStyle: ShapePath.SolidLine + strokeWidth: 1 + fillRule: ShapePath.WindingFill + PathText { + text: dummyText.text + font.pixelSize: fontSize.value + } + } + } + + RowLayout { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + TextField { + id: dummyText + text: "Foobar" + } + + Label { + text: "Scale:" + } + + Slider { + id: scaleSlider + from: 0.5 + to: 10 + value: 1 + } + + Label { + text: "Font size:" + } + + Slider { + id: fontSize + from: 1 + to: 1000 + value: 100 + Layout.fillWidth: true + } + + ComboBox { + id: styleCombo + model: [ "Normal", "Outline", "Raised", "Sunken" ] + } + + ComboBox { + id: renderTypeCombo + model: [ "QtRendering", "NativeRendering", "CurveRendering", "Qt Quick Shapes" ] + } + + ComboBox { + id: shapesRendererCombo + model: [ "GeometryRenderer", "CurveRenderer" ] + visible: renderTypeCombo.currentIndex > 2 + } + } +} -- cgit v1.2.3