Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
54a7e39
[core / ui]: Group can be connected and it coects all sub attributes.…
nicolas-lambert-tc Jun 17, 2025
3003461
[ui] Graph: We a GroupAttribute is connected, the subAttributes edges…
nicolas-lambert-tc Jun 18, 2025
79dcecd
[ui] GroupAttribute: GrpAttributes can be collapsed even if sub attri…
nicolas-lambert-tc Jun 18, 2025
d79dc08
[core] GroupAttribute: Deserialisation of a groupAttribute diredctly …
nicolas-lambert-tc Jun 19, 2025
f5d2b1d
[core] GroupAttribute: Allow serialization of a link between groupAtt…
nicolas-lambert-tc Jun 19, 2025
34ac261
[ui] GroupAttributes: Move the groupAttributes conenction to the ui.g…
nicolas-lambert-tc Jun 23, 2025
9efa9e8
[ui] GroupAttribute: Fix the navigate buttons were not connected for …
nicolas-lambert-tc Jun 23, 2025
0c38ebb
[ui] GroupAttributes: Remove the grayAttribute pin when only subAtrib…
nicolas-lambert-tc Jun 23, 2025
c0b93cd
[ui] GroupAttributes: Remove useless code
nicolas-lambert-tc Jun 23, 2025
5d76b84
[ui] GroupAttributes: Move the connection/disconnection responsabilit…
nicolas-lambert-tc Jun 25, 2025
196b210
[core] GroupAttribute: Move connection / disconnection logic to attri…
nicolas-lambert-tc Jun 26, 2025
43d4585
[core] GroupAttributes: refacto the disconnct method name to disconne…
nicolas-lambert-tc Jun 27, 2025
5246397
[ui/core] GroupAttributes: Make the connection worl for tests (core s…
nicolas-lambert-tc Jun 27, 2025
8d316bf
[ui] Edge: Remove edge visiblity because not used right now
nicolas-lambert-tc Jun 30, 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
157 changes: 144 additions & 13 deletions meshroom/core/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
from collections.abc import Iterable, Sequence
from string import Template
from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot
from meshroom.core.exception import InvalidEdgeError
from meshroom.core import desc, hashValue

from typing import TYPE_CHECKING

from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from meshroom.core.graph import Edge

Expand Down Expand Up @@ -45,7 +45,6 @@

return attr


class Attribute(BaseObject):
"""
"""
Expand Down Expand Up @@ -121,7 +120,7 @@
""" Name inside the Graph: graphName.nodeName.groupName.name """
graphName = self.node.graph.name if self.node.graph else "UNDEFINED"
return f'{graphName}.{self.getFullNameToNode()}'

def asLinkExpr(self) -> str:
""" Return link expression for this Attribute """
return "{" + self.getFullNameToNode() + "}"
Expand Down Expand Up @@ -237,6 +236,26 @@
self.valueChanged.emit()
self.validValueChanged.emit()

def _handleLinkValue(self, value) -> bool:
"""
Handle assignment of a link if `value` is a serialized link expression or in-memory Attribute reference.
Returns: Whether the value has been handled as a link, False otherwise.
"""
isAttribute = isinstance(value, Attribute)
isLinkExpression = Attribute.isLinkExpression(value)

if not isAttribute and not isLinkExpression:
return False

if isAttribute:
self._linkExpression = value.asLinkExpr()

Check warning on line 251 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L251

Added line #L251 was not covered by tests
# If the value is a direct reference to an attribute, it can be directly converted to an edge as
# the source attribute already exists in memory.
self._applyExpr()

Check warning on line 254 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L254

Added line #L254 was not covered by tests
elif isLinkExpression:
self._linkExpression = value
return True

@Slot()
def _onValueChanged(self):
self.node._onAttributeChanged(self)
Expand Down Expand Up @@ -315,8 +334,8 @@
""" Whether the input attribute is a link to another attribute. """
# note: directly use self.node.graph._edges to avoid using the property that may become
# invalid at some point
return self.node.graph and self.isInput and self.node.graph._edges and \
self in self.node.graph._edges.keys()
return bool(self.node.graph and self.isInput and self.node.graph._edges and \
self in self.node.graph._edges.keys())

@staticmethod
def isLinkExpression(value) -> bool:
Expand Down Expand Up @@ -383,7 +402,7 @@
if not g:
return
if isinstance(v, Attribute):
g.addEdge(v, self)
v.connectTo(self)
self.resetToDefaultValue()
elif self.isInput and Attribute.isLinkExpression(v):
# value is a link to another attribute
Expand All @@ -393,7 +412,7 @@
node = g.node(linkNodeName)
if not node:
raise KeyError(f"Node '{linkNodeName}' not found")
g.addEdge(node.attribute(linkAttrName), self)
node.attribute(linkAttrName).connectTo(self)
except KeyError as err:
logging.warning('Connect Attribute from Expression failed.')
logging.warning(f'Expression: "{v}"\nError: "{err}".')
Expand Down Expand Up @@ -497,6 +516,43 @@

return next((imageSemantic for imageSemantic in Attribute.VALID_IMAGE_SEMANTICS if self.desc.semantic == imageSemantic), None) is not None

@Slot(BaseObject, result=bool)
def isCompatibleWith(self, otherAttribute: "Attribute") -> bool:
""" Check if the given attribute can be conected to the current Attribute
"""
return self._isCompatibleWith(otherAttribute)

def _isCompatibleWith(self, otherAttribute: "Attribute") -> bool:
""" Implementation of the connection validation
.. note:
Override this method to use custom connection validation logic
"""
return self.baseType == otherAttribute.baseType

def connectTo(self, otherAttribute: "Attribute") -> Optional["Edge"]:
""" Connect the current attribute as the source of the given one
"""

if not (graph := self.node.graph):
return None

Check warning on line 537 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L537

Added line #L537 was not covered by tests

if isinstance(otherAttribute.root, Attribute):
otherAttribute.root.disconnectEdge()

return graph.addEdge(self, otherAttribute)

def disconnectEdge(self):
""" Disconnect the current attribute
"""

if not (graph := self.node.graph):
return

Check warning on line 549 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L549

Added line #L549 was not covered by tests

graph.removeEdge(self)

if isinstance(self.root, Attribute):
self.root.disconnectEdge()

name = Property(str, getName, constant=True)
fullName = Property(str, getFullName, constant=True)
fullNameToNode = Property(str, getFullNameToNode, constant=True)
Expand Down Expand Up @@ -560,7 +616,6 @@
return func(attr, *args, **kwargs)
return wrapper


class PushButtonParam(Attribute):
def __init__(self, node, attributeDesc: desc.PushButtonParam, isOutput: bool,
root=None, parent=None):
Expand Down Expand Up @@ -836,7 +891,6 @@
hasOutputConnections = Property(bool, hasOutputConnections.fget, notify=Attribute.hasOutputConnectionsChanged)



class GroupAttribute(Attribute):

def __init__(self, node, attributeDesc: desc.GroupAttribute, isOutput: bool,
Expand All @@ -852,7 +906,14 @@
except KeyError:
raise AttributeError(key)

def _get_value(self):
return self._value

def _set_value(self, exportedValue):

if self._handleLinkValue(exportedValue):
return

value = self.validateValue(exportedValue)
if isinstance(value, dict):
# set individual child attribute values
Expand Down Expand Up @@ -918,10 +979,38 @@
return hashValue(uids)

def _applyExpr(self):
for value in self._value:
value._applyExpr()

if not self._linkExpression:
for value in self._value:
value._applyExpr()
return

if not self.isInput or not (graph := self.node.graph):
return

Check warning on line 989 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L989

Added line #L989 was not covered by tests

link = self._linkExpression[1:-1]
linkNodeName, linkAttrName = link.split(".", 1)
try:
node = graph.node(linkNodeName)
if node is None:
raise InvalidEdgeError(self.fullNameToNode, link, "Source node does not exist")

Check warning on line 996 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L996

Added line #L996 was not covered by tests
attr = node.attribute(linkAttrName)
if attr is None:
raise InvalidEdgeError(self.fullNameToNode, link, "Source attribute does not exist")

Check warning on line 999 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L999

Added line #L999 was not covered by tests
attr.connectTo(self)
except InvalidEdgeError as err:
logging.warning(err)
except Exception as err:
logging.warning("Unexpected error happened during edge creation")
logging.warning(f"Expression '{self._linkExpression}': {err}")

Check warning on line 1005 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L1001-L1005

Added lines #L1001 - L1005 were not covered by tests

self._linkExpression = None
self.resetToDefaultValue()

def getExportValue(self):
if self.isLink:
return self.getLinkParam().asLinkExpr()

return {key: attr.getExportValue() for key, attr in self._value.objects.items()}

def _isDefault(self):
Expand Down Expand Up @@ -982,7 +1071,49 @@
def matchText(self, text: str) -> bool:
return super().matchText(text) or any(c.matchText(text) for c in self._value)

def _isCompatibleWith(self, otherAttribute: Attribute):
isCompatible = super()._isCompatibleWith(otherAttribute)

if not isCompatible:
return False

Check warning on line 1078 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L1078

Added line #L1078 was not covered by tests

return self._haveSameStructure(otherAttribute=otherAttribute)

def _haveSameStructure(self, otherAttribute: Attribute) -> bool:
""" Does the given attribute have the same number of attributes, and all ordered attributes have the same baseType
"""

if isinstance(otherAttribute._value, Iterable) and len(otherAttribute._value) != len(self._value):
return False

Check warning on line 1087 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L1087

Added line #L1087 was not covered by tests

for i, attr in enumerate(self.getSubAttributes()):
otherAttr = list(otherAttribute._value)[i]
if isinstance(attr, GroupAttribute):
return attr._haveSameStructure(otherAttr)
elif not otherAttr:
return False

Check warning on line 1094 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L1094

Added line #L1094 was not covered by tests
elif attr.baseType != otherAttr.baseType:
return False

return True

def getSubAttributes(self):
return list(self._value)

def connectTo(self, otherAttribute: "GroupAttribute") -> Optional["Edge"]:
""" Connect the current attribute as the source of the given one

It connects automatically the subgroups
"""

otherSubChildren = otherAttribute.getSubAttributes()

for idx, subAttr in enumerate(self.getSubAttributes()):
subAttr.connectTo(otherSubChildren[idx])

return super().connectTo(otherAttribute)

# Override value property
value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged)
value = Property(Variant, _get_value, _set_value, notify=Attribute.valueChanged)
isDefault = Property(bool, _isDefault, notify=Attribute.valueChanged)
flatStaticChildren = Property(Variant, getFlatStaticChildren, constant=True)
12 changes: 12 additions & 0 deletions meshroom/core/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,15 @@
class StopBranchVisit(GraphVisitMessage):
""" Immediately stop branch visit. """
pass


class AttributeCompatibilityError(GraphException):
"""
Raised when trying to connect attributes that are incompatible
"""

class InvalidEdgeError(GraphException):
"""Raised when an edge between two attributes cannot be created."""

def __init__(self, srcAttrName: str, dstAttrName: str, msg: str) -> None:
super().__init__(f"Failed to connect {srcAttrName}->{dstAttrName}: {msg}")

Check warning on line 71 in meshroom/core/exception.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/exception.py#L71

Added line #L71 was not covered by tests
20 changes: 13 additions & 7 deletions meshroom/core/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from meshroom.common import BaseObject, DictModel, Slot, Signal, Property
from meshroom.core import Version
from meshroom.core.attribute import Attribute, ListAttribute, GroupAttribute
from meshroom.core.exception import GraphCompatibilityError, StopGraphVisit, StopBranchVisit
from meshroom.core.exception import GraphCompatibilityError, StopGraphVisit, StopBranchVisit, AttributeCompatibilityError
from meshroom.core.graphIO import GraphIO, GraphSerializer, TemplateGraphSerializer, PartialGraphSerializer
from meshroom.core.node import BaseNode, Status, Node, CompatibilityNode
from meshroom.core.nodeFactory import nodeFactory
Expand All @@ -24,7 +24,7 @@
# Replace default encoder to support Enums

DefaultJSONEncoder = json.JSONEncoder # store the original one

logger = logging.getLogger(__name__)

class MyJSONEncoder(DefaultJSONEncoder): # declare a new one with Enum support
def default(self, obj):
Expand Down Expand Up @@ -893,12 +893,17 @@

@changeTopology
def addEdge(self, srcAttr, dstAttr):

assert isinstance(srcAttr, Attribute)
assert isinstance(dstAttr, Attribute)

if srcAttr.node.graph != self or dstAttr.node.graph != self:
raise RuntimeError('The attributes of the edge should be part of a common graph.')
if dstAttr in self.edges.keys():
raise RuntimeError(f'Destination attribute "{dstAttr.getFullNameToNode()}" is already connected.')
self.removeEdge(dstAttr)
if not dstAttr.isCompatibleWith(srcAttr):
raise AttributeCompatibilityError(f'Attribute: "{srcAttr.name}" can not be connected to "{dstAttr.name}" because they are not compatible')

Check warning on line 905 in meshroom/core/graph.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/graph.py#L905

Added line #L905 was not covered by tests

edge = Edge(srcAttr, dstAttr)
self.edges.add(edge)
self.markNodesDirty(dstAttr.node)
Expand All @@ -913,9 +918,11 @@
self.addEdge(*edge)

@changeTopology
def removeEdge(self, dstAttr):
if dstAttr not in self.edges.keys():
raise RuntimeError(f'Attribute "{dstAttr.getFullNameToNode()}" is not connected')
def removeEdge(self, dstAttr: 'Attribute'):

if not self.edges.get(dstAttr):
return

edge = self.edges.pop(dstAttr)
self.markNodesDirty(dstAttr.node)
dstAttr.valueChanged.emit()
Expand Down Expand Up @@ -1594,7 +1601,6 @@
canComputeLeavesChanged = Signal()
canComputeLeaves = Property(bool, lambda self: self._canComputeLeaves, notify=canComputeLeavesChanged)


def loadGraph(filepath, strictCompatibility: bool = False) -> Graph:
"""
Load a Graph from a Meshroom Graph (.mg) file.
Expand Down
12 changes: 7 additions & 5 deletions meshroom/core/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -875,7 +875,7 @@ def _computeUid(self):
for attr in self.invalidatingAttributes:
if not attr.enabled:
continue # Disabled params do not contribute to the uid
dynamicOutputAttr = attr.isLink and attr.getLinkParam(recursive=True).desc.isDynamicValue
dynamicOutputAttr = attr.getLinkParam(recursive=True) and attr.getLinkParam(recursive=True).desc.isDynamicValue
# For dynamic output attributes, the UID does not depend on the attribute value.
# In particular, when loading a project file, the UIDs are updated first,
# and the node status and the dynamic output values are not yet loaded,
Expand Down Expand Up @@ -1948,6 +1948,11 @@ def attributeDescFromName(refAttributes, name, value, strict=True):
if attrDesc is None:
return None

# If it is a serialized link expression (no proper value to set/evaluate)
if Attribute.isLinkExpression(value):
return attrDesc


# We have found a description, and we still need to
# check if the value matches the attribute description.

Expand All @@ -1959,10 +1964,7 @@ def attributeDescFromName(refAttributes, name, value, strict=True):
return None
return attrDesc

# If it is a serialized link expression (no proper value to set/evaluate)
if Attribute.isLinkExpression(value):
return attrDesc


# If it passes the 'matchDescription' test
if attrDesc.matchDescription(value, strict):
return attrDesc
Expand Down
Loading