diff --git a/data/gloperate/qml/GlOperatePipeline.qml b/data/gloperate/qml/GlOperatePipeline.qml index c147ba83..b1501024 100644 --- a/data/gloperate/qml/GlOperatePipeline.qml +++ b/data/gloperate/qml/GlOperatePipeline.qml @@ -13,8 +13,10 @@ Item signal slotChanged(string path, string slot, var status) - // Canvas scripting interface (accessed by 'root') - property var canvas: null + property var canvas: null ///< Canvas scripting interface (accessed by 'root') + property var internalStages: {} ///< Store stages only accessible for scripting and UI + property var internalTypes: null ///< Stores internal stage types, lazy initialize + property string editor: '' ///< State of editor function getStageTypes() { @@ -32,6 +34,8 @@ Item } } + types = types.concat(Object.keys(getInternalTypes())); + return types; } @@ -45,6 +49,11 @@ Item function createStage(path, name, type) { + if (getInternalTypes().hasOwnProperty(type)) + { + return createInternalStage(path, name, type); + } + if (canvas) { return canvas.createStage(path, name, type); @@ -71,12 +80,35 @@ Item { var connections = canvas ? canvas.getConnections(path) : null; - if (connections) return connections; - else return []; + if (!connections) + { + return []; + } + + if (path === "root") + { + for (var internalPath in internalStages) + { + if (isVisible(internalStages[internalPath])) + { + connections = connections.concat(internalStages[internalPath].getConnections()); + } + } + } + + return connections; } function createConnection(sourcePath, sourceSlot, destPath, destSlot) { + for (var internalPath in internalStages) + { + if (destPath === internalPath) + { + internalStages[internalPath].connectionCreated(sourcePath, sourceSlot, destPath, destSlot); + } + } + if (canvas) { canvas.createConnection(sourcePath, sourceSlot, destPath, destSlot); @@ -85,6 +117,14 @@ Item function removeConnection(path, slot) { + for (var internalPath in internalStages) + { + if (path === internalPath) + { + internalStages[internalPath].connectionRemoved(path, slot); + } + } + if (canvas) { canvas.removeConnection(path, slot); @@ -95,6 +135,26 @@ Item { var info = canvas ? canvas.getStage(path) : null; + if (info && path === "root") + { + for (var internalPath in internalStages) + { + var stage = internalStages[internalPath]; + if (isVisible(stage)) + { + info.stages.push(stage.name); + } + } + } + + for (var internalPath in internalStages) + { + if (path === internalPath) + { + return internalStages[internalPath].getDescription(); + } + } + if (info) { return info; } else { @@ -137,4 +197,81 @@ Item canvas.setValue(path, slot, value); } } + + // Functions regarding internal stages + function isVisible(stage) + { + return editor === 'internal' ? stage.isVisibleInternal : stage.isVisibleExternal; + } + + function getInternalTypes() + { + if (internalTypes === null) + { + initializeInternalTypes(); + } + return internalTypes; + } + + function getInternalStages() + { + var stages = {}; + + for (var internalPath in internalStages) + { + var stage = internalStages[internalPath]; + + if (isVisible(stage)) + { + stages[internalPath] = stage; + } + } + + return stages; + } + + function clearInternalStages() + { + internalStages = {}; + } + + function createInternalStage(path, name, type) + { + var realName = name; + + if (internalStages.hasOwnProperty(path + "." + name)) + { + // name already taken, add a number + var i = 2; + while (internalStages.hasOwnProperty(path + "." + name + i)) + { + i += 1; + } + realName = name + i; + } + + var stageComponent = getInternalTypes()[type]; + + var newStage = stageComponent.createObject(pipelineInterface, {}); + var stage = newStage.stageDefinition; + + stage.name = realName; + stage.path = path + "." + realName; + + internalStages[stage.path] = stage; + + return realName; + } + + function initializeInternalTypes() + { + internalTypes = {}; + + internalTypes['PreviewStage'] = previewStageComponent; + } + + property Component previewStageComponent : PreviewStage + { + id: previewStage + } } diff --git a/data/gloperate/qml/InternalStage.qml b/data/gloperate/qml/InternalStage.qml new file mode 100644 index 00000000..cc3aa847 --- /dev/null +++ b/data/gloperate/qml/InternalStage.qml @@ -0,0 +1,89 @@ + +import QtQuick 2.0 + + +/** +* InternalStage +* +* Describes the state of an internal stage for (scripting and UI), only supports inputs +*/ +Item +{ + id: stage + + signal connectionCreated(string sourcePath, string sourceSlot, string destPath, string destSlot) + signal connectionRemoved(string path, string slot) + + property string name: '' ///< Name of the stage (for display) + property string path: '' ///< Path of the stage + property var inputs: [] ///< Input configuration of the stage + property var connections: {} ///< Connections to this stage + + property bool isVisibleInternal: true ///< Whether this stage should be visible in 'internal' mode + property bool isVisibleExternal: true ///< Whether this stage should be visible in 'external' mode + + property bool configured: false ///< Whether this stage is already configured + + function onConfigure(stageItem) {} ///< To be overridden with actual configuration + + function configure(stageItem) { + if (configured) + { + return; + } + + onConfigure(stageItem); + + configured = true; + } + + onConnectionCreated: + { + if (connections === undefined) + { + connections = {}; + } + connections[destSlot] = sourcePath + '.' + sourceSlot; + } + + onConnectionRemoved: + { + if (connections === undefined) + { + return; + } + + delete connections[slot]; + } + + // Generate stage description with inputs, outputs, and stages + function getDescription() { + var inputNames = []; + + for (var i in inputs) + { + var input = inputs[i]; + inputNames.push(input.name); + } + + return { + name: path, + inputs: inputNames, + outputs: [], + stages: [] + }; + } + + // Generate stage connections + function getConnections() { + var connectionList = []; + + for (var slot in connections) + { + connectionList.push({from: connections[slot], to: path + '.' + slot}); + } + + return connectionList; + } + +} diff --git a/data/gloperate/qml/PipelinePage.qml b/data/gloperate/qml/PipelinePage.qml index b827a59c..5c1d8a9d 100644 --- a/data/gloperate/qml/PipelinePage.qml +++ b/data/gloperate/qml/PipelinePage.qml @@ -13,10 +13,10 @@ ScrollArea /// Called when a render stage has been selected signal renderStageSelected(string name) - property var categories: [] ///< List of categories - property var stages: {} ///< List of stages, sorted by categories ( { 'cat1': [ { name: '', description: '', ... } ], 'cat2': [], ...}) - property var currentCategory: 'Demo' ///< The currently selected category - property var currentStage: '' ///< The currently selected stage + property var categories: [] ///< List of categories + property var stages: {} ///< List of stages, sorted by categories ( { 'cat1': [ { name: '', description: '', ... } ], 'cat2': [], ...}) + property var currentCategory: 'Demos' ///< The currently selected category + property var currentStage: '' ///< The currently selected stage contentWidth: layout.width contentHeight: layout.height @@ -66,7 +66,7 @@ ScrollArea Repeater { - model: item.stages[item.currentCategory].length + model: item.stages === undefined ? 0 : item.stages[item.currentCategory].length ClickLabel { diff --git a/data/gloperate/qml/PipelineView.qml b/data/gloperate/qml/PipelineView.qml index 40b57cb6..e944c1ce 100644 --- a/data/gloperate/qml/PipelineView.qml +++ b/data/gloperate/qml/PipelineView.qml @@ -13,9 +13,11 @@ Item id: page signal closed() + signal toggled() - property var properties: null ///< Interface for communicating with the actual properties - property string path: '' ///< Path to pipeline + property var properties: null ///< Interface for communicating with the actual properties + property string path: '' ///< Path to pipeline + property string toggleButton: '' ///< Description of the toggle state button implicitWidth: pipelineEditor.implicitWidth implicitHeight: pipelineEditor.implicitHeight @@ -42,38 +44,53 @@ Item } } - Rectangle + Button { - x: 100 - y: 100 - width: 200 - height: 200 + text: page.toggleButton + visible: page.toggleButton === "" ? false : true - color: 'white' - border.color: 'black' - border.width: 1 + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Ui.style.paddingMedium - TextureItem + onClicked: { - anchors.fill: parent - anchors.margins: 1 - - path: 'ShapeDemo.Framebuffer.colorTexture' + page.toggled(); } + } + } - MouseArea - { - anchors.fill: parent - drag.target: parent - } + function load() + { + if (pipelineEditor.loaded) + { + return; } + + pipelineEditor.load(path); + + configureInternalStages(); + + pipelineEditor.pipeline.stageCreated.connect(configureInternalStages); + } + + function update() + { + pipelineEditor.update(); + + configureInternalStages(); } - onVisibleChanged: + function configureInternalStages() { - if (visible) + var internalStages = properties.getInternalStages(); + + for (var stageName in internalStages) { - pipelineEditor.load(path); + var stageDefinition = internalStages[stageName]; + var stageObject = pipelineEditor.pipeline.stageItems[stageDefinition.name]; + + stageDefinition.configure(stageObject); } } } diff --git a/data/gloperate/qml/PreviewItem.qml b/data/gloperate/qml/PreviewItem.qml new file mode 100644 index 00000000..f271586c --- /dev/null +++ b/data/gloperate/qml/PreviewItem.qml @@ -0,0 +1,36 @@ + +import QtQuick 2.0 + +import QmlToolbox.Base 1.0 + +import gloperate.rendering 1.0 + + +Item +{ + id: previewItem + + property string path: '' ///< holds the path to the texture + + implicitWidth: 200 + implicitHeight: 180 + + Rectangle + { + anchors.fill: parent + anchors.leftMargin: Ui.style.pipelineSlotSize + anchors.rightMargin: Ui.style.pipelineSlotSize / 2 + + color: 'white' + border.color: 'black' + border.width: 1 + + TextureItem + { + anchors.fill: parent + anchors.margins: 1 + + path: previewItem.path + } + } +} diff --git a/data/gloperate/qml/PreviewStage.qml b/data/gloperate/qml/PreviewStage.qml new file mode 100644 index 00000000..b78a31b8 --- /dev/null +++ b/data/gloperate/qml/PreviewStage.qml @@ -0,0 +1,57 @@ + +import QtQuick 2.0 + +import gloperate.rendering 1.0 + + +/** +* PreviewStage +* +* Describes the state of a preview stage +*/ +Item +{ + id: previewStage + + property var stageDefinition: internalStage ///< For access to stage definition + + property var previewComponent: null ///< Preview component on stage item in pipeline editor + + InternalStage + { + id: internalStage + + name: 'PreviewStage' + path: 'root.PreviewStage' + + inputs: [ + { + name: 'PreviewTexture', + type: 'texture', + value: null, + } + ] + + isVisibleInternal: true + isVisibleExternal: false + + onConnectionCreated: + { + previewComponent.path = sourcePath + '.' + sourceSlot; + } + + onConnectionRemoved: + { + previewComponent.path = ''; + } + + function onConfigure(stageItem) + { + previewComponent = stageItem.addComponent(newPreview, {}); + } + } + + property Component newPreview : PreviewItem + { + } +} diff --git a/data/gloperate/qml/SettingsPage.qml b/data/gloperate/qml/SettingsPage.qml index 3bcb1fa3..ae025047 100644 --- a/data/gloperate/qml/SettingsPage.qml +++ b/data/gloperate/qml/SettingsPage.qml @@ -122,6 +122,26 @@ Item } } + Label + { + Layout.alignment: Qt.AlignRight + + text: 'Pipeline Editor' + } + + ComboBox + { + model: [ 'external', 'internal' ] + + currentIndex: model.indexOf(settings.editor) + + onActivated: + { + var editor = model[index]; + settings.editor = editor; + } + } + Label { Layout.alignment: Qt.AlignRight | Qt.AlignTop diff --git a/data/gloperate/qml/Viewer.qml b/data/gloperate/qml/Viewer.qml index 70c3d1c0..20edb169 100644 --- a/data/gloperate/qml/Viewer.qml +++ b/data/gloperate/qml/Viewer.qml @@ -1,6 +1,7 @@ import QtQuick 2.4 import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 import QmlToolbox.Base 1.0 import QmlToolbox.Controls 1.0 @@ -163,8 +164,7 @@ ApplicationWindow onTriggered: { - pipelineView.visible = true; - mainView.visible = false; + showEditor(); } } } @@ -247,6 +247,8 @@ ApplicationWindow settings.stage = name; window.stage = name; propertyEditor.update(); + internalPipelineView.update(); + externalPipelineView.update(); } } @@ -322,22 +324,92 @@ ApplicationWindow // Pipeline view PipelineView { - id: pipelineView + id: internalPipelineView anchors.fill: parent visible: false - properties: gloperatePipeline - path: 'root' + properties: gloperatePipeline + path: 'root' + toggleButton: 'Open in new window' onClosed: { - mainView.visible = true; - pipelineView.visible = false; + hideEditor(); + } + + onToggled: + { + toggleEditor(); } } } + // Pipeline view in separate window + Window + { + id: popoutWindow + + title: 'Pipeline Editor' + + minimumWidth: 750 + minimumHeight: 550 + + PipelineView + { + id: externalPipelineView + + anchors.fill: parent + + properties: gloperatePipeline + path: 'root' + toggleButton: 'Show inside main window' + + onClosed: + { + hideEditor(); + } + + onToggled: + { + toggleEditor(); + } + } + } + + // Toggles between internal and external pipeline editor + function toggleEditor() + { + hideEditor(); + settings.editor = (settings.editor === "internal" ? "external" : "internal"); + showEditor(); + } + + // Shows the pipeline editor + function showEditor() + { + if (settings.editor === "internal") + { + internalPipelineView.visible = true; + mainView.visible = false; + internalPipelineView.load(); + } else { + popoutWindow.showMaximized(); + popoutWindow.raise(); + externalPipelineView.load(); + } + } + + // Hides the pipeline editor + function hideEditor() + { + // hide internal editor + mainView.visible = true; + internalPipelineView.visible = false; + // hide external editor + popoutWindow.hide(); + } + // Bottom Panel Panel { @@ -383,14 +455,19 @@ ApplicationWindow { id: gloperatePipeline + editor: settings.editor + onCanvasChanged: { propertyEditor.update(); - canvas.onStageInputChanged(function(slot, status) + if (canvas) { - gloperatePipeline.slotChanged('root', slot, status); - }); + canvas.onStageInputChanged(function(slot, status) + { + gloperatePipeline.slotChanged('root', slot, status); + }); + } } } @@ -405,6 +482,7 @@ ApplicationWindow property int logLevel: 3 property bool debugMode: false property string panelPosition: 'left' + property string editor: 'internal' property string stage: '' property string pluginPaths: '' @@ -424,6 +502,11 @@ ApplicationWindow gloperate.components.setPluginPaths(settings.pluginPaths); gloperate.components.scanPlugins(); } + + onEditorChanged: + { + gloperatePipeline.editor = settings.editor; + } } Component.onCompleted: @@ -440,6 +523,10 @@ ApplicationWindow // Scan for plugins gloperate.components.scanPlugins(); + // Initialize internal stages and add one preview stage + gloperatePipeline.clearInternalStages(); + gloperatePipeline.createInternalStage("root", "PreviewStage", "PreviewStage"); + // Set render stage window.stage = settings.stage; propertyEditor.update(); diff --git a/source/gloperate-qtquick/source/TextureItemRenderer_ogl.cpp b/source/gloperate-qtquick/source/TextureItemRenderer_ogl.cpp index 5b5de113..5ee3725f 100644 --- a/source/gloperate-qtquick/source/TextureItemRenderer_ogl.cpp +++ b/source/gloperate-qtquick/source/TextureItemRenderer_ogl.cpp @@ -7,6 +7,8 @@ #include #include +#include +#include #include #include #include @@ -15,6 +17,7 @@ #include #include #include +#include #include @@ -62,7 +65,15 @@ void TextureItemRenderer::renderTexture() Canvas * canvas = m_environment->canvases().front(); Stage * stage = canvas->renderStage(); if (!stage) return; - AbstractSlot * slot = stage->getSlot(m_path.toStdString()); + + std::string slotPath = m_path.toStdString(); + + // Replace root in slot path with render stage name + if (string::hasPrefix(slotPath, "root")) { + slotPath.replace(0, 4, stage->name()); + } + + AbstractSlot * slot = stage->getSlot(slotPath); if (!slot) return; // Check if it is a texture slot @@ -71,6 +82,16 @@ void TextureItemRenderer::renderTexture() texture = static_cast< Slot * >(slot)->value(); } + // Check if it is a texture slot + if (slot && slot->type() == typeid(gloperate::ColorRenderTarget *)) + { + gloperate::ColorRenderTarget * renderTarget = static_cast< Slot * >(slot)->value(); + if (renderTarget->currentTargetType() == RenderTargetType::UserDefinedFBOAttachment) + { + texture = renderTarget->framebufferAttachment()->asTextureAttachment()->texture(); + } + } + // Abort if texture is invalid if (!texture) { diff --git a/source/gloperate/source/base/Canvas.cpp b/source/gloperate/source/base/Canvas.cpp index d63273b4..27fb53bc 100644 --- a/source/gloperate/source/base/Canvas.cpp +++ b/source/gloperate/source/base/Canvas.cpp @@ -656,6 +656,11 @@ cppexpose::Variant Canvas::scr_getConnections(const std::string & path) (*connection.asMap())["from"] = from; (*connection.asMap())["to"] = to; + if (slot->isFeedback()) + { + (*connection.asMap())["feedback"] = true; + } + // Add connection obj.asArray()->push_back(connection); } @@ -857,6 +862,7 @@ cppexpose::Variant Canvas::getSlotStatus(const std::string & path, const std::st (*status.asMap())["name"] = slot->name(); (*status.asMap())["type"] = slot->typeName(); (*status.asMap())["value"] = slot->toVariant(); + (*status.asMap())["required"] = slot->isRequired(); // Include options const VariantMap & options = slot->options();