Skip to content

Commit e36804f

Browse files
authored
Merge pull request #2644 from alicevision/dev/EdgesRemoval
[ui]: Introduction of multiple ways to remove Node Edges
2 parents 64f806b + a20c1e7 commit e36804f

File tree

8 files changed

+325
-10
lines changed

8 files changed

+325
-10
lines changed

meshroom/ui/components/edge.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from PySide6.QtCore import Signal, Property, QPointF, Qt, QObject
1+
from PySide6.QtCore import Signal, Property, QPointF, Qt, QObject, Slot, QRectF
22
from PySide6.QtGui import QPainterPath, QVector2D
33
from PySide6.QtQuick import QQuickItem
44

@@ -110,6 +110,17 @@ def setContainsMouse(self, value):
110110
self._containsMouse = value
111111
self.containsMouseChanged.emit()
112112

113+
@Slot(QPointF, QPointF, result=bool)
114+
def intersectsSegment(self, p1, p2):
115+
""" Checks whether the given segment (p1, p2) intersects with the Path. """
116+
path = QPainterPath()
117+
# Starting point
118+
path.moveTo(p1)
119+
# Create a diagonal line to the other end of the rect
120+
path.lineTo(p2)
121+
v = self._path.intersects(path)
122+
return v
123+
113124
thicknessChanged = Signal()
114125
thickness = Property(float, getThickness, setThickness, notify=thicknessChanged)
115126
curveScaleChanged = Signal()

meshroom/ui/graph.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,25 @@ def removeEdge(self, edge):
870870
else:
871871
self.push(commands.RemoveEdgeCommand(self._graph, edge))
872872

873+
@Slot(list)
874+
def deleteEdgesByIndices(self, indices):
875+
with self.groupedGraphModification("Remove Edges"):
876+
copied = list(self._graph.edges)
877+
for index in indices:
878+
self.removeEdge(copied[index])
879+
880+
@Slot()
881+
def disconnectSelectedNodes(self):
882+
with self.groupedGraphModification("Disconnect Nodes"):
883+
selectedNodes = self.getSelectedNodes()
884+
for edge in self._graph.edges[:]:
885+
# Remove only the edges which are coming or going out of the current selection
886+
if edge.src.node in selectedNodes and edge.dst.node in selectedNodes:
887+
continue
888+
889+
if edge.dst.node in selectedNodes or edge.src.node in selectedNodes:
890+
self.removeEdge(edge)
891+
873892
@Slot(Edge, Attribute, Attribute, result=Edge)
874893
def replaceEdge(self, edge, newSrc, newDst):
875894
with self.groupedGraphModification(f"Replace Edge '{edge.src.getFullNameToNode()}'->'{edge.dst.getFullNameToNode()}' with '{newSrc.getFullNameToNode()}'->'{newDst.getFullNameToNode()}'"):
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import QtQuick
2+
import Meshroom.Helpers
3+
4+
/*
5+
A SelectionLine that can be used to select delegates in a model instantiator (Repeater, ListView...).
6+
Interesection test is done in the coordinate system of the container Item, using delegate's bounding boxes.
7+
The list of selected indices is emitted when the selection ends.
8+
*/
9+
10+
SelectionLine {
11+
id: root
12+
13+
// The Item instantiating the delegates.
14+
property Item modelInstantiator
15+
// The Item containing the delegates (used for coordinate mapping).
16+
property Item container
17+
// Emitted when the selection has ended, with the list of selected indices and modifiers.
18+
signal delegateSelectionEnded(list<int> indices, int modifiers)
19+
20+
onSelectionEnded: function(selectionP1, selectionP2, modifiers) {
21+
let selectedIndices = [];
22+
const mappedP1 = mapToItem(container, selectionP1);
23+
const mappedP2 = mapToItem(container, selectionP2);
24+
for (var i = 0; i < modelInstantiator.count; ++i) {
25+
const delegate = modelInstantiator.itemAt(i);
26+
if (delegate.intersectsSegment(mappedP1, mappedP2)) {
27+
selectedIndices.push(i);
28+
}
29+
}
30+
delegateSelectionEnded(selectedIndices, modifiers);
31+
}
32+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import QtQuick
2+
import QtQuick.Shapes
3+
4+
/*
5+
Simple selection line that can be used by a MouseArea.
6+
7+
Usage:
8+
1. Create a MouseArea and a selectionShape.
9+
2. Bind the selectionShape to the MouseArea by setting the `mouseArea` property.
10+
3. Call startSelection() with coordinates when the selection starts.
11+
4. Call endSelection() when the selection ends.
12+
5. Listen to the selectionEnded signal to get the segment (defined by 2 points).
13+
*/
14+
15+
Item {
16+
id: root
17+
18+
property MouseArea mouseArea
19+
20+
readonly property bool active: mouseArea.drag.target == dragTarget
21+
22+
signal selectionEnded(point selectionP1, point selectionP2, int modifiers)
23+
24+
function startSelection(mouse) {
25+
dragTarget.startPos.x = dragTarget.x = mouse.x;
26+
dragTarget.startPos.y = dragTarget.y = mouse.y;
27+
dragTarget.modifiers = mouse.modifiers;
28+
mouseArea.drag.target = dragTarget;
29+
}
30+
31+
function endSelection() {
32+
if (!active) {
33+
return;
34+
}
35+
mouseArea.drag.target = null;
36+
const p1 = Qt.point(selectionShape.x, selectionShape.y);
37+
const p2 = Qt.point(selectionShape.x + selectionShape.width, selectionShape.y + selectionShape.height);
38+
selectionEnded(p1, p2, dragTarget.modifiers);
39+
}
40+
41+
visible: active
42+
43+
Item {
44+
id: selectionShape
45+
x: dragTarget.startPos.x
46+
y: dragTarget.startPos.y
47+
width: dragTarget.x - dragTarget.startPos.x
48+
height: dragTarget.y - dragTarget.startPos.y
49+
50+
Shape {
51+
id: dynamicLine;
52+
width: selectionShape.width;
53+
height: selectionShape.height;
54+
anchors.fill: parent;
55+
56+
ShapePath {
57+
strokeWidth: 2;
58+
strokeStyle: ShapePath.DashLine;
59+
strokeColor: "#CC3E3E";
60+
dashPattern: [3, 2];
61+
62+
startX: 0;
63+
startY: 0;
64+
65+
PathLine {
66+
x: selectionShape.width;
67+
y: selectionShape.height;
68+
}
69+
}
70+
}
71+
}
72+
73+
Item {
74+
id: dragTarget
75+
property point startPos
76+
property var modifiers
77+
}
78+
}

meshroom/ui/qml/Controls/qmldir

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ MScrollBar 1.0 MScrollBar.qml
1818
MSplitView 1.0 MSplitView.qml
1919
DirectionalLightPane 1.0 DirectionalLightPane.qml
2020
SelectionBox 1.0 SelectionBox.qml
21-
DelegateSelectionBox 1.0 DelegateSelectionBox.qml
21+
SelectionLine 1.0 SelectionLine.qml
22+
DelegateSelectionBox 1.0 DelegateSelectionBox.qml
23+
DelegateSelectionLine 1.0 DelegateSelectionLine.qml

meshroom/ui/qml/GraphEditor/Edge.qml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Item {
2323
property int loopSize: 0
2424
property int iteration: 0
2525

26-
// BUG: edgeArea is destroyed before path, need to test if not null to avoid warnings
26+
// Note: edgeArea is destroyed before path, so we need to test if not null to avoid warnings.
2727
readonly property bool containsMouse: (loopArea && loopArea.containsMouse) || (edgeArea && edgeArea.containsMouse)
2828

2929
signal pressed(var event)
@@ -40,6 +40,16 @@ Item {
4040
property real endY: height
4141

4242

43+
function intersectsSegment(p1, p2) {
44+
/**
45+
* Detects whether a line along the given rects diagonal intersects with the edge mouse area.
46+
*/
47+
// The edgeArea is within the parent Item and its bounds and position are relative to its parent
48+
// Map the original rect to the coordinates of the edgeArea by subtracting the parent's coordinates from the rect
49+
// This mapped rect would ensure that the rect coordinates map to 0 of the edge area
50+
return edgeArea.intersectsSegment(Qt.point(p1.x - x, p1.y - y), Qt.point(p2.x - x, p2.y - y));
51+
}
52+
4353
Shape {
4454
anchors.fill: parent
4555
// Cause rendering artifacts when enabled (and don't support hot reload really well)

meshroom/ui/qml/GraphEditor/GraphEditor.qml

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,14 @@ Item {
113113
}
114114
} else if (event.key === Qt.Key_D) {
115115
duplicateNode(event.modifiers === Qt.AltModifier)
116-
} else if (event.key === Qt.Key_X && event.modifiers === Qt.ControlModifier) {
117-
copyNodes()
118-
uigraph.removeSelectedNodes()
116+
} else if (event.key === Qt.Key_X) {
117+
if (event.modifiers === Qt.ControlModifier) {
118+
copyNodes()
119+
uigraph.removeSelectedNodes()
120+
}
121+
else {
122+
uigraph.disconnectSelectedNodes()
123+
}
119124
} else if (event.key === Qt.Key_C) {
120125
if (event.modifiers === Qt.ControlModifier) {
121126
copyNodes()
@@ -138,6 +143,7 @@ Item {
138143
id: mouseArea
139144
anchors.fill: parent
140145
property double factor: 1.15
146+
property bool removingEdges: false;
141147
// Activate multisampling for edges antialiasing
142148
layer.enabled: true
143149
layer.samples: 8
@@ -146,7 +152,7 @@ Item {
146152
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
147153
drag.threshold: 0
148154
drag.smoothed: false
149-
cursorShape: drag.target == draggable ? Qt.ClosedHandCursor : Qt.ArrowCursor
155+
cursorShape: drag.target == draggable ? Qt.ClosedHandCursor : removingEdges ? Qt.CrossCursor : Qt.ArrowCursor
150156

151157
onWheel: function(wheel) {
152158
var zoomFactor = wheel.angleDelta.y > 0 ? factor : 1 / factor
@@ -171,9 +177,15 @@ Item {
171177
if (mouse.button == Qt.MiddleButton || (mouse.button == Qt.LeftButton && mouse.modifiers & Qt.AltModifier)) {
172178
drag.target = draggable // start drag
173179
}
180+
if (mouse.button == Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier) && (mouse.modifiers & Qt.AltModifier)) {
181+
edgeSelectionLine.startSelection(mouse);
182+
removingEdges = true;
183+
}
174184
}
175185

176186
onReleased: {
187+
removingEdges = false;
188+
edgeSelectionLine.endSelection()
177189
nodeSelectionBox.endSelection();
178190
drag.target = null;
179191
root.forceActiveFocus()
@@ -497,7 +509,7 @@ Item {
497509
if (event.button) {
498510
if (canEdit && (event.modifiers & Qt.AltModifier)) {
499511
uigraph.removeEdge(edge)
500-
} else {
512+
} else if (event.button == Qt.RightButton) {
501513
edgeMenu.currentEdge = edge
502514
edgeMenu.forLoop = forLoop
503515
var spawnPosition = mouseArea.mapToItem(draggable, mouseArea.mouseX, mouseArea.mouseY)
@@ -726,6 +738,13 @@ Item {
726738
pasteNodes()
727739
}
728740
}
741+
MenuItem {
742+
text: "Disconnect Node(s)";
743+
enabled: true;
744+
ToolTip.text: "Disconnect all edges from the selected Node(s)";
745+
ToolTip.visible: hovered;
746+
onTriggered: uigraph.disconnectSelectedNodes();
747+
}
729748
MenuItem {
730749
text: "Duplicate Node(s)" + (duplicateFollowingButton.hovered ? " From Here" : "")
731750
enabled: true
@@ -858,6 +877,10 @@ Item {
858877
onAttributePinCreated: function(attribute, pin) { registerAttributePin(attribute, pin) }
859878
onAttributePinDeleted: function(attribute, pin) { unregisterAttributePin(attribute, pin) }
860879

880+
onShaked: {
881+
uigraph.disconnectSelectedNodes();
882+
}
883+
861884
onPressed: function(mouse) {
862885
nodeRepeater.updateSelectionOnClick = true;
863886
nodeRepeater.ongoingDrag = true;
@@ -953,6 +976,10 @@ Item {
953976
if(!selected || !dragging) {
954977
return;
955978
}
979+
980+
// Check for shake on the node
981+
checkForShake();
982+
956983
// Compute offset between the delegate and the stored node position.
957984
const offset = Qt.point(x - node.x, y - node.y);
958985

@@ -999,6 +1026,16 @@ Item {
9991026
}
10001027
}
10011028

1029+
DelegateSelectionLine {
1030+
id: edgeSelectionLine
1031+
mouseArea: mouseArea
1032+
modelInstantiator: edgesRepeater
1033+
container: draggable
1034+
onDelegateSelectionEnded: function(selectedIndices, modifiers) {
1035+
uigraph.deleteEdgesByIndices(selectedIndices);
1036+
}
1037+
}
1038+
10021039
DropArea {
10031040
id: dropArea
10041041
anchors.fill: parent

0 commit comments

Comments
 (0)