[ui] Attribute: Attribute actually displayed in the 3dViewer are flagged with a visibility icon (eye)

This commit is contained in:
nicolas-lambert-tc 2025-05-09 18:11:38 +02:00
parent 4a67736d9e
commit cfd2b1e00a
7 changed files with 85 additions and 23 deletions

View file

@ -44,6 +44,9 @@ class Attribute(BaseObject):
""" """
""" """
stringIsLinkRe = re.compile(r'^\{[A-Za-z]+[A-Za-z0-9_.\[\]]*\}$') 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): def __init__(self, node, attributeDesc, isOutput, root=None, parent=None):
""" """
@ -418,6 +421,28 @@ class Attribute(BaseObject):
# Emit if the enable status has changed # Emit if the enable status has changed
self.setEnabled(self.getEnabled()) 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) name = Property(str, getName, constant=True)
fullName = Property(str, getFullName, constant=True) fullName = Property(str, getFullName, constant=True)
fullNameToNode = Property(str, getFullNameToNode, constant=True) fullNameToNode = Property(str, getFullNameToNode, constant=True)
@ -430,6 +455,8 @@ class Attribute(BaseObject):
type = Property(str, getType, constant=True) type = Property(str, getType, constant=True)
baseType = Property(str, getType, constant=True) baseType = Property(str, getType, constant=True)
isReadOnly = Property(bool, _isReadOnly, constant=True) isReadOnly = Property(bool, _isReadOnly, constant=True)
is3D = Property(bool, _is3D, constant=True)
is2D = Property(bool, _is2D, constant=True)
# Description of the attribute # Description of the attribute
descriptionChanged = Signal() descriptionChanged = Signal()

View file

@ -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, Return True if at least one attribute is a File that can be loaded in the 3D Viewer,
False otherwise. False otherwise.
""" """
# List of supported extensions, taken from Viewer3DSettings
supportedExts = ['.obj', '.stl', '.fbx', '.gltf', '.abc', '.ply'] return next((attr for attr in self._attributes if attr.enabled and attr.isOutput and attr.is3D), None) is not None
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
name = Property(str, getName, constant=True) name = Property(str, getName, constant=True)
defaultLabel = Property(str, getDefaultLabel, constant=True) defaultLabel = Property(str, getDefaultLabel, constant=True)

View file

@ -1105,20 +1105,30 @@ Page {
for (var i = 0; i < node.attributes.count; i++) { for (var i = 0; i < node.attributes.count; i++) {
var attr = node.attributes.at(i) var attr = node.attributes.at(i)
if (attr.isOutput && attr.desc.semantic !== "image") if (attr.isOutput && attr.desc.semantic !== "image")
if (!alreadyDisplay || attr.desc.semantic == "3D") if (!alreadyDisplay || attr.desc.semantic == "3D") {
if (workspaceView.viewIn3D(attr, mouse)) if (workspaceView.viewIn3D(attr, mouse))
alreadyDisplay = true alreadyDisplay = true
}
} }
} }
function viewIn2D(attribute, mouse) {
workspaceView.viewer2D.tryLoadNode(attribute.node)
workspaceView.viewer2D.setAttributeName(attribute.name)
}
function viewIn3D(attribute, mouse) { function viewIn3D(attribute, mouse) {
if (!panel3dViewer || (!attribute.node.has3DOutput && !attribute.node.hasAttribute("useBoundingBox")))
if (!panel3dViewer || (!attribute.node.has3DOutput && !attribute.node.hasAttribute("useBoundingBox"))) {
return false return false
}
var loaded = panel3dViewer.viewer3D.view(attribute) var loaded = panel3dViewer.viewer3D.view(attribute)
// solo media if Control modifier was held // 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) panel3dViewer.viewer3D.solo(attribute)
}
return loaded return loaded
} }
} }
@ -1343,8 +1353,21 @@ Page {
var n = _reconstruction.upgradeNode(node) var n = _reconstruction.upgradeNode(node)
_reconstruction.selectedNode = n _reconstruction.selectedNode = n
} }
onAttributeDoubleClicked: function(mouse, attribute) {
if (attribute.is2D) {
workspaceView.viewIn2D(attribute, mouse)
}
else if (attribute.is3D) {
workspaceView.viewIn3D(attribute, mouse)
}
}
} }
} }
} }
} }
} }

View file

@ -50,12 +50,12 @@ RowLayout {
padding: 0 padding: 0
Layout.preferredWidth: labelWidth || implicitWidth Layout.preferredWidth: labelWidth || implicitWidth
Layout.fillHeight: true Layout.fillHeight: true
RowLayout { RowLayout {
spacing: 0 spacing: 0
width: parent.width width: parent.width
height: parent.height height: parent.height
Label { Label {
id: parameterLabel id: parameterLabel
@ -155,6 +155,7 @@ RowLayout {
text: "Open File" text: "Open File"
onClicked: Qt.openUrlExternally(Filepath.stringToUrl(attribute.evalValue)) onClicked: Qt.openUrlExternally(Filepath.stringToUrl(attribute.evalValue))
} }
} }
onClicked: function(mouse) { onClicked: function(mouse) {
@ -171,7 +172,7 @@ RowLayout {
MaterialLabel { MaterialLabel {
text: MaterialIcons.visibility text: MaterialIcons.visibility
font.pointSize: 7 font.pointSize: 7
visible: attribute.isOutput && attribute === _reconstruction.displayedAttr2D visible: attribute.isOutput && (attribute === _reconstruction.displayedAttr2D || _reconstruction.displayedAttrs3D.count && _reconstruction.displayedAttrs3D.contains(attribute))
} }
MaterialLabel { MaterialLabel {
@ -180,7 +181,8 @@ RowLayout {
color: palette.mid color: palette.mid
font.pointSize: 8 font.pointSize: 8
padding: 4 padding: 4
} }
} }
} }

View file

@ -343,6 +343,10 @@ FocusScope {
return [] return []
} }
function setAttributeName(attrName) {
outputAttribute.setName(attrName)
}
onDisplayedNodeChanged: { onDisplayedNodeChanged: {
if (!displayedNode) { if (!displayedNode) {
root.source = "" root.source = ""
@ -1608,6 +1612,13 @@ FocusScope {
root.source = getImageFile() root.source = getImageFile()
root.sequence = getSequence() root.sequence = getSequence()
} }
function setName(attrName) {
const attrIndex = outputAttribute.names.indexOf(attrName)
if (attrIndex > -1) {
outputAttribute.currentIndex = attrIndex
}
}
} }
MaterialToolButton { MaterialToolButton {

View file

@ -107,6 +107,7 @@ Entity {
"label": label ? label : Filepath.basename(pathStr), "label": label ? label : Filepath.basename(pathStr),
"section": "External" "section": "External"
})) }))
} }
function view(attribute) { function view(attribute) {
@ -115,15 +116,16 @@ Entity {
return return
} }
var attrLabel = attribute.isOutput ? "" : attribute.fullName.replace(attribute.node.name, "")
var section = attribute.node.label var section = attribute.node.label
// Add file to the internal ListModel // Add file to the internal ListModel
m.mediaModel.append( m.mediaModel.append(
makeElement({ makeElement({
"label": section + attrLabel, "label": `${section}.${attribute.label}`,
"section": section, "section": section,
"attribute": attribute "attribute": attribute
})) }))
} }
function remove(index) { function remove(index) {
@ -384,11 +386,15 @@ Entity {
onObjectAdded: function(index, object) { onObjectAdded: function(index, object) {
// Notify object that it is now fully instantiated // Notify object that it is now fully instantiated
object.fullyInstantiated = true object.fullyInstantiated = true
_reconstruction.displayedAttrs3D.append(object.modelSource)
} }
onObjectRemoved: function(index, object) { onObjectRemoved: function(index, object) {
if (m.sourceToEntity[object.modelSource]) if (m.sourceToEntity[object.modelSource])
delete m.sourceToEntity[object.modelSource] delete m.sourceToEntity[object.modelSource]
_reconstruction.displayedAttrs3D.remove(object.modelSource)
} }
} }
} }

View file

@ -476,6 +476,7 @@ class Reconstruction(UIGraph):
# initialize activeAttributes (attributes currently visible in some viewers) # initialize activeAttributes (attributes currently visible in some viewers)
self._displayedAttr2D = None self._displayedAttr2D = None
self._displayedAttrs3D = meshroom.common.ListModel()
# - CameraInit # - CameraInit
self._cameraInit = None # current CameraInit node self._cameraInit = None # current CameraInit node
@ -515,7 +516,6 @@ class Reconstruction(UIGraph):
def setActive(self, active): def setActive(self, active):
self._active = active self._active = active
@Slot()
def clear(self): def clear(self):
self.clearActiveNodes() self.clearActiveNodes()
super().clear() super().clear()
@ -1069,7 +1069,10 @@ class Reconstruction(UIGraph):
liveSfmManager = Property(QObject, lambda self: self._liveSfmManager, constant=True) liveSfmManager = Property(QObject, lambda self: self._liveSfmManager, constant=True)
displayedAttr2DChanged = Signal() 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) @Slot(QObject)
def setActiveNode(self, node, categories=True, inputs=True): def setActiveNode(self, node, categories=True, inputs=True):