Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f42164b
[desc] Add classes to manage internal attributes based on a node's type
cbentejac Jan 13, 2026
84bacb6
[tests] test_compatibility: Update test case for `InputNode` with no …
cbentejac Jan 19, 2026
d5a9e20
[core] node: Add properties for new internal attributes
cbentejac Jan 13, 2026
d057b3b
[core] node: Add new `BackdropNode` class
cbentejac Jan 13, 2026
60e6b9a
[core] node: `MrNodeType.BACKDROP` is incomputable
cbentejac Jan 14, 2026
891bc11
[core] nodeFactory: Add method to choose correct node constructor
cbentejac Jan 13, 2026
b3c5061
[core] Use method to select correct node constructor
cbentejac Jan 13, 2026
bf114e5
[nodes] Add `Backdrop` node
cbentejac Jan 13, 2026
e055e4b
[components] geom2D: Add `rectRectFullIntersect` slot
cbentejac Jan 14, 2026
7d03876
[GraphEditor] Add component for Backdrop nodes
cbentejac Jan 13, 2026
3054ded
[GraphEditor] GraphEditor: Add loader for node delegate and `Backdrop…
cbentejac Jan 13, 2026
92b4295
[GraphEditor] Node: Add `isBackdropNode` property
cbentejac Jan 14, 2026
dd36db2
[Controls] DelegateSelectionBox: Handle backdrops and use correct acc…
cbentejac Jan 14, 2026
ad60607
[GraphEditor] NodeEditor: Correctly handle tab index for backdrops
cbentejac Jan 14, 2026
70ad240
[ui] graph: Add slots to resize `Backdrop` nodes
cbentejac Jan 14, 2026
6e3721c
[GraphEditor] Backdrop: Harmonize code and comments
cbentejac Jan 14, 2026
26606b1
[GraphEditor] GraphEditor: Stop using semicolons for the `Node` compo…
cbentejac Jan 14, 2026
69c0dfb
[GraphEditor] Backdrop: Wrap comments to the backdrop's size
cbentejac Jan 19, 2026
d0f3f80
[GraphEditor] Backdrop: Add actual resizing support
cbentejac Jan 19, 2026
0d4b0a3
[GraphEditor] Backdrop: Handle animations and backdrop attachment to …
cbentejac Jan 19, 2026
5ccbb34
[GraphEditor] GraphEditor: Stop using semicolons and harmonize the file
cbentejac Jan 19, 2026
69444c1
[Controls] DelegateSelectionBox: Ensure a child node cannot be pushed…
cbentejac Jan 19, 2026
7025ce9
[GraphEditor] Clean-up backdrop-related code
cbentejac Jan 19, 2026
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
4 changes: 3 additions & 1 deletion meshroom/core/desc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@
StaticNodeSize,
)
from .node import (
MrNodeType,
AVCommandLineNode,
BaseNode,
BackdropNode,
CommandLineNode,
InitNode,
InputNode,
InternalAttributesFactory,
MrNodeType,
Node,
)
129 changes: 103 additions & 26 deletions 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 StringParam, ColorParam, ChoiceParam
from .attribute import Attribute, ChoiceParam, ColorParam, 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 @@ -56,29 +56,11 @@ class MrNodeType(enum.Enum):
NODE = enum.auto()
COMMANDLINE = enum.auto()
INPUT = enum.auto()
BACKDROP = enum.auto()


class BaseNode(object):
"""
"""
cpu = Level.NORMAL
gpu = Level.NONE
ram = Level.NORMAL
packageName = ""
internalInputs = [
StringParam(
name="invalidation",
label="Invalidation Message",
description="A message that will invalidate the node's output folder.\n"
"This is useful for development, we can invalidate the output of the node "
"when we modify the code.\n"
"It is displayed in bold font in the invalidation/comment messages "
"tooltip.",
value="",
semantic="multiline",
advanced=True,
uidIgnoreValue="", # If the invalidation string is empty, it does not participate to the node's UID
),
class InternalAttributesFactory:
BASIC = [
StringParam(
name="comment",
label="Comments",
Expand Down Expand Up @@ -113,6 +95,74 @@ class BaseNode(object):
invalidate=False,
)
]

INVALIDATION = [
StringParam(
name="invalidation",
label="Invalidation Message",
description="A message that will invalidate the node's output folder.\n"
"This is useful for development, we can invalidate the output of the node "
"when we modify the code.\n"
"It is displayed in bold font in the invalidation/comment messages "
"tooltip.",
value="",
semantic="multiline",
advanced=True,
uidIgnoreValue="", # If the invalidation string is empty, it does not participate to the node's UID
),
]

RESIZABLE = [
IntParam(
name="fontSize",
label="Font Size",
description="Size of the font used to display the comments.",
value=12,
range=(6, 100, 1),
),
IntParam(
name="nodeWidth",
label="Node Width",
description="Width of the node in the graph editor.",
value=600,
range=None,
enabled=False, # Hidden
),
IntParam(
name="nodeHeight",
label="Node Height",
description="Height of the node in the graph editor.",
value=400,
range=None,
enabled=False, # Hidden
),
]

@classmethod
def getInternalAttributes(cls, mrNodeType: MrNodeType) -> list[Attribute]:
paramMap = {
MrNodeType.NONE: cls.BASIC,
MrNodeType.BASENODE: cls.INVALIDATION + cls.BASIC,
MrNodeType.NODE: cls.INVALIDATION + cls.BASIC,
MrNodeType.COMMANDLINE: cls.INVALIDATION + cls.BASIC,
MrNodeType.INPUT: cls.BASIC,
MrNodeType.BACKDROP: cls.BASIC + cls.RESIZABLE,
}

return paramMap.get(mrNodeType)


class BaseNode(object):
"""
"""
cpu = Level.NORMAL
gpu = Level.NONE
ram = Level.NORMAL
packageName = ""
_mrNodeType: MrNodeType = MrNodeType.BASENODE

internalInputs = InternalAttributesFactory.getInternalAttributes(_mrNodeType)

inputs = []
outputs = []
size = StaticNodeSize(1)
Expand All @@ -130,7 +180,7 @@ def __init__(self):
self.sourceCodeFolder = Path(getfile(self.__class__)).parent.resolve().as_posix()

def getMrNodeType(self):
return MrNodeType.BASENODE
return self._mrNodeType

def upgradeAttributeValues(self, attrValues, fromVersion):
return attrValues
Expand Down Expand Up @@ -286,24 +336,50 @@ class InputNode(BaseNode):
"""
Node that does not need to be processed, it is just a placeholder for inputs.
"""
_mrNodeType: MrNodeType = MrNodeType.INPUT
internalInputs = InternalAttributesFactory.getInternalAttributes(_mrNodeType)

def __init__(self):
super(InputNode, self).__init__()

def getMrNodeType(self):
return MrNodeType.INPUT
return self._mrNodeType

def processChunk(self, chunk):
pass

def process(self, node):
pass

class BackdropNode(BaseNode):
"""
Node that does not need to be processed, it is just a placeholder for grouping other nodes.
"""
_mrNodeType: MrNodeType = MrNodeType.BACKDROP
internalInputs = InternalAttributesFactory.getInternalAttributes(_mrNodeType)

def __init__(self):
super(BackdropNode, self).__init__()

def getMrNodeType(self):
return self._mrNodeType

def processChunk(self, chunk):
pass

def process(self, node):
pass


class Node(BaseNode):
pythonExecutable = "python"
_mrNodeType: MrNodeType = MrNodeType.NODE

def __init__(self):
super(Node, self).__init__()

def getMrNodeType(self):
return MrNodeType.NODE
return self._mrNodeType

def processChunkInEnvironment(self, chunk):
meshroomComputeCmd = f"{chunk.node.nodeDesc.pythonExecutable} {_MESHROOM_COMPUTE}" + \
Expand All @@ -326,12 +402,13 @@ class CommandLineNode(BaseNode):
commandLine = "" # need to be defined on the node
parallelization = None
commandLineRange = ""
_mrNodeType: MrNodeType = MrNodeType.COMMANDLINE

def __init__(self):
super(CommandLineNode, self).__init__()

def getMrNodeType(self):
return MrNodeType.COMMANDLINE
return self._mrNodeType

def buildCommandLine(self, chunk) -> str:
cmdLineVars = chunk.node.createCmdLineVars()
Expand Down
4 changes: 2 additions & 2 deletions meshroom/core/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from meshroom.core.exception import GraphCompatibilityError, InvalidEdgeError, StopGraphVisit, StopBranchVisit, CyclicDependencyError
from meshroom.core.graphIO import GraphIO, GraphSerializer, TemplateGraphSerializer, PartialGraphSerializer
from meshroom.core.node import BaseNode, Status, Node, CompatibilityNode
from meshroom.core.nodeFactory import nodeFactory
from meshroom.core.nodeFactory import nodeFactory, getNodeConstructor
from meshroom.core.mtyping import PathLike
from meshroom.core.submitter import BaseSubmittedJob, jobManager

Expand Down Expand Up @@ -669,7 +669,7 @@ def addNewNode(
if name and name in self._nodes.keys():
name = self._createUniqueNodeName(name)

node = self.addNode(Node(nodeType, position=position, **kwargs), uniqueName=name)
node = self.addNode(getNodeConstructor(nodeType, position=position, **kwargs), uniqueName=name)
node.updateInternals()
self._triggerNodeCreatedCallback([node])
return node
Expand Down
90 changes: 78 additions & 12 deletions meshroom/core/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,53 +860,81 @@ def getName(self):
def getDefaultLabel(self):
return self.nameToLabel(self._name)

def getLabel(self):
def getLabel(self) -> str:
"""
Returns:
str: the user-provided label if it exists, the high-level label of this node otherwise
The user-provided label if it exists, the high-level label of this node otherwise
"""
if self.hasInternalAttribute("label"):
label = self.internalAttribute("label").value.strip()
if label:
return label
return self.getDefaultLabel()

def getNodeLogLevel(self):
def getNodeLogLevel(self) -> str:
"""
Returns:
str: the user-provided log level used for logging on process launched by this node
The user-provided log level used for logging on process launched by this node
"""
if self.hasInternalAttribute("nodeDefaultLogLevel"):
return self.internalAttribute("nodeDefaultLogLevel").value.strip()
return "info"

def getColor(self):
def getColor(self) -> str:
"""
Returns:
str: the user-provided custom color of the node if it exists, empty string otherwise
The user-provided custom color of the node if it exists, empty string otherwise
"""
if self.hasInternalAttribute("color"):
return self.internalAttribute("color").value.strip()
return ""

def getInvalidationMessage(self):
def getInvalidationMessage(self) -> str:
"""
Returns:
str: the invalidation message on the node if it exists, empty string otherwise
The invalidation message on the node if it exists, empty string otherwise
"""
if self.hasInternalAttribute("invalidation"):
return self.internalAttribute("invalidation").value
return ""

def getComment(self):
def getComment(self) -> str:
"""
Returns:
str: the comments on the node if they exist, empty string otherwise
The comments on the node if they exist, empty string otherwise
"""
if self.hasInternalAttribute("comment"):
return self.internalAttribute("comment").value
return ""

def getFontSize(self) -> int:
"""
Returns:
The font size from the node if it exists, 0 otherwise.
"""
if self.hasInternalAttribute("fontSize"):
return self.internalAttribute("fontSize").value
return 0

def getNodeWidth(self) -> int:
"""
Returns:
The width of the node if it has a user-set width, 0 otherwise.
"""
if self.hasInternalAttribute("nodeWidth"):
return self.internalAttribute("nodeWidth").value
return 0

def getNodeHeight(self) -> int:
"""
Returns:
The height of the node if it has a user-set height, 0 otherwise.
"""
if self.hasInternalAttribute("nodeHeight"):
return self.internalAttribute("nodeHeight").value
return 0


@Slot(str, result=str)
def nameToLabel(self, name):
"""
Expand Down Expand Up @@ -1243,7 +1271,7 @@ def _isComputableType(self):
"""
# Ambiguous case for NONE, which could be used for compatibility nodes if we don't have
# any information about the node descriptor.
return self.getMrNodeType() != MrNodeType.INPUT
return self.getMrNodeType() != MrNodeType.INPUT and self.getMrNodeType() != MrNodeType.BACKDROP

def clearData(self):
""" Delete this Node internal folder.
Expand Down Expand Up @@ -1811,6 +1839,9 @@ def _isCompatibilityNode(self):
def _isInputNode(self):
return isinstance(self.nodeDesc, desc.InputNode)

def _isBackdropNode(self) -> bool:
return False

@property
def globalExecMode(self):
if not self._chunksCreated:
Expand Down Expand Up @@ -2069,6 +2100,9 @@ def _hasDisplayableShape(self):
color = Property(str, getColor, notify=internalAttributesChanged)
invalidation = Property(str, getInvalidationMessage, notify=internalAttributesChanged)
comment = Property(str, getComment, notify=internalAttributesChanged)
fontSize = Property(int, getFontSize, notify=internalAttributesChanged)
nodeWidth = Property(int, getNodeWidth, notify=internalAttributesChanged)
nodeHeight = Property(int, getNodeHeight, notify=internalAttributesChanged)
internalFolderChanged = Signal()
internalFolder = Property(str, internalFolder.fget, notify=internalFolderChanged)
valuesFile = Property(str, valuesFile.fget, notify=internalFolderChanged)
Expand All @@ -2089,9 +2123,9 @@ def _hasDisplayableShape(self):
elapsedTime = Property(float, lambda self: self.getFusedStatus().elapsedTime, notify=globalStatusChanged)
recursiveElapsedTime = Property(float, lambda self: self.getRecursiveFusedStatus().elapsedTime,
notify=globalStatusChanged)
# isCompatibilityNode: need lambda to evaluate the virtual function
isCompatibilityNode = Property(bool, lambda self: self._isCompatibilityNode(), constant=True)
isInputNode = Property(bool, lambda self: self._isInputNode(), constant=True)
isBackdropNode = Property(bool, lambda self: self._isBackdropNode(), constant=True)

globalExecMode = Property(str, globalExecMode.fget, notify=globalStatusChanged)
jobName = Property(str, lambda self: self._getJobName(), notify=globalStatusChanged)
Expand Down Expand Up @@ -2330,6 +2364,38 @@ def createChunks(self):
self.upgradeStatusFile()


class BackdropNode(BaseNode):
def __init__(self, nodeType: str, 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.internalInputs:
self._internalAttributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None),
isOutput=False, node=self))

def _isBackdropNode(self) -> bool:
return True

def toDict(self):
internalInputs = {k: v.getSerializedValue() for k, v in self._internalAttributes.objects.items()}

return {
'nodeType': self.nodeType,
'position': self._position,
'parallelization': {
'blockSize': 0,
'size': 0,
'split': 0
},
'uid': self._uid,
'internalInputs': {k: v for k, v in internalInputs.items() if v is not None},
}


class CompatibilityIssue(Enum):
"""
Enum describing compatibility issues when deserializing a Node.
Expand Down
Loading
Loading