[ui] Viewer3D: multi 3D media support

* use new media loading backend through MediaLibrary
* Inspector3D: new overlay UI that displays and allows to manipulate MediaLibrary content
This commit is contained in:
Yann Lanthony 2018-11-23 22:20:29 +01:00
parent e35076ef97
commit 97fcdf67bf
6 changed files with 380 additions and 401 deletions

View file

@ -0,0 +1,9 @@
.pragma library
function intToString(v) {
// use EN locale to get comma separated thousands
// + remove automatically added trailing decimals
// (this 'toLocaleString' does not take any option)
return v.toLocaleString(Qt.locale('en-US')).split('.')[0]
}

View file

@ -2,6 +2,7 @@ module Utils
SortFilterDelegateModel 1.0 SortFilterDelegateModel.qml SortFilterDelegateModel 1.0 SortFilterDelegateModel.qml
Request 1.0 request.js Request 1.0 request.js
Format 1.0 format.js
# causes random crash at application exit # causes random crash at application exit
# singleton Filepath 1.0 Filepath.qml # singleton Filepath 1.0 Filepath.qml
# singleton Scene3DHelper 1.0 Scene3DHelper.qml # singleton Scene3DHelper 1.0 Scene3DHelper.qml

View file

@ -0,0 +1,268 @@
import QtQuick 2.7
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import MaterialIcons 2.2
import Qt3D.Core 2.0
import Qt3D.Render 2.1
import QtQuick.Controls.Material 2.4
import Controls 1.0
import Utils 1.0
FloatingPane {
id: root
implicitWidth: 200
property int renderMode: 2
property Transform targetTransform
property Locator3D origin: null
property Grid3D grid: null
property MediaLibrary mediaLibrary
property Camera camera
signal mediaFocusRequest(var index)
signal mediaRemoveRequest(var index)
MouseArea { anchors.fill: parent; onWheel: wheel.accepted = true }
ColumnLayout {
width: parent.width
height: parent.height
spacing: 10
Label { text: "RENDER"; font.bold: true; font.pointSize: 8 }
Flow {
Layout.fillWidth: true
Repeater {
model: Viewer3DSettings.renderModes
delegate: MaterialToolButton {
text: modelData["icon"]
ToolTip.text: modelData["name"] + " (" + (index+1) + ")"
onClicked: Viewer3DSettings.renderMode = index
checked: Viewer3DSettings.renderMode === index
}
}
}
Label { text: "SCENE"; font.bold: true; font.pointSize: 8 }
GridLayout {
id: controlsLayout
Layout.fillWidth: true
columns: 3
columnSpacing: 6
Flow {
Layout.columnSpan: 3
Layout.fillWidth: true
spacing: 0
CheckBox {
text: "Grid"
checked: Viewer3DSettings.displayGrid
onClicked: Viewer3DSettings.displayGrid = !Viewer3DSettings.displayGrid
}
CheckBox {
text: "Locator"
checked: Viewer3DSettings.displayLocator
onClicked: Viewer3DSettings.displayLocator = !Viewer3DSettings.displayLocator
}
}
// Rotation Controls
Label {
font.family: MaterialIcons.fontFamily
text: MaterialIcons.rotation3D
font.pointSize: 14
Layout.rowSpan: 3
}
Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationX = value}
Label { text: "X" }
Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationY = value}
Label { text: "Y" }
Slider { Layout.fillWidth: true; from: -180; to: 180; onPositionChanged: targetTransform.rotationZ = value }
Label { text: "Z" }
Label { text: "Points" }
RowLayout {
Layout.columnSpan: 2
Slider {
Layout.fillWidth: true; from: 1; to: 20;stepSize: 0.1
value: Viewer3DSettings.pointSize
onValueChanged: Viewer3DSettings.pointSize = value
}
CheckBox {
text: "Fixed";
checked: Viewer3DSettings.fixedPointSize
onClicked: Viewer3DSettings.fixedPointSize = !Viewer3DSettings.fixedPointSize
}
}
}
Label { text: "MEDIA"; font.bold: true; font.pointSize: 8 }
ListView {
id: mediaListView
Layout.fillHeight: true
Layout.fillWidth: true
clip: true
model: mediaLibrary.model
spacing: 2
//section.property: "section"
ScrollBar.vertical: ScrollBar { id: scrollBar }
section.delegate: Pane {
width: parent.width
padding: 1
background: null
Label {
width: parent.width
padding: 4
background: Rectangle { color: Qt.darker(parent.palette.base, 1.15) }
text: section
}
}
Connections {
target: mediaLibrary
onLoadRequest: {
mediaListView.positionViewAtIndex(idx, ListView.Visible);
}
}
delegate: RowLayout {
// add mediaLibrary.count in the binding to ensure 'entity'
// is re-evaluated when mediaLibrary delegates are modified
property bool loading: model.status === SceneLoader.Loading
spacing: 2
width: parent.width - scrollBar.width / 2
property string src: model.source
onSrcChanged: focusAnim.restart()
RowLayout {
Layout.alignment: Qt.AlignTop
enabled: model.status === SceneLoader.Ready
spacing: 0
MaterialToolButton {
text: model.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off
font.pointSize: 10
ToolTip.text: model.visible ? "Hide" : "Show"
flat: true
opacity: model.visible ? 1.0 : 0.6
onClicked: {
if(hoverArea.modifiers & Qt.ControlModifier)
mediaLibrary.solo(index);
else
model.visible = !model.visible
}
// Handle modifiers on button click
MouseArea {
id: hoverArea
property int modifiers
anchors.fill: parent
hoverEnabled: true
onPositionChanged: modifiers = mouse.modifiers
onExited: modifiers = Qt.NoModifier
onPressed: {
modifiers = mouse.modifiers;
mouse.accepted = false;
}
}
}
MaterialToolButton {
text: MaterialIcons.filter_center_focus
font.pointSize: 10
ToolTip.text: "Frame"
onClicked: camera.viewEntity(mediaLibrary.entityAt(index))
flat: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 1
Layout.alignment: Qt.AlignTop
Label {
id: label
leftPadding: 0
rightPadding: 0
topPadding: 3
bottomPadding: topPadding
Layout.fillWidth: true
text: model.label
elide: Text.ElideMiddle
background: Rectangle {
Connections {
target: mediaLibrary
onLoadRequest: if(idx == index) focusAnim.restart()
}
ColorAnimation on color {
id: focusAnim
from: label.palette.highlight
to: "transparent"
duration: 2000
}
MouseArea {
anchors.fill: parent
onDoubleClicked: camera.viewEntity(mediaLibrary.entityAt(index))
}
}
}
Item {
Layout.fillWidth: true
implicitHeight: childrenRect.height
RowLayout {
visible: model.status === SceneLoader.Ready
MaterialLabel { visible: model.vertexCount; text: MaterialIcons.grain }
Label { visible: model.vertexCount; text: Format.intToString(model.vertexCount) }
MaterialLabel { visible: model.faceCount; text: MaterialIcons.details; rotation: -180 }
Label { visible: model.faceCount; text: Format.intToString(model.faceCount) }
MaterialLabel { visible: model.cameraCount; text: MaterialIcons.videocam }
Label { visible: model.cameraCount; text: model.cameraCount }
MaterialLabel { visible: model.textureCount; text: MaterialIcons.texture }
Label { visible: model.textureCount; text: model.textureCount }
}
}
}
MaterialToolButton {
id: requestMediaButton
Layout.alignment: Qt.AlignTop
enabled: !loading
text: loading || !model.requested ? MaterialIcons.radio_button_unchecked : MaterialIcons.radio_button_checked
font.pointSize: 10
palette.buttonText: model.valid ? "#4CAF50" : label.palette.buttonText
ToolTip.text: ""
onClicked: model.requested = !model.requested
}
MaterialToolButton {
Layout.alignment: Qt.AlignTop
visible: !loading
text: MaterialIcons.clear
font.pointSize: 10
ToolTip.text: "Remove"
onClicked: mediaLibrary.remove(index)
}
BusyIndicator {
visible: loading
running: visible
padding: 0
implicitHeight: 14
implicitWidth: requestMediaButton.width
}
}
}
}
}

View file

@ -1,6 +1,8 @@
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Controls 1.4 as Controls1
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQml.Models 2.2
import QtQuick.Scene3D 2.0 import QtQuick.Scene3D 2.0
import Qt3D.Core 2.1 import Qt3D.Core 2.1
import Qt3D.Render 2.1 import Qt3D.Render 2.1
@ -11,22 +13,12 @@ import MaterialIcons 2.2
import Controls 1.0 import Controls 1.0
FocusScope { FocusScope {
id: root id: root
property alias source: modelLoader.source
property alias abcSource: modelLoader.abcSource
property alias depthMapSource: modelLoader.depthMapSource
property int renderMode: 2 property int renderMode: 2
readonly property alias loading: modelLoader.loading property alias library: mediaLibrary
readonly property alias polyCount: modelLoader.polyCount
// Alembic optional support => won't be available if AlembicEntity plugin is not available
readonly property Component abcLoaderComp: Qt.createComponent("AlembicLoader.qml")
readonly property bool supportAlembic: abcLoaderComp.status == Component.Ready
readonly property Component depthMapLoaderComp: Qt.createComponent("DepthMapLoader.qml")
readonly property bool supportDepthMap: depthMapLoaderComp.status == Component.Ready
// functions // functions
function resetCameraCenter() { function resetCameraCenter() {
@ -40,122 +32,41 @@ FocusScope {
mainCamera.viewCenter = Qt.vector3d(0.0, 0.0, 0.0); mainCamera.viewCenter = Qt.vector3d(0.0, 0.0, 0.0);
} }
function findChildrenByProperty(node, propertyName, container) function load(filepath) {
{ mediaLibrary.load(filepath);
if(!node || !node.childNodes)
return;
for(var i=0; i < node.childNodes.length; ++i)
{
var childNode = node.childNodes[i];
if(!childNode)
continue;
if(childNode[propertyName] !== undefined)
container.push(childNode);
else
findChildrenByProperty(childNode, propertyName, container)
}
} }
// Remove automatically created DiffuseMapMaterial and function view(attribute) {
// instantiate a MaterialSwitcher instead mediaLibrary.view(attribute)
function setupMaterialSwitchers(rootEntity)
{
var materials = [];
findChildrenByProperty(rootEntity, "diffuse", materials);
var entities = []
materials.forEach(function(mat){
entities.push(mat.parent)
})
entities.forEach(function(entity) {
var mats = []
var hasTextures = false
// Create as many MaterialSwitcher as individual materials for this entity
// NOTE: we let each MaterialSwitcher modify the components of the entity
// and therefore remove the default material spawned by the sceneLoader
for(var i=0; i < entity.components.length; ++i)
{
var comp = entity.components[i]
// handle DiffuseMapMaterials created by SceneLoader
if(comp.toString().indexOf("QDiffuseMapMaterial") > -1)
{
// store material definition
var m = {
"diffuseMap": comp.diffuse.data[0].source,
"shininess": comp.shininess,
"specular": comp.specular,
"ambient": comp.ambient,
"mode": root.renderMode
}
mats.push(m)
hasTextures = true
}
if(comp.toString().indexOf("QPhongMaterial") > -1) {
// create MaterialSwitcher with default colors
mats.push({})
}
// Retrieve polycount using vertexPosition buffer
if(comp.toString().indexOf("Geometry") > -1) {
for(var k = 0; k < comp.geometry.attributes.length; ++k)
{
if(comp.geometry.attributes[k].name == "vertexPosition")
modelLoader.polyCount += comp.geometry.attributes[k].count / 3
}
}
}
modelLoader.meshHasTexture = mats.length > 0
mats.forEach(function(m){
// create a material switcher for each material definition
var matSwitcher = materialSwitcherComponent.createObject(entity, m)
matSwitcher.mode = Qt.binding(function(){ return root.renderMode })
})
})
} }
Component { function clear() {
id: materialSwitcherComponent mediaLibrary.clear()
MaterialSwitcher {}
}
function clear()
{
clearScene()
clearAbc()
}
function clearScene()
{
source = ''
}
function clearAbc()
{
abcSource = ''
}
function clearDepthMap()
{
depthMapSource = 'no_file'
depthMapSource = ''
} }
SystemPalette { id: activePalette } SystemPalette { id: activePalette }
Scene3D { Scene3D {
id: scene3D id: scene3D
anchors.fill: parent anchors.fill: parent
cameraAspectRatioMode: Scene3D.AutomaticAspectRatio // vs. UserAspectRatio cameraAspectRatioMode: Scene3D.AutomaticAspectRatio // vs. UserAspectRatio
hoverEnabled: false // if true, will trigger positionChanged events in attached MouseHandler hoverEnabled: true // if true, will trigger positionChanged events in attached MouseHandler
aspects: ["logic", "input"] aspects: ["logic", "input"]
focus: true focus: true
Keys.onPressed: { Keys.onPressed: {
if (event.key == Qt.Key_F) { if (event.key == Qt.Key_F) {
resetCameraCenter(); resetCameraCenter();
resetCameraPosition(); resetCameraPosition();
event.accepted = true; }
else if(Qt.Key_1 <= event.key && event.key <= Qt.Key_3)
{
Viewer3DSettings.renderMode = event.key - Qt.Key_1;
}
else {
event.accepted = false
} }
} }
@ -176,16 +87,30 @@ FocusScope {
Behavior on viewCenter { Behavior on viewCenter {
Vector3dAnimation { duration: 250 } Vector3dAnimation { duration: 250 }
} }
// Scene light, attached to the camera
Entity {
components: [
PointLight {
color: "white"
}
]
}
} }
// Scene light, attached to the camera
Entity { Entity {
components: [ components: [
PointLight { SphereMesh {
color: "white"
}, },
Transform { Transform {
translation: mainCamera.position id: viewCenterTransform
translation: mainCamera.viewCenter
scale: 0.005 * mainCamera.viewCenter.minus(mainCamera.position).length()
},
PhongMaterial {
ambient: "#FFF"
shininess: 0.2
diffuse: activePalette.highlight
specular: activePalette.highlight
} }
] ]
} }
@ -257,272 +182,49 @@ FocusScope {
Qt3DInput.InputSettings { } Qt3DInput.InputSettings { }
] ]
Entity { MediaLibrary {
id: modelLoader id: mediaLibrary
property string source renderMode: Viewer3DSettings.renderMode
property string abcSource // Picking to set focus point (camera view center)
property string depthMapSource // Only activate it when a double click may happen or when the 'Control' key is pressed
property int polyCount pickingEnabled: cameraController.pickingActive || doubleClickTimer.running
property bool meshHasTexture: false
// SceneLoader status is not reliable when loading a 3D file
property bool loading: false
onSourceChanged: {
polyCount = 0
meshHasTexture = false
loading = true
}
onAbcSourceChanged: {
if(root.supportAlembic)
loading = true
}
components: [sceneLoaderEntity, transform, picker] components: [
Transform {
// ObjectPicker used for view re-centering id: transform
ObjectPicker {
id: picker
// Triangle picking is expensive
// Only activate it when a double click may happen or when the 'Control' key is pressed
enabled: cameraController.pickingActive || doubleClickTimer.running
hoverEnabled: false
onPressed: {
if(pick.button == Qt.LeftButton)
mainCamera.viewCenter = pick.worldIntersection
doubleClickTimer.stop()
} }
}
Transform {
id: transform
}
Entity {
id: sceneLoaderEntity
enabled: showMeshCheckBox.checked
components: [
SceneLoader {
id: scene
source: modelLoader.source
onStatusChanged: {
if(scene.status != SceneLoader.Loading)
modelLoader.loading = false;
if(scene.status == SceneLoader.Ready)
{
setupMaterialSwitchers(modelLoader)
}
}
}
]
}
Entity {
id: abcLoaderEntity
// Instantiate the AlembicEntity dynamically
// to avoid import errors if the plugin is not available
property Entity abcLoader: null
enabled: showSfMCheckBox.checked
Component.onCompleted: {
if(!root.supportAlembic) // Alembic plugin not available
return
// destroy previously created entity
if(abcLoader != undefined)
abcLoader.destroy()
abcLoader = abcLoaderComp.createObject(abcLoaderEntity, {
'url': Qt.binding(function() { return modelLoader.abcSource } ),
'particleSize': Qt.binding(function() { return 0.01 * transform.scale }),
'locatorScale': Qt.binding(function() { return 0.2})
});
// urlChanged signal is emitted once the Alembic file is loaded
// set the 'loading' property to false when it's emitted
// TODO: AlembicEntity should expose a status
abcLoader.onUrlChanged.connect(function(){
modelLoader.loading = false
spawnCameraSelectors()
})
modelLoader.loading = false
}
function spawnCameraSelectors() {
// spawn camera selector for each camera
for(var i = 0; i < abcLoader.cameras.length; ++i)
{
var cam = abcLoader.cameras[i]
// retrieve view id
var viewId = cam.userProperties["mvg_viewId"]
if(viewId == undefined)
continue
var obj = camSelectionComponent.createObject(cam)
obj.viewId = viewId
}
}
// Camera selection picking and display
property Component camSelectionComponent: Component {
id: camSelectionComponent
Entity {
property string viewId
property alias ambient: mat.ambient
components: [
CuboidMesh { xExtent: 0.2; yExtent: 0.2; zExtent: 0.2},
PhongMaterial{
id: mat
ambient: viewId == _reconstruction.selectedViewId ? activePalette.highlight : "#CCC"
diffuse: ambient
},
ObjectPicker {
onClicked: _reconstruction.selectedViewId = viewId
}
]
}
}
}
Entity {
id: depthMapLoaderEntity
// Instantiate the DepthMapEntity dynamically
// to avoid import errors if the plugin is not available
property Entity depthMapLoader: null
enabled: showDepthMapCheckBox.checked
Component.onCompleted: {
if(!root.supportDepthMap) // DepthMap plugin not available
return
// destroy previously created entity
if(depthMapLoader != undefined)
depthMapLoader.destroy()
depthMapLoader = depthMapLoaderComp.createObject(depthMapLoaderEntity, {
'source': Qt.binding(function() { return modelLoader.depthMapSource } )
});
// 'sourceChanged' signal is emitted once the depthMap file is loaded
// set the 'loading' property to false when it's emitted
// TODO: DepthMapEntity should expose a status
depthMapLoader.onSourceChanged.connect(function(){ modelLoader.loading = false })
modelLoader.loading = false
}
}
Locator3D { enabled: locatorCheckBox.checked }
}
Grid3D { enabled: gridCheckBox.checked }
}
}
//
// UI Overlay
//
// Rotation/Scale
FloatingPane {
anchors { top: parent.top; left: parent.left }
GridLayout {
id: controlsLayout
columns: 3
columnSpacing: 6
property int sliderWidth: 70
// Rotation Controls
Label {
font.family: MaterialIcons.fontFamily
text: MaterialIcons.rotation3D
font.pointSize: 14
Layout.rowSpan: 3
}
Slider { implicitWidth: controlsLayout.sliderWidth; from: -180; to: 180; onPositionChanged: transform.rotationX = value }
Label { text: "X" }
Slider { implicitWidth: controlsLayout.sliderWidth; from: -180; to: 180; onPositionChanged: transform.rotationY = value }
Label { text: "Y" }
Slider { implicitWidth: controlsLayout.sliderWidth; from: -180; to: 180; onPositionChanged: transform.rotationZ = value }
Label { text: "Z" }
// Scale Control
Label { text: "Scale" }
Slider { Layout.columnSpan: 2; implicitWidth: controlsLayout.sliderWidth; from: 1; to: 10; onPositionChanged: transform.scale = value }
}
}
// Outliner
FloatingPane {
anchors { top: parent.top; right: parent.right }
Column {
Row {
visible: root.supportAlembic
CheckBox { id: showSfMCheckBox; text: "SfM"; checked: true; visible: root.supportAlembic; opacity: root.abcSource ? 1.0 : 0.6 }
ToolButton {
text: MaterialIcons.clear; font.family: MaterialIcons.fontFamily; visible: root.abcSource != '';
onClicked: clearAbc()
ToolTip.text: "Unload"
ToolTip.visible: hovered
}
}
Row {
visible: root.depthMapSource != ''
CheckBox { id: showDepthMapCheckBox; text: "DepthMap"; checked: true; }
ToolButton {
text: MaterialIcons.clear; font.family: MaterialIcons.fontFamily;
onClicked: clearDepthMap()
ToolTip.text: "Unload"
ToolTip.visible: hovered
}
}
Row {
CheckBox { id: showMeshCheckBox; text: "Mesh"; checked: true; opacity: root.source ? 1.0 : 0.6 }
ToolButton {
text: MaterialIcons.clear; font.family: MaterialIcons.fontFamily; visible: root.source != '';
onClicked: clearScene()
ToolTip.text: "Unload"
ToolTip.visible: hovered
}
}
CheckBox { id: gridCheckBox; text: "Grid"; checked: true }
CheckBox { id: locatorCheckBox; text: "Locator"; checked: true }
}
}
// Render Mode
FloatingPane {
anchors { bottom: parent.bottom; left: parent.left }
Row {
anchors.verticalCenter: parent.verticalCenter
Repeater {
model: [ // Can't use ListModel because of MaterialIcons expressions
{"name": "Solid", "icon": MaterialIcons.crop_din},
{"name": "Wireframe", "icon": MaterialIcons.grid_on},
{"name": "Textured", "icon": MaterialIcons.texture },
] ]
delegate: ToolButton {
text: modelData["icon"] onPressed: {
ToolTip.text: modelData["name"] if(pick.button == Qt.LeftButton)
ToolTip.visible: hovered {
font.family: MaterialIcons.fontFamily mainCamera.viewCenter = pick.worldIntersection;
font.pointSize: 11 }
padding: 4 doubleClickTimer.stop();
onClicked: root.renderMode = index
checkable: !checked // hack to disable check toggle on click
checked: renderMode === index
} }
Locator3D { enabled: Viewer3DSettings.displayLocator }
} }
Grid3D { enabled: Viewer3DSettings.displayGrid }
} }
} }
FloatingPane { // UI Overlay
anchors.right: parent.right Controls1.SplitView {
anchors.bottom: parent.bottom id: overlaySplitView
visible: modelLoader.polyCount > 0 anchors.fill: parent
Label {
text: modelLoader.polyCount + " faces" Item { Layout.fillWidth: true; Layout.minimumWidth: parent.width * 0.5 }
Inspector3D {
id: inspector
width: 220
Layout.minimumWidth: 5
camera: mainCamera
targetTransform: transform
mediaLibrary: mediaLibrary
} }
} }

View file

@ -21,7 +21,10 @@ Item {
property int renderMode: 2 property int renderMode: 2
// Rasterized point size // Rasterized point size
property real pointSize: 4 property real pointSize: 1.5
// Whether point size is fixed or view dependent // Whether point size is fixed or view dependent
property bool fixedPointSize: false property bool fixedPointSize: false
// Helpers display
property bool displayGrid: true
property bool displayLocator: true
} }

View file

@ -21,35 +21,33 @@ Item {
readonly property variant cameraInits: _reconstruction.cameraInits readonly property variant cameraInits: _reconstruction.cameraInits
property bool readOnly: false property bool readOnly: false
implicitWidth: 300 implicitWidth: 300
implicitHeight: 400 implicitHeight: 400
// Load a 3D media file in the 3D viewer // Load a 3D media file in the 3D viewer
function load3DMedia(filepath) function load3DMedia(filepath) {
{ viewer3D.load(filepath);
if(!Filepath.exists(Filepath.urlToString(filepath))) }
return
switch(Filepath.extension(filepath)) function viewAttribute(attr) {
{ viewer3D.view(attr);
case ".abc": viewer3D.abcSource = filepath; break;
case ".exr": viewer3D.depthMapSource = filepath; break;
case ".obj": viewer3D.source = filepath; break;
}
} }
Connections { Connections {
target: reconstruction target: reconstruction
onGraphChanged: { onGraphChanged: viewer3D.clear()
viewer3D.clear() onSfmChanged: viewSfM()
viewer2D.clear() onSfmReportChanged: viewSfM()
} }
onSfmReportChanged: { Component.onCompleted: viewSfM()
viewer3D.abcSource = ''
if(!reconstruction.sfm) // Load reconstruction's current SfM file
return function viewSfM() {
load3DMedia(Filepath.stringToUrl(reconstruction.sfm.attribute('output').value)) if(!reconstruction.sfm)
} return;
viewAttribute(reconstruction.sfm.attribute('output'));
} }
SystemPalette { id: activePalette } SystemPalette { id: activePalette }
@ -115,12 +113,16 @@ Item {
Panel { Panel {
title: "3D Viewer" title: "3D Viewer"
implicitWidth: Math.round(parent.width * 0.33) implicitWidth: Math.round(parent.width * 0.45)
Layout.minimumWidth: 20 Layout.minimumWidth: 20
Layout.minimumHeight: 80 Layout.minimumHeight: 80
Viewer3D { Viewer3D {
id: viewer3D id: viewer3D
readonly property var outputAttribute: _reconstruction.endNode ? _reconstruction.endNode.attribute("outputMesh") : null
readonly property bool outputReady: outputAttribute && _reconstruction.endNode.globalStatus === "SUCCESS"
readonly property int outputMediaIndex: library.find(outputAttribute)
anchors.fill: parent anchors.fill: parent
DropArea { DropArea {
anchors.fill: parent anchors.fill: parent
@ -129,20 +131,14 @@ Item {
} }
} }
Label {
anchors.centerIn: parent
text: "Loading..."
visible: viewer3D.loading
padding: 6
background: Rectangle { color: parent.palette.base; opacity: 0.5 }
}
// Load reconstructed model // Load reconstructed model
Button { Button {
text: "Load Model" text: "Load Model"
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 10 anchors.bottomMargin: 10
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
visible: viewer3D.outputReady && viewer3D.outputMediaIndex == -1
onClicked: viewAttribute(_reconstruction.endNode.attribute("outputMesh"))
} }
} }
} }