From cfd2b1e00aa7d99e7ed7a696395a53b2bee38aca Mon Sep 17 00:00:00 2001 From: nicolas-lambert-tc Date: Fri, 9 May 2025 18:11:38 +0200 Subject: [PATCH] [ui] Attribute: Attribute actually displayed in the 3dViewer are flagged with a visibility icon (eye) --- meshroom/core/attribute.py | 27 +++++++++++++++++ meshroom/core/node.py | 14 ++------- meshroom/ui/qml/Application.qml | 29 +++++++++++++++++-- .../qml/GraphEditor/AttributeItemDelegate.qml | 10 ++++--- meshroom/ui/qml/Viewer/Viewer2D.qml | 11 +++++++ meshroom/ui/qml/Viewer3D/MediaLibrary.qml | 10 +++++-- meshroom/ui/reconstruction.py | 7 +++-- 7 files changed, 85 insertions(+), 23 deletions(-) diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index b3ed89b9..f8feed0c 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -44,6 +44,9 @@ class Attribute(BaseObject): """ """ stringIsLinkRe = re.compile(r'^\{[A-Za-z]+[A-Za-z0-9_.\[\]]*\}$') + + VALID_IMAGE_SEMANTICS = ["image", "imageList", "sequence"] + VALID_3D_EXTENSIONS = ['.obj', '.stl', '.fbx', '.gltf', '.abc', '.ply'] def __init__(self, node, attributeDesc, isOutput, root=None, parent=None): """ @@ -418,6 +421,28 @@ class Attribute(BaseObject): # Emit if the enable status has changed self.setEnabled(self.getEnabled()) + def _is3D(self) -> bool: + """ Return True if the current attribute is considered as a 3d file """ + + # List of supported extensions, taken from Viewer3DSettings + if self.desc.semantic == "3d": + return True + + # If the attribute is a File attribute, it is an instance of str and can be iterated over + hasSupportedExt = isinstance(self.value, str) and any(ext in self.value for ext in Attribute.VALID_3D_EXTENSIONS) + if hasSupportedExt: + return True + + return False + + def _is2D(self) -> bool: + """ Return True if the current attribute is considered as a 2d file """ + + if not self.desc.semantic: + return False + + return next((imageSemantic for imageSemantic in Attribute.VALID_IMAGE_SEMANTICS if self.desc.semantic == imageSemantic), None) is not None + name = Property(str, getName, constant=True) fullName = Property(str, getFullName, constant=True) fullNameToNode = Property(str, getFullNameToNode, constant=True) @@ -430,6 +455,8 @@ class Attribute(BaseObject): type = Property(str, getType, constant=True) baseType = Property(str, getType, constant=True) isReadOnly = Property(bool, _isReadOnly, constant=True) + is3D = Property(bool, _is3D, constant=True) + is2D = Property(bool, _is2D, constant=True) # Description of the attribute descriptionChanged = Signal() diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 392e3316..a431a11f 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -1617,18 +1617,8 @@ class BaseNode(BaseObject): Return True if at least one attribute is a File that can be loaded in the 3D Viewer, False otherwise. """ - # List of supported extensions, taken from Viewer3DSettings - supportedExts = ['.obj', '.stl', '.fbx', '.gltf', '.abc', '.ply'] - for attr in self._attributes: - if not attr.enabled or not attr.isOutput: - continue - if attr.desc.semantic == "3d": - return True - # If the attribute is a File attribute, it is an instance of str and can be iterated over - hasSupportedExt = isinstance(attr.value, str) and any(ext in attr.value for ext in supportedExts) - if hasSupportedExt: - return True - return False + + return next((attr for attr in self._attributes if attr.enabled and attr.isOutput and attr.is3D), None) is not None name = Property(str, getName, constant=True) defaultLabel = Property(str, getDefaultLabel, constant=True) diff --git a/meshroom/ui/qml/Application.qml b/meshroom/ui/qml/Application.qml index 59e04d12..2591f900 100644 --- a/meshroom/ui/qml/Application.qml +++ b/meshroom/ui/qml/Application.qml @@ -1105,20 +1105,30 @@ Page { for (var i = 0; i < node.attributes.count; i++) { var attr = node.attributes.at(i) if (attr.isOutput && attr.desc.semantic !== "image") - if (!alreadyDisplay || attr.desc.semantic == "3D") + if (!alreadyDisplay || attr.desc.semantic == "3D") { if (workspaceView.viewIn3D(attr, mouse)) alreadyDisplay = true + } + } } + function viewIn2D(attribute, mouse) { + workspaceView.viewer2D.tryLoadNode(attribute.node) + workspaceView.viewer2D.setAttributeName(attribute.name) + } + function viewIn3D(attribute, mouse) { - if (!panel3dViewer || (!attribute.node.has3DOutput && !attribute.node.hasAttribute("useBoundingBox"))) + + if (!panel3dViewer || (!attribute.node.has3DOutput && !attribute.node.hasAttribute("useBoundingBox"))) { return false + } var loaded = panel3dViewer.viewer3D.view(attribute) // solo media if Control modifier was held - if (loaded && mouse && mouse.modifiers & Qt.ControlModifier) + if (loaded && mouse && mouse.modifiers & Qt.ControlModifier) { panel3dViewer.viewer3D.solo(attribute) + } return loaded } } @@ -1343,8 +1353,21 @@ Page { var n = _reconstruction.upgradeNode(node) _reconstruction.selectedNode = n } + + onAttributeDoubleClicked: function(mouse, attribute) { + + if (attribute.is2D) { + workspaceView.viewIn2D(attribute, mouse) + } + + else if (attribute.is3D) { + workspaceView.viewIn3D(attribute, mouse) + } + + } } } } } + } \ No newline at end of file diff --git a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml index d8fce7e9..b9a845ba 100644 --- a/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml +++ b/meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml @@ -50,12 +50,12 @@ RowLayout { padding: 0 Layout.preferredWidth: labelWidth || implicitWidth Layout.fillHeight: true - + RowLayout { spacing: 0 width: parent.width height: parent.height - + Label { id: parameterLabel @@ -155,6 +155,7 @@ RowLayout { text: "Open File" onClicked: Qt.openUrlExternally(Filepath.stringToUrl(attribute.evalValue)) } + } onClicked: function(mouse) { @@ -171,7 +172,7 @@ RowLayout { MaterialLabel { text: MaterialIcons.visibility font.pointSize: 7 - visible: attribute.isOutput && attribute === _reconstruction.displayedAttr2D + visible: attribute.isOutput && (attribute === _reconstruction.displayedAttr2D || _reconstruction.displayedAttrs3D.count && _reconstruction.displayedAttrs3D.contains(attribute)) } MaterialLabel { @@ -180,7 +181,8 @@ RowLayout { color: palette.mid font.pointSize: 8 padding: 4 - } + } + } } diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 1ea00de1..efb4d8f6 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -343,6 +343,10 @@ FocusScope { return [] } + function setAttributeName(attrName) { + outputAttribute.setName(attrName) + } + onDisplayedNodeChanged: { if (!displayedNode) { root.source = "" @@ -1608,6 +1612,13 @@ FocusScope { root.source = getImageFile() root.sequence = getSequence() } + + function setName(attrName) { + const attrIndex = outputAttribute.names.indexOf(attrName) + if (attrIndex > -1) { + outputAttribute.currentIndex = attrIndex + } + } } MaterialToolButton { diff --git a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml index bb305d75..54cf1af5 100644 --- a/meshroom/ui/qml/Viewer3D/MediaLibrary.qml +++ b/meshroom/ui/qml/Viewer3D/MediaLibrary.qml @@ -107,6 +107,7 @@ Entity { "label": label ? label : Filepath.basename(pathStr), "section": "External" })) + } function view(attribute) { @@ -115,15 +116,16 @@ Entity { return } - var attrLabel = attribute.isOutput ? "" : attribute.fullName.replace(attribute.node.name, "") var section = attribute.node.label + // Add file to the internal ListModel m.mediaModel.append( makeElement({ - "label": section + attrLabel, + "label": `${section}.${attribute.label}`, "section": section, "attribute": attribute })) + } function remove(index) { @@ -384,11 +386,15 @@ Entity { onObjectAdded: function(index, object) { // Notify object that it is now fully instantiated object.fullyInstantiated = true + _reconstruction.displayedAttrs3D.append(object.modelSource) } onObjectRemoved: function(index, object) { if (m.sourceToEntity[object.modelSource]) + delete m.sourceToEntity[object.modelSource] + _reconstruction.displayedAttrs3D.remove(object.modelSource) } + } } diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index fd6ebc66..3098f59b 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -476,6 +476,7 @@ class Reconstruction(UIGraph): # initialize activeAttributes (attributes currently visible in some viewers) self._displayedAttr2D = None + self._displayedAttrs3D = meshroom.common.ListModel() # - CameraInit self._cameraInit = None # current CameraInit node @@ -515,7 +516,6 @@ class Reconstruction(UIGraph): def setActive(self, active): self._active = active - @Slot() def clear(self): self.clearActiveNodes() super().clear() @@ -1069,7 +1069,10 @@ class Reconstruction(UIGraph): liveSfmManager = Property(QObject, lambda self: self._liveSfmManager, constant=True) displayedAttr2DChanged = Signal() - displayedAttr2D = makeProperty(QObject, "_displayedAttr2D", displayedAttr2DChanged) + displayedAttr2D = makeProperty(QObject, "_displayedAttr2D", displayedAttr2DChanged) + + displayedAttrs3DChanged = Signal() + displayedAttrs3D = Property(QObject, lambda self: self._displayedAttrs3D, notify=displayedAttrs3DChanged) @Slot(QObject) def setActiveNode(self, node, categories=True, inputs=True):