Merge pull request #134 from alicevision/dev_connectListAttributes

Linkable ListAttribute
This commit is contained in:
Grégoire De Lillo 2018-06-21 16:04:42 +02:00 committed by GitHub
commit f4b3364275
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 108 additions and 53 deletions

View file

@ -39,7 +39,7 @@ json.JSONEncoder = MyJSONEncoder # replace the default implementation with our
stringIsLinkRe = re.compile('^\{[A-Za-z]+[A-Za-z0-9_.]*\}$')
def isLink(value):
def isLinkExpression(value):
"""
Return whether the given argument is a link expression.
A link expression is a string matching the {nodeName.attrName} pattern.
@ -170,7 +170,7 @@ class Attribute(BaseObject):
if self._value == value:
return
if isinstance(value, Attribute) or (isinstance(value, pyCompatibility.basestring) and isLink(value)):
if isinstance(value, Attribute) or isLinkExpression(value):
# if we set a link to another attribute
self._value = value
else:
@ -188,6 +188,9 @@ class Attribute(BaseObject):
self.requestGraphUpdate()
self.valueChanged.emit()
def resetValue(self):
self._value = ""
def requestGraphUpdate(self):
if self.node.graph:
self.node.graph.markNodesDirty(self.node)
@ -235,13 +238,13 @@ class Attribute(BaseObject):
return
if isinstance(v, Attribute):
g.addEdge(v, self)
self._value = ""
elif self.isInput and isinstance(v, pyCompatibility.basestring) and isLink(v):
self.resetValue()
elif self.isInput and isLinkExpression(v):
# value is a link to another attribute
link = v[1:-1]
linkNode, linkAttr = link.split('.')
g.addEdge(g.node(linkNode).attribute(linkAttr), self)
self._value = ""
self.resetValue()
def getExportValue(self):
if self.isLink:
@ -279,27 +282,53 @@ class Attribute(BaseObject):
isDefault = Property(bool, _isDefault, notify=valueChanged)
def raiseIfLink(func):
""" If Attribute instance is a link, raise a RuntimeError."""
def wrapper(attr, *args, **kwargs):
if attr.isLink:
raise RuntimeError("Can't modify connected Attribute")
return func(attr, *args, **kwargs)
return wrapper
class ListAttribute(Attribute):
def __init__(self, node, attributeDesc, isOutput, root=None, parent=None):
super(ListAttribute, self).__init__(node, attributeDesc, isOutput, root, parent)
self._value = ListModel(parent=self)
def __getitem__(self, item):
return self._value.at(item)
def __len__(self):
return len(self._value)
return len(self.value)
def at(self, idx):
""" Returns child attribute at index 'idx' """
# implement 'at' rather than '__getitem__'
# since the later is called spuriously when object is used in QML
return self.value.at(idx)
def index(self, item):
return self.value.indexOf(item)
def resetValue(self):
self._value = ListModel(parent=self)
def _set_value(self, value):
self.desc.validateValue(value)
self._value.clear()
self.extend(value)
if self.node.graph:
self.remove(0, len(self))
# Link to another attribute
if isinstance(value, ListAttribute) or isLinkExpression(value):
self._value = value
# New value
else:
self.desc.validateValue(value)
self.extend(value)
self.requestGraphUpdate()
@raiseIfLink
def append(self, value):
self.extend([value])
@raiseIfLink
def insert(self, index, value):
values = value if isinstance(value, list) else [value]
attrs = [attribute_factory(self.attributeDesc.elementDesc, v, self.isOutput, self.node, self) for v in values]
@ -308,36 +337,45 @@ class ListAttribute(Attribute):
self._applyExpr()
self.requestGraphUpdate()
def index(self, item):
return self._value.indexOf(item)
@raiseIfLink
def extend(self, values):
self.insert(len(self._value), values)
@raiseIfLink
def remove(self, index, count=1):
if self.node.graph:
for i in range(index, index + count):
attr = self[i]
if attr.isLink:
self.node.graph.removeEdge(attr) # delete edge if the attribute is linked
# remove potential links
with GraphModification(self.node.graph):
for i in range(index, index + count):
attr = self._value.at(i)
if attr.isLink:
# delete edge if the attribute is linked
self.node.graph.removeEdge(attr)
self._value.removeAt(index, count)
self.requestGraphUpdate()
self.valueChanged.emit()
def uid(self, uidIndex):
uids = []
for value in self._value:
if uidIndex in value.desc.uid:
uids.append(value.uid(uidIndex))
return hash(uids)
if isinstance(self.value, ListModel):
uids = []
for value in self.value:
if uidIndex in value.desc.uid:
uids.append(value.uid(uidIndex))
return hash(uids)
return super(ListAttribute, self).uid(uidIndex)
def _applyExpr(self):
if not self.node.graph:
return
for value in self._value:
value._applyExpr()
if isinstance(self._value, ListAttribute) or isLinkExpression(self._value):
super(ListAttribute, self)._applyExpr()
else:
for value in self._value:
value._applyExpr()
def getExportValue(self):
if self.isLink:
return self.getLinkParam().asLinkExpr()
return [attr.getExportValue() for attr in self._value]
def defaultValue(self):
@ -353,7 +391,9 @@ class ListAttribute(Attribute):
return [attr.getPrimitiveValue(exportDefault=exportDefault) for attr in self._value if not attr.isDefault]
def getValueStr(self):
return self.attributeDesc.joinChar.join([v.getValueStr() for v in self._value])
if isinstance(self.value, ListModel):
return self.attributeDesc.joinChar.join([v.getValueStr() for v in self.value])
return super(ListAttribute, self).getValueStr()
# Override value property setter
value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged)

View file

@ -93,6 +93,10 @@ class AddNodeCommand(GraphCommand):
for key, value in self.kwargs.items():
if isinstance(value, Attribute):
self.kwargs[key] = value.asLinkExpr()
elif isinstance(value, list):
for idx, v in enumerate(value):
if isinstance(v, Attribute):
value[idx] = v.asLinkExpr()
def redoImpl(self):
node = self.graph.addNewNode(self.nodeType, **self.kwargs)
@ -133,7 +137,7 @@ class SetAttributeCommand(GraphCommand):
super(SetAttributeCommand, self).__init__(graph, parent)
self.attrName = attribute.fullName()
self.value = value
self.oldValue = attribute.getPrimitiveValue(exportDefault=True)
self.oldValue = attribute.getExportValue()
self.setText("Set Attribute '{}'".format(attribute.fullName()))
def redoImpl(self):

View file

@ -256,10 +256,10 @@ class UIGraph(QObject):
@Slot(graph.Attribute, graph.Attribute)
def addEdge(self, src, dst):
if isinstance(dst, graph.ListAttribute):
if isinstance(dst, graph.ListAttribute) and not isinstance(src, graph.ListAttribute):
with self.groupedGraphModification("Insert and Add Edge on {}".format(dst.fullName())):
self.appendAttribute(dst)
self.push(commands.AddEdgeCommand(self._graph, src, dst[-1]))
self.push(commands.AddEdgeCommand(self._graph, src, dst.at(-1)))
else:
self.push(commands.AddEdgeCommand(self._graph, src, dst))
@ -297,7 +297,7 @@ class UIGraph(QObject):
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)}
serialized["attributes"] = {k: v for k, v in serialized["attributes"].items() if not graph.isLinkExpression(v)}
# create a new node of the same type and with the same attributes values
node = self.addNewNode(serialized["nodeType"], **serialized["attributes"])
return node
@ -324,7 +324,7 @@ class UIGraph(QObject):
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)}
links = {k: v for k, v in srcNode.toDict()["attributes"].items() if graph.isLinkExpression(v)}
for attr, link in links.items():
link = link[1:-1] # remove starting '{' and trailing '}'
# get source node and attribute name

View file

@ -59,7 +59,7 @@ RowLayout {
MenuItem {
text: "Reset To Default Value"
enabled: !attribute.isOutput && !attribute.isLink && !attribute.isDefault
enabled: root.editable && !attribute.isDefault
onTriggered: _reconstruction.resetAttribute(attribute)
}
@ -286,7 +286,7 @@ RowLayout {
var cpt = Qt.createComponent("AttributeItemDelegate.qml")
var obj = cpt.createObject(item,
{'attribute': Qt.binding(function() { return item.childAttrib }),
'readOnly': Qt.binding(function() { return root.readOnly })
'readOnly': Qt.binding(function() { return !root.editable })
})
obj.Layout.fillWidth = true
obj.label.text = index

View file

@ -27,7 +27,8 @@ RowLayout {
// Instantiate empty Items for each child attribute
Repeater {
model: isList ? attribute.value : ""
id: childrenRepeater
model: isList && !attribute.isLink ? attribute.value : 0
onItemAdded: {childPinCreated(item.childAttribute, item)}
onItemRemoved: {childPinDeleted(item.childAttribute, item)}
delegate: Item {
@ -57,7 +58,9 @@ RowLayout {
if( drag.source.objectName != dragTarget.objectName // not an edge connector
|| drag.source.nodeItem == dragTarget.nodeItem // connection between attributes of the same node
|| dragTarget.isOutput // connection on an output
|| dragTarget.attribute.isLink) // already connected attribute
|| dragTarget.attribute.isLink // already connected attribute
|| childrenRepeater.count // attribute has children
)
{
drag.accepted = false
}

View file

@ -227,6 +227,7 @@ Item {
baseColor: root.selectedNode == node ? Qt.lighter("#607D8B", 1.2) : "#607D8B"
onAttributePinCreated: registerAttributePin(attribute, pin)
onAttributePinDeleted: unregisterAttributePin(attribute, pin)
onPressed: {
if(mouse.modifiers & Qt.AltModifier)
@ -299,6 +300,10 @@ Item {
{
root._attributeToDelegate[attribute] = pin
}
function unregisterAttributePin(attribute, pin)
{
delete root._attributeToDelegate[attribute]
}
function boundingBox()
{

View file

@ -14,6 +14,7 @@ Item {
signal pressed(var mouse)
signal doubleClicked(var mouse)
signal attributePinCreated(var attribute, var pin)
signal attributePinDeleted(var attribute, var pin)
signal computeRequest()
signal submitRequest()
@ -145,7 +146,9 @@ Item {
attribute: object
readOnly: root.readOnly
Component.onCompleted: attributePinCreated(attribute, inPin)
Component.onDestruction: attributePinDeleted(attribute, inPin)
onChildPinCreated: attributePinCreated(childAttribute, inPin)
onChildPinDeleted: attributePinDeleted(childAttribute, inPin)
}
}
}

View file

@ -122,7 +122,7 @@ class LiveSfmManager(QObject):
def imagePathsInCameraInit(self, node):
""" Get images in the given CameraInit node. """
assert node.nodeType == 'CameraInit'
return [vp.path.value for vp in node.viewpoints]
return [vp.path.value for vp in node.viewpoints.value]
def imagesInStep(self):
""" Get images in the current augmentation step. """
@ -304,11 +304,11 @@ class Reconstruction(UIGraph):
def allImagePaths(self):
""" Get all image paths in the reconstruction. """
return [vp.path.value for node in self._cameraInits for vp in node.viewpoints]
return [vp.path.value for node in self._cameraInits for vp in node.viewpoints.value]
def allViewIds(self):
""" Get all view Ids involved in the reconstruction. """
return [vp.viewId.value for node in self._cameraInits for vp in node.viewpoints]
return [vp.viewId.value for node in self._cameraInits for vp in node.viewpoints.value]
@Slot(QObject, graph.Node)
def handleFilesDrop(self, drop, cameraInit):

View file

@ -17,10 +17,10 @@ def test_multiviewPipeline():
{'path': '/non/existing/file2', 'intrinsicId': 55}
])
assert graph1.findNode('CameraInit').viewpoints[0].path.value == '/non/existing/fileA'
assert graph1.findNode('CameraInit').viewpoints.at(0).path.value == '/non/existing/fileA'
assert len(graph2.findNode('CameraInit').viewpoints) == 0
assert graph3.findNode('CameraInit').viewpoints[0].path.value == '/non/existing/file1'
assert graph4.findNode('CameraInit').viewpoints[0].path.value == '/non/existing/file1'
assert graph3.findNode('CameraInit').viewpoints.at(0).path.value == '/non/existing/file1'
assert graph4.findNode('CameraInit').viewpoints.at(0).path.value == '/non/existing/file1'
assert len(graph1.findNode('CameraInit').viewpoints) == 1
assert len(graph2.findNode('CameraInit').viewpoints) == 0
@ -28,15 +28,15 @@ def test_multiviewPipeline():
assert len(graph4.findNode('CameraInit').viewpoints) == 2
viewpoints = graph3.findNode('CameraInit').viewpoints
assert viewpoints[0].path.value == '/non/existing/file1'
assert viewpoints.at(0).path.value == '/non/existing/file1'
assert viewpoints[0].path.value == '/non/existing/file1'
assert viewpoints[0].intrinsicId.value == -1
assert viewpoints[1].path.value == '/non/existing/file2'
assert viewpoints[1].intrinsicId.value == -1
assert viewpoints.at(0).path.value == '/non/existing/file1'
assert viewpoints.at(0).intrinsicId.value == -1
assert viewpoints.at(1).path.value == '/non/existing/file2'
assert viewpoints.at(1).intrinsicId.value == -1
assert viewpoints[0].path.isDefault == False
assert viewpoints[0].intrinsicId.isDefault == True
assert not viewpoints.at(0).path.isDefault
assert viewpoints.at(0).intrinsicId.isDefault
assert viewpoints.getPrimitiveValue(exportDefault=False) == [
{"path": '/non/existing/file1'},
{"path": '/non/existing/file2'},
@ -44,10 +44,10 @@ def test_multiviewPipeline():
for graph in (graph4, graph4b):
viewpoints = graph.findNode('CameraInit').viewpoints
assert viewpoints[0].path.value == '/non/existing/file1'
assert viewpoints[0].intrinsicId.value == 50
assert viewpoints[1].path.value == '/non/existing/file2'
assert viewpoints[1].intrinsicId.value == 55
assert viewpoints.at(0).path.value == '/non/existing/file1'
assert viewpoints.at(0).intrinsicId.value == 50
assert viewpoints.at(1).path.value == '/non/existing/file2'
assert viewpoints.at(1).intrinsicId.value == 55
# Ensure that all output UIDs are different as the input is different:
# graph1 != graph2 != graph3 != graph4