diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 66d4645c..4bb0b825 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -676,41 +676,6 @@ class Graph(BaseObject): return duplicates - def pasteNodes(self, data, position): - """ - Paste node(s) in the graph with their connections. The connections can only be between - the pasted nodes and not with the rest of the graph. - - Args: - data (dict): the dictionary containing the information about the nodes to paste, with their names and - links already updated to be added to the graph - position (list): the list of positions for each node to paste - - Returns: - list: the list of Node objects that were pasted and added to the graph - """ - nodes = [] - with GraphModification(self): - positionCnt = 0 # always valid because we know the data is sorted the same way as the position list - for key in sorted(data): - nodeType = data[key].get("nodeType", None) - if not nodeType: # this case should never occur, as the data should have been prefiltered first - pass - - attributes = {} - attributes.update(data[key].get("inputs", {})) - attributes.update(data[key].get("outputs", {})) - attributes.update(data[key].get("internalInputs", {})) - - node = Node(nodeType, position=position[positionCnt], **attributes) - self._addNode(node, key) - - nodes.append(node) - positionCnt += 1 - - self._applyExpr() - return nodes - def outEdges(self, attribute): """ Return the list of edges starting from the given attribute """ # type: (Attribute,) -> [Edge] diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index 7d8ccc1f..d5d0abe0 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -211,15 +211,27 @@ class PasteNodesCommand(GraphCommand): """ Handle node pasting in a Graph. """ - def __init__(self, graph, data, position=None, parent=None): + def __init__(self, graph: "Graph", data: dict, position: Position, parent=None): super(PasteNodesCommand, self).__init__(graph, parent) self.data = data self.position = position - self.nodeNames = [] + self.nodeNames: list[str] = [] def redoImpl(self): - data = self.graph.updateImportedProject(self.data) - nodes = self.graph.pasteNodes(data, self.position) + graph = Graph("") + try: + graph._deserialize(self.data) + except: + return False + + boundingBoxCenter = self._boundingBoxCenter(graph.nodes) + offset = Position(self.position.x - boundingBoxCenter.x, self.position.y - boundingBoxCenter.y) + + for node in graph.nodes: + node.position = Position(node.position.x + offset.x, node.position.y + offset.y) + + nodes = self.graph.importGraphContent(graph) + self.nodeNames = [node.name for node in nodes] self.setText("Paste Node{} ({})".format("s" if len(self.nodeNames) > 1 else "", ", ".join(self.nodeNames))) return nodes @@ -228,6 +240,24 @@ class PasteNodesCommand(GraphCommand): for name in self.nodeNames: self.graph.removeNode(name) + def _boundingBox(self, nodes) -> tuple[int, int, int, int]: + if not nodes: + return (0, 0, 0 , 0) + + minX = maxX = nodes[0].x + minY = maxY = nodes[0].y + + for node in nodes[1:]: + minX = min(minX, node.x) + minY = min(minY, node.y) + maxX = max(maxX, node.x) + maxY = max(maxY, node.y) + + return (minX, minY, maxX, maxY) + + def _boundingBoxCenter(self, nodes): + minX, minY, maxX, maxY = self._boundingBox(nodes) + return Position((minX + maxX) / 2, (minY + maxY) / 2) class ImportProjectCommand(GraphCommand): """ diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 3727fc8e..c7dabc14 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -1050,126 +1050,43 @@ class UIGraph(QObject): """ if not self._nodeSelection.hasSelection(): return "" - serializedSelection = {node.name: node.toDict() for node in self.iterSelectedNodes()} - return json.dumps(serializedSelection, indent=4) + graphData = self._graph.serializePartial(self.getSelectedNodes()) + return json.dumps(graphData, indent=4) - @Slot(str, QPoint, bool, result=list) - def pasteNodes(self, clipboardContent, position=None, centerPosition=False) -> list[Node]: + @Slot(str, QPoint, result=list) + def pasteNodes(self, serializedData: str, position: Optional[QPoint]=None) -> list[Node]: """ - Parse the content of the clipboard to see whether it contains - valid node descriptions. If that is the case, the nodes described - in the clipboard are built with the available information. - Otherwise, nothing is done. + Import string-serialized graph content `serializedData` in the current graph, optionally at the given + `position`. + If the `serializedData` does not contain valid serialized graph data, nothing is done. - This function does not need to be preceded by a call to "getSelectedNodesContent". - Any clipboard content that contains at least a node type with a valid JSON - formatting (dictionary form with double quotes around the keys and values) - can be used to generate a node. + This method can be used with the result of "getSelectedNodesContent". + But it also accepts any serialized content that matches the graph data or graph content format. For example, it is enough to have: {"nodeName_1": {"nodeType":"CameraInit"}, "nodeName_2": {"nodeType":"FeatureMatching"}} - in the clipboard to create a default CameraInit and a default FeatureMatching nodes. + in `serializedData` to create a default CameraInit and a default FeatureMatching nodes. Args: - clipboardContent (str): the string contained in the clipboard, that may or may not contain valid - node information - position (QPoint): the position of the mouse in the Graph Editor when the function was called - centerPosition (bool): whether the provided position is not the top-left corner of the pasting - zone, but its center + serializedData: The string-serialized graph data. + position: The position where to paste the nodes. If None, the nodes are pasted at (0, 0). Returns: list: the list of Node objects that were pasted and added to the graph """ - if not clipboardContent: - return - try: - d = json.loads(clipboardContent) - except ValueError as e: - raise ValueError(e) + graphData = json.loads(serializedData) + except json.JSONDecodeError: + logging.warning("Content is not a valid JSON string.") + return [] - if not isinstance(d, dict): - raise ValueError("The clipboard does not contain a valid node. Cannot paste it.") + pos = Position(position.x(), position.y()) if position else Position(0, 0) + result = self.push(commands.PasteNodesCommand(self._graph, graphData, pos)) + if result is False: + logging.warning("Content is not a valid graph data.") + return [] + return result - # If the clipboard contains a header, then a whole file is contained in the clipboard - # Extract the "graph" part and paste it all, ignore the rest - if d.get("header", None): - d = d.get("graph", None) - if not d: - return - - if isinstance(position, QPoint): - position = Position(position.x(), position.y()) - if self.hoveredNode: - # If a node is hovered, add an offset to prevent complete occlusion - position = Position(position.x + self.layout.gridSpacing, position.y + self.layout.gridSpacing) - - # Get the position of the first node in a zone whose top-left corner is the mouse and the bottom-right - # corner the (x, y) coordinates, with x the maximum of all the nodes' position along the x-axis, and y the - # maximum of all the nodes' position along the y-axis. All nodes with a position will be placed relatively - # to the first node within that zone. - firstNodePos = None - minX = 0 - maxX = 0 - minY = 0 - maxY = 0 - for key in sorted(d): - nodeType = d[key].get("nodeType", None) - if not nodeType: - raise ValueError("Invalid node description: no provided node type for '{}'".format(key)) - - pos = d[key].get("position", None) - if pos: - if not firstNodePos: - firstNodePos = pos - minX = pos[0] - maxX = pos[0] - minY = pos[1] - maxY = pos[1] - else: - if minX > pos[0]: - minX = pos[0] - if maxX < pos[0]: - maxX = pos[0] - if minY > pos[1]: - minY = pos[1] - if maxY < pos[1]: - maxY = pos[1] - - # Ensure there will not be an error if no node has a specified position - if not firstNodePos: - firstNodePos = [0, 0] - - # Position of the first node within the zone - position = Position(position.x + firstNodePos[0] - minX, position.y + firstNodePos[1] - minY) - - if centerPosition: # Center the zone around the mouse's position (mouse's position might be artificial) - maxX = maxX + self.layout.nodeWidth # maxX and maxY are the position of the furthest node's top-left corner - maxY = maxY + self.layout.nodeHeight # We want the position of the furthest node's bottom-right corner - position = Position(position.x - ((maxX - minX) / 2), position.y - ((maxY - minY) / 2)) - - finalPosition = None - prevPosition = None - positions = [] - - for key in sorted(d): - currentPosition = d[key].get("position", None) - if not finalPosition: - finalPosition = position - else: - if prevPosition and currentPosition: - # If the nodes both have a position, recreate the distance between them with a different - # starting point - x = finalPosition.x + (currentPosition[0] - prevPosition[0]) - y = finalPosition.y + (currentPosition[1] - prevPosition[1]) - finalPosition = Position(x, y) - else: - # If either the current node or previous one lacks a position, use a custom one - finalPosition = Position(finalPosition.x + self.layout.gridSpacing + self.layout.nodeWidth, finalPosition.y) - prevPosition = currentPosition - positions.append(finalPosition) - - return self.push(commands.PasteNodesCommand(self.graph, d, position=positions)) undoStack = Property(QObject, lambda self: self._undoStack, constant=True) graphChanged = Signal() diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 9a6ab00d..b9191634 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -82,25 +82,18 @@ Item { /// Paste content of clipboard to graph editor and create new node if valid function pasteNodes() { - var finalPosition = undefined - var centerPosition = false + let finalPosition = undefined; if (mouseArea.containsMouse) { - if (uigraph.hoveredNode !== null) { - var node = nodeDelegate(uigraph.hoveredNode) - finalPosition = Qt.point(node.mousePosition.x + node.x, node.mousePosition.y + node.y) - } else { - finalPosition = mapToItem(draggable, mouseArea.mouseX, mouseArea.mouseY) - } + finalPosition = mapToItem(draggable, mouseArea.mouseX, mouseArea.mouseY); } else { - finalPosition = getCenterPosition() - centerPosition = true + finalPosition = getCenterPosition(); } - var copiedContent = Clipboard.getText() - var nodes = uigraph.pasteNodes(copiedContent, finalPosition, centerPosition) + const copiedContent = Clipboard.getText(); + const nodes = uigraph.pasteNodes(copiedContent, finalPosition); if (nodes.length > 0) { - uigraph.selectedNode = nodes[0] - uigraph.selectNodes(nodes) + uigraph.selectedNode = nodes[0]; + uigraph.selectNodes(nodes); } }