Skip to content

Commit e54eadd

Browse files
authored
Merge pull request #2914 from alicevision/dev/shapesGeo
Shapes: Refactored `ShapeAttribute` to use new `GeometryAttribute`
2 parents f939161 + e025862 commit e54eadd

File tree

15 files changed

+622
-509
lines changed

15 files changed

+622
-509
lines changed

meshroom/core/attribute.py

Lines changed: 140 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,15 +1003,12 @@ def matchText(self, text: str) -> bool:
10031003
isDefault = Property(bool, lambda self: all(v.isDefault for v in self.value), notify=Attribute.valueChanged)
10041004

10051005

1006-
class ShapeAttribute(GroupAttribute):
1006+
class GeometryAttribute(GroupAttribute):
10071007
"""
1008-
GroupAttribute subtype tailored for shape-specific handling.
1008+
GroupAttribute subtype tailored for geometry-specific handling.
10091009
"""
10101010

1011-
def __init__(self, node, attributeDesc: desc.Shape, isOutput: bool,
1012-
root=None, parent=None):
1013-
self._visible = True
1014-
self._color = "#2A82DA" # default shape color
1011+
def __init__(self, node, attributeDesc: desc.Geometry, isOutput: bool, root=None, parent=None):
10151012
super().__init__(node, attributeDesc, isOutput, root, parent)
10161013

10171014
# Override
@@ -1038,20 +1035,20 @@ def upgradeValue(self, exportedValue):
10381035
def getSerializedValue(self):
10391036
if self.isLink:
10401037
return self._getInputLink().asLinkExpr()
1041-
return {key: attr.getSerializedValue() for key, attr in self._value.objects.items()}
1038+
return super().getSerializedValue()
10421039

10431040
def getValueAsDict(self) -> dict:
10441041
"""
1045-
Return the shape attribute value as dict.
1046-
For not keyable shape, this is the same as getSerializedValue().
1047-
For keyable shape, the dict is indexed by key.
1042+
Return the geometry attribute value as dict.
1043+
For not keyable geometry, this is the same as getSerializedValue().
1044+
For keyable geometry, the dict is indexed by key.
10481045
"""
10491046
from collections import defaultdict
10501047
outValue = defaultdict(dict)
1051-
if not self.shapeKeyable:
1048+
if not self.observationKeyable:
10521049
return super().getSerializedValue()
10531050
for attribute in self.value:
1054-
if isinstance(attribute, ShapeAttribute):
1051+
if isinstance(attribute, GeometryAttribute):
10551052
attributeDict = attribute.getValueAsDict()
10561053
if attributeDict:
10571054
for key, value in attributeDict.items():
@@ -1061,115 +1058,57 @@ def getValueAsDict(self) -> dict:
10611058
outValue[str(pair.key)][attribute.name] = pair.value
10621059
return dict(outValue)
10631060

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
1092-
1093-
def _getVisible(self) -> bool:
1094-
"""
1095-
Return whether the shape attribute is visible for display.
1096-
"""
1097-
return self._visible
1098-
1099-
def _setVisible(self, visible:bool):
1100-
"""
1101-
Set the shape attribute visibility for display.
1102-
"""
1103-
self._visible = visible
1104-
self.shapeChanged.emit()
1105-
1106-
def _getColor(self) -> str:
1107-
"""
1108-
Return the shape attribute color for display.
1109-
"""
1110-
if self.isLink:
1111-
return self.inputLink.shapeColor
1112-
return self._color
1113-
1114-
@raiseIfLink
1115-
def _setColor(self, color: str):
1116-
"""
1117-
Set the shape attribute color for display.
1118-
"""
1119-
self._color = color
1120-
self.shapeChanged.emit()
1121-
11221061
def _hasKeyableChilds(self) -> bool:
11231062
"""
11241063
Whether all child attributes are keyable.
11251064
"""
1126-
return all((isinstance(attribute, ShapeAttribute) and attribute.shapeKeyable) or
1065+
return all((isinstance(attribute, GeometryAttribute) and attribute.observationKeyable) or
11271066
attribute.keyable for attribute in self.value)
11281067

11291068
def _getNbObservations(self) -> int:
11301069
"""
1131-
Return the shape attribute number of observations.
1070+
Return the geometry attribute number of observations.
11321071
Note: Observation is a value defined across all child attributes for a specific key.
11331072
"""
1134-
if self.shapeKeyable:
1073+
if self.observationKeyable:
11351074
firstAttribute = next(iter(self.value.values()))
1136-
if isinstance(firstAttribute, ShapeAttribute):
1075+
if isinstance(firstAttribute, GeometryAttribute):
11371076
return firstAttribute.nbObservations
11381077
return len(firstAttribute.keyValues.pairs)
11391078
return 1
11401079

11411080
def _getObservationKeys(self) -> list:
11421081
"""
1143-
Return the shape attribute list of observation keys.
1082+
Return the geometry attribute list of observation keys.
11441083
Note: Observation is a value defined across all child attributes for a specific key.
11451084
"""
1146-
if not self.shapeKeyable:
1085+
if not self.observationKeyable:
11471086
return []
11481087
firstAttribute = next(iter(self.value.values()))
1149-
if isinstance(firstAttribute, ShapeAttribute):
1088+
if isinstance(firstAttribute, GeometryAttribute):
11501089
return firstAttribute.observationKeys
11511090
return firstAttribute.keyValues.getKeys()
11521091

11531092
@Slot(str, result=bool)
11541093
def hasObservation(self, key: str) -> bool:
11551094
"""
1156-
Whether the shape attribute has an observation for the given key.
1095+
Whether the geometry attribute has an observation for the given key.
11571096
Note: Observation is a value defined across all child attributes for a specific key.
11581097
"""
1159-
if not self.shapeKeyable:
1098+
if not self.observationKeyable:
11601099
return True
1161-
return all((isinstance(attribute, ShapeAttribute) and attribute.hasObservation(key)) or
1162-
(not isinstance(attribute, ShapeAttribute) and attribute.keyValues.hasKey(key))
1100+
return all((isinstance(attribute, GeometryAttribute) and attribute.hasObservation(key)) or
1101+
(not isinstance(attribute, GeometryAttribute) and attribute.keyValues.hasKey(key))
11631102
for attribute in self.value)
11641103

11651104
@raiseIfLink
11661105
def removeObservation(self, key: str):
11671106
"""
1168-
Remove the shape attribute observation for the given key.
1107+
Remove the geometry attribute observation for the given key.
11691108
Note: Observation is a value defined across all child attributes for a specific key.
11701109
"""
11711110
for attribute in self.value:
1172-
if isinstance(attribute, ShapeAttribute):
1111+
if isinstance(attribute, GeometryAttribute):
11731112
attribute.removeObservation(key)
11741113
else:
11751114
if attribute.keyable:
@@ -1181,15 +1120,15 @@ def removeObservation(self, key: str):
11811120
@raiseIfLink
11821121
def setObservation(self, key: str, observation: Variant):
11831122
"""
1184-
Set the shape attribute observation for the given key with the given observation.
1123+
Set the geometry attribute observation for the given key with the given observation.
11851124
Note: Observation is a value defined across all child attributes for a specific key.
11861125
"""
11871126
for attributeStr, value in observation.items():
11881127
attribute = self.childAttribute(attributeStr)
11891128
if attribute is None:
1190-
raise RuntimeError(f"Cannot set shape observation for attribute {self._getFullName()} \
1129+
raise RuntimeError(f"Cannot set geometry observation for attribute {self._getFullName()} \
11911130
observation is incorrect.")
1192-
if isinstance(attribute, ShapeAttribute):
1131+
if isinstance(attribute, GeometryAttribute):
11931132
attribute.setObservation(key, value)
11941133
else:
11951134
if attribute.keyable:
@@ -1201,17 +1140,17 @@ def setObservation(self, key: str, observation: Variant):
12011140
@Slot(str, result=Variant)
12021141
def getObservation(self, key: str) -> Variant:
12031142
"""
1204-
Return the shape attribute observation for the given key.
1143+
Return the geometry attribute observation for the given key.
12051144
Note: Observation is a value defined across all child attributes for a specific key.
12061145
"""
12071146
observation = {}
12081147
for attribute in self.value:
1209-
if isinstance(attribute, ShapeAttribute):
1210-
shapeObservation = attribute.getObservation(key)
1211-
if shapeObservation is None:
1148+
if isinstance(attribute, GeometryAttribute):
1149+
geoObservation = attribute.getObservation(key)
1150+
if geoObservation is None:
12121151
return None
12131152
else :
1214-
observation[attribute.name] = shapeObservation
1153+
observation[attribute.name] = geoObservation
12151154
else:
12161155
if attribute.keyable:
12171156
if attribute.keyValues.hasKey(key):
@@ -1222,43 +1161,135 @@ def getObservation(self, key: str) -> Variant:
12221161
observation[attribute.name] = attribute.value
12231162
return observation
12241163

1164+
# Properties and signals
1165+
# Emitted when a geometry observation changed.
1166+
observationsChanged = Signal()
1167+
# Whether the geometry attribute childs are keyable.
1168+
observationKeyable = Property(bool,_hasKeyableChilds, constant=True)
1169+
# The list of geometry observation keys.
1170+
observationKeys = Property(Variant, _getObservationKeys, notify=observationsChanged)
1171+
# The number of geometry observation defined.
1172+
nbObservations = Property(int, _getNbObservations, notify=observationsChanged)
1173+
1174+
1175+
1176+
class ShapeAttribute(GroupAttribute):
1177+
"""
1178+
GroupAttribute subtype tailored for shape-specific handling.
1179+
"""
1180+
1181+
def __init__(self, node, attributeDesc: desc.Shape, isOutput: bool, root=None, parent=None):
1182+
super().__init__(node, attributeDesc, isOutput, root, parent)
1183+
self._visible = True
1184+
1185+
# Override
1186+
# Connect geometry attribute valueChanged to emit geometryChanged signal.
1187+
def _initValue(self):
1188+
super()._initValue()
1189+
# Using Attribute.valueChanged for the userName, userColor, geometry properties results in a segmentation fault.
1190+
# As a workaround, we manually connect valueChanged to shapeChanged or geometryChanged.
1191+
self.value.get("userName").valueChanged.connect(self._onShapeChanged)
1192+
self.value.get("userColor").valueChanged.connect(self._onShapeChanged)
1193+
self.geometry.valueChanged.connect(self._onGeometryChanged)
1194+
1195+
# Override
1196+
# Fix missing link expression serialization.
1197+
# Should be remove if link expression serialization is added in GroupAttribute.
1198+
def getSerializedValue(self):
1199+
if self.isLink:
1200+
return self._getInputLink().asLinkExpr()
1201+
return super().getSerializedValue()
1202+
1203+
def getShapeAsDict(self) -> dict:
1204+
"""
1205+
Return the shape attribute as dict with the shape file structure.
1206+
"""
1207+
outDict = {
1208+
"name" : self.userName if self.userName else self.rootName,
1209+
"type" : self.type,
1210+
"properties" : { "color": self.userColor }
1211+
}
1212+
if not self.geometry.observationKeyable:
1213+
# Not keyable geometry, use properties.
1214+
outDict.get("properties").update(self.geometry.getSerializedValue())
1215+
else:
1216+
# Keyable geometry, use observations.
1217+
outDict.update({ "observations" : self.geometry.getValueAsDict()})
1218+
return outDict
1219+
1220+
def _getVisible(self) -> bool:
1221+
"""
1222+
Return whether the shape attribute is visible for display.
1223+
"""
1224+
return self._visible
1225+
1226+
def _setVisible(self, visible:bool):
1227+
"""
1228+
Set the shape attribute visibility for display.
1229+
"""
1230+
self._visible = visible
1231+
self.shapeChanged.emit()
1232+
1233+
def _getUserName(self) -> str:
1234+
"""
1235+
Return the shape attribute user name for display.
1236+
"""
1237+
return self.value.get("userName").value
1238+
1239+
def _getUserColor(self) -> str:
1240+
"""
1241+
Return the shape attribute user color for display.
1242+
"""
1243+
return self.value.get("userColor").value
1244+
1245+
@Slot()
1246+
def _onShapeChanged(self):
1247+
"""
1248+
Emit shapeChanged signal.
1249+
Used when shape userName or userColor value changed.
1250+
"""
1251+
self.shapeChanged.emit()
1252+
1253+
@Slot()
1254+
def _onGeometryChanged(self):
1255+
"""
1256+
Emit geometryChanged signal.
1257+
Used when geometry attribute value changed.
1258+
"""
1259+
self.geometryChanged.emit()
1260+
12251261
# Properties and signals
12261262
# Emitted when a shape related property changed (color, visibility).
12271263
shapeChanged = Signal()
12281264
# Emitted when a shape observation changed.
1229-
observationsChanged = Signal()
1265+
geometryChanged = Signal()
12301266
# Whether the shape is displayable.
12311267
isVisible = Property(bool, _getVisible, _setVisible, notify=shapeChanged)
1232-
# The shape color for display.
1233-
shapeColor = Property(str, _getColor, _setColor, notify=shapeChanged)
1234-
# The shape list of observation keys.
1235-
observationKeys = Property(Variant, _getObservationKeys, notify=observationsChanged)
1236-
# The number of observation defined.
1237-
nbObservations = Property(int, _getNbObservations, notify=observationsChanged)
1238-
# Whether the shape attribute childs are keyable.
1239-
shapeKeyable = Property(bool,_hasKeyableChilds, constant=True)
1268+
# The shape user name for display.
1269+
userName = Property(str, _getUserName, notify=shapeChanged)
1270+
# The shape user color for display.
1271+
userColor = Property(str, _getUserColor, notify=shapeChanged)
1272+
# The shape geometry group attribute.
1273+
geometry = Property(Variant, lambda self: self.value.get("geometry"), notify=geometryChanged)
12401274
# Override hasDisplayableShape property.
12411275
hasDisplayableShape = Property(bool, lambda self: True, constant=True)
1242-
# Override value property.
1243-
value = Property(Variant, Attribute._getValue, _setValue, notify=Attribute.valueChanged)
12441276

12451277
class ShapeListAttribute(ListAttribute):
12461278
"""
12471279
ListAttribute subtype tailored for shape-specific handling.
12481280
"""
12491281

1250-
def __init__(self, node, attributeDesc: desc.ShapeList, isOutput: bool,
1251-
root=None, parent=None):
1252-
self._visible = True
1282+
def __init__(self, node, attributeDesc: desc.ShapeList, isOutput: bool, root=None, parent=None):
12531283
super().__init__(node, attributeDesc, isOutput, root, parent)
1284+
self._visible = True
12541285

1255-
def getValuesAsDicts(self):
1286+
def getGeometriesAsDict(self):
12561287
"""
1257-
Return the values of the children of the shape list attribute.
1288+
Return the geometries values of the children of the shape list attribute.
12581289
"""
1259-
return [shapeAttribute.getValueAsDict() for shapeAttribute in self.value]
1290+
return [shapeAttribute.geometry.getValueAsDict() for shapeAttribute in self.value]
12601291

1261-
def getShapesAsDicts(self):
1292+
def getShapesAsDict(self):
12621293
"""
12631294
Return the children of the shape list attribute.
12641295
"""

meshroom/core/desc/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@
1111
PushButtonParam,
1212
StringParam,
1313
)
14+
from .geometryAttribute import (
15+
Geometry,
16+
Size2d,
17+
Vec2d,
18+
)
1419
from .shapeAttribute import (
1520
Shape,
1621
ShapeList,
17-
Size2d,
1822
Point2d,
1923
Line2d,
2024
Rectangle,

0 commit comments

Comments
 (0)