From 91ce0065ab2efeee48e43e06dabac876d6138fa2 Mon Sep 17 00:00:00 2001 From: Andrzej Biniek Date: Tue, 7 Jan 2025 10:18:15 +0100 Subject: [PATCH] New nodes implementation Change-Id: I54649bd1b1f655439eb9eafee9c1402c097e2696 Reviewed-by: spyro-adb --- .../imports/Editor/ContextMenu.qml | 167 ++++++++++ .../nodegrapheditor/imports/Editor/Graph.qml | 29 +- .../nodegrapheditor/imports/Editor/Port.qml | 5 +- .../imports/NewNodes/BaseGroup.qml | 102 ++++++ .../imports/NewNodes/BaseNode.qml | 52 +++ .../imports/NewNodes/ColorEditorPopup.qml | 62 ++++ .../imports/NewNodes/ColorNode.qml | 52 +++ .../imports/NewNodes/Components.qml | 152 +++++++++ .../imports/NewNodes/InputNode.qml | 70 +++++ .../imports/NewNodes/MaterialViewerNode.qml | 115 +++++++ .../imports/NewNodes/MetalnessNode.qml | 45 +++ .../imports/NewNodes/NormalNode.qml | 38 +++ .../imports/NewNodes/OcclusionNode.qml | 45 +++ .../imports/NewNodes/OpacityNode.qml | 45 +++ .../imports/NewNodes/OutputNode.qml | 48 +++ .../NewNodes/PrincipledMaterialNode.qml | 136 ++++++++ .../imports/NewNodes/ProviderNode.qml | 68 ++++ .../imports/NewNodes/RoughnessNode.qml | 45 +++ .../imports/NewNodes/TextureNode.qml | 108 +++++++ .../imports/NewNodes/ValueNode.qml | 295 ++++++++++++++++++ .../nodegrapheditor/imports/NewNodes/qmldir | 23 ++ 21 files changed, 1689 insertions(+), 13 deletions(-) create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/BaseGroup.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/BaseNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ColorEditorPopup.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ColorNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/Components.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/InputNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/MaterialViewerNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/MetalnessNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/NormalNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OcclusionNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OpacityNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OutputNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/PrincipledMaterialNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ProviderNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/RoughnessNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/TextureNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ValueNode.qml create mode 100644 share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/qmldir diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/ContextMenu.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/ContextMenu.qml index 18fbf89e84e..279b63b0068 100644 --- a/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/ContextMenu.qml +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/ContextMenu.qml @@ -3,12 +3,15 @@ import QtQuick import QtQuick.Controls +import QtQuick3D as QtQuick3D import HelperWidgets as HelperWidgets import StudioControls as StudioControls import StudioTheme as StudioTheme import Nodes as Nodes +import NewNodes as NewNodes +import QuickQanava as Qan StudioControls.Menu { id: contextMenu @@ -27,6 +30,169 @@ StudioControls.Menu { contextMenu.inputsModel = null; } + StudioControls.Menu { + title: "NewNodes.ProviderNodes" + + Component { + id: providerNode + + NewNodes.ProviderNode { + } + } + + Repeater { + model: [ + { + text: "Color", + action: () => { + const node = internal.createNode(providerNode); + node.item.sourceComponent = node.item.components["colorPicker"]; + node.item.text = "Color provider"; + } + }, + { + text: "CheckBox", + action: () => { + const node = internal.createNode(providerNode); + node.item.sourceComponent = node.item.components["checkBox"]; + node.item.text = "Boolean provider"; + } + }, + { + text: "Number", + action: () => { + const node = internal.createNode(providerNode); + node.item.sourceComponent = node.item.components["realSpinBox"]; + node.item.text = "Number provider"; + } + }, + { + text: "Texture Source", + action: () => { + const node = internal.createNode(providerNode); + node.item.sourceComponent = node.item.components["imageSource"]; + node.item.text = "Texture Source provider"; + } + }, + { + text: "Texture Mapping Mode", + action: () => { + const node = internal.createNode(providerNode); + node.item.sourceComponent = node.item.components["textureMappingMode"]; + node.item.text = "Texture Mapping Mode provider"; + } + }, + { + text: "Texture Tiling Mode", + action: () => { + const node = internal.createNode(providerNode); + node.item.sourceComponent = node.item.components["textureTilingMode"]; + node.item.text = "Texture Tiling Mode provider"; + } + }, + { + text: "Texture Filtering", + action: () => { + const node = internal.createNode(providerNode); + node.item.sourceComponent = node.item.components["textureFiltering"]; + node.item.text = "Texture Filtering provider"; + } + }, + { + text: "Material Alpha Mode", + action: () => { + const node = internal.createNode(providerNode); + node.item.sourceComponent = node.item.components["materialAlphaMode"]; + node.item.text = "Material Alpha Mode provider"; + } + }, + { + text: "Material Blend Mode", + action: () => { + const node = internal.createNode(providerNode); + node.item.sourceComponent = node.item.components["materialBlendMode"]; + node.item.text = "Material Blend Mode provider"; + } + }, + { + text: "Material Channel", + action: () => { + const node = internal.createNode(providerNode); + node.item.sourceComponent = node.item.components["materialChannel"]; + node.item.text = "Material Channel provider"; + } + }, + { + text: "Material Lighting", + action: () => { + const node = internal.createNode(providerNode); + node.item.sourceComponent = node.item.components["materialLighting"]; + node.item.text = "Material Lighting provider"; + } + }, + ] + + delegate: StudioControls.MenuItem { + text: modelData.text + + onTriggered: () => { + modelData.action(); + } + } + } + } + + StudioControls.MenuItem { + text: qsTr("NewNodes.TextureNode") + + onTriggered: () => { + let gnode = contextMenu.graph.insertGroup(textureNode); + gnode.item.x = contextMenu.newPosition.x; + gnode.item.y = contextMenu.newPosition.y; + gnode.label = "Texture"; + } + + Component { + id: textureNode + + NewNodes.TextureNode { + } + } + } + + StudioControls.MenuItem { + text: qsTr("NewNodes.PrincipledMaterialNode") + + onTriggered: () => { + let gnode = contextMenu.graph.insertGroup(principledMaterialNode); + gnode.item.x = contextMenu.newPosition.x; + gnode.item.y = contextMenu.newPosition.y; + gnode.label = "PrincipledMaterial"; + } + + Component { + id: principledMaterialNode + + NewNodes.PrincipledMaterialNode { + } + } + } + + StudioControls.MenuItem { + text: qsTr("NewNodes.MaterialViewerNode") + + onTriggered: () => { + const node = internal.createNode(materialViewerNode); + } + + Component { + id: materialViewerNode + + NewNodes.MaterialViewerNode { + } + } + } + StudioControls.MenuItem { text: qsTr("BaseColor") @@ -143,6 +309,7 @@ StudioControls.Menu { const nodeItem = node.item; nodeItem.x = contextMenu.newPosition.x; nodeItem.y = contextMenu.newPosition.y; + return node; } } } diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/Graph.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/Graph.qml index 172ac31c24e..d0aa4429d9e 100644 --- a/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/Graph.qml +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/Graph.qml @@ -16,8 +16,10 @@ Qan.Graph { } onConnectorRequestEdgeCreation: (srcNode, dstNode, srcPortItem, dstPortItem) => { - if (srcPortItem.dataType !== dstPortItem.dataType) { - return; + if (dstPortItem.dataType !== "Any") { + if (srcPortItem.dataType !== dstPortItem.dataType) { + return; + } } const edge = root.insertEdge(srcNode, dstNode); @@ -35,7 +37,15 @@ Qan.Graph { }); } } - onNodeRemoved: node => {NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;} + onEdgeInserted: { + NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true; + } + onNodeInserted: { + NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true; + } + onNodeRemoved: node => { + NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true; + } onOnEdgeRemoved: edge => { const srcNode = edge.getSource(); const dstNode = edge.getDestination(); @@ -43,14 +53,11 @@ Qan.Graph { const dstPortItem = edge.item.destinationItem; // TODO: add reset binding function - dstNode.item.value[dstPortItem.dataName] = dstNode.item.reset[dstPortItem.dataName]; - NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true; - } - - onEdgeInserted: { - NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true; - } - onNodeInserted: { + if (dstPortItem.dataReset) { + dstPortItem.dataReset(); + } else { + dstNode.item.value[dstPortItem.dataName] = dstNode.item.reset[dstPortItem.dataName]; + } NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true; } } diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/Port.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/Port.qml index 328edb7ba6b..ea5cb462e09 100644 --- a/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/Port.qml +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/Editor/Port.qml @@ -9,7 +9,8 @@ Qan.Port { id: root property var dataBinding: null - property string dataName: "" - property string dataType: "" property string dataId: "" + property string dataName: "" + property var dataReset: null + property string dataType: "" } diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/BaseGroup.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/BaseGroup.qml new file mode 100644 index 00000000000..a53cd21b64b --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/BaseGroup.qml @@ -0,0 +1,102 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts + +import QuickQanava as Qan +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +Qan.GroupItem { + id: root + + property QtObject metadata: QtObject { + property var nodes: [] + } + readonly property string uuid: NodeGraphEditorBackend.widget.generateUUID() + + acceptDrops: false + container: content + droppable: false + height: c.visible ? content.height + header.height : header.height + resizable: false + width: content.width + + Component.onCompleted: { + internal.initialize(); + } + Component.onDestruction: { + internal.terminate(); + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + id: header + + Layout.fillWidth: true + Layout.preferredHeight: 35 + color: "green" + + RowLayout { + anchors.fill: parent + + StudioControls.AbstractButton { + id: buttonAlignTop + + buttonIcon: "\u0187" + + onClicked: { + c.visible = !c.visible; + } + } + + Text { + text: root.node.label + } + } + } + + Rectangle { + id: c + + Layout.fillHeight: true + Layout.fillWidth: true + color: "white" + + Column { + id: content + + spacing: 3 + } + } + } + + QtObject { + id: internal + + readonly property QtObject data: QtObject { + readonly property var instances: [] + } + readonly property var initialize: () => { + const graph = root.graph; + + root.metadata.nodes.forEach(data => { + const metadata = Components[data.metadata]; + const node = metadata.constructor(graph, root, data); + graph.groupNode(root.node, node, null, false); + + internal.data.instances.push(node); + }); + } + readonly property var terminate: () => { + internal.data.instances.forEach(n => { + graph.removeNode(n); + }); + } + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/BaseNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/BaseNode.qml new file mode 100644 index 00000000000..075f601fa02 --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/BaseNode.qml @@ -0,0 +1,52 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +Qan.NodeItem { + id: root + + default property alias data: content.data + property QtObject metadata: QtObject { + property var ports: [] + } + readonly property string uuid: NodeGraphEditorBackend.widget.generateUUID() + + connectable: Qan.NodeItem.UnConnectable + height: 35 + resizable: false + width: 350 + + Component.onCompleted: { + internal.initialize(); + } + + QtObject { + id: internal + + readonly property var initialize: () => { + const graph = root.graph; + + root.metadata.ports.forEach(data => { + const port = graph.insertPort(root.node, data.dock, data.type, data.text, data.id); + port.dataBinding = data.binding; + port.dataReset = data.reset; + + port.dataType = "Any"; + }); + } + } + + Item { + id: content + + anchors.fill: parent + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ColorEditorPopup.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ColorEditorPopup.qml new file mode 100644 index 00000000000..6ecdc191729 --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ColorEditorPopup.qml @@ -0,0 +1,62 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +StudioControls.PopupDialog { + id: colorPopup + + required property color currentColor + property QtObject loaderItem: loader.item + property color originalColor + + signal activateColor(color: color) + + function open(showItem) { + loader.ensureActive(); + colorPopup.show(showItem); + loader.updateOriginalColor(); + } + + width: 260 + + onClosing: loader.active = false + onOriginalColorChanged: loader.updateOriginalColor() + + Loader { + id: loader + + function ensureActive() { + if (!loader.active) + loader.active = true; + } + + function updateOriginalColor() { + if (loader.status === Loader.Ready) + loader.item.originalColor = colorPopup.originalColor; + } + + sourceComponent: StudioControls.ColorEditorPopup { + visible: colorPopup.visible + width: colorPopup.contentWidth + + onActivateColor: color => { + colorPopup.activateColor(color); + } + } + + onLoaded: { + loader.updateOriginalColor(); + colorPopup.titleBar = loader.item.titleBarContent; + } + + Binding { + property: "color" + target: loader.item + value: colorPopup.currentColor + when: loader.status === Loader.Ready + } + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ColorNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ColorNode.qml new file mode 100644 index 00000000000..c91950cb85a --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ColorNode.qml @@ -0,0 +1,52 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick3D as QtQuick3D + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +BaseGroup { + id: root + + readonly property QtObject value: QtObject { + property int channel + property color color + property QtQuick3D.Texture map + property bool singleChannelEnabled + } + + metadata: QtObject { + property var nodes: [ + { + metadata: "input", + text: "Color", + sourceComponent: "colorPicker", + value: "color" + }, + { + metadata: "input", + text: "Map", + value: "map", + resetValue: null + }, + { + metadata: "input", + text: "Use Single Channel", + sourceComponent: "checkBox", + value: "singleChannelEnabled" + }, + { + metadata: "input", + text: "Channel", + sourceComponent: "materialChannel", + value: "channel" + }, + ] + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/Components.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/Components.qml new file mode 100644 index 00000000000..cf9f92c185b --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/Components.qml @@ -0,0 +1,152 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +pragma Singleton + +import QtQuick + +QtObject { + readonly property QtObject color: QtObject { + id: color + + readonly property Component component: Component { + ColorNode { + } + } + readonly property var constructor: (graph, self, data) => { + const node = graph.insertGroup(color.component); + + node.item.draggable = false; + node.label = data.text; + data.bindings(node.item.value); + + return node; + } + } + readonly property QtObject input: QtObject { + id: input + + readonly property Component component: Component { + InputNode { + } + } + readonly property var constructor: (graph, self, data) => { + const node = graph.insertNode(input.component); + + node.item.draggable = false; + node.item.text = data.text; + node.item.sourceComponent = node.item.components[data.sourceComponent]; + node.item.value.data = self.value[data.value]; + node.item.value.reset = data.resetValue; + self.value[data.value] = Qt.binding(() => { + return node.item.value.data; + }); + + return node; + } + } + readonly property QtObject metalness: QtObject { + id: metalness + + readonly property Component component: Component { + MetalnessNode { + } + } + readonly property var constructor: (graph, self, data) => { + const node = graph.insertGroup(metalness.component); + + node.item.draggable = false; + node.label = data.text; + data.bindings(node.item.value); + + return node; + } + } + readonly property QtObject normal: QtObject { + id: normal + + readonly property Component component: Component { + NormalNode { + } + } + readonly property var constructor: (graph, self, data) => { + const node = graph.insertGroup(normal.component); + + node.item.draggable = false; + node.label = data.text; + data.bindings(node.item.value); + + return node; + } + } + readonly property QtObject occlusion: QtObject { + id: occlusion + + readonly property Component component: Component { + OcclusionNode { + } + } + readonly property var constructor: (graph, self, data) => { + const node = graph.insertGroup(occlusion.component); + + node.item.draggable = false; + node.label = data.text; + data.bindings(node.item.value); + + return node; + } + } + readonly property QtObject opacity: QtObject { + id: opacity + + readonly property Component component: Component { + OpacityNode { + } + } + readonly property var constructor: (graph, self, data) => { + const node = graph.insertGroup(opacity.component); + + node.item.draggable = false; + node.label = data.text; + data.bindings(node.item.value); + + return node; + } + } + readonly property QtObject output: QtObject { + id: output + + readonly property Component component: Component { + OutputNode { + } + } + readonly property var constructor: (graph, self, data) => { + const node = graph.insertNode(output.component); + + node.item.draggable = false; + node.item.text = data.text; + node.item.value.data = self.value; + // self.value = Qt.binding(() => { + // return node.item.value; + // }); + + return node; + } + } + readonly property QtObject roughness: QtObject { + id: roughness + + readonly property Component component: Component { + RoughnessNode { + } + } + readonly property var constructor: (graph, self, data) => { + const node = graph.insertGroup(roughness.component); + + node.item.draggable = false; + node.label = data.text; + data.bindings(node.item.value); + + return node; + } + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/InputNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/InputNode.qml new file mode 100644 index 00000000000..7c16c3be559 --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/InputNode.qml @@ -0,0 +1,70 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +ValueNode { + id: root + + property alias sourceComponent: loader.sourceComponent + property alias text: label.text + + metadata: QtObject { + property var ports: [ + { + dock: Qan.NodeItem.Left, + type: Qan.PortItem.In, + text: "", + id: "value_input", + binding: value => { + loader.visible = false; + root.value.data = Qt.binding(() => { + return value.data; + }); + }, + reset: () => { + loader.visible = true; + root.value.data = (root.value.reset !== undefined) ? root.value.reset : root.value.data; + } + } + ] + } + + RowLayout { + anchors.fill: parent + anchors.margins: 3 + spacing: 0 + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 2 + + Text { + id: label + + anchors.verticalCenter: parent.verticalCenter + text: "{{placeholder}}" + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 2 + + Loader { + id: loader + + anchors.centerIn: parent + } + } + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/MaterialViewerNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/MaterialViewerNode.qml new file mode 100644 index 00000000000..4692ffd51c7 --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/MaterialViewerNode.qml @@ -0,0 +1,115 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick3D as QtQuick3D + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +BaseNode { + id: root + + readonly property QtObject value: QtObject { + property QtQuick3D.PrincipledMaterial data: QtQuick3D.PrincipledMaterial { + } + property string type: "undefined" + } + + height: 150 + width: 150 + + metadata: QtObject { + property var ports: [ + { + dock: Qan.NodeItem.Left, + type: Qan.PortItem.In, + text: "", + id: "value_input", + binding: value => { + root.value.data = Qt.binding(() => { + return value.data; + }); + }, + reset: () => { + root.value.data = null; + } + } + ] + } + + Rectangle { + anchors.fill: parent + color: "white" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 3 + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.margins: 2 + Layout.preferredHeight: 35 + + Text { + id: label + + anchors.centerIn: parent + text: "Material Viewer" + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 2 + + QtQuick3D.View3D { + id: view3D + + anchors.fill: parent + + camera: QtQuick3D.PerspectiveCamera { + clipFar: 1000 + clipNear: 1 + eulerRotation.x: -5.71 + y: 70 + z: 200 + } + environment: QtQuick3D.SceneEnvironment { + antialiasingMode: QtQuick3D.SceneEnvironment.MSAA + antialiasingQuality: QtQuick3D.SceneEnvironment.High + backgroundMode: QtQuick3D.SceneEnvironment.Transparent + clearColor: "#000000" + } + + QtQuick3D.Node { + QtQuick3D.DirectionalLight { + brightness: 1 + eulerRotation.x: -26 + eulerRotation.y: -50 + } + + QtQuick3D.Node { + y: 60 + + QtQuick3D.Model { + id: model + + eulerRotation.y: 45 + materials: root.value.data + scale: Qt.vector3d(1.7, 1.7, 1.7) + source: "#Sphere" + } + } + } + } + } + } + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/MetalnessNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/MetalnessNode.qml new file mode 100644 index 00000000000..8fe9deaf69f --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/MetalnessNode.qml @@ -0,0 +1,45 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick3D as QtQuick3D + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +BaseGroup { + id: root + + readonly property QtObject value: QtObject { + property int channel + property QtQuick3D.Texture map + property real metalness + } + + metadata: QtObject { + property var nodes: [ + { + metadata: "input", + text: "Metalness", + sourceComponent: "realSpinBox", + value: "metalness" + }, + { + metadata: "input", + text: "Map", + value: "map", + resetValue: null + }, + { + metadata: "input", + text: "Channel", + sourceComponent: "materialChannel", + value: "channel" + }, + ] + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/NormalNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/NormalNode.qml new file mode 100644 index 00000000000..67eeb0345a6 --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/NormalNode.qml @@ -0,0 +1,38 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick3D as QtQuick3D + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +BaseGroup { + id: root + + readonly property QtObject value: QtObject { + property QtQuick3D.Texture map + property real strength: 1.0 + } + + metadata: QtObject { + property var nodes: [ + { + metadata: "input", + text: "Map", + value: "map", + resetValue: null + }, + { + metadata: "input", + text: "Strength", + sourceComponent: "realSpinBox", + value: "strength" + }, + ] + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OcclusionNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OcclusionNode.qml new file mode 100644 index 00000000000..3a437423dbc --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OcclusionNode.qml @@ -0,0 +1,45 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick3D as QtQuick3D + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +BaseGroup { + id: root + + readonly property QtObject value: QtObject { + property int channel + property QtQuick3D.Texture map + property real occlusion + } + + metadata: QtObject { + property var nodes: [ + { + metadata: "input", + text: "Occlusion", + sourceComponent: "realSpinBox", + value: "occlusion" + }, + { + metadata: "input", + text: "Map", + value: "map", + resetValue: null + }, + { + metadata: "input", + text: "Channel", + sourceComponent: "materialChannel", + value: "channel" + }, + ] + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OpacityNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OpacityNode.qml new file mode 100644 index 00000000000..f6032c022cf --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OpacityNode.qml @@ -0,0 +1,45 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick3D as QtQuick3D + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +BaseGroup { + id: root + + readonly property QtObject value: QtObject { + property int channel + property QtQuick3D.Texture map + property real opacity: 1.0 + } + + metadata: QtObject { + property var nodes: [ + { + metadata: "input", + text: "Opacity", + sourceComponent: "realSpinBox", + value: "opacity" + }, + { + metadata: "input", + text: "Map", + value: "map", + resetValue: null + }, + { + metadata: "input", + text: "Channel", + sourceComponent: "materialChannel", + value: "channel" + }, + ] + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OutputNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OutputNode.qml new file mode 100644 index 00000000000..31ae0baca3b --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/OutputNode.qml @@ -0,0 +1,48 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +ValueNode { + id: root + + property alias text: label.text + + metadata: QtObject { + property var ports: [ + { + dock: Qan.NodeItem.Right, + type: Qan.PortItem.Out, + text: "", + id: "value_output" + } + ] + } + + RowLayout { + anchors.fill: parent + anchors.margins: 3 + spacing: 0 + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 2 + + Text { + id: label + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + text: "{{placeholder}}" + } + } + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/PrincipledMaterialNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/PrincipledMaterialNode.qml new file mode 100644 index 00000000000..045105e7106 --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/PrincipledMaterialNode.qml @@ -0,0 +1,136 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick3D as QtQuick3D + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +BaseGroup { + id: root + + readonly property QtQuick3D.PrincipledMaterial value: QtQuick3D.PrincipledMaterial { + } + + metadata: QtObject { + property var nodes: [ + { + metadata: "output", + text: "Output" + }, + { + metadata: "input", + text: "Alpha Mode", + sourceComponent: "materialAlphaMode", + value: "alphaMode" + }, + { + metadata: "input", + text: "Blend Mode", + sourceComponent: "materialBlendMode", + value: "blendMode" + }, + { + metadata: "input", + text: "Lighting", + sourceComponent: "materialLighting", + value: "lighting" + }, + { + metadata: "color", + text: "Base Color", + bindings: values => { + value.baseColor = Qt.binding(() => { + return values.color; + }); + value.baseColorChannel = Qt.binding(() => { + return values.channel; + }); + value.baseColorMap = Qt.binding(() => { + return values.map; + }); + value.baseColorSingleChannelEnabled = Qt.binding(() => { + return values.singleChannelEnabled; + }); + } + }, + { + metadata: "metalness", + text: "Metalness", + bindings: values => { + value.metalness = Qt.binding(() => { + return values.metalness; + }); + value.metalnessChannel = Qt.binding(() => { + return values.channel; + }); + value.metalnessMap = Qt.binding(() => { + return values.map; + }); + } + }, + { + metadata: "roughness", + text: "Roughness", + bindings: values => { + value.roughness = Qt.binding(() => { + return values.roughness; + }); + value.roughnessChannel = Qt.binding(() => { + return values.channel; + }); + value.roughnessMap = Qt.binding(() => { + return values.map; + }); + } + }, + { + metadata: "normal", + text: "Normal", + bindings: values => { + value.normalMap = Qt.binding(() => { + return values.map; + }); + value.normalStrength = Qt.binding(() => { + return values.strength; + }); + } + }, + { + metadata: "occlusion", + text: "Occlusion", + bindings: values => { + value.occlusionAmount = Qt.binding(() => { + return values.occlusion; + }); + value.occlusionChannel = Qt.binding(() => { + return values.channel; + }); + value.occlusionMap = Qt.binding(() => { + return values.map; + }); + } + }, + { + metadata: "opacity", + text: "Opacity", + bindings: values => { + value.opacity = Qt.binding(() => { + return values.opacity; + }); + value.opacityChannel = Qt.binding(() => { + return values.channel; + }); + value.opacityMap = Qt.binding(() => { + return values.map; + }); + } + }, + ] + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ProviderNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ProviderNode.qml new file mode 100644 index 00000000000..73b6e92fd14 --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ProviderNode.qml @@ -0,0 +1,68 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +ValueNode { + id: root + + property alias sourceComponent: loader.sourceComponent + property alias text: label.text + + height: 100 + width: 150 + + metadata: QtObject { + property var ports: [ + { + dock: Qan.NodeItem.Right, + type: Qan.PortItem.Out, + text: "", + id: "value_output" + } + ] + } + + Rectangle { + anchors.fill: parent + color: "white" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 3 + spacing: 0 + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 2 + + Text { + id: label + + anchors.centerIn: parent + text: "{{placeholder}}" + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 2 + + Loader { + id: loader + + anchors.centerIn: parent + } + } + } + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/RoughnessNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/RoughnessNode.qml new file mode 100644 index 00000000000..60a7aabd852 --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/RoughnessNode.qml @@ -0,0 +1,45 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick3D as QtQuick3D + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +BaseGroup { + id: root + + readonly property QtObject value: QtObject { + property int channel + property QtQuick3D.Texture map + property real roughness + } + + metadata: QtObject { + property var nodes: [ + { + metadata: "input", + text: "Roughness", + sourceComponent: "realSpinBox", + value: "roughness" + }, + { + metadata: "input", + text: "Map", + value: "map", + resetValue: null + }, + { + metadata: "input", + text: "Channel", + sourceComponent: "materialChannel", + value: "channel" + }, + ] + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/TextureNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/TextureNode.qml new file mode 100644 index 00000000000..4616e01ad7b --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/TextureNode.qml @@ -0,0 +1,108 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts +import QtQuick3D as QtQuick3D + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +BaseGroup { + id: root + + readonly property QtQuick3D.Texture value: QtQuick3D.Texture { + sourceItem: image + } + + metadata: QtObject { + property var nodes: [ + { + metadata: "output", + text: "Output" + }, + { + metadata: "input", + text: "Source", + sourceComponent: "imageSource", + value: "source" + }, + { + metadata: "input", + text: "Scale.U", + sourceComponent: "realSpinBox", + value: "scaleU" + }, + { + metadata: "input", + text: "Scale.V", + sourceComponent: "realSpinBox", + value: "scaleV" + }, + { + metadata: "input", + text: "Flip.U", + sourceComponent: "checkBox", + value: "flipU" + }, + { + metadata: "input", + text: "Flip.V", + sourceComponent: "checkBox", + value: "flipV" + }, + { + metadata: "input", + text: "Auto Orientation", + sourceComponent: "checkBox", + value: "autoOrientation" + }, + { + metadata: "input", + text: "Mapping Mode", + sourceComponent: "textureMappingMode", + value: "mappingMode" + }, + { + metadata: "input", + text: "Tiling.U", + sourceComponent: "textureTilingMode", + value: "tilingModeHorizontal" + }, + { + metadata: "input", + text: "Tiling.V", + sourceComponent: "textureTilingMode", + value: "tilingModeVertical" + }, + { + metadata: "input", + text: "Mag Filter", + sourceComponent: "textureFiltering", + value: "magFilter" + }, + { + metadata: "input", + text: "Min Filter", + sourceComponent: "textureFiltering", + value: "minFilter" + }, + { + metadata: "input", + text: "Mip Filter", + sourceComponent: "textureFiltering", + value: "mipFilter" + }, + ] + } + + Image { + id: image + + source: root.value.source + visible: false + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ValueNode.qml b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ValueNode.qml new file mode 100644 index 00000000000..8a7f435e187 --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/ValueNode.qml @@ -0,0 +1,295 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Layouts + +import QuickQanava as Qan +import HelperWidgets as HelperWidgets +import StudioControls as StudioControls + +import NodeGraphEditorBackend + +BaseNode { + id: root + + readonly property QtObject components: QtObject { + readonly property Component checkBox: Component { + StudioControls.CheckBox { + id: checkBox + + actionIndicatorVisible: false + + //checked: root.value.data + + onCheckedChanged: () => { + root.value.data = checkBox.checked; + } + } + } + readonly property Component colorPicker: Component { + Rectangle { + id: colorPicker + + border.color: "black" + border.width: 2 + color: root.value.data + height: 32 + width: 32 + + Component.onCompleted: { + if (!root.value.data) { + root.value.data = "red"; + } + } + + MouseArea { + anchors.fill: parent + + onClicked: colorEditorPopup.open(colorPicker) + } + + ColorEditorPopup { + id: colorEditorPopup + + currentColor: root.value.data + + onActivateColor: color => { + root.value.data = color; + } + } + } + } + readonly property Component imageSource: Component { + StudioControls.ComboBox { + id: imageSource + + actionIndicatorVisible: false + model: fileModel.model + textRole: "fileName" + valueRole: "relativeFilePath" + + onCurrentValueChanged: () => { + root.value.data = `image://qmldesigner_nodegrapheditor/${imageSource.currentValue}`; + } + + HelperWidgets.FileResourcesModel { + id: fileModel + + filter: "*.png *.gif *.jpg *.bmp *.jpeg *.svg *.pbm *.pgm *.ppm *.xbm *.xpm *.hdr *.ktx *.webp" + modelNodeBackendProperty: modelNodeBackend + } + } + } + readonly property Component materialAlphaMode: Component { + StudioControls.ComboBox { + id: materialAlphaMode + + actionIndicatorVisible: false + model: [ + { + text: "Default", + value: 0 + }, + { + text: "Blend", + value: 2 + }, + { + text: "Opaque", + value: 3 + }, + { + text: "Mask", + value: 1 + }, + ] + textRole: "text" + valueRole: "value" + + onCurrentValueChanged: () => { + root.value.data = materialAlphaMode.currentValue; + } + } + } + readonly property Component materialBlendMode: Component { + StudioControls.ComboBox { + id: materialBlendMode + + actionIndicatorVisible: false + model: [ + { + text: "SourceOver", + value: 0 + }, + { + text: "Screen", + value: 1 + }, + { + text: "Multiply", + value: 2 + }, + ] + textRole: "text" + valueRole: "value" + + onCurrentValueChanged: () => { + root.value.data = materialBlendMode.currentValue; + } + } + } + readonly property Component materialChannel: Component { + StudioControls.ComboBox { + id: materialChannel + + actionIndicatorVisible: false + model: [ + { + text: "R", + value: 0 + }, + { + text: "G", + value: 1 + }, + { + text: "B", + value: 2 + }, + { + text: "A", + value: 3 + }, + ] + textRole: "text" + valueRole: "value" + + onCurrentValueChanged: () => { + root.value.data = materialChannel.currentValue; + } + } + } + readonly property Component materialLighting: Component { + StudioControls.ComboBox { + id: materialLighting + + actionIndicatorVisible: false + model: [ + { + text: "NoLighting", + value: 0 + }, + { + text: "FragmentLighting", + value: 1 + }, + ] + textRole: "text" + valueRole: "value" + + onCurrentValueChanged: () => { + root.value.data = materialLighting.currentValue; + } + } + } + readonly property Component realSpinBox: Component { + StudioControls.RealSpinBox { + id: realSpinBox + + actionIndicatorVisible: false + realValue: root.value.data + + onRealValueModified: () => { + root.value.data = realSpinBox.realValue; + } + } + } + readonly property Component textureFiltering: Component { + StudioControls.ComboBox { + id: textureFiltering + + actionIndicatorVisible: false + model: [ + { + text: "None", + value: 0 + }, + { + text: "Nearest", + value: 1 + }, + { + text: "Linear", + value: 2 + }, + ] + textRole: "text" + valueRole: "value" + + onCurrentValueChanged: () => { + root.value.data = textureFiltering.currentValue; + } + } + } + readonly property Component textureMappingMode: Component { + StudioControls.ComboBox { + id: textureMappingMode + + actionIndicatorVisible: false + model: [ + { + text: "UV", + value: 0 + }, + { + text: "Environment", + value: 1 + }, + { + text: "LightProbe", + value: 2 + }, + ] + textRole: "text" + valueRole: "value" + + onCurrentValueChanged: () => { + root.value.data = textureMappingMode.currentValue; + } + } + } + readonly property Component textureTilingMode: Component { + StudioControls.ComboBox { + id: textureTilingMode + + actionIndicatorVisible: false + model: [ + { + text: "Repeat", + value: 3 + }, + { + text: "ClampToEdge", + value: 1 + }, + { + text: "MirroredRepeat", + value: 2 + }, + ] + textRole: "text" + valueRole: "value" + + onCurrentValueChanged: () => { + root.value.data = textureTilingMode.currentValue; + } + } + } + } + readonly property QtObject value: QtObject { + property var data: undefined + property var reset: undefined + property string type: "undefined" + } +} diff --git a/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/qmldir b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/qmldir new file mode 100644 index 00000000000..00ccbdfadf0 --- /dev/null +++ b/share/qtcreator/qmldesigner/nodegrapheditor/imports/NewNodes/qmldir @@ -0,0 +1,23 @@ +module NewNodes + +BaseGroup 1.0 BaseGroup.qml +BaseNode 1.0 BaseNode.qml + +ValueNode 1.0 ValueNode.qml +OutputNode 1.0 OutputNode.qml +InputNode 1.0 InputNode.qml +ProviderNode 1.0 ProviderNode.qml + +ColorNode 1.0 ColorNode.qml +MetalnessNode 1.0 MetalnessNode.qml +RoughnessNode 1.0 RoughnessNode.qml +NormalNode 1.0 NormalNode.qml +OcclusionNode 1.0 OcclusionNode.qml +OpacityNode 1.0 OpacityNode.qml + +TextureNode 1.0 TextureNode.qml +PrincipledMaterialNode 1.0 PrincipledMaterialNode.qml + +MaterialViewerNode 1.0 MaterialViewerNode.qml + +singleton Components 1.0 Components.qml