This commit is contained in:
Yann Lanthony 2025-04-14 23:59:33 +00:00 committed by GitHub
commit 2fe0608353
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 115 additions and 68 deletions

View file

@ -3,6 +3,7 @@
import copy import copy
import os import os
import re import re
from typing import Optional
import weakref import weakref
import types import types
import logging import logging
@ -11,6 +12,7 @@ from collections.abc import Iterable, Sequence
from string import Template from string import Template
from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot
from meshroom.core import desc, hashValue from meshroom.core import desc, hashValue
from meshroom.core.exception import InvalidEdgeError
def attributeFactory(description, value, isOutput, node, root=None, parent=None): def attributeFactory(description, value, isOutput, node, root=None, parent=None):
@ -72,6 +74,7 @@ class Attribute(BaseObject):
# invalidation value for output attributes # invalidation value for output attributes
self._invalidationValue = "" self._invalidationValue = ""
self._linkExpression: Optional[str] = None
self._value = None self._value = None
self.initValue() self.initValue()
@ -191,9 +194,9 @@ class Attribute(BaseObject):
if self._value == value: if self._value == value:
return return
if isinstance(value, Attribute) or Attribute.isLinkExpression(value): if self._handleLinkValue(value):
# if we set a link to another attribute return
self._value = value
elif isinstance(value, types.FunctionType): elif isinstance(value, types.FunctionType):
# evaluate the function # evaluate the function
self._value = value(self) self._value = value(self)
@ -218,6 +221,27 @@ class Attribute(BaseObject):
self.valueChanged.emit() self.valueChanged.emit()
self.validValueChanged.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()
# 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()
elif isLinkExpression:
self._linkExpression = value
return True
@Slot() @Slot()
def _onValueChanged(self): def _onValueChanged(self):
self.node._onAttributeChanged(self) self.node._onAttributeChanged(self)
@ -332,25 +356,29 @@ class Attribute(BaseObject):
this function convert the expression into a real edge in the graph this function convert the expression into a real edge in the graph
and clear the string value. and clear the string value.
""" """
v = self._value if not self.isInput or not self._linkExpression:
g = self.node.graph
if not g:
return return
if isinstance(v, Attribute):
g.addEdge(v, self) if not (graph := self.node.graph):
self.resetToDefaultValue() return
elif self.isInput and Attribute.isLinkExpression(v):
# value is a link to another attribute link = self._linkExpression[1:-1]
link = v[1:-1] linkNodeName, linkAttrName = link.split(".")
linkNodeName, linkAttrName = link.split('.')
try: try:
node = g.node(linkNodeName) node = graph.node(linkNodeName)
if not node: if node is None:
raise KeyError(f"Node '{linkNodeName}' not found") raise InvalidEdgeError(self.fullNameToNode, link, "Source node does not exist")
g.addEdge(node.attribute(linkAttrName), self) attr = node.attribute(linkAttrName)
except KeyError as err: if attr is None:
logging.warning('Connect Attribute from Expression failed.') raise InvalidEdgeError(self.fullNameToNode, link, "Source attribute does not exist")
logging.warning('Expression: "{exp}"\nError: "{err}".'.format(exp=v, err=err)) graph.addEdge(attr, 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}")
self._linkExpression = None
self.resetToDefaultValue() self.resetToDefaultValue()
def getExportValue(self): def getExportValue(self):
@ -576,9 +604,8 @@ class ListAttribute(Attribute):
def _set_value(self, value): def _set_value(self, value):
if self.node.graph: if self.node.graph:
self.remove(0, len(self)) self.remove(0, len(self))
# Link to another attribute if self._handleLinkValue(value):
if isinstance(value, ListAttribute) or Attribute.isLinkExpression(value): return
self._value = value
# New value # New value
else: else:
# During initialization self._value may not be set # During initialization self._value may not be set
@ -589,10 +616,10 @@ class ListAttribute(Attribute):
self.requestGraphUpdate() self.requestGraphUpdate()
def upgradeValue(self, exportedValues): def upgradeValue(self, exportedValues):
if not isinstance(exportedValues, list): if self._handleLinkValue(exportedValues):
if isinstance(exportedValues, ListAttribute) or Attribute.isLinkExpression(exportedValues):
self._set_value(exportedValues)
return return
if not isinstance(exportedValues, list):
raise RuntimeError("ListAttribute.upgradeValue: the given value is of type " + raise RuntimeError("ListAttribute.upgradeValue: the given value is of type " +
str(type(exportedValues)) + " but a 'list' is expected.") str(type(exportedValues)) + " but a 'list' is expected.")
@ -653,10 +680,8 @@ class ListAttribute(Attribute):
return super(ListAttribute, self).uid() return super(ListAttribute, self).uid()
def _applyExpr(self): def _applyExpr(self):
if not self.node.graph: if self._linkExpression:
return super()._applyExpr()
if isinstance(self._value, ListAttribute) or Attribute.isLinkExpression(self._value):
super(ListAttribute, self)._applyExpr()
else: else:
for value in self._value: for value in self._value:
value._applyExpr() value._applyExpr()
@ -723,6 +748,9 @@ class GroupAttribute(Attribute):
raise AttributeError(key) raise AttributeError(key)
def _set_value(self, exportedValue): def _set_value(self, exportedValue):
if self._handleLinkValue(exportedValue):
return
value = self.validateValue(exportedValue) value = self.validateValue(exportedValue)
if isinstance(value, dict): if isinstance(value, dict):
# set individual child attribute values # set individual child attribute values
@ -737,6 +765,8 @@ class GroupAttribute(Attribute):
raise AttributeError("Failed to set on GroupAttribute: {}".format(str(value))) raise AttributeError("Failed to set on GroupAttribute: {}".format(str(value)))
def upgradeValue(self, exportedValue): def upgradeValue(self, exportedValue):
if self._handleLinkValue(exportedValue):
return
value = self.validateValue(exportedValue) value = self.validateValue(exportedValue)
if isinstance(value, dict): if isinstance(value, dict):
# set individual child attribute values # set individual child attribute values
@ -781,6 +811,9 @@ class GroupAttribute(Attribute):
return None return None
def uid(self): def uid(self):
if self.isLink:
return super().uid()
uids = [] uids = []
for k, v in self._value.items(): for k, v in self._value.items():
if v.enabled and v.invalidate: if v.enabled and v.invalidate:
@ -788,13 +821,20 @@ class GroupAttribute(Attribute):
return hashValue(uids) return hashValue(uids)
def _applyExpr(self): def _applyExpr(self):
if self._linkExpression:
super()._applyExpr()
else:
for value in self._value: for value in self._value:
value._applyExpr() value._applyExpr()
def getExportValue(self): def getExportValue(self):
return {key: attr.getExportValue() for key, attr in self._value.objects.items()} if linkParam := self.getLinkParam():
return linkParam.asLinkExpr()
return {key: attr.getExportValue() for key, attr in self._value.items()}
def _isDefault(self): def _isDefault(self):
if linkParam := self.getLinkParam():
return linkParam._isDefault()
return all(v.isDefault for v in self._value) return all(v.isDefault for v in self._value)
def defaultValue(self): def defaultValue(self):

View file

@ -12,6 +12,13 @@ class GraphException(MeshroomException):
pass pass
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}")
class GraphCompatibilityError(GraphException): class GraphCompatibilityError(GraphException):
""" """
Raised when node compatibility issues occur when loading a graph. Raised when node compatibility issues occur when loading a graph.

View file

@ -16,7 +16,7 @@ import meshroom.core
from meshroom.common import BaseObject, DictModel, Slot, Signal, Property from meshroom.common import BaseObject, DictModel, Slot, Signal, Property
from meshroom.core import Version from meshroom.core import Version
from meshroom.core.attribute import Attribute, ListAttribute, GroupAttribute from meshroom.core.attribute import Attribute, ListAttribute, GroupAttribute
from meshroom.core.exception import GraphCompatibilityError, StopGraphVisit, StopBranchVisit from meshroom.core.exception import GraphCompatibilityError, InvalidEdgeError, StopGraphVisit, StopBranchVisit
from meshroom.core.graphIO import GraphIO, GraphSerializer, TemplateGraphSerializer, PartialGraphSerializer from meshroom.core.graphIO import GraphIO, GraphSerializer, TemplateGraphSerializer, PartialGraphSerializer
from meshroom.core.node import BaseNode, Status, Node, CompatibilityNode from meshroom.core.node import BaseNode, Status, Node, CompatibilityNode
from meshroom.core.nodeFactory import nodeFactory from meshroom.core.nodeFactory import nodeFactory
@ -485,41 +485,38 @@ class Graph(BaseObject):
node._applyExpr() node._applyExpr()
return node return node
def copyNode(self, srcNode, withEdges=False): def copyNode(self, srcNode: Node, withEdges: bool=False):
""" """
Get a copy instance of a node outside the graph. Get a copy instance of a node outside the graph.
Args: Args:
srcNode (Node): the node to copy srcNode: the node to copy
withEdges (bool): whether to copy edges withEdges: whether to copy edges
Returns: Returns:
Node, dict: the created node instance, The created node instance and the mapping of skipped edge per attribute (always empty if `withEdges` is True).
a dictionary of linked attributes with their original value (empty if withEdges is True)
""" """
def _removeLinkExpressions(attribute: Attribute, removed: dict[Attribute, str]):
"""Recursively remove link expressions from the given root `attribute`."""
# Link expressions are only stored on input attributes.
if attribute.isOutput:
return
if attribute._linkExpression:
removed[attribute] = attribute._linkExpression
attribute._linkExpression = None
elif isinstance(attribute, (ListAttribute, GroupAttribute)):
for child in attribute.value:
_removeLinkExpressions(child, removed)
with GraphModification(self): with GraphModification(self):
# create a new node of the same type and with the same attributes values node = nodeFactory(srcNode.toDict(), name=srcNode.nodeType)
# keep links as-is so that CompatibilityNodes attributes can be created with correct automatic description
# (File params for link expressions)
node = nodeFactory(srcNode.toDict(), srcNode.nodeType) # use nodeType as name
# skip edges: filter out attributes which are links by resetting default values
skippedEdges = {} skippedEdges = {}
if not withEdges: if not withEdges:
for n, attr in node.attributes.items(): for _, attr in node.attributes.items():
if attr.isOutput: _removeLinkExpressions(attr, skippedEdges)
# edges are declared in input with an expression linking
# to another param (which could be an output)
continue
# find top-level links
if Attribute.isLinkExpression(attr.value):
skippedEdges[attr] = attr.value
attr.resetToDefaultValue()
# find links in ListAttribute children
elif isinstance(attr, (ListAttribute, GroupAttribute)):
for child in attr.value:
if Attribute.isLinkExpression(child.value):
skippedEdges[child] = child.value
child.resetToDefaultValue()
return node, skippedEdges return node, skippedEdges
def duplicateNodes(self, srcNodes): def duplicateNodes(self, srcNodes):
@ -850,13 +847,16 @@ class Graph(BaseObject):
return set(self._nodes) - nodesWithInputLink return set(self._nodes) - nodesWithInputLink
@changeTopology @changeTopology
def addEdge(self, srcAttr, dstAttr): def addEdge(self, srcAttr: Attribute, dstAttr: Attribute):
assert isinstance(srcAttr, Attribute) if not (srcAttr.node.graph == dstAttr.node.graph == self):
assert isinstance(dstAttr, Attribute) raise InvalidEdgeError(
if srcAttr.node.graph != self or dstAttr.node.graph != self: srcAttr.fullNameToGraph, dstAttr.fullNameToGraph, "Attributes do not belong to this Graph"
raise RuntimeError('The attributes of the edge should be part of a common graph.') )
if dstAttr in self.edges.keys(): if dstAttr in self.edges.keys():
raise RuntimeError('Destination attribute "{}" is already connected.'.format(dstAttr.getFullNameToNode())) raise InvalidEdgeError(
srcAttr.fullNameToNode, dstAttr.fullNameToNode, "Destination is already connected"
)
edge = Edge(srcAttr, dstAttr) edge = Edge(srcAttr, dstAttr)
self.edges.add(edge) self.edges.add(edge)
self.markNodesDirty(dstAttr.node) self.markNodesDirty(dstAttr.node)

View file

@ -692,7 +692,7 @@ RowLayout {
var obj = cpt.createObject(groupItem, var obj = cpt.createObject(groupItem,
{ {
'model': Qt.binding(function() { return attribute.value }), 'model': Qt.binding(function() { return attribute.value }),
'readOnly': Qt.binding(function() { return root.readOnly }), 'readOnly': Qt.binding(function() { return !root.editable }),
'labelWidth': 100, // Reduce label width for children (space gain) 'labelWidth': 100, // Reduce label width for children (space gain)
'objectsHideable': Qt.binding(function() { return root.objectsHideable }), 'objectsHideable': Qt.binding(function() { return root.objectsHideable }),
'filterText': Qt.binding(function() { return root.filterText }), 'filterText': Qt.binding(function() { return root.filterText }),