Skip to content

Commit f2628eb

Browse files
committed
Put deferred "insert" operators in correct place
This rewires the deferred "insertion" operators (operators that are being inserted somewhere in an existing pipeline) so that they visually appear where they are going to actually be placed when they execute. Before, they would appear as if a new branch was being formed, but then move to their insertion location when applied. When "Cancel" is clicked, or escape/x button so that the dialog is closed, the connected operators are rewired according to their initial wiring (before the operator was added), and the pipeline is not re-executed. Signed-off-by: Patrick Avery <patrick.avery@kitware.com>
1 parent d2794b8 commit f2628eb

6 files changed

Lines changed: 178 additions & 55 deletions

File tree

tomviz/AddPythonSourceReaction.cxx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ void AddPythonSourceReaction::onTriggered()
4040

4141
// Add the source to the pipeline before opening the dialog. This is
4242
// what gives us the cancel-rollback symmetry with transform
43-
// insertion: NodeEditDialog::onCancel removes the node from the
43+
// insertion: NodeEditDialog::reject() removes the node from the
4444
// pipeline (and deletes it). On OK we finish the standard source
4545
// scaffolding (sinks, color map, execute) via completeSourceSetup.
4646
LoadDataReaction::addSourceToPipeline(source);

tomviz/TransformUtils.cxx

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "pipeline/TransformNode.h"
1919

2020
#include <QApplication>
21+
#include <QSet>
2122
#include <QtDebug>
2223

2324
namespace tomviz {
@@ -43,6 +44,49 @@ static pipeline::OutputPort* findCompatibleOutputPort(
4344
return nullptr;
4445
}
4546

47+
/// All nodes reachable downstream from @a start (inclusive), following output
48+
/// links. This is exactly the set Node::markStale() cascades over when @a
49+
/// start is marked stale, so it is the set whose state we must snapshot to be
50+
/// able to undo an eager insertion.
51+
static QSet<pipeline::Node*> downstreamClosure(pipeline::Node* start)
52+
{
53+
QSet<pipeline::Node*> result;
54+
if (!start) {
55+
return result;
56+
}
57+
QList<pipeline::Node*> stack;
58+
stack.append(start);
59+
while (!stack.isEmpty()) {
60+
auto* node = stack.takeLast();
61+
if (result.contains(node)) {
62+
continue;
63+
}
64+
result.insert(node);
65+
for (auto* out : node->outputPorts()) {
66+
for (auto* link : out->links()) {
67+
if (link->to() && link->to()->node()) {
68+
stack.append(link->to()->node());
69+
}
70+
}
71+
}
72+
}
73+
return result;
74+
}
75+
76+
/// Snapshot the node and output-port states of @a nodes into @a deferred so
77+
/// they can be restored verbatim if an eager insertion is canceled. Must be
78+
/// called before the insertion mutates the pipeline.
79+
static void captureStates(const QSet<pipeline::Node*>& nodes,
80+
pipeline::DeferredLinkInfo& deferred)
81+
{
82+
for (auto* node : nodes) {
83+
deferred.nodeStates.append({ node, node->state() });
84+
for (auto* out : node->outputPorts()) {
85+
deferred.portStaleStates.append({ out, out->isStale() });
86+
}
87+
}
88+
}
89+
4690
/// Append a transform at the given targetPort, moving sink/group links to a
4791
/// compatible output of the new transform. Sinks with no compatible output
4892
/// on the new transform are left connected to @a targetPort.
@@ -80,17 +124,32 @@ static void appendTransformAtPort(
80124
}
81125
}
82126

83-
/// Deferred variant: adds node and input link only, returns deferred info
84-
/// for the output links to be completed later.
127+
/// Deferred variant of appendTransformAtPort: performs the full append
128+
/// eagerly (so the preview shows the final topology) and returns the info
129+
/// needed to undo it if the user cancels.
85130
static pipeline::DeferredLinkInfo appendTransformAtPortDeferred(
86131
pipeline::Pipeline* pip,
87132
pipeline::TransformNode* transform,
88133
pipeline::OutputPort* targetPort)
89134
{
135+
// Snapshot the downstream subtree before mutating anything so a cancel can
136+
// restore it exactly (the sink moves below mark it stale).
137+
pipeline::DeferredLinkInfo deferred;
138+
captureStates(downstreamClosure(targetPort->node()), deferred);
139+
90140
pip->addNode(transform);
91141
pip->createLink(targetPort, transform->inputPorts()[0]);
92142

93-
pipeline::DeferredLinkInfo deferred;
143+
// Move terminal (sink) links from targetPort onto the new transform's
144+
// compatible outputs now, instead of deferring it to commit, so the preview
145+
// shows the sinks already routed through the transform. Record each
146+
// original link so it can be recreated on cancel.
147+
struct Move
148+
{
149+
pipeline::Link* link;
150+
pipeline::OutputPort* newOut;
151+
};
152+
QList<Move> moves;
94153
for (auto* link : targetPort->links()) {
95154
if (link->to()->node() == transform) {
96155
continue; // skip the link we just created
@@ -102,8 +161,13 @@ static pipeline::DeferredLinkInfo appendTransformAtPortDeferred(
102161
if (!newOut) {
103162
continue;
104163
}
105-
deferred.linksToBreak.append({ targetPort, link->to() });
106-
deferred.linksToCreate.append({ newOut, link->to() });
164+
moves.append({ link, newOut });
165+
}
166+
for (const auto& m : moves) {
167+
auto* sinkInput = m.link->to();
168+
deferred.linksToRestore.append({ targetPort, sinkInput });
169+
pip->removeLink(m.link);
170+
pip->createLink(m.newOut, sinkInput);
107171
}
108172
return deferred;
109173
}
@@ -140,7 +204,9 @@ static void insertTransformAtLink(
140204
pip->createLink(transform->outputPorts()[0], toPort);
141205
}
142206

143-
/// Deferred variant for insert-at-link.
207+
/// Deferred variant of insertTransformAtLink: performs the full insertion
208+
/// eagerly (so the preview shows the transform in its final, inserted
209+
/// position) and returns the info needed to undo it if the user cancels.
144210
static pipeline::DeferredLinkInfo insertTransformAtLinkDeferred(
145211
pipeline::Pipeline* pip,
146212
pipeline::TransformNode* transform,
@@ -149,12 +215,19 @@ static pipeline::DeferredLinkInfo insertTransformAtLinkDeferred(
149215
auto* fromPort = link->from();
150216
auto* toPort = link->to();
151217

218+
// Snapshot the downstream subtree before mutating anything so a cancel can
219+
// restore it exactly (createLink() below marks this subtree stale).
220+
pipeline::DeferredLinkInfo deferred;
221+
captureStates(downstreamClosure(toPort->node()), deferred);
222+
deferred.linksToRestore.append({ fromPort, toPort });
223+
224+
// Perform the full insertion now (break from->to, splice the transform in)
225+
// rather than only connecting the input and deferring the rest to commit.
226+
pip->removeLink(link);
152227
pip->addNode(transform);
153228
pip->createLink(fromPort, transform->inputPorts()[0]);
229+
pip->createLink(transform->outputPorts()[0], toPort);
154230

155-
pipeline::DeferredLinkInfo deferred;
156-
deferred.linksToBreak.append({ fromPort, toPort });
157-
deferred.linksToCreate.append({ transform->outputPorts()[0], toPort });
158231
return deferred;
159232
}
160233

tomviz/pipeline/DeferredLinkInfo.h

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,61 @@
44
#ifndef tomvizPipelineDeferredLinkInfo_h
55
#define tomvizPipelineDeferredLinkInfo_h
66

7+
#include "InputPort.h"
8+
#include "Node.h"
9+
#include "NodeState.h"
10+
#include "OutputPort.h"
11+
712
#include <QList>
13+
#include <QPointer>
814

915
namespace tomviz {
1016
namespace pipeline {
1117

12-
class InputPort;
13-
class OutputPort;
14-
15-
/// Captures deferred link operations for a pending transform insertion.
16-
/// When the transform is accepted, old links are removed and new links
17-
/// are created to complete the insertion.
18+
/// Captures everything needed to undo an eagerly-performed transform
19+
/// insertion if the user cancels the edit dialog.
20+
///
21+
/// The insertion is applied to the pipeline immediately so the preview shows
22+
/// the transform in its final, inserted position rather than branching off
23+
/// the upstream port. If the user cancels, the new node is removed (which
24+
/// drops its own links) and this struct is used to put the pipeline back
25+
/// exactly as it was: the original links are recreated and the node/port
26+
/// states captured before the insertion are restored, so cancel is a true
27+
/// no-op and nothing is left spuriously stale.
28+
///
29+
/// For source-shaped insertions (no links to break) this is left empty; the
30+
/// dialog then just removes the node on cancel.
1831
struct DeferredLinkInfo
1932
{
2033
struct LinkEndpoints
2134
{
22-
OutputPort* from = nullptr;
23-
InputPort* to = nullptr;
35+
QPointer<OutputPort> from;
36+
QPointer<InputPort> to;
2437
};
2538

26-
/// Old links to remove on commit (looked up by matching from/to ports).
27-
QList<LinkEndpoints> linksToBreak;
28-
29-
/// New output links to create from the new transform's output ports.
30-
QList<LinkEndpoints> linksToCreate;
39+
struct NodeStateSnapshot
40+
{
41+
QPointer<Node> node;
42+
NodeState state;
43+
};
3144

32-
bool isEmpty() const
45+
struct PortStaleSnapshot
3346
{
34-
return linksToBreak.isEmpty() && linksToCreate.isEmpty();
35-
}
47+
QPointer<OutputPort> port;
48+
bool stale;
49+
};
50+
51+
/// Original links to recreate on cancel.
52+
QList<LinkEndpoints> linksToRestore;
53+
54+
/// Node states captured before the insertion, restored on cancel.
55+
QList<NodeStateSnapshot> nodeStates;
56+
57+
/// Output-port stale flags captured before the insertion, restored on
58+
/// cancel.
59+
QList<PortStaleSnapshot> portStaleStates;
60+
61+
bool isEmpty() const { return linksToRestore.isEmpty(); }
3662
};
3763

3864
} // namespace pipeline

tomviz/pipeline/NodeEditDialog.cxx

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
#include "NodeEditDialog.h"
55

66
#include "EditNodeWidget.h"
7+
#include "InputPort.h"
78
#include "Link.h"
89
#include "Node.h"
10+
#include "OutputPort.h"
911
#include "Pipeline.h"
1012

1113
#include "Utilities.h"
@@ -77,7 +79,7 @@ void NodeEditDialog::init()
7779
connect(m_buttonBox, &QDialogButtonBox::accepted, this,
7880
&NodeEditDialog::onOkay);
7981
connect(m_buttonBox, &QDialogButtonBox::rejected, this,
80-
&NodeEditDialog::onCancel);
82+
&NodeEditDialog::reject);
8183
connect(m_buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked,
8284
this, &NodeEditDialog::onApply);
8385

@@ -136,18 +138,40 @@ void NodeEditDialog::onOkay()
136138
accept();
137139
}
138140

139-
void NodeEditDialog::onCancel()
141+
void NodeEditDialog::reject()
140142
{
141143
if (m_isNewInsertion && !m_insertionCompleted) {
142-
// Remove the node and its links. For transforms the input link
143-
// exists; for sources there are no inputs. Output links were
144-
// never created, so the pipeline remains valid.
144+
// The insertion was applied eagerly when the dialog opened. Undo it so
145+
// that cancel is a true no-op. Removing the new node also drops its own
146+
// input/output links (for sources there are none).
145147
m_pipeline->removeNode(m_node);
146148
m_node = nullptr;
149+
150+
// Recreate the original links that were broken to insert the node.
151+
for (const auto& ep : m_deferred.linksToRestore) {
152+
if (ep.from && ep.to) {
153+
m_pipeline->createLink(ep.from, ep.to);
154+
}
155+
}
156+
157+
// createLink() above marks the affected downstream subtree stale.
158+
// Restore the states captured before the insertion so nothing is left
159+
// spuriously stale (which would otherwise force a needless re-run).
160+
for (const auto& ps : m_deferred.portStaleStates) {
161+
if (ps.port) {
162+
ps.port->setStale(ps.stale);
163+
}
164+
}
165+
for (const auto& ns : m_deferred.nodeStates) {
166+
if (ns.node) {
167+
ns.node->setStateNoCascade(ns.state);
168+
}
169+
}
170+
147171
emit insertionCanceled();
148172
}
149173

150-
reject();
174+
QDialog::reject();
151175
}
152176

153177
void NodeEditDialog::showEvent(QShowEvent* event)
@@ -208,22 +232,10 @@ void NodeEditDialog::restoreGeometry()
208232

209233
void NodeEditDialog::completeInsertion()
210234
{
211-
// Break old links
212-
for (const auto& ep : m_deferred.linksToBreak) {
213-
// Find the actual Link* by matching from/to ports
214-
for (auto* link : m_pipeline->links()) {
215-
if (link->from() == ep.from && link->to() == ep.to) {
216-
m_pipeline->removeLink(link);
217-
break;
218-
}
219-
}
220-
}
221-
222-
// Create new output links
223-
for (const auto& ep : m_deferred.linksToCreate) {
224-
m_pipeline->createLink(ep.from, ep.to);
225-
}
226-
235+
// The insertion (node + link rewiring) was performed eagerly when the
236+
// dialog opened, so the pipeline already has its final topology. There is
237+
// nothing left to rewire here -- just mark the insertion committed so the
238+
// cancel path will not try to roll it back.
227239
m_insertionCompleted = true;
228240
m_isNewInsertion = false;
229241
emit insertionCompleted(m_node);

tomviz/pipeline/NodeEditDialog.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ class Pipeline;
2121
/// A dialog for configuring node parameters with Apply/OK/Cancel.
2222
///
2323
/// Two modes:
24-
/// - **Insertion mode**: The node is being added. Apply/OK completes
25-
/// the insertion and executes the pipeline. Cancel removes the node
26-
/// (and any input/output links described by DeferredLinkInfo).
24+
/// - **Insertion mode**: The node has already been added and spliced into
25+
/// the pipeline (eagerly, so the preview shows its final position).
26+
/// Apply/OK executes the pipeline and commits. Cancel removes the node
27+
/// and restores the original links and node states captured in
28+
/// DeferredLinkInfo, so it is a true no-op.
2729
/// - **Edit mode**: The node already exists. Apply/OK re-applies
2830
/// parameters and executes. Cancel just closes the dialog.
2931
///
@@ -48,6 +50,11 @@ class NodeEditDialog : public QDialog
4850

4951
Node* node() const;
5052

53+
/// Overridden so that every cancel path -- the Cancel button, the Escape
54+
/// key, and the window close button -- funnels through here and rolls back
55+
/// an in-progress insertion.
56+
void reject() override;
57+
5158
signals:
5259
/// Emitted after Apply/OK completes an insertion.
5360
void insertionCompleted(Node* node);
@@ -58,7 +65,6 @@ class NodeEditDialog : public QDialog
5865
private slots:
5966
void onApply();
6067
void onOkay();
61-
void onCancel();
6268

6369
protected:
6470
void showEvent(QShowEvent* event) override;

tomviz/pipeline/PipelineStripWidget.cxx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,10 +1052,16 @@ Link* PipelineStripWidget::linkHitTest(const QPoint& pos) const
10521052
{
10531053
QPainterPathStroker stroker;
10541054
stroker.setWidth(8.0);
1055-
for (auto& lg : m_linkGeometries) {
1056-
QPainterPath hitPath = stroker.createStroke(lg.path);
1055+
// Iterate in reverse so the topmost-painted link wins when hit areas
1056+
// overlap. paintConnections() draws m_linkGeometries front-to-back, so the
1057+
// last entry is on top. Links between non-adjacent nodes share a routing
1058+
// gutter and become collinear there, so a forward search would return a
1059+
// link drawn underneath the one the user actually clicked.
1060+
for (auto it = m_linkGeometries.rbegin(); it != m_linkGeometries.rend();
1061+
++it) {
1062+
QPainterPath hitPath = stroker.createStroke(it->path);
10571063
if (hitPath.contains(pos)) {
1058-
return lg.link;
1064+
return it->link;
10591065
}
10601066
}
10611067
return nullptr;

0 commit comments

Comments
 (0)