mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-07-29 22:47:17 +02:00
[ui] Refactor node pasting using graph partial serialization
Re-implement node pasting by relying on the graph partial serializer, to serialize only the subset of selected nodes. On pasting, use standard graph deserialization and import the content of the serialized graph in the active graph instance. Simplify the positioning of pasted nodes to only consider mouse position or center of the graph, which works well for the major variety of use-cases. Compute the offset to apply to imported nodes by using the de-serialized graph content's bounding box.
This commit is contained in:
parent
f8f03b0bd5
commit
d54ba012a0
4 changed files with 63 additions and 158 deletions
|
@ -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]
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue