mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-07-28 22:17:40 +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
|
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):
|
def outEdges(self, attribute):
|
||||||
""" Return the list of edges starting from the given attribute """
|
""" Return the list of edges starting from the given attribute """
|
||||||
# type: (Attribute,) -> [Edge]
|
# type: (Attribute,) -> [Edge]
|
||||||
|
|
|
@ -211,15 +211,27 @@ class PasteNodesCommand(GraphCommand):
|
||||||
"""
|
"""
|
||||||
Handle node pasting in a Graph.
|
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)
|
super(PasteNodesCommand, self).__init__(graph, parent)
|
||||||
self.data = data
|
self.data = data
|
||||||
self.position = position
|
self.position = position
|
||||||
self.nodeNames = []
|
self.nodeNames: list[str] = []
|
||||||
|
|
||||||
def redoImpl(self):
|
def redoImpl(self):
|
||||||
data = self.graph.updateImportedProject(self.data)
|
graph = Graph("")
|
||||||
nodes = self.graph.pasteNodes(data, self.position)
|
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.nodeNames = [node.name for node in nodes]
|
||||||
self.setText("Paste Node{} ({})".format("s" if len(self.nodeNames) > 1 else "", ", ".join(self.nodeNames)))
|
self.setText("Paste Node{} ({})".format("s" if len(self.nodeNames) > 1 else "", ", ".join(self.nodeNames)))
|
||||||
return nodes
|
return nodes
|
||||||
|
@ -228,6 +240,24 @@ class PasteNodesCommand(GraphCommand):
|
||||||
for name in self.nodeNames:
|
for name in self.nodeNames:
|
||||||
self.graph.removeNode(name)
|
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):
|
class ImportProjectCommand(GraphCommand):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1050,126 +1050,43 @@ class UIGraph(QObject):
|
||||||
"""
|
"""
|
||||||
if not self._nodeSelection.hasSelection():
|
if not self._nodeSelection.hasSelection():
|
||||||
return ""
|
return ""
|
||||||
serializedSelection = {node.name: node.toDict() for node in self.iterSelectedNodes()}
|
graphData = self._graph.serializePartial(self.getSelectedNodes())
|
||||||
return json.dumps(serializedSelection, indent=4)
|
return json.dumps(graphData, indent=4)
|
||||||
|
|
||||||
@Slot(str, QPoint, bool, result=list)
|
@Slot(str, QPoint, result=list)
|
||||||
def pasteNodes(self, clipboardContent, position=None, centerPosition=False) -> list[Node]:
|
def pasteNodes(self, serializedData: str, position: Optional[QPoint]=None) -> list[Node]:
|
||||||
"""
|
"""
|
||||||
Parse the content of the clipboard to see whether it contains
|
Import string-serialized graph content `serializedData` in the current graph, optionally at the given
|
||||||
valid node descriptions. If that is the case, the nodes described
|
`position`.
|
||||||
in the clipboard are built with the available information.
|
If the `serializedData` does not contain valid serialized graph data, nothing is done.
|
||||||
Otherwise, nothing is done.
|
|
||||||
|
|
||||||
This function does not need to be preceded by a call to "getSelectedNodesContent".
|
This method can be used with the result of "getSelectedNodesContent".
|
||||||
Any clipboard content that contains at least a node type with a valid JSON
|
But it also accepts any serialized content that matches the graph data or graph content format.
|
||||||
formatting (dictionary form with double quotes around the keys and values)
|
|
||||||
can be used to generate a node.
|
|
||||||
|
|
||||||
For example, it is enough to have:
|
For example, it is enough to have:
|
||||||
{"nodeName_1": {"nodeType":"CameraInit"}, "nodeName_2": {"nodeType":"FeatureMatching"}}
|
{"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:
|
Args:
|
||||||
clipboardContent (str): the string contained in the clipboard, that may or may not contain valid
|
serializedData: The string-serialized graph data.
|
||||||
node information
|
position: The position where to paste the nodes. If None, the nodes are pasted at (0, 0).
|
||||||
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
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: the list of Node objects that were pasted and added to the graph
|
list: the list of Node objects that were pasted and added to the graph
|
||||||
"""
|
"""
|
||||||
if not clipboardContent:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
d = json.loads(clipboardContent)
|
graphData = json.loads(serializedData)
|
||||||
except ValueError as e:
|
except json.JSONDecodeError:
|
||||||
raise ValueError(e)
|
logging.warning("Content is not a valid JSON string.")
|
||||||
|
return []
|
||||||
|
|
||||||
if not isinstance(d, dict):
|
pos = Position(position.x(), position.y()) if position else Position(0, 0)
|
||||||
raise ValueError("The clipboard does not contain a valid node. Cannot paste it.")
|
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)
|
undoStack = Property(QObject, lambda self: self._undoStack, constant=True)
|
||||||
graphChanged = Signal()
|
graphChanged = Signal()
|
||||||
|
|
|
@ -82,25 +82,18 @@ Item {
|
||||||
|
|
||||||
/// Paste content of clipboard to graph editor and create new node if valid
|
/// Paste content of clipboard to graph editor and create new node if valid
|
||||||
function pasteNodes() {
|
function pasteNodes() {
|
||||||
var finalPosition = undefined
|
let finalPosition = undefined;
|
||||||
var centerPosition = false
|
|
||||||
if (mouseArea.containsMouse) {
|
if (mouseArea.containsMouse) {
|
||||||
if (uigraph.hoveredNode !== null) {
|
finalPosition = mapToItem(draggable, mouseArea.mouseX, mouseArea.mouseY);
|
||||||
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)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
finalPosition = getCenterPosition()
|
finalPosition = getCenterPosition();
|
||||||
centerPosition = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var copiedContent = Clipboard.getText()
|
const copiedContent = Clipboard.getText();
|
||||||
var nodes = uigraph.pasteNodes(copiedContent, finalPosition, centerPosition)
|
const nodes = uigraph.pasteNodes(copiedContent, finalPosition);
|
||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
uigraph.selectedNode = nodes[0]
|
uigraph.selectedNode = nodes[0];
|
||||||
uigraph.selectNodes(nodes)
|
uigraph.selectNodes(nodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue