Skip to content

Commit e88f34a

Browse files
authored
Merge pull request #2908 from alicevision/dev/shapes
Introduce shape attributes, shape viewer, and shape editor
2 parents 3e1d496 + 35035d0 commit e88f34a

35 files changed

+3057
-19
lines changed

meshroom/core/attribute.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,8 @@ def matchText(self, text: str) -> bool:
539539
is2dDisplayable = Property(bool, _is2dDisplayable, constant=True)
540540
# Whether the attribute value is displayable in 3d.
541541
is3dDisplayable = Property(bool, _is3dDisplayable, constant=True)
542+
# Whether the attribute is a shape or a shape list, managed by the ShapeEditor and ShapeViewer.
543+
hasDisplayableShape = Property(bool, lambda self: False, constant=True)
542544

543545
# Attribute link properties and signals
544546
inputLinksChanged = Signal()
@@ -999,3 +1001,261 @@ def matchText(self, text: str) -> bool:
9991001
# Override value property
10001002
value = Property(Variant, Attribute._getValue, _setValue, notify=Attribute.valueChanged)
10011003
isDefault = Property(bool, lambda self: all(v.isDefault for v in self.value), notify=Attribute.valueChanged)
1004+
1005+
1006+
class ShapeAttribute(GroupAttribute):
1007+
"""
1008+
GroupAttribute subtype tailored for shape-specific handling.
1009+
"""
1010+
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
1015+
super().__init__(node, attributeDesc, isOutput, root, parent)
1016+
1017+
# Override
1018+
# Signal observationsChanged should be emitted.
1019+
def _setValue(self, exportedValue):
1020+
super()._setValue(exportedValue)
1021+
self.observationsChanged.emit()
1022+
1023+
# Override
1024+
# Signal observationsChanged should be emitted.
1025+
def resetToDefaultValue(self):
1026+
super().resetToDefaultValue()
1027+
self.observationsChanged.emit()
1028+
1029+
# Override
1030+
# Signal observationsChanged should be emitted.
1031+
def upgradeValue(self, exportedValue):
1032+
super().upgradeValue(exportedValue)
1033+
self.observationsChanged.emit()
1034+
1035+
# Override
1036+
# Fix missing link expression serialization.
1037+
# Should be remove if link expression serialization is added in GroupAttribute.
1038+
def getSerializedValue(self):
1039+
if self.isLink:
1040+
return self._getInputLink().asLinkExpr()
1041+
return {key: attr.getSerializedValue() for key, attr in self._value.objects.items()}
1042+
1043+
def getValueAsDict(self) -> dict:
1044+
"""
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.
1048+
"""
1049+
from collections import defaultdict
1050+
outValue = defaultdict(dict)
1051+
if self.isLink:
1052+
return self._getInputLink().asLinkExpr()
1053+
if not self.shapeKeyable:
1054+
return super().getSerializedValue()
1055+
for attribute in self.value:
1056+
if isinstance(attribute, ShapeAttribute):
1057+
attributeDict = attribute.getValueAsDict()
1058+
if attributeDict:
1059+
for key, value in attributeDict.items():
1060+
outValue[key][attribute.name] = value
1061+
else:
1062+
for pair in attribute.keyValues.pairs:
1063+
outValue[str(pair.key)][attribute.name] = pair.value
1064+
return dict(outValue)
1065+
1066+
def _getVisible(self) -> bool:
1067+
"""
1068+
Return whether the shape attribute is visible for display.
1069+
"""
1070+
return self._visible
1071+
1072+
def _setVisible(self, visible:bool):
1073+
"""
1074+
Set the shape attribute visibility for display.
1075+
"""
1076+
self._visible = visible
1077+
self.shapeChanged.emit()
1078+
1079+
def _getColor(self) -> str:
1080+
"""
1081+
Return the shape attribute color for display.
1082+
"""
1083+
if self.isLink:
1084+
return self.inputLink.shapeColor
1085+
return self._color
1086+
1087+
@raiseIfLink
1088+
def _setColor(self, color: str):
1089+
"""
1090+
Set the shape attribute color for display.
1091+
"""
1092+
self._color = color
1093+
self.shapeChanged.emit()
1094+
1095+
def _hasKeyableChilds(self) -> bool:
1096+
"""
1097+
Whether all child attributes are keyable.
1098+
"""
1099+
return all((isinstance(attribute, ShapeAttribute) and attribute.shapeKeyable) or
1100+
attribute.keyable for attribute in self.value)
1101+
1102+
def _getNbObservations(self) -> int:
1103+
"""
1104+
Return the shape attribute number of observations.
1105+
Note: Observation is a value defined across all child attributes for a specific key.
1106+
"""
1107+
if self.shapeKeyable:
1108+
firstAttribute = next(iter(self.value.values()))
1109+
if isinstance(firstAttribute, ShapeAttribute):
1110+
return firstAttribute.nbObservations
1111+
return len(firstAttribute.keyValues.pairs)
1112+
return 1
1113+
1114+
def _getObservationKeys(self) -> list:
1115+
"""
1116+
Return the shape attribute list of observation keys.
1117+
Note: Observation is a value defined across all child attributes for a specific key.
1118+
"""
1119+
if not self.shapeKeyable:
1120+
return []
1121+
firstAttribute = next(iter(self.value.values()))
1122+
if isinstance(firstAttribute, ShapeAttribute):
1123+
return firstAttribute.observationKeys
1124+
return firstAttribute.keyValues.getKeys()
1125+
1126+
@Slot(str, result=bool)
1127+
def hasObservation(self, key: str) -> bool:
1128+
"""
1129+
Whether the shape attribute has an observation for the given key.
1130+
Note: Observation is a value defined across all child attributes for a specific key.
1131+
"""
1132+
if not self.shapeKeyable:
1133+
return True
1134+
return all((isinstance(attribute, ShapeAttribute) and attribute.hasObservation(key)) or
1135+
(not isinstance(attribute, ShapeAttribute) and attribute.keyValues.hasKey(key))
1136+
for attribute in self.value)
1137+
1138+
@raiseIfLink
1139+
def removeObservation(self, key: str):
1140+
"""
1141+
Remove the shape attribute observation for the given key.
1142+
Note: Observation is a value defined across all child attributes for a specific key.
1143+
"""
1144+
for attribute in self.value:
1145+
if isinstance(attribute, ShapeAttribute):
1146+
attribute.removeObservation(key)
1147+
else:
1148+
if attribute.keyable:
1149+
attribute.keyValues.remove(key)
1150+
else:
1151+
attribute.resetToDefaultValue()
1152+
self.observationsChanged.emit()
1153+
1154+
@raiseIfLink
1155+
def setObservation(self, key: str, observation: Variant):
1156+
"""
1157+
Set the shape attribute observation for the given key with the given observation.
1158+
Note: Observation is a value defined across all child attributes for a specific key.
1159+
"""
1160+
for attributeStr, value in observation.items():
1161+
attribute = self.childAttribute(attributeStr)
1162+
if attribute is None:
1163+
raise RuntimeError(f"Cannot set shape observation for attribute {self._getFullName()} \
1164+
observation is incorrect.")
1165+
if isinstance(attribute, ShapeAttribute):
1166+
attribute.setObservation(key, value)
1167+
else:
1168+
if attribute.keyable:
1169+
attribute.keyValues.add(key, value)
1170+
else:
1171+
attribute.value = value
1172+
self.observationsChanged.emit()
1173+
1174+
@Slot(str, result=Variant)
1175+
def getObservation(self, key: str) -> Variant:
1176+
"""
1177+
Return the shape attribute observation for the given key.
1178+
Note: Observation is a value defined across all child attributes for a specific key.
1179+
"""
1180+
observation = {}
1181+
for attribute in self.value:
1182+
if isinstance(attribute, ShapeAttribute):
1183+
shapeObservation = attribute.getObservation(key)
1184+
if shapeObservation is None:
1185+
return None
1186+
else :
1187+
observation[attribute.name] = shapeObservation
1188+
else:
1189+
if attribute.keyable:
1190+
if attribute.keyValues.hasKey(key):
1191+
observation[attribute.name] = attribute.keyValues.getValueAtKeyOrDefault(key)
1192+
else:
1193+
return None
1194+
else:
1195+
observation[attribute.name] = attribute.value
1196+
return observation
1197+
1198+
# Properties and signals
1199+
# Emitted when a shape related property changed (color, visibility).
1200+
shapeChanged = Signal()
1201+
# Emitted when a shape observation changed.
1202+
observationsChanged = Signal()
1203+
# Whether the shape is displayable.
1204+
isVisible = Property(bool, _getVisible, _setVisible, notify=shapeChanged)
1205+
# The shape color for display.
1206+
shapeColor = Property(str, _getColor, _setColor, notify=shapeChanged)
1207+
# The shape list of observation keys.
1208+
observationKeys = Property(Variant, _getObservationKeys, notify=observationsChanged)
1209+
# The number of observation defined.
1210+
nbObservations = Property(int, _getNbObservations, notify=observationsChanged)
1211+
# Whether the shape attribute childs are keyable.
1212+
shapeKeyable = Property(bool,_hasKeyableChilds, constant=True)
1213+
# Override hasDisplayableShape property.
1214+
hasDisplayableShape = Property(bool, lambda self: True, constant=True)
1215+
# Override value property.
1216+
value = Property(Variant, Attribute._getValue, _setValue, notify=Attribute.valueChanged)
1217+
1218+
class ShapeListAttribute(ListAttribute):
1219+
"""
1220+
ListAttribute subtype tailored for shape-specific handling.
1221+
"""
1222+
1223+
def __init__(self, node, attributeDesc: desc.ShapeList, isOutput: bool,
1224+
root=None, parent=None):
1225+
self._visible = True
1226+
super().__init__(node, attributeDesc, isOutput, root, parent)
1227+
1228+
def getShapesAsDicts(self):
1229+
"""
1230+
Return the shape list attribute value as dict.
1231+
"""
1232+
return [shapeAttribute.getValueAsDict() for shapeAttribute in self.value]
1233+
1234+
def _getVisible(self) -> bool:
1235+
"""
1236+
Return whether the shape list is visible for display.
1237+
"""
1238+
if self.isLink:
1239+
return self.inputLink.isVisible
1240+
return self._visible
1241+
1242+
def _setVisible(self, visible:bool):
1243+
"""
1244+
Set the shape visibility for display.
1245+
"""
1246+
if self.isLink:
1247+
self.inputLink.isVisible = visible
1248+
else:
1249+
self._visible = visible
1250+
for attribute in self.value:
1251+
if isinstance(attribute, ShapeAttribute):
1252+
attribute.isVisible = visible
1253+
self.shapeListChanged.emit()
1254+
1255+
# Properties and signals
1256+
# Emitted when a shape list related property changed.
1257+
shapeListChanged = Signal()
1258+
# Whether the shape list is displayable.
1259+
isVisible = Property(bool, _getVisible, _setVisible, notify=shapeListChanged)
1260+
# Override hasDisplayableShape property.
1261+
hasDisplayableShape = Property(bool, lambda self: True, constant=True)

meshroom/core/desc/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
PushButtonParam,
1212
StringParam,
1313
)
14+
from .shapeAttribute import (
15+
Shape,
16+
ShapeList,
17+
Size2d,
18+
Point2d,
19+
Line2d,
20+
Rectangle,
21+
Circle
22+
)
1423
from .computation import (
1524
DynamicNodeSize,
1625
Level,

0 commit comments

Comments
 (0)