Merge pull request #94 from alicevision/dev_duplicateNodes

GraphEditor: allow nodes duplication
This commit is contained in:
Fabien Castan 2018-02-13 16:16:23 +01:00 committed by GitHub
commit c3bf936b9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 132 additions and 21 deletions

View file

@ -51,6 +51,15 @@ else:
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):
return isinstance(v, collections.Iterable) and not isinstance(v, basestring)
@ -170,7 +179,7 @@ class Attribute(BaseObject):
if self._value == value:
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
self._value = value
else:
@ -236,7 +245,7 @@ class Attribute(BaseObject):
if isinstance(v, Attribute):
g.addEdge(v, self)
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
link = v[1:-1]
linkNode, linkAttr = link.split('.')

View file

@ -288,6 +288,61 @@ class UIGraph(QObject):
""" Reset 'attribute' to its default value """
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)
def appendAttribute(self, attribute, value=QJsonValue()):
if isinstance(value, QJsonValue):

View file

@ -225,11 +225,34 @@ Item {
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)
onComputeRequest: uigraph.execute(node)
onSubmitRequest: uigraph.submit(node)
onDuplicateRequest: duplicate(duplicateFollowingNodes)
onRemoveRequest: 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
}
// Really basic auto-layout based on node depths
function doAutoLayout()
/** Basic auto-layout based on node depths
* @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'
var grid = new Array(nodeRepeater.count)
for(var i=0; i< nodeRepeater.count; ++i)
grid[i] = new Array(nodeRepeater.count)
for(var i=0; i<nodeRepeater.count; ++i)
{
var obj = nodeRepeater.itemAt(i);
}
// default values
from = from === undefined ? 0 : from
to = to === undefined ? nodeRepeater.count : to
startX = startX === undefined ? 0 : startX
startY = startY === undefined ? 0 : startY
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;
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;
}
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)
{
grid[x][y].x = x * (root.nodeWidth + root.gridSpacing)
grid[x][y].y = y * (root.nodeHeight + root.gridSpacing)
grid[x][y].x = startX + x * (root.nodeWidth + root.gridSpacing)
grid[x][y].y = startY + y * (root.nodeHeight + root.gridSpacing)
}
}
}

View file

@ -16,9 +16,11 @@ Item {
signal computeRequest()
signal submitRequest()
signal duplicateRequest(var duplicateFollowingNodes)
signal removeRequest()
implicitHeight: body.height
objectName: node.name
MouseArea {
anchors.fill: parent
@ -51,6 +53,15 @@ Item {
onTriggered: Qt.openUrlExternally(node.internalFolder)
}
MenuSeparator {}
MenuItem {
text: "Duplicate"
onTriggered: duplicateRequest(false)
}
MenuItem {
text: "Duplicate From Here"
onTriggered: duplicateRequest(true)
}
MenuSeparator {}
MenuItem {
text: "Clear Data"
enabled: !root.readOnly