Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 42 additions & 1 deletion meshroom/core/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ def _applyExpr(self):
node = graph.node(linkNodeName)
if node is None:
raise InvalidEdgeError(self.fullName, link, "Source node does not exist.")
attr = node.attribute(linkAttrName)
# Search in regular attributes first, then internal attributes (e.g. Flow outputs)
attr = node.attribute(linkAttrName) if node.hasAttribute(linkAttrName) else node.internalAttribute(linkAttrName)
if attr is None:
raise InvalidEdgeError(self.fullName, link, "Source attribute does not exist.")
attr.connectTo(self)
Expand Down Expand Up @@ -1579,3 +1580,43 @@ def _setVisible(self, visible: bool):
isVisible = Property(bool, _getVisible, _setVisible, notify=shapeListChanged)
# Override hasDisplayableShape property.
hasDisplayableShape = Property(bool, lambda self: True, constant=True)


class Flow(Attribute):
"""
An Attribute that holds no data but can be connected to create dependencies between nodes.
Unlike other attributes, Flow has no value to save in the graph file.
Only connections (edges) to/from Flow are serialized.
"""

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

# Override
def getSerializedValue(self):
if self.isLink:
return self._getInputLink().asLinkExpr()
return None

# Override
def getDefaultValue(self):
return None

# Override
def _isDefault(self):
return not self.isLink

# Override
def uid(self):
if self.isLink:
return super().uid()
return hashValue(None)

# Re-declare the isDefault Property binding so Python's property resolution
# uses this class's _isDefault override rather than the parent's version.
isDefault = Property(bool, _isDefault, notify=Attribute.valueChanged)


# Backward-compatibility alias
FlowAttribute = Flow
2 changes: 2 additions & 0 deletions meshroom/core/desc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
ChoiceParam,
ColorParam,
File,
Flow,
FlowAttribute,
FloatParam,
GroupAttribute,
IntParam,
Expand Down
31 changes: 31 additions & 0 deletions meshroom/core/desc/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,3 +691,34 @@ def checkValueTypes(self):
if not isinstance(self.value, str):
return self.name, ValueTypeErrors.TYPE
return "", ValueTypeErrors.NONE


class Flow(Attribute):
"""
An Attribute that holds no data but can be connected to create dependencies between nodes.
Unlike other attributes, Flow has no value to save in the graph file.
Only connections (edges) to/from Flow are serialized.
"""
def __init__(self, name, label=None, description=None, advanced=False, enabled=True,
visible=True, exposed=False):
super().__init__(
name=name, label=label, description=description,
value=None, invalidate=False, commandLineGroup="",
advanced=advanced, semantic="", enabled=enabled,
visible=visible, exposed=exposed
)

def getInstanceType(self):
# Import within the method to prevent cyclic dependencies
from meshroom.core.attribute import Flow
return Flow

def validateValue(self, value):
return None

def checkValueTypes(self):
return "", ValueTypeErrors.NONE


# Backward-compatibility alias
FlowAttribute = Flow
47 changes: 46 additions & 1 deletion meshroom/core/desc/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from meshroom.core.utils import VERBOSE_LEVEL

from .computation import Level, StaticNodeSize
from .attribute import Attribute, ChoiceParam, ColorParam, IntParam, StringParam
from .attribute import Attribute, ChoiceParam, ColorParam, Flow, IntParam, StringParam

_MESHROOM_ROOT = Path(meshroom.__file__).parent.parent.as_posix()
_MESHROOM_COMPUTE = (Path(_MESHROOM_ROOT) / "bin" / "meshroom_compute").as_posix()
Expand Down Expand Up @@ -148,6 +148,23 @@ class InternalAttributesFactory:
),
]

FLOW_IN = [
Flow(
name="flowIn",
label="Flow In",
description="Incoming flow connection to express a dependency from another node.",
exposed=True,
),
]

FLOW_OUT = [
Flow(
name="flowOut",
label="Flow Out",
description="Outgoing flow connection to express a dependency on another node.",
),
]

@classmethod
def getInternalAttributes(cls, mrNodeType: MrNodeType) -> list[Attribute]:
paramMap = {
Expand All @@ -161,6 +178,28 @@ def getInternalAttributes(cls, mrNodeType: MrNodeType) -> list[Attribute]:

return paramMap.get(mrNodeType)

@classmethod
def getInternalFlowInputs(cls, mrNodeType: MrNodeType) -> list[Attribute]:
"""Return the list of internal input Flow attributes for a given node type.

These are added to internal attributes so they appear as connection pins in
the graph editor header while remaining separate from the regular attribute list.
"""
if mrNodeType == MrNodeType.BACKDROP:
return []
return cls.FLOW_IN

@classmethod
def getInternalFlowOutputs(cls, mrNodeType: MrNodeType) -> list[Attribute]:
"""Return the list of internal output Flow attributes for a given node type.

These are added to internal attributes so they appear as connection pins in
the graph editor header while remaining separate from the regular attribute list.
"""
if mrNodeType == MrNodeType.BACKDROP:
return []
return cls.FLOW_OUT


class BaseNode(object):
"""
Expand All @@ -173,6 +212,8 @@ class BaseNode(object):
_mrNodeType: MrNodeType = MrNodeType.BASENODE

internalInputs = InternalAttributesFactory.getInternalAttributes(_mrNodeType)
internalFlowInputs = InternalAttributesFactory.getInternalFlowInputs(_mrNodeType)
internalFlowOutputs = InternalAttributesFactory.getInternalFlowOutputs(_mrNodeType)

inputs = []
outputs = []
Expand Down Expand Up @@ -394,6 +435,8 @@ class InputNode(BaseNode):
"""
_mrNodeType: MrNodeType = MrNodeType.INPUT
internalInputs = InternalAttributesFactory.getInternalAttributes(_mrNodeType)
internalFlowInputs = InternalAttributesFactory.getInternalFlowInputs(_mrNodeType)
internalFlowOutputs = InternalAttributesFactory.getInternalFlowOutputs(_mrNodeType)

def __init__(self):
super(InputNode, self).__init__()
Expand All @@ -413,6 +456,8 @@ class BackdropNode(BaseNode):
"""
_mrNodeType: MrNodeType = MrNodeType.BACKDROP
internalInputs = InternalAttributesFactory.getInternalAttributes(_mrNodeType)
internalFlowInputs = InternalAttributesFactory.getInternalFlowInputs(_mrNodeType)
internalFlowOutputs = InternalAttributesFactory.getInternalFlowOutputs(_mrNodeType)

def __init__(self):
super(BackdropNode, self).__init__()
Expand Down
15 changes: 15 additions & 0 deletions meshroom/core/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,8 @@
def _applyExpr(self):
for attr in self._attributes:
attr._applyExpr()
for attr in self._internalAttributes:
attr._applyExpr()

@property
def nodeType(self):
Expand Down Expand Up @@ -2199,42 +2201,55 @@

class Node(BaseNode):
"""
A standard Graph node based on a node type.
"""
def __init__(self, nodeType, position=None, parent=None, uid=None, **kwargs):
super().__init__(nodeType, position, parent=parent, uid=uid, **kwargs)

if not self.nodeDesc:
raise UnknownNodeTypeError(nodeType)

self.packageName = self.nodeDesc.packageName

for attrDesc in self.nodeDesc.inputs:
self._attributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None),
isOutput=False, node=self))

for attrDesc in self.nodeDesc.outputs:
self._attributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None),
isOutput=True, node=self))

for attrDesc in self.nodeDesc.internalInputs:
self._internalAttributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None),
isOutput=False, node=self))

# Add internal flow input/output attributes to _internalAttributes.
# Skip any that are already defined by the node itself (in inputs/outputs).
existingAttrNames = set(self._attributes.keys())
for attrDesc in self.nodeDesc.internalFlowInputs:
if attrDesc.name not in existingAttrNames:
self._internalAttributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None),
isOutput=False, node=self))

for attrDesc in self.nodeDesc.internalFlowOutputs:
if attrDesc.name not in existingAttrNames:
self._internalAttributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None),
isOutput=True, node=self))

# Declare events for specific output attributes
for attr in self._attributes:
if attr.isOutput and attr.desc.semantic == "image":
attr.enabledChanged.connect(self.outputAttrChanged)
if attr.isOutput:
attr.expressionApplied.connect(self.outputAttrChanged)

# List attributes per UID
for attr in self._attributes:
if attr.isInput and attr.invalidate:
self.invalidatingAttributes.add(attr)

# Add internal attributes with a UID to the list
for attr in self._internalAttributes:

Check notice on line 2252 in meshroom/core/node.py

View check run for this annotation

codefactor.io / CodeFactor

meshroom/core/node.py#L2204-L2252

Complex Method
if attr.invalidate:
self.invalidatingAttributes.add(attr)

Expand Down
63 changes: 59 additions & 4 deletions meshroom/core/nodeFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,12 @@ def _checkAttributesNamesMatchDescription(self) -> bool:
)

def _checkAttributesAreCompatibleWithDescription(self) -> bool:
# Combine regular internal attributes with internal flow inputs for compatibility checking,
# as internal flow inputs (when connected) appear in the 'internalInputs' section of the file.
allInternalDescriptions = list(self.nodeDesc.internalInputs) + list(self.nodeDesc.internalFlowInputs)
return (
self._checkAttributesCompatibility(self.nodeDesc.inputs, self.inputs)
and self._checkAttributesCompatibility(self.nodeDesc.internalInputs,
self.internalInputs)
and self._checkAttributesCompatibility(allInternalDescriptions, self.internalInputs)
and self._checkAttributesCompatibility(self.nodeDesc.outputs, self.outputs)
)

Expand All @@ -143,10 +145,21 @@ def serializedInput(attr: desc.Attribute) -> bool:
if isinstance(attr, desc.PushButtonParam):
# PushButtonParam are not serialized has they do not hold a value.
return False
if isinstance(attr, desc.Flow):
# Flow inputs are only serialized when connected (as link expressions).
# They are optional in the serialized data, so they are handled separately.
return False
return True

def optionalInput(attr: desc.Attribute) -> bool:
""" Returns True if the attribute may optionally be serialized (present or absent in file). """
return isinstance(attr, desc.Flow)

refAttributes = filter(serializedInput, self.nodeDesc.inputs)
return self._checkAttributesNamesStrictlyMatch(refAttributes, self.inputs)
# User-defined Flow inputs in nodeDesc.inputs are optional in the 'inputs' section.
# Internal flow inputs (internalFlowInputs) now go in 'internalInputs', not 'inputs'.
optionalFlowAttrs = list(filter(optionalInput, self.nodeDesc.inputs))
return self._checkAttributesNamesMatchWithOptional(refAttributes, self.inputs, optionalFlowAttrs)

def _checkOutputAttributesNames(self) -> bool:
def serializedOutput(attr: desc.Attribute) -> bool:
Expand All @@ -155,14 +168,24 @@ def serializedOutput(attr: desc.Attribute) -> bool:
# Dynamic outputs values are not serialized with the node,
# as their value is written in the computed output data.
return False
if isinstance(attr, desc.Flow):
# Flow outputs hold no data and are never serialized.
return False
return True

refAttributes = filter(serializedOutput, self.nodeDesc.outputs)
return self._checkAttributesNamesStrictlyMatch(refAttributes, self.outputs)

def _checkInternalAttributesNames(self) -> bool:
# Required: all invalidating internal attributes must be present.
invalidatingDescAttributes = [attr.name for attr in self.nodeDesc.internalInputs if attr.invalidate]
return all(attr in self.internalInputs.keys() for attr in invalidatingDescAttributes)
if not all(attr in self.internalInputs.keys() for attr in invalidatingDescAttributes):
return False
# Optional: internal Flow attributes may optionally appear in internalInputs
# (as link expressions when connected).
allInternalDescriptions = list(self.nodeDesc.internalInputs) + list(self.nodeDesc.internalFlowInputs)
allowedNames = {attr.name for attr in allInternalDescriptions}
return all(k in allowedNames for k in self.internalInputs.keys())

def _checkAttributesNamesStrictlyMatch(
self, descAttributes: Iterable[desc.Attribute], attributesDict: dict[str, Any]
Expand All @@ -171,6 +194,38 @@ def _checkAttributesNamesStrictlyMatch(
attrNames = sorted(attributesDict.keys())
return refNames == attrNames

def _checkAttributesNamesMatchWithOptional(
self,
requiredDescAttributes: Iterable[desc.Attribute],
attributesDict: dict[str, Any],
optionalDescAttributes: Iterable[desc.Attribute],
) -> bool:
"""Check that attribute names in 'attributesDict' match the expected description,
where 'requiredDescAttributes' must all be present and 'optionalDescAttributes'
may optionally be present.

Args:
requiredDescAttributes: Desc attributes that must be serialized.
attributesDict: The serialized attribute dict to check against.
optionalDescAttributes: Desc attributes that may or may not be serialized.

Returns:
True if all required attributes are present and no unknown attributes exist.
"""
requiredNames = set(attr.name for attr in requiredDescAttributes)
optionalNames = set(attr.name for attr in optionalDescAttributes)
allowedNames = requiredNames | optionalNames
attrNames = set(attributesDict.keys())

# All required attributes must be present in the serialized data (subset check).
if not requiredNames <= attrNames:
return False
# All serialized attribute names must be either required or optional (no unknown attrs).
if not attrNames <= allowedNames:
return False

return True

def _checkAttributesCompatibility(
self, descAttributes: list[desc.Attribute], attributesDict: dict[str, Any]
) -> bool:
Expand Down
10 changes: 9 additions & 1 deletion meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ RowLayout {

sourceComponent: {
// PushButtonParam always has value == undefined, so it needs to be excluded from this check
if (attribute.type != "PushButtonParam" && !attribute.keyable && attribute.value === undefined) {
if (attribute.type != "PushButtonParam" && attribute.type != "Flow" && !attribute.keyable && attribute.value === undefined) {
return notComputedComponent
}
switch (attribute.type) {
Expand All @@ -316,6 +316,8 @@ RowLayout {
return textFieldComponent
case "ColorParam":
return colorComponent
case "Flow":
return flowComponent
default:
return textFieldComponent
}
Expand Down Expand Up @@ -349,6 +351,12 @@ RowLayout {
}
}

Component {
id: flowComponent
// Flow has no data to display; connection state is shown via the attribute pin
Item {}
}

Component {
id: textFieldComponent
TextField {
Expand Down
9 changes: 7 additions & 2 deletions meshroom/ui/qml/GraphEditor/AttributePin.qml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ RowLayout {
property bool readOnly: false
/// Whether to display an output pin for input attribute
property bool displayOutputPinForInput: true
/// Compact mode: hides the attribute label, showing only the connection circle.
/// Useful for embedding the pin in space-constrained areas like the node header.
property bool compact: false

// position of the anchor for attaching and edge to this attribute pin
readonly property point inputAnchorPos: Qt.point(inputAnchor.x + inputAnchor.width / 2,
Expand Down Expand Up @@ -65,7 +68,7 @@ RowLayout {
Item {
width: childrenRect.width
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.fillWidth: !root.compact
Layout.fillHeight: true

Rectangle {
Expand Down Expand Up @@ -239,7 +242,9 @@ RowLayout {
id: nameContainer
implicitHeight: childrenRect.height
implicitWidth: childrenRect.width
Layout.fillWidth: true
visible: !root.compact
Layout.fillWidth: !root.compact
Layout.maximumWidth: root.compact ? 0 : Number.POSITIVE_INFINITY
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter

Expand Down
Loading