mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-08-06 10:18:42 +02:00
Merge pull request #94 from alicevision/dev_duplicateNodes
GraphEditor: allow nodes duplication
This commit is contained in:
commit
c3bf936b9f
4 changed files with 132 additions and 21 deletions
|
@ -51,6 +51,15 @@ else:
|
||||||
|
|
||||||
stringIsLinkRe = re.compile('^\{[A-Za-z]+[A-Za-z0-9_.]*\}$')
|
stringIsLinkRe = re.compile('^\{[A-Za-z]+[A-Za-z0-9_.]*\}$')
|
||||||
|
|
||||||
|
|
||||||
|
def isLink(value):
|
||||||
|
"""
|
||||||
|
Return whether the given argument is a link expression.
|
||||||
|
A link expression is a string matching the {nodeName.attrName} pattern.
|
||||||
|
"""
|
||||||
|
return isinstance(value, basestring) and stringIsLinkRe.match(value)
|
||||||
|
|
||||||
|
|
||||||
def isCollection(v):
|
def isCollection(v):
|
||||||
return isinstance(v, collections.Iterable) and not isinstance(v, basestring)
|
return isinstance(v, collections.Iterable) and not isinstance(v, basestring)
|
||||||
|
|
||||||
|
@ -170,7 +179,7 @@ class Attribute(BaseObject):
|
||||||
if self._value == value:
|
if self._value == value:
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(value, Attribute) or (isinstance(value, basestring) and stringIsLinkRe.match(value)):
|
if isinstance(value, Attribute) or (isinstance(value, basestring) and isLink(value)):
|
||||||
# if we set a link to another attribute
|
# if we set a link to another attribute
|
||||||
self._value = value
|
self._value = value
|
||||||
else:
|
else:
|
||||||
|
@ -236,7 +245,7 @@ class Attribute(BaseObject):
|
||||||
if isinstance(v, Attribute):
|
if isinstance(v, Attribute):
|
||||||
g.addEdge(v, self)
|
g.addEdge(v, self)
|
||||||
self._value = ""
|
self._value = ""
|
||||||
elif self.isInput and isinstance(v, basestring) and stringIsLinkRe.match(v):
|
elif self.isInput and isinstance(v, basestring) and isLink(v):
|
||||||
# value is a link to another attribute
|
# value is a link to another attribute
|
||||||
link = v[1:-1]
|
link = v[1:-1]
|
||||||
linkNode, linkAttr = link.split('.')
|
linkNode, linkAttr = link.split('.')
|
||||||
|
|
|
@ -288,6 +288,61 @@ class UIGraph(QObject):
|
||||||
""" Reset 'attribute' to its default value """
|
""" Reset 'attribute' to its default value """
|
||||||
self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue()))
|
self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue()))
|
||||||
|
|
||||||
|
@Slot(graph.Node)
|
||||||
|
def duplicateNode(self, srcNode, createEdges=True):
|
||||||
|
"""
|
||||||
|
Duplicate 'srcNode'.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
srcNode (graph.Node): the node to duplicate
|
||||||
|
createEdges (bool): whether to replicate 'srcNode' edges on the duplicated node
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
graph.Node: the duplicated node
|
||||||
|
"""
|
||||||
|
serialized = srcNode.toDict()
|
||||||
|
with self.groupedGraphModification("Duplicate Node {}".format(srcNode.name)):
|
||||||
|
# skip edges: filter out attributes which are links
|
||||||
|
if not createEdges:
|
||||||
|
serialized["attributes"] = {k: v for k, v in serialized["attributes"].items() if not graph.isLink(v)}
|
||||||
|
# create a new node of the same type and with the same attributes values
|
||||||
|
node = self.addNode(serialized["nodeType"], **serialized["attributes"])
|
||||||
|
return node
|
||||||
|
|
||||||
|
@Slot(graph.Node, result="QVariantList")
|
||||||
|
def duplicateNodes(self, fromNode):
|
||||||
|
"""
|
||||||
|
Duplicate 'fromNode' and all the following nodes towards graph's leaves.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fromNode (graph.Node): the node to start the duplication from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[graph.Nodes]: the duplicated nodes
|
||||||
|
"""
|
||||||
|
srcNodes, srcEdges = self._graph.nodesFromNode(fromNode)
|
||||||
|
srcNodes = srcNodes[1:] # skip fromNode
|
||||||
|
duplicates = {}
|
||||||
|
with self.groupedGraphModification("Duplicate {} Nodes".format(len(srcNodes))):
|
||||||
|
# duplicate the first node with its external edges
|
||||||
|
duplicates[fromNode.name] = self.duplicateNode(fromNode)
|
||||||
|
# duplicate all the following nodes and remap their edges internally
|
||||||
|
for srcNode in srcNodes:
|
||||||
|
duplicate = self.duplicateNode(srcNode, createEdges=False)
|
||||||
|
duplicates[srcNode.name] = duplicate # original node to duplicate map
|
||||||
|
# get link attributes
|
||||||
|
links = {k: v for k, v in srcNode.toDict()["attributes"].items() if graph.isLink(v)}
|
||||||
|
for attr, link in links.items():
|
||||||
|
link = link[1:-1] # remove starting '{' and trailing '}'
|
||||||
|
# get source node and attribute name
|
||||||
|
edgeSrcNode, edgeSrcAttrName = link.split(".", 1)
|
||||||
|
# if the edge's source node has been duplicated, use the duplicate
|
||||||
|
# otherwise use the original node
|
||||||
|
edgeSrcNode = duplicates.get(edgeSrcNode, self._graph.node(edgeSrcNode))
|
||||||
|
self.addEdge(edgeSrcNode.attribute(edgeSrcAttrName), duplicate.attribute(attr))
|
||||||
|
|
||||||
|
return duplicates.values()
|
||||||
|
|
||||||
@Slot(graph.Attribute, QJsonValue)
|
@Slot(graph.Attribute, QJsonValue)
|
||||||
def appendAttribute(self, attribute, value=QJsonValue()):
|
def appendAttribute(self, attribute, value=QJsonValue()):
|
||||||
if isinstance(value, QJsonValue):
|
if isinstance(value, QJsonValue):
|
||||||
|
|
|
@ -225,11 +225,34 @@ Item {
|
||||||
|
|
||||||
onAttributePinCreated: registerAttributePin(attribute, pin)
|
onAttributePinCreated: registerAttributePin(attribute, pin)
|
||||||
|
|
||||||
onPressed: draggable.selectNode(nodeDelegate)
|
onPressed: {
|
||||||
|
if(mouse.modifiers & Qt.AltModifier)
|
||||||
|
{
|
||||||
|
var delegates = duplicate(true)
|
||||||
|
draggable.selectNode(delegates[0])
|
||||||
|
}
|
||||||
|
else
|
||||||
|
draggable.selectNode(nodeDelegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
function duplicate(duplicateFollowingNodes) {
|
||||||
|
var nodes = duplicateFollowingNodes ? uigraph.duplicateNodes(node) : [uigraph.duplicateNode(node)]
|
||||||
|
var delegates = []
|
||||||
|
var from = nodeRepeater.count - nodes.length
|
||||||
|
var to = nodeRepeater.count
|
||||||
|
for(var i=from; i < to; ++i)
|
||||||
|
{
|
||||||
|
delegates.push(nodeRepeater.itemAt(i))
|
||||||
|
}
|
||||||
|
doAutoLayout(from, to, x, y + (root.nodeHeight + root.gridSpacing))
|
||||||
|
return delegates
|
||||||
|
}
|
||||||
|
|
||||||
onDoubleClicked: root.nodeDoubleClicked(node)
|
onDoubleClicked: root.nodeDoubleClicked(node)
|
||||||
|
|
||||||
onComputeRequest: uigraph.execute(node)
|
onComputeRequest: uigraph.execute(node)
|
||||||
onSubmitRequest: uigraph.submit(node)
|
onSubmitRequest: uigraph.submit(node)
|
||||||
|
onDuplicateRequest: duplicate(duplicateFollowingNodes)
|
||||||
onRemoveRequest: uigraph.removeNode(node)
|
onRemoveRequest: uigraph.removeNode(node)
|
||||||
|
|
||||||
Keys.onDeletePressed: uigraph.removeNode(node)
|
Keys.onDeletePressed: uigraph.removeNode(node)
|
||||||
|
@ -295,40 +318,53 @@ Item {
|
||||||
draggable.y = bbox.y*draggable.scale*-1 + (root.height-bbox.height*draggable.scale)*0.5
|
draggable.y = bbox.y*draggable.scale*-1 + (root.height-bbox.height*draggable.scale)*0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
// Really basic auto-layout based on node depths
|
/** Basic auto-layout based on node depths
|
||||||
function doAutoLayout()
|
* @param {int} from the index of the node to start the layout from (default: 0)
|
||||||
|
* @param {int} to the index of the node end the layout at (default: nodeCount)
|
||||||
|
* @param {real} startX layout origin x coordinate (default: 0)
|
||||||
|
* @param {real} startY layout origin y coordinate (default: 0)
|
||||||
|
*/
|
||||||
|
function doAutoLayout(from, to, startX, startY)
|
||||||
{
|
{
|
||||||
var depthProperty = useMinDepth ? 'minDepth' : 'depth'
|
// default values
|
||||||
var grid = new Array(nodeRepeater.count)
|
from = from === undefined ? 0 : from
|
||||||
for(var i=0; i< nodeRepeater.count; ++i)
|
to = to === undefined ? nodeRepeater.count : to
|
||||||
grid[i] = new Array(nodeRepeater.count)
|
startX = startX === undefined ? 0 : startX
|
||||||
for(var i=0; i<nodeRepeater.count; ++i)
|
startY = startY === undefined ? 0 : startY
|
||||||
{
|
|
||||||
var obj = nodeRepeater.itemAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(var i=0; i<nodeRepeater.count; ++i)
|
var count = to - from;
|
||||||
|
|
||||||
|
var depthProperty = useMinDepth ? 'minDepth' : 'depth'
|
||||||
|
var grid = new Array(count)
|
||||||
|
|
||||||
|
for(var i=0; i< count; ++i)
|
||||||
|
grid[i] = new Array(count)
|
||||||
|
|
||||||
|
// retrieve reference depth from start node
|
||||||
|
var zeroDepth = from > 0 ? nodeRepeater.itemAt(from).node[depthProperty] : 0
|
||||||
|
|
||||||
|
for(var i=0; i<count; ++i)
|
||||||
{
|
{
|
||||||
var obj = nodeRepeater.itemAt(i);
|
var obj = nodeRepeater.itemAt(from + i);
|
||||||
var j=0;
|
var j=0;
|
||||||
while(1)
|
while(1)
|
||||||
{
|
{
|
||||||
if(grid[obj.node[depthProperty]][j] == undefined)
|
if(grid[obj.node[depthProperty]-zeroDepth][j] == undefined)
|
||||||
{
|
{
|
||||||
grid[obj.node[depthProperty]][j] = obj;
|
grid[obj.node[depthProperty]-zeroDepth][j] = obj;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
j++;
|
j++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(var x= 0; x<nodeRepeater.count; ++x)
|
for(var x=0; x<count; ++x)
|
||||||
{
|
{
|
||||||
for(var y=0; y<nodeRepeater.count; ++y)
|
for(var y=0; y<count; ++y)
|
||||||
{
|
{
|
||||||
if(grid[x][y] != undefined)
|
if(grid[x][y] != undefined)
|
||||||
{
|
{
|
||||||
grid[x][y].x = x * (root.nodeWidth + root.gridSpacing)
|
grid[x][y].x = startX + x * (root.nodeWidth + root.gridSpacing)
|
||||||
grid[x][y].y = y * (root.nodeHeight + root.gridSpacing)
|
grid[x][y].y = startY + y * (root.nodeHeight + root.gridSpacing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,11 @@ Item {
|
||||||
|
|
||||||
signal computeRequest()
|
signal computeRequest()
|
||||||
signal submitRequest()
|
signal submitRequest()
|
||||||
|
signal duplicateRequest(var duplicateFollowingNodes)
|
||||||
signal removeRequest()
|
signal removeRequest()
|
||||||
|
|
||||||
implicitHeight: body.height
|
implicitHeight: body.height
|
||||||
|
objectName: node.name
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -51,6 +53,15 @@ Item {
|
||||||
onTriggered: Qt.openUrlExternally(node.internalFolder)
|
onTriggered: Qt.openUrlExternally(node.internalFolder)
|
||||||
}
|
}
|
||||||
MenuSeparator {}
|
MenuSeparator {}
|
||||||
|
MenuItem {
|
||||||
|
text: "Duplicate"
|
||||||
|
onTriggered: duplicateRequest(false)
|
||||||
|
}
|
||||||
|
MenuItem {
|
||||||
|
text: "Duplicate From Here"
|
||||||
|
onTriggered: duplicateRequest(true)
|
||||||
|
}
|
||||||
|
MenuSeparator {}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "Clear Data"
|
text: "Clear Data"
|
||||||
enabled: !root.readOnly
|
enabled: !root.readOnly
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue