Skip to content

Commit f939161

Browse files
authored
Merge pull request #2913 from alicevision/dev/shapesImprovements
Shapes: Various improvements
2 parents 7cac2bf + e13747c commit f939161

File tree

8 files changed

+146
-64
lines changed

8 files changed

+146
-64
lines changed

meshroom/core/attribute.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,8 +1048,6 @@ def getValueAsDict(self) -> dict:
10481048
"""
10491049
from collections import defaultdict
10501050
outValue = defaultdict(dict)
1051-
if self.isLink:
1052-
return self._getInputLink().asLinkExpr()
10531051
if not self.shapeKeyable:
10541052
return super().getSerializedValue()
10551053
for attribute in self.value:
@@ -1062,6 +1060,35 @@ def getValueAsDict(self) -> dict:
10621060
for pair in attribute.keyValues.pairs:
10631061
outValue[str(pair.key)][attribute.name] = pair.value
10641062
return dict(outValue)
1063+
1064+
def getShapeAsDict(self) -> dict:
1065+
"""
1066+
Return the shape attribute as dict with the shape file structure.
1067+
"""
1068+
outDict = {
1069+
"name" : self.rootName,
1070+
"type" : self.type,
1071+
"properties" : { "color": self._color }
1072+
}
1073+
1074+
if not self.shapeKeyable:
1075+
# Not keyable shape, use properties.
1076+
outDict.get("properties").update(super().getSerializedValue())
1077+
else:
1078+
# Keyable shape, use observations.
1079+
from collections import defaultdict
1080+
outObservations = defaultdict(dict)
1081+
for attribute in self.value:
1082+
if isinstance(attribute, ShapeAttribute):
1083+
attributeDict = attribute.getValueAsDict()
1084+
if attributeDict:
1085+
for key, value in attributeDict.items():
1086+
outObservations[key][attribute.name] = value
1087+
else:
1088+
for pair in attribute.keyValues.pairs:
1089+
outObservations[str(pair.key)][attribute.name] = pair.value
1090+
outDict.update({ "observations" : dict(outObservations)})
1091+
return outDict
10651092

10661093
def _getVisible(self) -> bool:
10671094
"""
@@ -1225,12 +1252,18 @@ def __init__(self, node, attributeDesc: desc.ShapeList, isOutput: bool,
12251252
self._visible = True
12261253
super().__init__(node, attributeDesc, isOutput, root, parent)
12271254

1228-
def getShapesAsDicts(self):
1255+
def getValuesAsDicts(self):
12291256
"""
1230-
Return the shape list attribute value as dict.
1257+
Return the values of the children of the shape list attribute.
12311258
"""
12321259
return [shapeAttribute.getValueAsDict() for shapeAttribute in self.value]
12331260

1261+
def getShapesAsDicts(self):
1262+
"""
1263+
Return the children of the shape list attribute.
1264+
"""
1265+
return [shapeAttribute.getShapeAsDict() for shapeAttribute in self.value]
1266+
12341267
def _getVisible(self) -> bool:
12351268
"""
12361269
Return whether the shape list is visible for display.

meshroom/ui/components/shapes/shapeFilesHelper.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
from meshroom.ui.reconstruction import Reconstruction
22
from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, Slot
33
from meshroom.core.attribute import GroupAttribute, ListAttribute
4+
from shiboken6 import isValid
45
from .shapeFile import ShapeFile
56

7+
# Filter runtime warning when closing Meshroom with active shape files
8+
import warnings
9+
warnings.filterwarnings("ignore", message=".*Failed to disconnect.*", category=RuntimeWarning)
10+
611
class ShapeFilesHelper(BaseObject):
712
"""
813
Manages active project selected node shape files.
@@ -11,6 +16,7 @@ class ShapeFilesHelper(BaseObject):
1116
def __init__(self, activeProject:Reconstruction, parent=None):
1217
super().__init__(parent)
1318
self._activeProject = activeProject
19+
self._currentNode = activeProject.selectedNode
1420
self._shapeFiles = ListModel()
1521
self._activeProject.selectedViewIdChanged.connect(self._onSelectedViewIdChanged)
1622
self._activeProject.selectedNodeChanged.connect(self._onSelectedNodeChanged)
@@ -28,6 +34,15 @@ def _loadShapeFilesFromAttributes(self, attributes):
2834
viewId=self._activeProject.selectedViewId,
2935
parent=self._shapeFiles))
3036

37+
@Slot()
38+
def _loadShapeFiles(self):
39+
"""Load/Reload active project selected node shape files."""
40+
# clear shapeFiles model
41+
self._shapeFiles.clear()
42+
# load node shape files
43+
self._loadShapeFilesFromAttributes(self._activeProject.selectedNode.attributes)
44+
self.nodeShapeFilesChanged.emit()
45+
3146
@Slot()
3247
def _onSelectedViewIdChanged(self):
3348
"""Callback when the active project selected view id changes."""
@@ -37,17 +52,31 @@ def _onSelectedViewIdChanged(self):
3752
@Slot()
3853
def _onSelectedNodeChanged(self):
3954
"""Callback when the active project selected node changes."""
40-
# clear shapeFiles model
41-
self._shapeFiles = ListModel()
42-
# check current node
43-
if self._activeProject.selectedNode is None:
44-
return
45-
# check current node has displayable shape
46-
if not self._activeProject.selectedNode.hasDisplayableShape:
55+
# disconnect internalFolderChanged signal
56+
if self._currentNode is not None:
57+
try:
58+
self._currentNode.internalFolderChanged.disconnect(self._loadShapeFiles)
59+
except RuntimeError:
60+
# Signal was already disconnected or never connected
61+
pass
62+
# check selected node exists and selected node has displayable shape
63+
if self._activeProject.selectedNode is None or not self._activeProject.selectedNode.hasDisplayableShape:
64+
# clear shapeFiles model
65+
if isValid(self._shapeFiles):
66+
self._shapeFiles.clear()
67+
# clear current node
68+
self._currentNode = None
4769
return
70+
# update current node
71+
self._currentNode = self._activeProject.selectedNode
72+
# connect internalFolderChanged signal
73+
try:
74+
self._currentNode.internalFolderChanged.connect(self._loadShapeFiles)
75+
except RuntimeError:
76+
# Signal was already disconnected or never connected
77+
pass
4878
# load node shape files
49-
self._loadShapeFilesFromAttributes(self._activeProject.selectedNode.attributes)
50-
self.nodeShapeFilesChanged.emit()
79+
self._loadShapeFiles()
5180

5281
# Properties and signals
5382
nodeShapeFilesChanged = Signal()

meshroom/ui/qml/Shapes/Editor/Items/ShapeAttributeItem.qml

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,5 @@ Column {
4646
isAttribute: true
4747
}
4848

49-
// Expandable list
50-
Loader {
51-
active: itemHeader.isExpanded
52-
width: parent.width
53-
height: active ? (item ? item.implicitHeight || item.height : 0) : 0
54-
55-
sourceComponent: Pane {
56-
background: Rectangle { color: "transparent" }
57-
padding: 0
58-
implicitWidth: parent.width
59-
implicitHeight: 20
60-
61-
//Shape attribute observation
62-
}
63-
}
49+
// Perhaps add an expandable list for current observations later
6450
}

meshroom/ui/qml/Shapes/Editor/Items/ShapeDataItem.qml

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,5 @@ Column {
2828
isAttribute: false
2929
}
3030

31-
// Expandable list
32-
Loader {
33-
active: itemHeader.isExpanded
34-
width: parent.width
35-
height: active ? (item ? item.implicitHeight || item.height : 0) : 0
36-
37-
sourceComponent: Pane {
38-
background: Rectangle { color: "transparent" }
39-
padding: 0
40-
implicitWidth: parent.width
41-
implicitHeight: 20
42-
43-
//Shape data observation
44-
}
45-
}
31+
// Perhaps add an expandable list for current observations later
4632
}

meshroom/ui/qml/Shapes/Editor/Items/Utils/ItemHeader.qml

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -171,22 +171,26 @@ Pane {
171171
}
172172

173173
// Shape attributes dropdown
174-
MaterialToolButton {
175-
font.pointSize: 11
176-
padding: 2
177-
text: {
178-
if(isExpanded) {
179-
return (isShape) ? MaterialIcons.arrow_drop_down : MaterialIcons.keyboard_arrow_down
180-
}
181-
else {
182-
return (isShape) ? MaterialIcons.arrow_right : MaterialIcons.keyboard_arrow_right
174+
// For now, only for ShapeFile and ShapeListAttribute
175+
Loader {
176+
active: !isShape
177+
sourceComponent: MaterialToolButton {
178+
font.pointSize: 11
179+
padding: 2
180+
text: {
181+
if(isExpanded) {
182+
return (isShape) ? MaterialIcons.arrow_drop_down : MaterialIcons.keyboard_arrow_down
183+
}
184+
else {
185+
return (isShape) ? MaterialIcons.arrow_right : MaterialIcons.keyboard_arrow_right
186+
}
183187
}
188+
onClicked: { isExpanded = !isExpanded }
189+
enabled: true
190+
ToolTip.text: isExpanded ? "Collapse" : "Expand"
191+
ToolTip.visible: hovered
192+
ToolTip.delay: 800
184193
}
185-
onClicked: { isExpanded = !isExpanded }
186-
enabled: true
187-
ToolTip.text: isExpanded ? "Collapse" : "Expand"
188-
ToolTip.visible: hovered
189-
ToolTip.delay: 800
190194
}
191195

192196
// Shape color

meshroom/ui/qml/Shapes/Editor/ShapeEditor.qml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ Item {
2626
ScrollView {
2727
anchors.fill: parent
2828

29-
// Disable horizontal scroll
29+
// Disable horizontal scrolling
3030
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
31+
32+
// Ensure that vertical scrolling is always enabled when necessary
33+
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
34+
ScrollBar.vertical.visible: contentHeight > height
3135

3236
ColumnLayout {
3337
anchors.fill: parent
@@ -62,7 +66,7 @@ Item {
6266
model: ShapeFilesHelper.nodeShapeFiles
6367
delegate: ShapeEditorItem {
6468
model: object
65-
width: ListView.view.width
69+
width: ListView.view.width
6670
}
6771
}
6872
}

meshroom/ui/qml/Shapes/Editor/ShapeEditorItem.qml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Loader {
1717

1818
// Source component
1919
sourceComponent: {
20-
switch(model.type) {
20+
switch(itemLoader.model.type) {
2121
case "ShapeFile": return shapeFileComponent
2222
case "ShapeList": return shapeListAttributeComponent
2323
default: return shapeAttributeComponent

tests/test_attributeShape.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ def test_linkAttribute(self):
422422
assert nodeB.pointList.getSerializedValue() == nodeA.pointList.asLinkExpr()
423423

424424

425-
def test_valueAsDict(self):
425+
def test_exportDict(self):
426426
graph = Graph("")
427427
node = graph.addNewNode(NodeWithShapeAttributes.__name__)
428428

@@ -434,18 +434,48 @@ def test_valueAsDict(self):
434434

435435
# Check uninitialized shape attribute
436436
# Shape list attribute should be empty list
437+
assert node.pointList.getValuesAsDicts() == []
438+
assert node.keyablePointList.getValuesAsDicts() == []
437439
assert node.pointList.getShapesAsDicts() == []
438440
assert node.keyablePointList.getShapesAsDicts() == []
439441
# Not keyable shape attribute should be default
440442
assert node.point.getValueAsDict() == {"x" : -1, "y" : -1}
441443
assert node.line.getValueAsDict() == {"a" : {"x" : -1, "y" : -1}, "b" : {"x" : -1, "y" : -1}}
442444
assert node.rectangle.getValueAsDict() == {"center" : {"x" : -1, "y" : -1}, "size" : {"width" : -1, "height" : -1}}
443445
assert node.circle.getValueAsDict() == {"center" : {"x" : -1, "y" : -1}, "radius" : -1}
446+
assert node.point.getShapeAsDict() == {"name" : node.point.rootName,
447+
"type" : node.point.type,
448+
"properties" : {"color" : node.point.shapeColor, "x" : -1, "y" : -1}}
449+
assert node.line.getShapeAsDict() == {"name" : node.line.rootName,
450+
"type" : node.line.type,
451+
"properties" : {"color" : node.line.shapeColor, "a" : {"x" : -1, "y" : -1}, "b" : {"x" : -1, "y" : -1}}}
452+
assert node.rectangle.getShapeAsDict() == {"name" : node.rectangle.rootName,
453+
"type" : node.rectangle.type,
454+
"properties" : {"color" : node.rectangle.shapeColor, "center" : {"x" : -1, "y" : -1}, "size" : {"width" : -1, "height" : -1}}}
455+
assert node.circle.getShapeAsDict() == {"name" : node.circle.rootName,
456+
"type" : node.circle.type,
457+
"properties" : {"color" : node.circle.shapeColor, "center" : {"x" : -1, "y" : -1}, "radius" : -1}}
444458
# Keyable shape attribute should be empty dict
445459
assert node.keyablePoint.getValueAsDict() == {}
446460
assert node.keyableLine.getValueAsDict() == {}
447461
assert node.keyableRectangle.getValueAsDict() == {}
448462
assert node.keyableCircle.getValueAsDict() == {}
463+
assert node.keyablePoint.getShapeAsDict() == {"name" : node.keyablePoint.rootName,
464+
"type" : node.keyablePoint.type,
465+
"properties" : {"color" : node.keyablePoint.shapeColor},
466+
"observations" : {}}
467+
assert node.keyableLine.getShapeAsDict() == {"name" : node.keyableLine.rootName,
468+
"type" : node.keyableLine.type,
469+
"properties" : {"color" : node.keyableLine.shapeColor},
470+
"observations" : {}}
471+
assert node.keyableRectangle.getShapeAsDict() == {"name" : node.keyableRectangle.rootName,
472+
"type" : node.keyableRectangle.type,
473+
"properties" : {"color" : node.keyableRectangle.shapeColor},
474+
"observations" : {}}
475+
assert node.keyableCircle.getShapeAsDict() == {"name" : node.keyableCircle.rootName,
476+
"type" : node.keyableCircle.type,
477+
"properties" : {"color" : node.keyableCircle.shapeColor},
478+
"observations" : {}}
449479

450480
# Add one shape with an observation
451481
node.pointList.append(observationPoint)
@@ -463,15 +493,25 @@ def test_valueAsDict(self):
463493

464494
# Check shape attribute
465495
# Shape list attribute should be empty dict
466-
assert node.pointList.getShapesAsDicts() == [observationPoint]
467-
assert node.keyablePointList.getShapesAsDicts() == [{"0" : observationPoint}]
496+
assert node.pointList.getValuesAsDicts() == [observationPoint]
497+
assert node.keyablePointList.getValuesAsDicts() == [{"0" : observationPoint}]
498+
assert node.pointList.getShapesAsDicts()[0].get("properties") == {"color" : node.keyablePoint.shapeColor} | observationPoint
499+
assert node.keyablePointList.getShapesAsDicts()[0].get("observations") == {"0" : observationPoint}
468500
# Not keyable shape attribute should be default
469501
assert node.point.getValueAsDict() == observationPoint
470502
assert node.line.getValueAsDict() == observationLine
471503
assert node.rectangle.getValueAsDict() == observationRectangle
472504
assert node.circle.getValueAsDict() == observationCircle
505+
assert node.point.getShapeAsDict().get("properties") == {"color" : node.point.shapeColor} | observationPoint
506+
assert node.line.getShapeAsDict().get("properties") == {"color" : node.line.shapeColor} | observationLine
507+
assert node.rectangle.getShapeAsDict().get("properties") == {"color" : node.rectangle.shapeColor} | observationRectangle
508+
assert node.circle.getShapeAsDict().get("properties") == {"color" : node.circle.shapeColor} | observationCircle
473509
# Keyable shape attribute should be empty dict
474510
assert node.keyablePoint.getValueAsDict() == {"0" : observationPoint}
475511
assert node.keyableLine.getValueAsDict() == {"0" : observationLine}
476512
assert node.keyableRectangle.getValueAsDict() == {"0" : observationRectangle}
477-
assert node.keyableCircle.getValueAsDict() == {"0" : observationCircle}
513+
assert node.keyableCircle.getValueAsDict() == {"0" : observationCircle}
514+
assert node.keyablePoint.getShapeAsDict().get("observations") == {"0" : observationPoint}
515+
assert node.keyableLine.getShapeAsDict().get("observations") == {"0" : observationLine}
516+
assert node.keyableRectangle.getShapeAsDict().get("observations") == {"0" : observationRectangle}
517+
assert node.keyableCircle.getShapeAsDict().get("observations") == {"0" : observationCircle}

0 commit comments

Comments
 (0)