Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
f1d4b53
[core] attribute: Add `hasDisplayableShape` property
gregoire-dl Sep 26, 2025
c2264b7
[core] node: Add `hasDisplayableShape` property
gregoire-dl Sep 26, 2025
7d266e1
[core] attribute: Add `ShapeAttribute`
gregoire-dl Sep 26, 2025
988dbda
[core] attribute: Add `ShapeListAttribute`
gregoire-dl Sep 26, 2025
bcc28ff
[core] desc: Add `shapeAttribute`
gregoire-dl Sep 26, 2025
013cb41
[ui] commands: Add `SetObservationCommand`
gregoire-dl Sep 26, 2025
f29d0c0
[ui] commands: Add `RemoveObservationCommand`
gregoire-dl Sep 26, 2025
b0e1f56
[ui] shapes: Add `shapeFile`
gregoire-dl Sep 26, 2025
98e8ff1
[ui] shapes: Add `ShapeFilesHelper`
gregoire-dl Sep 26, 2025
46c0ddd
[ui] shapes: Add `ShapeViewerHelper`
gregoire-dl Sep 26, 2025
7377bdd
[ui] shapes: Add `__init__.py`
gregoire-dl Sep 26, 2025
0accb11
[ui] app: Set context property for `ShapeFilesHelper` and `ShapeViewe…
gregoire-dl Sep 26, 2025
ec2b149
[ui] graph: Add methods `setObservation` and `removeObservation`
gregoire-dl Sep 26, 2025
710b3a9
[ui] graph: Add method `setObservationFromName`
gregoire-dl Sep 26, 2025
fecfcba
[qml] Shape/Viewer: Add `Handle` component
gregoire-dl Sep 26, 2025
d71defb
[qml] Shape/Viewer: Add `BaseLayer` component
gregoire-dl Sep 26, 2025
f9174e9
[qml] Shape/Viewer: Add `PointLayer` component
gregoire-dl Sep 26, 2025
be179a9
[qml] Shape/Viewer: Add `LineLayer` component
gregoire-dl Sep 26, 2025
9a4742a
[qml] Shape/Viewer: Add `RectangleLayer` component
gregoire-dl Sep 26, 2025
1af798e
[qml] Shape/Viewer: Add `CircleLayer` component
gregoire-dl Sep 26, 2025
cb51aeb
[qml] Shape/Viewer: Add `TextLayer` component
gregoire-dl Sep 26, 2025
80d0b34
[qml] Shape/Viewer: Add `ShapeViewerLayer` component
gregoire-dl Sep 26, 2025
820c7d1
[qml] Shape/Viewer: Add `ShapeViewerAttributeLayer` component
gregoire-dl Sep 26, 2025
b2d0367
[qml] Shape/Viewer: Add `ShapeViewerAttributeLoader` component
gregoire-dl Sep 26, 2025
19b6dff
[qml] Shape: Add `ShapeViewer` component
gregoire-dl Sep 26, 2025
9dbc9a4
[qml] Shape/Editor: Add `ItemHeader` component
gregoire-dl Sep 26, 2025
141f1d5
[qml] Shape/Editor: Add `ShapeDataItem` component
gregoire-dl Sep 26, 2025
f056fea
[qml] Shape/Editor: Add `ShapeFileItem` component
gregoire-dl Sep 26, 2025
a3ad3a2
[qml] Shape/Editor: Add `ShapeAttributeItem` component
gregoire-dl Sep 26, 2025
8143fde
[qml] Shape/Editor: Add `ShapeListAttributeItem` component
gregoire-dl Sep 26, 2025
87db577
[qml] Shape/Editor: Add `ShapeEditorItem` component
gregoire-dl Sep 26, 2025
d1c3ad6
[qml] Shape: Add `ShapeEditor` component
gregoire-dl Sep 26, 2025
d67a330
[qml] Shapes: Add `qmldir`
gregoire-dl Sep 26, 2025
6e89198
[qml] Viewer2D: Add `ShapeViewer` loader
gregoire-dl Sep 26, 2025
104b920
[qml] NodeEditor: Add `SplitView` with `ShapeEditor` loader
gregoire-dl Sep 26, 2025
943b7fa
[qml] AttributeEditor: Do not display `ShapeAttribute` and `ShapeList…
gregoire-dl Sep 26, 2025
8599ca1
[tests] Add new test `attributeShape`
gregoire-dl Sep 26, 2025
30ea7f8
[core] node: Remove redundant backslash
gregoire-dl Sep 29, 2025
f623cda
[core] attribute: Remove redundant backslashes
gregoire-dl Sep 29, 2025
7836d8f
[qml] Shape/Viewer: Add orientation arrow to `LineLayer`
gregoire-dl Oct 1, 2025
e9d7921
[qml] Shape/Viewer: Add name text to `PointLayer`
gregoire-dl Oct 1, 2025
d526e51
[qml] Shape/Editor: Display empty output shape file
gregoire-dl Oct 1, 2025
b2a10ce
[ui] Shapes: Handle json format: object with "shapes" key
gregoire-dl Oct 1, 2025
4100dfc
[ui] Shapes: Fix missing property `nbObservations` in `ShapeData`
gregoire-dl Oct 1, 2025
b299595
[core] keyValues: Add `getKeys` method
gregoire-dl Oct 1, 2025
640f660
[core] attribute: Add property `observationKeys` for `ShapeAttribute`
gregoire-dl Oct 1, 2025
6c20a75
[ui] Shapes: Add property `observationKeys` for `ShapeData`
gregoire-dl Oct 1, 2025
8d41395
[qml] Shape/Editor: Change remove attribute icon
gregoire-dl Oct 1, 2025
e867eb6
[core] attribute: Remove `raiseIfLink` for `_getObservationKeys` method
gregoire-dl Oct 1, 2025
16d49bd
[qml] Shape/Editor: Add previous / next key arrows
gregoire-dl Oct 1, 2025
79c4ae0
[qml] Shape/Editor: Add tooltips
gregoire-dl Oct 1, 2025
6c399d0
[qml] Shape/Viewer: Add margin for `PointLayer` tap and hover handlers
gregoire-dl Oct 1, 2025
35035d0
[qml] Shape/Editor: Fix tooltip text case
gregoire-dl Oct 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 260 additions & 0 deletions meshroom/core/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,8 @@ def matchText(self, text: str) -> bool:
is2dDisplayable = Property(bool, _is2dDisplayable, constant=True)
# Whether the attribute value is displayable in 3d.
is3dDisplayable = Property(bool, _is3dDisplayable, constant=True)
# Whether the attribute is a shape or a shape list, managed by the ShapeEditor and ShapeViewer.
hasDisplayableShape = Property(bool, lambda self: False, constant=True)

# Attribute link properties and signals
inputLinksChanged = Signal()
Expand Down Expand Up @@ -999,3 +1001,261 @@ def matchText(self, text: str) -> bool:
# Override value property
value = Property(Variant, Attribute._getValue, _setValue, notify=Attribute.valueChanged)
isDefault = Property(bool, lambda self: all(v.isDefault for v in self.value), notify=Attribute.valueChanged)


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

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

# Override
# Signal observationsChanged should be emitted.
def _setValue(self, exportedValue):
super()._setValue(exportedValue)
self.observationsChanged.emit()

# Override
# Signal observationsChanged should be emitted.
def resetToDefaultValue(self):
super().resetToDefaultValue()
self.observationsChanged.emit()

# Override
# Signal observationsChanged should be emitted.
def upgradeValue(self, exportedValue):
super().upgradeValue(exportedValue)
self.observationsChanged.emit()

# 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 {key: attr.getSerializedValue() for key, attr in self._value.objects.items()}

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.
"""
from collections import defaultdict
outValue = defaultdict(dict)
if self.isLink:
return self._getInputLink().asLinkExpr()
if not self.shapeKeyable:
return super().getSerializedValue()
for attribute in self.value:
if isinstance(attribute, ShapeAttribute):
attributeDict = attribute.getValueAsDict()
if attributeDict:
for key, value in attributeDict.items():
outValue[key][attribute.name] = value
else:
for pair in attribute.keyValues.pairs:
outValue[str(pair.key)][attribute.name] = pair.value
return dict(outValue)

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
attribute.keyable for attribute in self.value)

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

def _getObservationKeys(self) -> list:
"""
Return the shape attribute list of observation keys.
Note: Observation is a value defined across all child attributes for a specific key.
"""
if not self.shapeKeyable:
return []
firstAttribute = next(iter(self.value.values()))
if isinstance(firstAttribute, ShapeAttribute):
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.
Note: Observation is a value defined across all child attributes for a specific key.
"""
if not self.shapeKeyable:
return True
return all((isinstance(attribute, ShapeAttribute) and attribute.hasObservation(key)) or
(not isinstance(attribute, ShapeAttribute) 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.
Note: Observation is a value defined across all child attributes for a specific key.
"""
for attribute in self.value:
if isinstance(attribute, ShapeAttribute):
attribute.removeObservation(key)
else:
if attribute.keyable:
attribute.keyValues.remove(key)
else:
attribute.resetToDefaultValue()
self.observationsChanged.emit()

@raiseIfLink
def setObservation(self, key: str, observation: Variant):
"""
Set the shape 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()} \
observation is incorrect.")
if isinstance(attribute, ShapeAttribute):
attribute.setObservation(key, value)
else:
if attribute.keyable:
attribute.keyValues.add(key, value)
else:
attribute.value = value
self.observationsChanged.emit()

@Slot(str, result=Variant)
def getObservation(self, key: str) -> Variant:
"""
Return the shape 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:
return None
else :
observation[attribute.name] = shapeObservation
else:
if attribute.keyable:
if attribute.keyValues.hasKey(key):
observation[attribute.name] = attribute.keyValues.getValueAtKeyOrDefault(key)
else:
return None
else:
observation[attribute.name] = attribute.value
return observation

# Properties and signals
# Emitted when a shape related property changed (color, visibility).
shapeChanged = Signal()
# Emitted when a shape observation changed.
observationsChanged = 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)
# 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
super().__init__(node, attributeDesc, isOutput, root, parent)

def getShapesAsDicts(self):
"""
Return the shape list attribute value as dict.
"""
return [shapeAttribute.getValueAsDict() for shapeAttribute in self.value]

def _getVisible(self) -> bool:
"""
Return whether the shape list is visible for display.
"""
if self.isLink:
return self.inputLink.isVisible
return self._visible

def _setVisible(self, visible:bool):
"""
Set the shape visibility for display.
"""
if self.isLink:
self.inputLink.isVisible = visible
else:
self._visible = visible
for attribute in self.value:
if isinstance(attribute, ShapeAttribute):
attribute.isVisible = visible
self.shapeListChanged.emit()

# Properties and signals
# Emitted when a shape list related property changed.
shapeListChanged = Signal()
# Whether the shape list is displayable.
isVisible = Property(bool, _getVisible, _setVisible, notify=shapeListChanged)
# Override hasDisplayableShape property.
hasDisplayableShape = Property(bool, lambda self: True, constant=True)
9 changes: 9 additions & 0 deletions meshroom/core/desc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
PushButtonParam,
StringParam,
)
from .shapeAttribute import (
Shape,
ShapeList,
Size2d,
Point2d,
Line2d,
Rectangle,
Circle
)
from .computation import (
DynamicNodeSize,
Level,
Expand Down
Loading