Skip to content

Commit 3eb43bc

Browse files
fix: fix LayerDM Pipeline cleanup
Add several steps to avoid crash in pipelines during or just after cleanup: - Add BlockUpdateObserver to block triggers to the OnUpdate callback - Add BlockInteractionProcessing to block pipeline from being processed during interactions - Add SetFrozen to freeze all interactions with pipeline during and after removal - Add LoseInteraction when node is removed during interaction process
1 parent ee9423d commit 3eb43bc

6 files changed

Lines changed: 142 additions & 1 deletion

LayerDM/MRMLDM/vtkMRMLLayerDMInteractionLogic.cxx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ std::tuple<double, int> vtkMRMLLayerDMInteractionLogic::PrioritizeCanProcessPipe
6464
int maxState = this->MinWidgetState();
6565
for (const auto& pipeline : m_pipelines)
6666
{
67+
if (pipeline->IsInteractionProcessingBlocked())
68+
{
69+
continue;
70+
}
71+
6772
double pipelineDistance = std::numeric_limits<double>::max();
6873
if (pipeline->CanProcessInteractionEvent(eventData, pipelineDistance))
6974
{
@@ -102,6 +107,10 @@ void vtkMRMLLayerDMInteractionLogic::AddPipeline(const vtkSmartPointer<vtkMRMLLa
102107

103108
void vtkMRMLLayerDMInteractionLogic::RemovePipeline(const vtkSmartPointer<vtkMRMLLayerDMPipelineI>& pipeline)
104109
{
110+
if (this->m_prevFocusedPipeline == pipeline)
111+
{
112+
this->LoseFocus();
113+
}
105114
this->m_pipelines.erase(std::find(this->m_pipelines.begin(), this->m_pipelines.end(), pipeline));
106115
}
107116

@@ -133,6 +142,11 @@ bool vtkMRMLLayerDMInteractionLogic::ProcessInteractionEvent(vtkMRMLInteractionE
133142
{
134143
for (const auto& pipeline : m_canProcess)
135144
{
145+
if (pipeline->IsInteractionProcessingBlocked())
146+
{
147+
continue;
148+
}
149+
136150
// If pipeline can process, store pipeline for further interaction events
137151
if (pipeline->ProcessInteractionEvent(eventData))
138152
{

LayerDM/MRMLDM/vtkMRMLLayerDMPipelineI.cxx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,33 @@ void vtkMRMLLayerDMPipelineI::SetRenderer(vtkRenderer* renderer)
5959

6060
bool vtkMRMLLayerDMPipelineI::BlockResetDisplay(bool isBlocked)
6161
{
62+
if (this->m_isFrozen)
63+
{
64+
return true;
65+
}
66+
6267
bool prev = this->m_isResetDisplayBlocked;
6368
this->m_isResetDisplayBlocked = isBlocked;
6469
return prev;
6570
}
6671

72+
bool vtkMRMLLayerDMPipelineI::BlockInteractionProcessing(bool isBlocked)
73+
{
74+
if (this->m_isFrozen)
75+
{
76+
return true;
77+
}
78+
79+
const auto prev = this->m_isInteractionProcessingBlocked;
80+
this->m_isInteractionProcessingBlocked = isBlocked;
81+
return prev;
82+
}
83+
84+
bool vtkMRMLLayerDMPipelineI::IsInteractionProcessingBlocked() const
85+
{
86+
return this->m_isInteractionProcessingBlocked;
87+
}
88+
6789
bool vtkMRMLLayerDMPipelineI::CanProcessInteractionEvent(vtkMRMLInteractionEventData* eventData, double& distance2)
6890
{
6991
return false;
@@ -142,6 +164,37 @@ vtkMRMLAbstractViewNode* vtkMRMLLayerDMPipelineI::GetViewNode() const
142164
return this->m_viewNode;
143165
}
144166

167+
bool vtkMRMLLayerDMPipelineI::BlockUpdateObserver(bool isBlocked) const
168+
{
169+
if (this->m_isFrozen)
170+
{
171+
return true;
172+
}
173+
174+
return this->m_obs->SetBlocked(isBlocked);
175+
}
176+
177+
void vtkMRMLLayerDMPipelineI::SetFrozen(bool isFrozen)
178+
{
179+
if (this->m_isFrozen == isFrozen)
180+
{
181+
return;
182+
}
183+
184+
// Block states are only updated when the pipeline is not frozen.
185+
// Unfreeze to update all before saving the frozen state.
186+
this->m_isFrozen = false;
187+
this->BlockInteractionProcessing(isFrozen);
188+
this->BlockResetDisplay(isFrozen);
189+
this->BlockUpdateObserver(isFrozen);
190+
this->m_isFrozen = isFrozen;
191+
}
192+
193+
bool vtkMRMLLayerDMPipelineI::IsFrozen() const
194+
{
195+
return this->m_isFrozen;
196+
}
197+
145198
vtkMRMLNode* vtkMRMLLayerDMPipelineI::GetDisplayNode() const
146199
{
147200
return this->m_displayNode;
@@ -182,6 +235,8 @@ vtkMRMLLayerDMPipelineI::vtkMRMLLayerDMPipelineI()
182235
, m_displayNode{ nullptr }
183236
, m_renderer{ nullptr }
184237
, m_isResetDisplayBlocked{ false }
238+
, m_isFrozen{ false }
239+
, m_isInteractionProcessingBlocked{ false }
185240
, m_obs(vtkSmartPointer<vtkMRMLLayerDMObjectEventObserver>::New())
186241
, m_pipelineManager(nullptr)
187242
{

LayerDM/MRMLDM/vtkMRMLLayerDMPipelineI.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,26 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe
123123
/// If \param isBlocked is true, \sa UpdatePipeline is not called during \sa ResetDisplay.
124124
bool BlockResetDisplay(bool isBlocked);
125125

126+
/// If \param isBlocked is true, blocks \sa CanProcessInteractionEvent and \sa ProcessInteractionEvent to be called.
127+
/// \sa vtkMRMLLayerDMInteractionLogic
128+
bool BlockInteractionProcessing(bool isBlocked);
129+
130+
/// Current state of interaction processing blocking.
131+
bool IsInteractionProcessingBlocked() const;
132+
133+
/// If \param isBlocked is true, blocks \sa OnUpdate method from being triggered by external changes.
134+
bool BlockUpdateObserver(bool isBlocked) const;
135+
136+
/// @{
137+
/// If \param isFrozen is true, blocks all reactiveness from the pipeline (ResetDisplay, Interaction and Update).
138+
/// Reactiveness cannot be toggled back on unless the pipeline is unfrozen first.
139+
/// Used to deactivate pipelines during removal.
140+
///
141+
/// \sa BlockResetDisplay \sa BlockInteractionProcessing \sa BlockUpdateObserver
142+
void SetFrozen(bool isFrozen);
143+
bool IsFrozen() const;
144+
/// @}
145+
126146
/// Returns the current display node.
127147
vtkMRMLNode* GetDisplayNode() const;
128148

@@ -189,6 +209,8 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe
189209
vtkWeakPointer<vtkMRMLNode> m_displayNode;
190210
vtkWeakPointer<vtkRenderer> m_renderer;
191211
bool m_isResetDisplayBlocked;
212+
bool m_isFrozen;
213+
bool m_isInteractionProcessingBlocked;
192214
vtkSmartPointer<vtkMRMLLayerDMObjectEventObserver> m_obs;
193215
vtkWeakPointer<vtkMRMLLayerDMPipelineManager> m_pipelineManager;
194216
vtkWeakPointer<vtkMRMLScene> m_scene;

LayerDM/MRMLDM/vtkMRMLLayerDMPipelineManager.cxx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,10 @@ bool vtkMRMLLayerDMPipelineManager::RemovePipeline(vtkMRMLNode* displayNode)
128128
}
129129

130130
RequestRenderOnceGuard renderGuard{ *this };
131-
this->m_layerManager->RemovePipeline(pipeline);
131+
pipeline->SetFrozen(true);
132+
// Let interaction logic process the removal first if the pipeline needs to lose focus.
132133
this->m_interactionLogic->RemovePipeline(pipeline);
134+
this->m_layerManager->RemovePipeline(pipeline);
133135
this->m_pipelineMap.erase(displayNode);
134136
this->InvokeEvent(vtkCommand::ModifiedEvent);
135137
return true;

LayerDM/Testing/Python/InteractionLogicTest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,33 @@ def test_on_new_interaction_different_from_focused_loses_focus(self):
105105
p2.mockProcess.assert_called_once_with(self.event)
106106

107107
assert self.logic.GetLastFocusedPipeline() == p2
108+
109+
def test_pipelines_with_blocked_interactions_are_not_called_during_can_process(self):
110+
p1 = MockPipeline(canProcess=True, processDistance=0)
111+
self.logic.AddPipeline(p1)
112+
113+
assert not p1.BlockInteractionProcessing(True)
114+
self.logic.CanProcessInteractionEvent(self.event, self.distance)
115+
p1.mockCanProcess.assert_not_called()
116+
117+
assert p1.BlockInteractionProcessing(False)
118+
self.logic.CanProcessInteractionEvent(self.event, self.distance)
119+
p1.mockCanProcess.assert_called_once()
120+
121+
def test_pipelines_with_blocked_interactions_are_not_called_during_process(self):
122+
# Blocking can happen after the pipeline had claimed it could process the interaction.
123+
# Make sure that it doesn't actually process it if blocked afterwards.
124+
p1 = MockPipeline(canProcess=True, didProcess=True, processDistance=0)
125+
self.logic.AddPipeline(p1)
126+
127+
assert self.logic.CanProcessInteractionEvent(self.event, self.distance)
128+
129+
assert not p1.BlockInteractionProcessing(True)
130+
assert not self.logic.ProcessInteractionEvent(self.event)
131+
p1.mockProcess.assert_not_called()
132+
133+
assert p1.BlockInteractionProcessing(False)
134+
assert self.logic.CanProcessInteractionEvent(self.event, self.distance)
135+
assert self.logic.ProcessInteractionEvent(self.event)
136+
137+
p1.mockProcess.assert_called_once()

LayerDM/Testing/Python/PipelineManagerTest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,21 @@ def test_notifies_pipelines_when_references_are_added_or_removed(self):
296296
# Unset the display node and expect the pipeline to have been notified
297297
markups.SetAndObserveDisplayNodeID("")
298298
m1.mockOnReferenceToDisplayNodeRemoved.assert_called_once_with(markups, "display")
299+
300+
def test_pipelines_removed_are_frozen_during_cleanup(self):
301+
m1 = self.triggerMockPipelineCreation(MockPipeline())
302+
assert not m1.IsFrozen()
303+
self.pipelineManager.RemoveNode(m1.GetDisplayNode())
304+
assert m1.IsFrozen()
305+
306+
def test_pipelines_removed_while_having_interaction_lose_focus_during_cleanup(self):
307+
m1 = self.triggerMockPipelineCreation(MockPipeline())
308+
m1.mockCanProcess.return_value = (True, 0.0)
309+
m1.mockProcess.return_value = True
310+
distance = ref(0.0)
311+
self.pipelineManager.CanProcessInteractionEvent(vtkMRMLInteractionEventData(), distance)
312+
self.pipelineManager.ProcessInteractionEvent(vtkMRMLInteractionEventData())
313+
m1.mockProcess.assert_called_once()
314+
m1.mockLoseFocus.assert_not_called()
315+
self.pipelineManager.RemoveNode(m1.GetDisplayNode())
316+
m1.mockLoseFocus.assert_called_once()

0 commit comments

Comments
 (0)