Qt Quick 3D - Stencil Outline Extension Example | Qt Quick 3D 6.7.2
Description: Demonstrates how the QtQuick3D Render Extension can be used to implement stencil outlining.

- Qt 6.7
- Qt Quick 3D
- Qt Quick 3D Examples and Tutorials
- Qt Quick 3D - Stencil Outline Extension Example
- Qt Quick 3D | Commercial or GPLv3

# Qt Quick 3D - Stencil Outline Extension Example

     // Copyright (C) 2023 The Qt Company Ltd.
     // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

     import QtQuick
     import QtQuick.Controls
     import QtQuick.Layouts

     Item {
         id: colorPicker
         property bool isHovered: false
         // Mirror the internal value until re-bound
         property color color: root.color
         onColorChanged: {
             if (root.color != color)
                 root.color = color;
         }

         // When we manually edit a new color
         // this is the new color proposal
         signal colorModified(color: color)

         width: 100
         height: 26

         Rectangle {
             id: border
             color: "transparent"
             border.width: 2
             border.color: colorPicker.isHovered ? palette.dark : palette.alternateBase
             anchors.fill: parent

             Image {
                 anchors.fill: parent
                 anchors.margins: 4
                 source: "images/grid_8x8.png"
                 fillMode: Image.Tile
             }

             Rectangle {
                 anchors.fill: parent
                 anchors.margins: 4
                 color: root.color
             }

             MouseArea {
                 anchors.fill: parent
                 hoverEnabled: true
                 onEntered: colorPicker.isHovered = true
                 onExited: colorPicker.isHovered = false
                 onClicked: {
                     colorPickerPopup.open()
                 }
             }
         }

         Dialog {
             id: colorPickerPopup
             title: "Color Picker"

             anchors.centerIn: Overlay.overlay
             modal: true
             focus: true
             closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent

             Pane {
                 RowLayout {
                     id: root
                     property color color: "red"

                     Item {
                         implicitWidth: 135
                         implicitHeight: 135
                         ShaderEffect {
                             id: hsvWheel
                             anchors.centerIn: parent
                             blending: true
                             width: 128
                             height: 128
                             property real value: 1.0

                             fragmentShader: "qrc:/shaders/huesaturation.frag.qsb"

                             Item {
                                 id: reticule

                                 function reflectColor(hue, saturation) {
                                     let angleDegrees = hue * 360
                                     if (angleDegrees > 180)
                                         angleDegrees = angleDegrees - 360
                                     let angleRadians = angleDegrees * (Math.PI / 180)
                                     let vector = Qt.vector2d(Math.cos(angleRadians), Math.sin(angleRadians)).normalized().times(0.5).times(saturation)
                                     vector = vector.plus(Qt.vector2d(0.5, 0.5))
                                     reticule.x = vector.x * hsvWheel.width
                                     reticule.y = vector.y * hsvWheel.width
                                 }

                                 Rectangle {
                                     x: -5
                                     y: -5
                                     width: 10
                                     height: 10
                                     radius: 5
                                     color: "transparent"
                                     border.width: 1
                                     border.color: "black"

                                     Rectangle {
                                         x: 0.5
                                         y: 0.5
                                         width: 9
                                         height: 9
                                         radius: 4.5
                                         color: root.color
                                         border.width: 1
                                         border.color: "white"
                                     }
                                 }
                             }

                             MouseArea {
                                 anchors.fill: parent
                                 preventStealing: true

                                 function handleMouseMove(x : real, y : real, width : real) : vector2d {
                                     let normalizedX = x / width - 0.5;
                                     let normalizedY = y / width - 0.5;
                                     let angle = Math.atan2(normalizedY, normalizedX);
                                     let toCenter = Qt.vector2d(normalizedX, normalizedY);
                                     let radius = toCenter.length() * 2.0;
                                     let degrees = angle * (180 / Math.PI)
                                     if (degrees < 0)
                                         degrees = 360 + degrees
                                     let hue = degrees / 360

                                     let color = Qt.hsva(hue, radius, root.color.hsvValue, root.color.a)
                                     colorPicker.colorModified(color)
                                     root.color = color

                                     if (radius <= 1.0)
                                         return Qt.vector2d(x, y)
                                     // Limit to radius of 1.0
                                     toCenter = toCenter.normalized();
                                     let halfWidth = width * 0.5;
                                     let newX = halfWidth * toCenter.x + halfWidth
                                     let newY = halfWidth * toCenter.y + halfWidth
                                     return Qt.vector2d(newX, newY)
                                 }

                                 onClicked: (mouse) => {
                                                 let pos = handleMouseMove(mouse.x, mouse.y, hsvWheel.width)
                                                 reticule.x = pos.x;
                                                 reticule.y = pos.y;
                                 }

                                 onPositionChanged: (mouse) => {
                                                     let pos = handleMouseMove(mouse.x, mouse.y, hsvWheel.width)
                                                     reticule.x = pos.x;
                                                     reticule.y = pos.y;
                                 }
                             }
                         }
                     }

                     Component.onCompleted: {
                         updateColorSections()
                         reticule.reflectColor(root.color.hsvHue, root.color.hsvSaturation)
                     }

                     Connections {
                         target: root
                         function onColorChanged() {
                             root.updateColorSections()
                             reticule.reflectColor(root.color.hsvHue, root.color.hsvSaturation)
                         }
                     }
                     Connections {
                         target: colorPicker
                         function onColorChanged() {
                             root.color = colorPicker.color
                         }
                     }

                     function updateColorSections() {
                         rgbSection.redValue = root.color.r * 255
                         rgbSection.greenValue = root.color.g * 255
                         rgbSection.blueValue = root.color.b * 255
                         hsvSection.hueValue = root.color.hsvHue * 360
                         hsvSection.saturationValue = root.color.hsvSaturation * 100
                         hsvSection.valueValue = root.color.hsvValue  * 100
                         alphaSection.alphaValue = root.color.a * 255
                     }
                     ColumnLayout {

                         SectionLayout {
                             title: "RGB"
                             id: rgbSection
                             property int redValue: 0
                             property int greenValue: 0
                             property int blueValue: 0
                             implicitWidth: 300

                             function updateRGB() {
                                 let color =  Qt.rgba(redValue / 255, greenValue / 255, blueValue / 255, alphaSection.alphaValue / 255)
                                 colorPicker.colorModified(color)
                                 root.color = color
                             }

                             onRedValueChanged: hexTextField.updateText()
                             onGreenValueChanged: hexTextField.updateText()
                             onBlueValueChanged: hexTextField.updateText()

                             RowLayout {
                                 Layout.fillWidth: true
                                 Label {
                                     text: "R:"
                                 }
                                 Slider {
                                     id: redSlider
                                     Layout.fillWidth: true
                                     from: 0
                                     to: 255
                                     value: rgbSection.redValue
                                     onValueChanged: {
                                         if (value !== rgbSection.redValue) {
                                             rgbSection.redValue = value
                                             if (pressed)
                                                 rgbSection.updateRGB()
                                         }
                                     }
                                 }
                             }
                             SpinBox {
                                 Layout.fillWidth: true
                                 from: 0
                                 to: 255
                                 value: rgbSection.redValue
                                 onValueChanged: {
                                     if (value !== rgbSection.redValue)
                                         rgbSection.redValue = value
                                 }
                                 onValueModified: {
                                     rgbSection.updateRGB();
                                 }
                             }

                             RowLayout {
                                 Layout.fillWidth: true
                                 Label {
                                     text: "G:"
                                 }
                                 Slider {
                                     id: greenSlider
                                     Layout.fillWidth: true
                                     from: 0
                                     to: 255
                                     value: rgbSection.greenValue
                                     onValueChanged: {
                                         if (value !== rgbSection.greenValue) {
                                             rgbSection.greenValue = value
                                             if (pressed)
                                                 rgbSection.updateRGB()
                                         }
                                     }
                                 }
                             }
                             SpinBox {
                                 Layout.fillWidth: true
                                 from: 0
                                 to: 255
                                 value: rgbSection.greenValue
                                 onValueChanged: {
                                     if (value !== rgbSection.greenValue)
                                         rgbSection.greenValue = value
                                 }
                                 onValueModified: {
                                     rgbSection.updateRGB()
                                 }
                             }

                             RowLayout {
                                 Layout.fillWidth: true
                                 Label {
                                     text: "B:"
                                 }
                                 Slider {
                                     id: blueSlider
                                     Layout.fillWidth: true
                                     from: 0
                                     to: 255
                                     value: rgbSection.blueValue
                                     onValueChanged: {
                                         if (value !== rgbSection.blueValue) {
                                             rgbSection.blueValue = value
                                             if (pressed)
                                                 rgbSection.updateRGB()
                                         }
                                     }
                                 }
                             }
                             SpinBox {
                                 Layout.fillWidth: true
                                 from: 0
                                 to: 255
                                 value: rgbSection.blueValue
                                 onValueChanged: {
                                     if (value !== rgbSection.blueValue)
                                         rgbSection.blueValue = value
                                 }
                                 onValueModified: {
                                     rgbSection.updateRGB()
                                 }
                             }

                             Label {
                                 text: "Hex:"
                                 Layout.fillWidth: true
                             }
                             TextField {
                                 id: hexTextField
                                 maximumLength: 6
                                 Layout.alignment: Qt.AlignRight

                                 function updateText() {
                                     if (activeFocus)
                                         return

                                     let redText = rgbSection.redValue.toString(16).toUpperCase()
                                     let greenText = rgbSection.greenValue.toString(16).toUpperCase()
                                     let blueText = rgbSection.blueValue.toString(16).toUpperCase()
                                     if (redText.length == 1)
                                         redText = "0" + redText
                                     if (greenText.length == 1)
                                         greenText = "0" + greenText
                                     if (blueText.length == 1)
                                         blueText = "0" + blueText
                                     text = redText + greenText + blueText;
                                 }

                                 function expandText(text) {
                                     let newText = text.toUpperCase()
                                     let expandLength = 6 - newText.length
                                     for (let i = 0; i < expandLength; ++i)
                                         newText = "0" + newText
                                     return newText
                                 }

                                 Component.onCompleted: updateText()
                                 validator: RegularExpressionValidator {
                                     regularExpression: /^[0-9A-Fa-f]{0,6}$/
                                 }
                                 onTextChanged: {
                                     if (!acceptableInput)
                                         return;
                                     let colorText = expandText(text)
                                     rgbSection.redValue = parseInt(colorText.substr(0, 2), 16)
                                     rgbSection.greenValue = parseInt(colorText.substr(2, 2), 16)
                                     rgbSection.blueValue = parseInt(colorText.substr(4, 2), 16)
                                     if (activeFocus)
                                         rgbSection.updateRGB()
                                 }

                                 onAccepted: {
                                     text = expandText(text)
                                 }
                             }
                         }

                         SectionLayout {
                             title: "HSV"
                             id: hsvSection
                             property int hueValue: 0
                             property int saturationValue: 0
                             property int valueValue: 0
                             implicitWidth: 300

                             function updateHSV() {
                                 let color = Qt.hsva(hueValue / 360, saturationValue / 100, valueValue / 100, alphaSection.alphaValue / 255)
                                 colorPicker.colorModified(color)
                                 root.color = color
                             }

                             RowLayout {
                                 Layout.fillWidth: true
                                 Label {
                                     text: "H:"
                                 }
                                 Slider {
                                     id: hueSlider
                                     Layout.fillWidth: true
                                     from: 0
                                     to: 360
                                     value: hsvSection.hueValue
                                     onValueChanged: {
                                         if (value !== hsvSection.hueValue) {
                                             hsvSection.hueValue = value
                                             if (pressed)
                                                 hsvSection.updateHSV()
                                         }
                                     }
                                 }
                             }
                             SpinBox {
                                 Layout.fillWidth: true
                                 from: 0
                                 to: 360
                                 value: hsvSection.hueValue
                                 onValueChanged: {
                                     if (value !== hsvSection.hueValue)
                                         hsvSection.hueValue = value
                                 }
                                 onValueModified: {
                                     hsvSection.updateHSV()
                                 }
                             }

                             RowLayout {
                                 Layout.fillWidth: true
                                 Label {
                                     text: "S:"
                                 }
                                 Slider {
                                     id: saturationSlider
                                     Layout.fillWidth: true
                                     from: 0
                                     to: 100
                                     value: hsvSection.saturationValue
                                     onValueChanged: {
                                         if (value !== hsvSection.saturationValue) {
                                             hsvSection.saturationValue = value
                                             if (pressed)
                                                 hsvSection.updateHSV()
                                         }
                                     }
                                 }
                             }
                             SpinBox {
                                 Layout.fillWidth: true
                                 from: 0
                                 to: 100
                                 value: hsvSection.saturationValue
                                 onValueChanged: {
                                     if (value !== hsvSection.saturationValue)
                                         hsvSection.saturationValue = value
                                 }
                                 onValueModified: {
                                     hsvSection.updateHSV()
                                 }
                             }

                             RowLayout {
                                 Layout.fillWidth: true
                                 Label {
                                     text: "V:"
                                 }
                                 Slider {
                                     id: valueSlider
                                     Layout.fillWidth: true
                                     from: 0
                                     to: 100
                                     value: hsvSection.valueValue
                                     onValueChanged: {
                                         if (value !== hsvSection.valueValue) {
                                             hsvSection.valueValue = value
                                             if (pressed)
                                                 hsvSection.updateHSV()
                                         }
                                     }
                                 }
                             }
                             SpinBox {
                                 Layout.fillWidth: true
                                 from: 0
                                 to: 100
                                 value: hsvSection.valueValue
                                 onValueChanged: {
                                     if (value !== hsvSection.valueValue)
                                         hsvSection.valueValue = value
                                 }
                                 onValueModified: {
                                     hsvSection.updateHSV()
                                 }
                             }

                         }

                         SectionLayout {
                             title: "Opacity / Alpha"
                             id: alphaSection
                             property int alphaValue: 0
                             implicitWidth: 300

                             RowLayout {
                                 Layout.fillWidth: true
                                 Label {
                                     text: "V:"
                                 }
                                 Slider {
                                     id: alphaSlider
                                     Layout.fillWidth: true
                                     from: 0
                                     to: 255
                                     value: alphaSection.alphaValue
                                     onValueChanged: {
                                         if (value !== alphaSection.alphaValue) {
                                             alphaSection.alphaValue = value
                                             if (pressed)
                                                 hsvSection.updateHSV()
                                         }
                                     }
                                 }
                             }
                             SpinBox {
                                 Layout.fillWidth: true
                                 from: 0
                                 to: 255
                                 value: alphaSection.alphaValue
                                 onValueChanged: {
                                     if (value !== alphaSection.alphaValue)
                                         alphaSection.alphaValue = value
                                 }
                                 onValueModified: {
                                     hsvSection.updateHSV()
                                 }
                             }
                         }
                     }
                 }
             }
         }
     }