diff --git a/meshroom/ui/qml/Controls/ExpandableGroup.qml b/meshroom/ui/qml/Controls/ExpandableGroup.qml new file mode 100644 index 0000000000..afd4459cbf --- /dev/null +++ b/meshroom/ui/qml/Controls/ExpandableGroup.qml @@ -0,0 +1,76 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.11 +import MaterialIcons 2.2 + +/** + * A custom GroupBox with predefined header that can be hidden and expanded. + */ +GroupBox { + id: root + + title: "" + property int sidePadding: 6 + property alias labelBackground: labelBg + property alias toolBarContent: toolBar.data + property bool expanded: expandButton.checked + + padding: 2 + leftPadding: sidePadding + rightPadding: sidePadding + topPadding: label.height + padding + background: Item {} + + label: Pane { + background: Rectangle { + id: labelBg + color: palette.base + opacity: 0.8 + + MouseArea { + anchors.fill: parent + onClicked: { + expandButton.checked = !expandButton.checked + } + } + } + padding: 2 + width: root.width + + RowLayout { + width: parent.width + Label { + text: root.title + Layout.fillWidth: true + elide: Text.ElideRight + padding: 3 + font.bold: true + font.pointSize: 8 + } + RowLayout { + id: toolBar + height: parent.height + + MaterialToolButton { + id: expandButton + ToolTip.text: "Expand More" + text: MaterialIcons.expand_more + font.pointSize: 10 + implicitHeight: parent.height + checkable: true + checked: false + + onCheckedChanged: { + if (checked) { + ToolTip.text = "Expand Less" + text = MaterialIcons.expand_less + } else { + ToolTip.text = "Expand More" + text = MaterialIcons.expand_more + } + } + } + } + } + } +} diff --git a/meshroom/ui/qml/Controls/qmldir b/meshroom/ui/qml/Controls/qmldir index f4c247b140..7598fadf52 100644 --- a/meshroom/ui/qml/Controls/qmldir +++ b/meshroom/ui/qml/Controls/qmldir @@ -1,6 +1,7 @@ module Controls ColorChart 1.0 ColorChart.qml +ExpandableGroup 1.0 ExpandableGroup.qml FloatingPane 1.0 FloatingPane.qml Group 1.0 Group.qml KeyValue 1.0 KeyValue.qml diff --git a/meshroom/ui/qml/MaterialIcons/MaterialIcons.qml b/meshroom/ui/qml/MaterialIcons/MaterialIcons.qml index de733b7456..bcc39d794b 100644 --- a/meshroom/ui/qml/MaterialIcons/MaterialIcons.qml +++ b/meshroom/ui/qml/MaterialIcons/MaterialIcons.qml @@ -92,6 +92,7 @@ QtObject { readonly property string attachment: "\ue2bc" readonly property string audiotrack: "\ue3a1" readonly property string autorenew: "\ue863" + readonly property string auto_awesome_motion: "\ue661" readonly property string av_timer: "\ue01b" readonly property string backspace: "\ue14a" readonly property string backup: "\ue864" diff --git a/meshroom/ui/qml/Viewer3D/Inspector3D.qml b/meshroom/ui/qml/Viewer3D/Inspector3D.qml index 64299ba3f3..6674946f35 100644 --- a/meshroom/ui/qml/Viewer3D/Inspector3D.qml +++ b/meshroom/ui/qml/Viewer3D/Inspector3D.qml @@ -31,7 +31,8 @@ FloatingPane { anchors.fill: parent spacing: 4 - Group { + ExpandableGroup { + id: displayGroup Layout.fillWidth: true title: "DISPLAY" @@ -43,6 +44,7 @@ FloatingPane { Flow { Layout.columnSpan: 2 Layout.fillWidth: true + visible: displayGroup.expanded spacing: 1 MaterialToolButton { text: MaterialIcons.grid_on @@ -63,8 +65,13 @@ FloatingPane { onClicked: Viewer3DSettings.displayOrigin = !Viewer3DSettings.displayOrigin } } - MaterialLabel { text: MaterialIcons.grain; padding: 2 } + MaterialLabel { + text: MaterialIcons.grain + padding: 2 + visible: displayGroup.expanded + } RowLayout { + visible: displayGroup.expanded Slider { Layout.fillWidth: true; from: 0; to: 5; stepSize: 0.001 value: Viewer3DSettings.pointSize @@ -83,8 +90,13 @@ FloatingPane { } } - MaterialLabel { text: MaterialIcons.videocam; padding: 2 } + MaterialLabel { + text: MaterialIcons.videocam + padding: 2 + visible: displayGroup.expanded + } Slider { + visible: displayGroup.expanded value: Viewer3DSettings.cameraScale from: 0 to: 2 @@ -99,25 +111,30 @@ FloatingPane { } } - Group { + ExpandableGroup { + id: cameraGroup Layout.fillWidth: true title: "CAMERA" + ColumnLayout { width: parent.width // Image/Camera synchronization Flow { Layout.fillWidth: true + visible: cameraGroup.expanded spacing: 2 + // Synchronization MaterialToolButton { id: syncViewpointCamera enabled: _reconstruction ? _reconstruction.sfmReport : false text: MaterialIcons.linked_camera - ToolTip.text: "Sync with Image Selection" + ToolTip.text: "View Through The Active Camera" checked: enabled && Viewer3DSettings.syncViewpointCamera onClicked: Viewer3DSettings.syncViewpointCamera = !Viewer3DSettings.syncViewpointCamera } + // Image Overlay controls RowLayout { visible: syncViewpointCamera.enabled && Viewer3DSettings.syncViewpointCamera @@ -152,6 +169,109 @@ FloatingPane { onClicked: Viewer3DSettings.viewpointImageFrame = !Viewer3DSettings.viewpointImageFrame } } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + visible: cameraGroup.expanded + + RowLayout { + Layout.fillHeight: true + spacing: 2 + + MaterialToolButton { + id: resectionIdButton + text: MaterialIcons.switch_video + ToolTip.text: "Timeline Of Camera Reconstruction Groups" + ToolTip.visible: hovered + enabled: Viewer3DSettings.resectionIdCount + checked: enabled && Viewer3DSettings.displayResectionIds + onClicked: { + Viewer3DSettings.displayResectionIds = !Viewer3DSettings.displayResectionIds + Viewer3DSettings.resectionId = Viewer3DSettings.resectionIdCount + resectionIdSlider.value = Viewer3DSettings.resectionId + } + + onEnabledChanged: { + Viewer3DSettings.resectionId = Viewer3DSettings.resectionIdCount + resectionIdSlider.value = Viewer3DSettings.resectionId + if (!enabled) { + Viewer3DSettings.displayResectionIds = false + } + } + } + + Slider { + id: resectionIdSlider + value: Viewer3DSettings.resectionId + from: 0 + to: Viewer3DSettings.resectionIdCount + stepSize: 1 + onMoved: Viewer3DSettings.resectionId = value + Layout.fillWidth: true + leftPadding: 2 + rightPadding: 2 + visible: Viewer3DSettings.displayResectionIds + } + + Label { + text: resectionIdSlider.value + "/" + Viewer3DSettings.resectionIdCount + visible: Viewer3DSettings.displayResectionIds + } + } + + RowLayout { + spacing: 10 + Layout.fillWidth: true + Layout.margins: 2 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + MaterialToolLabel { + iconText: MaterialIcons.stop + label: { + var id = undefined + // Ensure there are entries in resectionGroups and a valid resectionId before accessing anything + if (Viewer3DSettings.resectionId !== undefined && Viewer3DSettings.resectionGroups.length > 0) + id = Math.min(Viewer3DSettings.resectionId, Viewer3DSettings.resectionIdCount) + if (id !== undefined && Viewer3DSettings.resectionGroups[id] !== undefined) + return Viewer3DSettings.resectionGroups[id] + return 0 + + } + ToolTip.text: "Number Of Cameras In Current Resection Group" + visible: Viewer3DSettings.displayResectionIds + } + + MaterialToolLabel { + iconText: MaterialIcons.auto_awesome_motion + label: { + let currentCameras = 0 + for (let i = 0; i <= Viewer3DSettings.resectionIdCount; i++) { + if (i <= Viewer3DSettings.resectionId) + currentCameras += Viewer3DSettings.resectionGroups[i] + } + + return currentCameras + } + ToolTip.text: "Number Of Cumulated Cameras" + visible: Viewer3DSettings.displayResectionIds + } + + MaterialToolLabel { + iconText: MaterialIcons.videocam + label: { + let totalCameras = 0 + for (let i = 0; i <= Viewer3DSettings.resectionIdCount; i++) { + totalCameras += Viewer3DSettings.resectionGroups[i] + } + + return totalCameras + } + ToolTip.text: "Total Number Of Cameras" + visible: Viewer3DSettings.displayResectionIds + } + } + } } } @@ -192,6 +312,12 @@ FloatingPane { ScrollBar.vertical: ScrollBar { id: scrollBar } + onCountChanged: { + if (mediaListView.count === 0) { + Viewer3DSettings.resectionIdCount = 0 + } + } + currentIndex: -1 Connections { @@ -217,13 +343,25 @@ FloatingPane { // add mediaLibrary.count in the binding to ensure 'entity' // is re-evaluated when mediaLibrary delegates are modified property bool loading: model.status === SceneLoader.Loading - property bool hovered: model.attribute ? (uigraph ? uigraph.hoveredNode === model.attribute.node : false) : containsMouse + property bool hovered: model.attribute ? (uigraph ? uigraph.hoveredNode === model.attribute.node : false) : containsMouse property bool isSelectedNode: model.attribute ? (uigraph ? uigraph.selectedNode === model.attribute.node : false) : false onIsSelectedNodeChanged: updateCurrentIndex() function updateCurrentIndex() { - if(isSelectedNode) { mediaListView.currentIndex = index } + if (isSelectedNode) { + mediaListView.currentIndex = index + } + + // If the index is updated, and the resection ID count is available, update every resection-related variable: + // this covers the changes of index that occur when a node whose output is already loaded in the 3D viewer is + // clicked/double-clicked, and when the active entry is removed from the list. + if (model.resectionIdCount) { + Viewer3DSettings.resectionIdCount = model.resectionIdCount + Viewer3DSettings.resectionGroups = model.resectionGroups + Viewer3DSettings.resectionId = model.resectionId + resectionIdSlider.value = model.resectionId + } } height: childrenRect.height @@ -234,22 +372,41 @@ FloatingPane { } hoverEnabled: true - onEntered: { if (model.attribute) uigraph.hoveredNode = model.attribute.node } - onExited: { if (model.attribute) uigraph.hoveredNode = null } + onEntered: { + if (model.attribute) + uigraph.hoveredNode = model.attribute.node + } + onExited: { + if (model.attribute) + uigraph.hoveredNode = null + } onClicked: { if (model.attribute) - uigraph.selectedNode = model.attribute.node; + uigraph.selectedNode = model.attribute.node else - uigraph.selectedNode = null; + uigraph.selectedNode = null if (mouse.button == Qt.RightButton) - contextMenu.popup(); - mediaListView.currentIndex = index; + contextMenu.popup() + mediaListView.currentIndex = index + + // Update the resection ID-related objects based on the active model + Viewer3DSettings.resectionIdCount = model.resectionIdCount + Viewer3DSettings.resectionGroups = model.resectionGroups + Viewer3DSettings.resectionId = model.resectionId + resectionIdSlider.value = model.resectionId } onDoubleClicked: { model.visible = true; nodeActivated(model.attribute.node); } + Connections { + target: resectionIdSlider + function onValueChanged() { + model.resectionId = resectionIdSlider.value + } + } + RowLayout { width: parent.width spacing: 2 diff --git a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml index e636614fc5..aa60488d08 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml @@ -58,6 +58,9 @@ Entity { "faceCount": 0, "cameraCount": 0, "textureCount": 0, + "resectionIdCount": 0, + "resectionId": 0, + "resectionGroups": [], "status": SceneLoader.None }) } @@ -308,12 +311,11 @@ Entity { if (object) { // bind media info to corresponding model roles // (test for object validity to avoid error messages right after object has been deleted) - var boundProperties = ["vertexCount", "faceCount", "cameraCount", "textureCount"] + var boundProperties = ["vertexCount", "faceCount", "cameraCount", "textureCount", "resectionIdCount", "resectionId", "resectionGroups"] boundProperties.forEach(function(prop) { model[prop] = Qt.binding(function() { return object ? object[prop] : 0 }) }) - } - else if (finalSource && status === Component.Ready) { + } else if (finalSource && status === Component.Ready) { // source was valid but no loader was created, remove element // check if component is ready to avoid removing element from the model before adding instance to the node remove(index) diff --git a/meshroom/ui/qml/Viewer3D/MediaLoader.qml b/meshroom/ui/qml/Viewer3D/MediaLoader.qml index a767cf5426..ee6ba0046f 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLoader.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLoader.qml @@ -99,8 +99,9 @@ import Utils 1.0 'source': source, 'pointSize': Qt.binding(function() { return 0.01 * Viewer3DSettings.pointSize }), 'locatorScale': Qt.binding(function() { return Viewer3DSettings.cameraScale }), - 'cameraPickingEnabled': Qt.binding(function() { return root.enabled }) - }) + 'cameraPickingEnabled': Qt.binding(function() { return root.enabled }), + 'resectionId': Qt.binding(function() { return Viewer3DSettings.resectionId }) + }); obj.statusChanged.connect(function() { if (obj.status === SceneLoader.Ready) { @@ -109,6 +110,11 @@ import Utils 1.0 } cameraCount = obj.spawnCameraSelectors(); } + Viewer3DSettings.resectionIdCount = obj.countResectionIds(); + Viewer3DSettings.resectionGroups = obj.countResectionGroups(Viewer3DSettings.resectionIdCount + 1); + resectionIdCount = Viewer3DSettings.resectionIdCount + resectionGroups = Viewer3DSettings.resectionGroups + resectionId = Viewer3DSettings.resectionIdCount root.status = obj.status; }) } diff --git a/meshroom/ui/qml/Viewer3D/MediaLoaderEntity.qml b/meshroom/ui/qml/Viewer3D/MediaLoaderEntity.qml index cbe56e5b03..9017024052 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLoaderEntity.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLoaderEntity.qml @@ -17,4 +17,10 @@ Entity { property int cameraCount /// Number of textures property int textureCount + /// Number of resection IDs + property int resectionIdCount + /// Current resection ID + property int resectionId + /// Groups of cameras based on resection IDs + property var resectionGroups } diff --git a/meshroom/ui/qml/Viewer3D/SfmDataLoader.qml b/meshroom/ui/qml/Viewer3D/SfmDataLoader.qml index e5637a1b5e..8dbaf33545 100644 --- a/meshroom/ui/qml/Viewer3D/SfmDataLoader.qml +++ b/meshroom/ui/qml/Viewer3D/SfmDataLoader.qml @@ -33,6 +33,34 @@ SfmDataEntity { return validCameras; } + function countResectionIds() { + var maxResectionId = 0 + for (var i = 0; i < root.cameras.length; i++) { + var cam = root.cameras[i] + var resectionId = cam.resectionId + if (resectionId === undefined || resectionId === 4294967295) + continue + if (resectionId > maxResectionId) + maxResectionId = resectionId + } + + return maxResectionId + } + + + function countResectionGroups(resectionIdCount) { + var arr = Array(resectionIdCount).fill(0) + for (var i = 0; i < root.cameras.length; i++) { + var cam = root.cameras[i] + var resectionId = cam.resectionId + if (resectionId === undefined || resectionId === 4294967295) + continue + arr[resectionId] = arr[resectionId] + 1 + } + + return arr + } + SystemPalette { id: activePalette } diff --git a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml index 39fb2bf3ed..68f939cfa6 100644 --- a/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml +++ b/meshroom/ui/qml/Viewer3D/Viewer3DSettings.qml @@ -56,4 +56,10 @@ Item { readonly property bool showViewpointImageOverlay: syncViewpointCamera && viewpointImageOverlay property bool viewpointImageFrame: false readonly property bool showViewpointImageFrame: syncViewpointCamera && viewpointImageFrame + + // Cameras' resection IDs + property bool displayResectionIds: false + property int resectionIdCount: 0 + property int resectionId: resectionIdCount + property var resectionGroups: [] // Number of cameras for each resection ID }