Skip to content
249 changes: 140 additions & 109 deletions meshroom/core/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -1003,15 +1003,12 @@ def matchText(self, text: str) -> bool:
isDefault = Property(bool, lambda self: all(v.isDefault for v in self.value), notify=Attribute.valueChanged)


class ShapeAttribute(GroupAttribute):
class GeometryAttribute(GroupAttribute):
"""
GroupAttribute subtype tailored for shape-specific handling.
GroupAttribute subtype tailored for geometry-specific handling.
"""

def __init__(self, node, attributeDesc: desc.Shape, isOutput: bool,
root=None, parent=None):
self._visible = True
self._color = "#2A82DA" # default shape color
def __init__(self, node, attributeDesc: desc.Geometry, isOutput: bool, root=None, parent=None):
super().__init__(node, attributeDesc, isOutput, root, parent)

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

def getValueAsDict(self) -> dict:
"""
Return the shape attribute value as dict.
For not keyable shape, this is the same as getSerializedValue().
For keyable shape, the dict is indexed by key.
Return the geometry attribute value as dict.
For not keyable geometry, this is the same as getSerializedValue().
For keyable geometry, the dict is indexed by key.
"""
from collections import defaultdict
outValue = defaultdict(dict)
if not self.shapeKeyable:
if not self.observationKeyable:
return super().getSerializedValue()
for attribute in self.value:
if isinstance(attribute, ShapeAttribute):
if isinstance(attribute, GeometryAttribute):
attributeDict = attribute.getValueAsDict()
if attributeDict:
for key, value in attributeDict.items():
Expand All @@ -1061,115 +1058,57 @@ def getValueAsDict(self) -> dict:
outValue[str(pair.key)][attribute.name] = pair.value
return dict(outValue)

def getShapeAsDict(self) -> dict:
"""
Return the shape attribute as dict with the shape file structure.
"""
outDict = {
"name" : self.rootName,
"type" : self.type,
"properties" : { "color": self._color }
}

if not self.shapeKeyable:
# Not keyable shape, use properties.
outDict.get("properties").update(super().getSerializedValue())
else:
# Keyable shape, use observations.
from collections import defaultdict
outObservations = defaultdict(dict)
for attribute in self.value:
if isinstance(attribute, ShapeAttribute):
attributeDict = attribute.getValueAsDict()
if attributeDict:
for key, value in attributeDict.items():
outObservations[key][attribute.name] = value
else:
for pair in attribute.keyValues.pairs:
outObservations[str(pair.key)][attribute.name] = pair.value
outDict.update({ "observations" : dict(outObservations)})
return outDict

def _getVisible(self) -> bool:
"""
Return whether the shape attribute is visible for display.
"""
return self._visible

def _setVisible(self, visible:bool):
"""
Set the shape attribute visibility for display.
"""
self._visible = visible
self.shapeChanged.emit()

def _getColor(self) -> str:
"""
Return the shape attribute color for display.
"""
if self.isLink:
return self.inputLink.shapeColor
return self._color

@raiseIfLink
def _setColor(self, color: str):
"""
Set the shape attribute color for display.
"""
self._color = color
self.shapeChanged.emit()

def _hasKeyableChilds(self) -> bool:
"""
Whether all child attributes are keyable.
"""
return all((isinstance(attribute, ShapeAttribute) and attribute.shapeKeyable) or
return all((isinstance(attribute, GeometryAttribute) and attribute.observationKeyable) or
attribute.keyable for attribute in self.value)

def _getNbObservations(self) -> int:
"""
Return the shape attribute number of observations.
Return the geometry attribute number of observations.
Note: Observation is a value defined across all child attributes for a specific key.
"""
if self.shapeKeyable:
if self.observationKeyable:
firstAttribute = next(iter(self.value.values()))
if isinstance(firstAttribute, ShapeAttribute):
if isinstance(firstAttribute, GeometryAttribute):
return firstAttribute.nbObservations
return len(firstAttribute.keyValues.pairs)
return 1

def _getObservationKeys(self) -> list:
"""
Return the shape attribute list of observation keys.
Return the geometry attribute list of observation keys.
Note: Observation is a value defined across all child attributes for a specific key.
"""
if not self.shapeKeyable:
if not self.observationKeyable:
return []
firstAttribute = next(iter(self.value.values()))
if isinstance(firstAttribute, ShapeAttribute):
if isinstance(firstAttribute, GeometryAttribute):
return firstAttribute.observationKeys
return firstAttribute.keyValues.getKeys()

@Slot(str, result=bool)
def hasObservation(self, key: str) -> bool:
"""
Whether the shape attribute has an observation for the given key.
Whether the geometry attribute has an observation for the given key.
Note: Observation is a value defined across all child attributes for a specific key.
"""
if not self.shapeKeyable:
if not self.observationKeyable:
return True
return all((isinstance(attribute, ShapeAttribute) and attribute.hasObservation(key)) or
(not isinstance(attribute, ShapeAttribute) and attribute.keyValues.hasKey(key))
return all((isinstance(attribute, GeometryAttribute) and attribute.hasObservation(key)) or
(not isinstance(attribute, GeometryAttribute) and attribute.keyValues.hasKey(key))
for attribute in self.value)

@raiseIfLink
def removeObservation(self, key: str):
"""
Remove the shape attribute observation for the given key.
Remove the geometry attribute observation for the given key.
Note: Observation is a value defined across all child attributes for a specific key.
"""
for attribute in self.value:
if isinstance(attribute, ShapeAttribute):
if isinstance(attribute, GeometryAttribute):
attribute.removeObservation(key)
else:
if attribute.keyable:
Expand All @@ -1181,15 +1120,15 @@ def removeObservation(self, key: str):
@raiseIfLink
def setObservation(self, key: str, observation: Variant):
"""
Set the shape attribute observation for the given key with the given observation.
Set the geometry attribute observation for the given key with the given observation.
Note: Observation is a value defined across all child attributes for a specific key.
"""
for attributeStr, value in observation.items():
attribute = self.childAttribute(attributeStr)
if attribute is None:
raise RuntimeError(f"Cannot set shape observation for attribute {self._getFullName()} \
raise RuntimeError(f"Cannot set geometry observation for attribute {self._getFullName()} \
observation is incorrect.")
if isinstance(attribute, ShapeAttribute):
if isinstance(attribute, GeometryAttribute):
attribute.setObservation(key, value)
else:
if attribute.keyable:
Expand All @@ -1201,17 +1140,17 @@ def setObservation(self, key: str, observation: Variant):
@Slot(str, result=Variant)
def getObservation(self, key: str) -> Variant:
"""
Return the shape attribute observation for the given key.
Return the geometry attribute observation for the given key.
Note: Observation is a value defined across all child attributes for a specific key.
"""
observation = {}
for attribute in self.value:
if isinstance(attribute, ShapeAttribute):
shapeObservation = attribute.getObservation(key)
if shapeObservation is None:
if isinstance(attribute, GeometryAttribute):
geoObservation = attribute.getObservation(key)
if geoObservation is None:
return None
else :
observation[attribute.name] = shapeObservation
observation[attribute.name] = geoObservation
else:
if attribute.keyable:
if attribute.keyValues.hasKey(key):
Expand All @@ -1222,43 +1161,135 @@ def getObservation(self, key: str) -> Variant:
observation[attribute.name] = attribute.value
return observation

# Properties and signals
# Emitted when a geometry observation changed.
observationsChanged = Signal()
# Whether the geometry attribute childs are keyable.
observationKeyable = Property(bool,_hasKeyableChilds, constant=True)
# The list of geometry observation keys.
observationKeys = Property(Variant, _getObservationKeys, notify=observationsChanged)
# The number of geometry observation defined.
nbObservations = Property(int, _getNbObservations, notify=observationsChanged)



class ShapeAttribute(GroupAttribute):
"""
GroupAttribute subtype tailored for shape-specific handling.
"""

def __init__(self, node, attributeDesc: desc.Shape, isOutput: bool, root=None, parent=None):
super().__init__(node, attributeDesc, isOutput, root, parent)
self._visible = True

# Override
# Connect geometry attribute valueChanged to emit geometryChanged signal.
def _initValue(self):
super()._initValue()
# Using Attribute.valueChanged for the userName, userColor, geometry properties results in a segmentation fault.
# As a workaround, we manually connect valueChanged to shapeChanged or geometryChanged.
self.value.get("userName").valueChanged.connect(self._onShapeChanged)
self.value.get("userColor").valueChanged.connect(self._onShapeChanged)
self.geometry.valueChanged.connect(self._onGeometryChanged)

# Override
# Fix missing link expression serialization.
# Should be remove if link expression serialization is added in GroupAttribute.
def getSerializedValue(self):
if self.isLink:
return self._getInputLink().asLinkExpr()
return super().getSerializedValue()

def getShapeAsDict(self) -> dict:
"""
Return the shape attribute as dict with the shape file structure.
"""
outDict = {
"name" : self.userName if self.userName else self.rootName,
"type" : self.type,
"properties" : { "color": self.userColor }
}
if not self.geometry.observationKeyable:
# Not keyable geometry, use properties.
outDict.get("properties").update(self.geometry.getSerializedValue())
else:
# Keyable geometry, use observations.
outDict.update({ "observations" : self.geometry.getValueAsDict()})
return outDict

def _getVisible(self) -> bool:
"""
Return whether the shape attribute is visible for display.
"""
return self._visible

def _setVisible(self, visible:bool):
"""
Set the shape attribute visibility for display.
"""
self._visible = visible
self.shapeChanged.emit()

def _getUserName(self) -> str:
"""
Return the shape attribute user name for display.
"""
return self.value.get("userName").value

def _getUserColor(self) -> str:
"""
Return the shape attribute user color for display.
"""
return self.value.get("userColor").value

@Slot()
def _onShapeChanged(self):
"""
Emit shapeChanged signal.
Used when shape userName or userColor value changed.
"""
self.shapeChanged.emit()

@Slot()
def _onGeometryChanged(self):
"""
Emit geometryChanged signal.
Used when geometry attribute value changed.
"""
self.geometryChanged.emit()

# Properties and signals
# Emitted when a shape related property changed (color, visibility).
shapeChanged = Signal()
# Emitted when a shape observation changed.
observationsChanged = Signal()
geometryChanged = Signal()
# Whether the shape is displayable.
isVisible = Property(bool, _getVisible, _setVisible, notify=shapeChanged)
# The shape color for display.
shapeColor = Property(str, _getColor, _setColor, notify=shapeChanged)
# The shape list of observation keys.
observationKeys = Property(Variant, _getObservationKeys, notify=observationsChanged)
# The number of observation defined.
nbObservations = Property(int, _getNbObservations, notify=observationsChanged)
# Whether the shape attribute childs are keyable.
shapeKeyable = Property(bool,_hasKeyableChilds, constant=True)
# The shape user name for display.
userName = Property(str, _getUserName, notify=shapeChanged)
# The shape user color for display.
userColor = Property(str, _getUserColor, notify=shapeChanged)
# The shape geometry group attribute.
geometry = Property(Variant, lambda self: self.value.get("geometry"), notify=geometryChanged)
# Override hasDisplayableShape property.
hasDisplayableShape = Property(bool, lambda self: True, constant=True)
# Override value property.
value = Property(Variant, Attribute._getValue, _setValue, notify=Attribute.valueChanged)

class ShapeListAttribute(ListAttribute):
"""
ListAttribute subtype tailored for shape-specific handling.
"""

def __init__(self, node, attributeDesc: desc.ShapeList, isOutput: bool,
root=None, parent=None):
self._visible = True
def __init__(self, node, attributeDesc: desc.ShapeList, isOutput: bool, root=None, parent=None):
super().__init__(node, attributeDesc, isOutput, root, parent)
self._visible = True

def getValuesAsDicts(self):
def getGeometriesAsDict(self):
"""
Return the values of the children of the shape list attribute.
Return the geometries values of the children of the shape list attribute.
"""
return [shapeAttribute.getValueAsDict() for shapeAttribute in self.value]
return [shapeAttribute.geometry.getValueAsDict() for shapeAttribute in self.value]

def getShapesAsDicts(self):
def getShapesAsDict(self):
"""
Return the children of the shape list attribute.
"""
Expand Down
6 changes: 5 additions & 1 deletion meshroom/core/desc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
PushButtonParam,
StringParam,
)
from .geometryAttribute import (
Geometry,
Size2d,
Vec2d,
)
from .shapeAttribute import (
Shape,
ShapeList,
Size2d,
Point2d,
Line2d,
Rectangle,
Expand Down
Loading