diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 6ca5ff7..7077347 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -28,10 +28,10 @@ jobs: PIP_EXTRA_INDEX_URL: "https://vtk.org/files/wheel-sdks" CIBW_ENVIRONMENT_PASS_LINUX: "PIP_EXTRA_INDEX_URL" CIBW_BUILD: ${{ inputs.build-target }} - # Required to make tests pass (VTK needs them) - CIBW_BEFORE_ALL_LINUX: "yum install -y mesa-libGL-devel glx-utils freeglut-devel" + CIBW_BEFORE_ALL_LINUX: "yum install -y mesa-libGL-devel glx-utils freeglut-devel mesa-libOSMesa" + CIBW_ENVIRONMENT_LINUX: "VTK_DEFAULT_OPENGL_WINDOW=vtkOSOpenGLRenderWindow" # Global test setup - CIBW_TEST_REQUIRES: "pytest virtualenv" + CIBW_TEST_REQUIRES: "pytest" # Do not repair, everything is already put just as needed to make it work CIBW_REPAIR_WHEEL_COMMAND_LINUX: " python -m pip install wheel && @@ -58,7 +58,7 @@ jobs: - name: Build wheel uses: pypa/cibuildwheel@v3.3.1 env: - CIBW_TEST_COMMAND: "python -m pytest {project}/LayerDM -m runtime --verbose" + CIBW_TEST_COMMAND: "python -m pytest {project}/LayerDM --verbose" with: package-dir: LayerDM output-dir: wheelhouse @@ -66,7 +66,8 @@ jobs: - name: Build wheel SDK uses: pypa/cibuildwheel@v3.3.1 env: - CIBW_TEST_COMMAND: "python -m pytest {project}/LayerDM -m sdk --verbose" + CIBW_TEST_REQUIRES: "pytest virtualenv" + CIBW_TEST_COMMAND: "python -m pytest {project}/LayerDMSDK --verbose" with: package-dir: LayerDMSDK output-dir: wheelhouse diff --git a/Docs/_static/css/custom.css b/Docs/_static/css/custom.css index 6e79967..afa3d4d 100644 --- a/Docs/_static/css/custom.css +++ b/Docs/_static/css/custom.css @@ -86,3 +86,39 @@ a.icon-home img.logo { .rst-content dl dd { margin: 0px; } + +/* Ensure mermaid fullscreen modal and container stay light themed for rtd compatibility*/ +.mermaid-fullscreen-modal, +.mermaid-fullscreen-modal.dark-theme { + background: rgba(255, 255, 255, 0.98) !important; +} + +.mermaid-container-fullscreen, +.mermaid-container-fullscreen.dark-theme { + background: white !important; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3) !important; +} + +.mermaid-fullscreen-close, +.mermaid-fullscreen-close.dark-theme { + background: rgba(255, 255, 255, 0.95) !important; + border: 1px solid rgba(0, 0, 0, 0.2) !important; + color: #333 !important; +} + +.mermaid-fullscreen-close:hover, +.mermaid-fullscreen-close.dark-theme:hover { + background: white !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; +} + +.mermaid-fullscreen-btn.dark-theme { + background: rgba(255, 255, 255, 0.95) !important; + border: 1px solid rgba(0, 0, 0, 0.3) !important; + color: #333 !important; +} + +.mermaid-fullscreen-btn.dark-theme:hover { + background: rgba(255, 255, 255, 1) !important; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3) !important; +} diff --git a/Docs/conf.py b/Docs/conf.py index 94ffa7b..465232e 100644 --- a/Docs/conf.py +++ b/Docs/conf.py @@ -96,6 +96,19 @@ def custom_exhale_mapping(kind): "css/custom.css", ] +# -- Mermaid configuration ---------------------------------------------------- +mermaid_version = "12.0.0" + +# Force light theme to match RTD light background +mermaid_light_theme = "redux-color" +mermaid_dark_theme = "redux-color" + +# Mermaid initialization settings +mermaid_init_config = { + "theme": "redux-color", + "look": "neo", +} + # -- Graphviz options ------------------------------------------------- graphviz_output_format = "svg" graphviz_dot_args = [ diff --git a/Docs/extension_architecture.md b/Docs/extension_architecture.md index 154004a..90a4e36 100644 --- a/Docs/extension_architecture.md +++ b/Docs/extension_architecture.md @@ -31,11 +31,11 @@ The main classes of the library and their responsibilities are summarized below | Class | Description | |------------------------------------------|----------------------------------------------------------------------------------------------| -| vtkMRMLLayerDMPipelineI | Interface for display pipelines. Handles interaction, rendering, camera, and observer logic. | +| vtkMRMLLayerDMPipeline | Interface for display pipelines. Handles interaction, rendering, camera, and observer logic. | | vtkMRMLLayerDisplayableManager | Main displayable manager. Initializes pipeline manager and delegates scene updates. | | vtkMRMLLayerDMCameraSynchronizer | Synchronizes default camera with renderer or slice node state. | | vtkMRMLLayerDMLayerManager | Manages renderer layers based on pipeline layer/camera pairs. | -| vtkMRMLLayerDMPipelineCreatorI | Interface for pipeline creation. Supports custom instantiation logic. | +| vtkMRMLLayerDMPipelineCreator | Interface for pipeline creation. Supports custom instantiation logic. | | vtkMRMLLayerDMPipelineCallbackCreator | Callback-based implementation of pipeline creator. | | vtkMRMLLayerDMPipelineScriptedCreator | Python lambda-based pipeline creator. | | vtkMRMLLayerDMPipelineFactory | Singleton factory for pipeline instantiation and registration. | diff --git a/Docs/getting_started.md b/Docs/getting_started.md index fac14cc..19842be 100644 --- a/Docs/getting_started.md +++ b/Docs/getting_started.md @@ -7,7 +7,7 @@ To get started, developers need to implement the following classes / logic: ## Pipeline -Pipeline objects derive from the vtkMRMLLayerDMPipelineI base class in C++ and the vtkMRMLLayerDMScriptedPipeline in +Pipeline objects derive from the vtkMRMLLayerDMPipeline base class in C++ and the vtkMRMLLayerDMScriptedPipeline in python. Pipeline implementation can be split in two: @@ -55,15 +55,15 @@ class MyPipeline(vtkMRMLLayerDMScriptedPipeline): ## Refreshing the display By default, the display is refreshed when the pipeline is created. Afterwards, refreshing its display should be -connected to the object events and the `ResetDisplay` method should be called. +connected to the object events and the `UpdateDisplay` method should be called. -Calling this method will call the `UpdatePipeline` method and will request a render from the widget. +Calling this method will call the `UpdateFromMRML` method and will request a render from the widget. ```python class MyPipeline(vtkMRMLLayerDMScriptedPipeline): - def UpdatePipeline(self): + def UpdateFromMRML(self): """ - Triggered by self.ResetDisplay() calls: + Triggered by self.UpdateDisplay() calls: - Called automatically at pipeline creation / add to the render window - Called automatically when switching renderer Override to update the representation of the pipeline in the different views. @@ -78,15 +78,15 @@ class MyPipeline(vtkMRMLLayerDMScriptedPipeline): def OnUpdate(self, obj, eventId, callData): """ Observer update callback. - Triggered when any object & events observed using UpdateObserver is triggered. + Triggered when any object & events observed using UpdateObservation is triggered. :param obj: vtkObject instance which triggered the callback :param eventId: Event id which triggered the callback :param callData: Optional observer call data. Use self.CastCallData(callData, vtkType) to convert to Python """ - # Calling reset display will trigger the UpdatePipeline method call - self.ResetDisplay() + # Calling reset display will trigger the UpdateFromMRML method call + self.UpdateDisplay() ``` ## Rendering on top of other actors @@ -123,16 +123,16 @@ modified, the event ID and call data. * `virtual void OnUpdate(vtkObject* obj, unsigned long eventId, void* callData)`: Callback when observed event triggers. -To observe other objet events, the `UpdateObserver` can be used. +To observe other objet events, the `UpdateObservation` can be used. -* `bool UpdateObserver(vtkObject* prevObj, vtkObject* obj, const std::vector& events) const`: Update +* `bool UpdateObservation(vtkObject* prevObj, vtkObject* obj, const std::vector& events) const`: Update observer with multiple events. -* `bool UpdateObserver(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent) const`: +* `bool UpdateObservation(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent) const`: Update observer with single event. -* `void RemoveObserver(vtkObject* prevObj) const`: Remove all observed events. +* `void RemoveObservations(vtkObject* prevObj)`: Remove all observed events. ```{warning} -UpdateObserver doesn't mutate the prevObj (for Python wrapping compatibility reasons). The object's value should be +UpdateObservation doesn't mutate the prevObj (for Python wrapping compatibility reasons). The object's value should be updated manually after this update call. ``` @@ -142,8 +142,8 @@ event. * `virtual void SetViewNode(vtkMRMLAbstractViewNode* viewNode)`: Called at initialization. * `virtual void SetDisplayNode(vtkMRMLNode* displayNode)`: Called at initialization. -During the `OnUpdate` call if the display should be update, then the `ResetDisplay` method should be called. This method -will trigger the `UpdatePipeline` method which can be overridden to update display properties and will also trigger a +During the `OnUpdate` call if the display should be update, then the `UpdateDisplay` method should be called. This method +will trigger the `UpdateFromMRML` method which can be overridden to update display properties and will also trigger a request render. ```python @@ -161,14 +161,14 @@ class MyPipeline(vtkMRMLLayerDMScriptedPipeline): # The two attributes below are used to connect observers on the modelNode and the modelTransform ModifiedEvent # The vtkMRMLLayerDMScriptedPipeline base class provides convenience methods to simply observers # See also: OnUpdate - # See also: UpdateObserver + # See also: UpdateObservation self._modelNode = None self._modelTransform = None def OnUpdate(self, obj, eventId, callData): """ Observer update callback. - Triggered when any object & events observed using UpdateObserver is triggered. + Triggered when any object & events observed using UpdateObservation is triggered. :param obj: vtkObject instance which triggered the callback :param eventId: Event id which triggered the callback @@ -178,7 +178,7 @@ class MyPipeline(vtkMRMLLayerDMScriptedPipeline): if obj == self._modelNode: self._ObserveModelTransformNode() - self.ResetDisplay() + self.UpdateDisplay() def _ObserveModelTransformNode(self): """ @@ -191,7 +191,7 @@ class MyPipeline(vtkMRMLLayerDMScriptedPipeline): if self._modelTransform == transformNode: return - self.UpdateObserver(self._modelTransform, transformNode, vtkMRMLTransformNode.TransformModifiedEvent) + self.UpdateObservation(self._modelTransform, transformNode, vtkMRMLTransformNode.TransformModifiedEvent) self._modelTransform = transformNode ``` @@ -332,7 +332,7 @@ Example of registration for one view type with multiple display node / display p void vtkMRMLCPRPipelineCreatorLogic::RegisterPipelines() { - static vtkSmartPointer creator{ nullptr }; + static vtkSmartPointer creator{ nullptr }; if (!creator) { @@ -380,7 +380,7 @@ vtkMRMLLayerDMPipelineFactory.GetInstance().AddPipelineCreator(pipeline_creator) ``` ```cpp - static vtkSmartPointer creator{ nullptr }; + static vtkSmartPointer creator{ nullptr }; if (!creator) { diff --git a/Examples/Cpp/ModelGlowDM/vtkMRMLModelGlowPipeline.cxx b/Examples/Cpp/ModelGlowDM/vtkMRMLModelGlowPipeline.cxx index 87061d8..5260430 100644 --- a/Examples/Cpp/ModelGlowDM/vtkMRMLModelGlowPipeline.cxx +++ b/Examples/Cpp/ModelGlowDM/vtkMRMLModelGlowPipeline.cxx @@ -50,7 +50,7 @@ void vtkMRMLModelGlowPipeline::OnRendererAdded(vtkRenderer* renderer) renderer->SetPass(glowPass); } -void vtkMRMLModelGlowPipeline::UpdatePipeline() +void vtkMRMLModelGlowPipeline::UpdateFromMRML() { if (this->GetDisplayNode() && this->ModelNode) { @@ -66,7 +66,7 @@ void vtkMRMLModelGlowPipeline::UpdatePipeline() void vtkMRMLModelGlowPipeline::SetModelNode(vtkMRMLModelNode* node) { - UpdateObserver(this->ModelNode, node); + UpdateObservation(this->ModelNode, node); this->ModelNode = node; if (!this->ModelNode) @@ -78,7 +78,7 @@ void vtkMRMLModelGlowPipeline::SetModelNode(vtkMRMLModelNode* node) mapper->SetInputData(this->ModelNode->GetPolyData()); this->GlowActor->SetMapper(mapper); - // UpdateObserver(this->ModelNode, node); + // UpdateObservation(this->ModelNode, node); auto transformNode = this->ModelNode->GetParentTransformNode(); if (transformNode) @@ -101,7 +101,7 @@ void vtkMRMLModelGlowPipeline::OnUpdate(vtkObject* node, unsigned long eventId, SetDisplayNode(displayNode); } - ResetDisplay(); + UpdateDisplay(); } bool vtkMRMLModelGlowPipeline::CanProcessInteractionEvent(vtkMRMLInteractionEventData* eventData, double& distance2) diff --git a/Examples/Cpp/ModelGlowDM/vtkMRMLModelGlowPipeline.h b/Examples/Cpp/ModelGlowDM/vtkMRMLModelGlowPipeline.h index f13c463..d6cf76a 100644 --- a/Examples/Cpp/ModelGlowDM/vtkMRMLModelGlowPipeline.h +++ b/Examples/Cpp/ModelGlowDM/vtkMRMLModelGlowPipeline.h @@ -3,7 +3,7 @@ #include "vtkSlicerModelGlowModuleMRMLExport.h" // Slicer includes -#include "vtkMRMLLayerDMPipelineI.h" +#include "vtkMRMLLayerDMPipeline.h" // VTK includes #include @@ -15,17 +15,17 @@ class vtkMRMLInteractionEventData; class vtkActor; class vtkRenderer; -class VTK_SLICER_MODEL_GLOW_MODULE_MRML_EXPORT vtkMRMLModelGlowPipeline : public vtkMRMLLayerDMPipelineI +class VTK_SLICER_MODEL_GLOW_MODULE_MRML_EXPORT vtkMRMLModelGlowPipeline : public vtkMRMLLayerDMPipeline { public: static vtkMRMLModelGlowPipeline* New(); - vtkTypeMacro(vtkMRMLModelGlowPipeline, vtkMRMLLayerDMPipelineI); + vtkTypeMacro(vtkMRMLModelGlowPipeline, vtkMRMLLayerDMPipeline); void SetDisplayNode(vtkMRMLNode* displayNode) override; void OnRendererRemoved(vtkRenderer* renderer) override; void OnRendererAdded(vtkRenderer* renderer) override; unsigned int GetRenderOrder() const override; - void UpdatePipeline() override; + void UpdateFromMRML() override; // void SetViewNode(vtkMRMLAbstractViewNode* viewNode) override; // void SetScene(vtkMRMLScene* scene) override; diff --git a/Examples/Python/CustomVR/CustomVRLib/CustomVRPipeline.py b/Examples/Python/CustomVR/CustomVRLib/CustomVRPipeline.py index 0177eb1..fdcd099 100644 --- a/Examples/Python/CustomVR/CustomVRLib/CustomVRPipeline.py +++ b/Examples/Python/CustomVR/CustomVRLib/CustomVRPipeline.py @@ -70,7 +70,7 @@ def __init__(self): # The attributes below are used to connect observers on the volumeNode and the volumeTransform ModifiedEvent # The vtkMRMLLayerDMScriptedPipeline base class provides convenience methods to simply observers # See also: OnUpdate - # See also: UpdateObserver + # See also: UpdateObservation self._volumeNode = None self._volumeTransform = None self._imageData = None @@ -141,8 +141,8 @@ def OnRendererAdded(self, renderer: vtkRenderer) -> None: default behavior: does nothing. Here, we add our actor to the input renderer. - If the pipeline renderer has changed, the pipeline's ResetDisplay method will be triggered and in turn its - UpdatePipeline method will be triggered. + If the pipeline renderer has changed, the pipeline's UpdateDisplay method will be triggered and in turn its + UpdateFromMRML method will be triggered. Since we don't control the actual renderer used by the pipeline, this should be used systematically. See also: self.GetRenderer() @@ -158,8 +158,8 @@ def OnRendererRemoved(self, renderer: vtkRenderer) -> None: default behavior: does nothing. Here, we add our actor to the input renderer. - If the pipeline renderer has changed, the pipeline's ResetDisplay method will be triggered and in turn its - UpdatePipeline method will be triggered. + If the pipeline renderer has changed, the pipeline's UpdateDisplay method will be triggered and in turn its + UpdateFromMRML method will be triggered. Since we don't control the actual renderer used by the pipeline, this should be used systematically. See also: self.GetRenderer() @@ -169,9 +169,9 @@ def OnRendererRemoved(self, renderer: vtkRenderer) -> None: return renderer.RemoveViewProp(self._actor) - def UpdatePipeline(self): + def UpdateFromMRML(self): """ - Triggered by self.ResetDisplay() calls: + Triggered by self.UpdateDisplay() calls: - Called automatically at pipeline creation / add to the render window - Called automatically when switching renderer Override to update the representation of the pipeline in the different views. @@ -186,7 +186,7 @@ def UpdatePipeline(self): def OnUpdate(self, obj, eventId, callData): """ Observer update callback. - Triggered when any object & events observed using UpdateObserver is triggered. + Triggered when any object & events observed using UpdateObservation is triggered. :param obj: vtkObject instance which triggered the callback :param eventId: Event id which triggered the callback @@ -197,13 +197,13 @@ def OnUpdate(self, obj, eventId, callData): self._ObserveVolumeTransformNode() self._ObserveVolumeImageData() - self.ResetDisplay() + self.UpdateDisplay() def SetDisplayNode(self, node): """ Set the display node for the pipeline has changed (initialization). default behavior: Stored and display node is observed for vtkCommand::ModifiedEvent. - See also: self.UpdateObserver(prevObj, newObj, eventIds) + See also: self.UpdateObservation(prevObj, newObj, eventIds) See also: self.OnUpdate(obj, eventId, callData) :param node: The new instance of display node for the pipeline @@ -304,9 +304,9 @@ def _ObserveVolumeNode(self): """ Convenience method to update the volume and the volume's transform node observers. - Here, we use the UpdateObserver method: - - UpdateObserver(vtkObject* prevObj, vtkObject* obj, const std::vector& events) -> bool - - UpdateObserver(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent) -> bool + Here, we use the UpdateObservation method: + - UpdateObservation(vtkObject* prevObj, vtkObject* obj, const std::vector& events) -> bool + - UpdateObservation(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent) -> bool This method should be used to add an observer on VTK object. By default, the object's modified event will be observed. @@ -321,7 +321,7 @@ def _ObserveVolumeNode(self): if self._volumeNode == self._GetVolumeNode(): return - self.UpdateObserver(self._volumeNode, self._GetVolumeNode()) + self.UpdateObservation(self._volumeNode, self._GetVolumeNode()) self._volumeNode = self._GetVolumeNode() self._ObserveVolumeTransformNode() self._ObserveVolumeImageData() @@ -339,7 +339,7 @@ def _ObserveVolumeTransformNode(self): if self._volumeTransform == transformNode: return - self.UpdateObserver( + self.UpdateObservation( self._volumeTransform, transformNode, vtkMRMLTransformNode.TransformModifiedEvent, @@ -351,7 +351,7 @@ def _ObserveVolumeImageData(self): if self._imageData == imageData: return - self.UpdateObserver(self._imageData, imageData) + self.UpdateObservation(self._imageData, imageData) self._imageData = imageData @classmethod diff --git a/Examples/Python/ModelGlowDM/ModelGlowDM.py b/Examples/Python/ModelGlowDM/ModelGlowDM.py index 5f7d0c5..ba1dee1 100644 --- a/Examples/Python/ModelGlowDM/ModelGlowDM.py +++ b/Examples/Python/ModelGlowDM/ModelGlowDM.py @@ -249,7 +249,7 @@ def __init__(self): # The two attributes below are used to connect observers on the modelNode and the modelTransform ModifiedEvent # The vtkMRMLLayerDMScriptedPipeline base class provides convenience methods to simply observers # See also: OnUpdate - # See also: UpdateObserver + # See also: UpdateObservation self._modelNode = None self._modelTransform = None @@ -263,8 +263,8 @@ def OnRendererAdded(self, renderer: vtkRenderer) -> None: default behavior: does nothing. Here, we add our actor to the input renderer. - If the pipeline renderer has changed, the pipeline's ResetDisplay method will be triggered and in turn its - UpdatePipeline method will be triggered. + If the pipeline renderer has changed, the pipeline's UpdateDisplay method will be triggered and in turn its + UpdateFromMRML method will be triggered. Since we don't control the actual renderer used by the pipeline, this should be used systematically. See also: self.GetRenderer() @@ -280,8 +280,8 @@ def OnRendererRemoved(self, renderer: vtkRenderer) -> None: default behavior: does nothing. Here, we add our actor to the input renderer. - If the pipeline renderer has changed, the pipeline's ResetDisplay method will be triggered and in turn its - UpdatePipeline method will be triggered. + If the pipeline renderer has changed, the pipeline's UpdateDisplay method will be triggered and in turn its + UpdateFromMRML method will be triggered. Since we don't control the actual renderer used by the pipeline, this should be used systematically. See also: self.GetRenderer() @@ -298,9 +298,9 @@ def GetRenderOrder(self) -> int: """ return GlowDMPassPipeline.GetGlowPassRenderOrder() - def UpdatePipeline(self): + def UpdateFromMRML(self): """ - Triggered by self.ResetDisplay() calls: + Triggered by self.UpdateDisplay() calls: - Called automatically at pipeline creation / add to the render window - Called automatically when switching renderer Override to update the representation of the pipeline in the different views. @@ -319,7 +319,7 @@ def UpdatePipeline(self): def OnUpdate(self, obj, eventId, callData): """ Observer update callback. - Triggered when any object & events observed using UpdateObserver is triggered. + Triggered when any object & events observed using UpdateObservation is triggered. :param obj: vtkObject instance which triggered the callback :param eventId: Event id which triggered the callback @@ -333,13 +333,13 @@ def OnUpdate(self, obj, eventId, callData): if obj == self._modelNode: self._ObserveModelTransformNode() - self.ResetDisplay() + self.UpdateDisplay() def SetDisplayNode(self, node): """ Set the display node for the pipeline has changed (initialization). default behavior: Stored and display node is observed for vtkCommand::ModifiedEvent. - See also: self.UpdateObserver(prevObj, newObj, eventIds) + See also: self.UpdateObservation(prevObj, newObj, eventIds) See also: self.OnUpdate(obj, eventId, callData) :param node: The new instance of display node for the pipeline @@ -498,9 +498,9 @@ def _ObserveModelNode(self): """ Convenience method to update the model and the model's transform node observers. - Here, we use the UpdateObserver method: - - UpdateObserver(vtkObject* prevObj, vtkObject* obj, const std::vector& events) -> bool - - UpdateObserver(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent) -> bool + Here, we use the UpdateObservation method: + - UpdateObservation(vtkObject* prevObj, vtkObject* obj, const std::vector& events) -> bool + - UpdateObservation(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent) -> bool This method should be used to add an observer on VTK object. By default, the object's modified event will be observed. @@ -515,7 +515,7 @@ def _ObserveModelNode(self): if self._modelNode == self._GetModelNode(): return - self.UpdateObserver(self._modelNode, self._GetModelNode()) + self.UpdateObservation(self._modelNode, self._GetModelNode()) self._modelNode = self._GetModelNode() self._ObserveModelTransformNode() @@ -532,7 +532,7 @@ def _ObserveModelTransformNode(self): if self._modelTransform == transformNode: return - self.UpdateObserver(self._modelTransform, transformNode, vtkMRMLTransformNode.TransformModifiedEvent) + self.UpdateObservation(self._modelTransform, transformNode, vtkMRMLTransformNode.TransformModifiedEvent) self._modelTransform = transformNode @classmethod diff --git a/LayerDM/Logic/extension.cmake b/LayerDM/Logic/extension.cmake index 44ca18a..1bc5b44 100644 --- a/LayerDM/Logic/extension.cmake +++ b/LayerDM/Logic/extension.cmake @@ -5,12 +5,6 @@ set(KIT ${PROJECT_NAME}) set(${KIT}_EXPORT_DIRECTIVE "VTK_SLICER_${MODULE_NAME_UPPER}_MODULE_LOGIC_EXPORT") set(${KIT}_INCLUDE_DIRECTORIES) -configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/vtkSlicerLayerDMVersion.h.in - ${CMAKE_CURRENT_BINARY_DIR}/vtkSlicerLayerDMVersion.h - @ONLY -) - set(${KIT}_SRCS vtkSlicer${MODULE_NAME}Logic.cxx vtkSlicer${MODULE_NAME}Logic.h diff --git a/LayerDM/Logic/vtkSlicerLayerDMLogic.cxx b/LayerDM/Logic/vtkSlicerLayerDMLogic.cxx index 5121fe7..3da64fe 100644 --- a/LayerDM/Logic/vtkSlicerLayerDMLogic.cxx +++ b/LayerDM/Logic/vtkSlicerLayerDMLogic.cxx @@ -1,7 +1,24 @@ -#include "vtkSlicerLayerDMLogic.h" +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. -// LayerDM includes -#include +==============================================================================*/ + +#include "vtkSlicerLayerDMLogic.h" // Slicer includes #include @@ -9,43 +26,28 @@ // VTK includes #include +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkSlicerLayerDMLogic); +//----------------------------------------------------------------------------- void vtkSlicerLayerDMLogic::RegisterNodes() { - RegisterNodeIfNeeded(GetMRMLScene()); -} - -int vtkSlicerLayerDMLogic::GetVersionMajor() -{ - return vtkSlicerLayerDM_VERSION_MAJOR; -} - -int vtkSlicerLayerDMLogic::GetVersionMinor() -{ - return vtkSlicerLayerDM_VERSION_MINOR; -} - -int vtkSlicerLayerDMLogic::GetVersionPatch() -{ - return vtkSlicerLayerDM_VERSION_PATCH; -} - -std::string vtkSlicerLayerDMLogic::GetVersion() -{ - return vtkSlicerLayerDM_VERSION; + RegisterNodeIfNeeded(this->GetMRMLScene()); } +//----------------------------------------------------------------------------- void vtkSlicerLayerDMLogic::SetWidgetEventTranslationNode(vtkMRMLNode* node, vtkMRMLLayerDMWidgetEventTranslationNode* translationNode) { SetReferenceNode(node, translationNode, EventTranslationRole); } +//----------------------------------------------------------------------------- vtkMRMLLayerDMWidgetEventTranslationNode* vtkSlicerLayerDMLogic::GetWidgetEventTranslationNode(vtkMRMLNode* node) { return GetReferenceNode(node, EventTranslationRole); } +//----------------------------------------------------------------------------- vtkMRMLLayerDMWidgetEventTranslationNode* vtkSlicerLayerDMLogic::GetWidgetEventTranslationSingleton(vtkMRMLScene* scene, const std::string& singletonId) { if (!scene) @@ -56,6 +58,7 @@ vtkMRMLLayerDMWidgetEventTranslationNode* vtkSlicerLayerDMLogic::GetWidgetEventT return vtkMRMLLayerDMWidgetEventTranslationNode::SafeDownCast(scene->GetNodeByID(singletonId)); } +//----------------------------------------------------------------------------- vtkMRMLLayerDMWidgetEventTranslationNode* vtkSlicerLayerDMLogic::GetWidgetEventTranslationSingleton( vtkMRMLScene* scene, const std::string& singletonId, @@ -70,6 +73,7 @@ vtkMRMLLayerDMWidgetEventTranslationNode* vtkSlicerLayerDMLogic::GetWidgetEventT return node; } +//----------------------------------------------------------------------------- vtkMRMLLayerDMWidgetEventTranslationNode* vtkSlicerLayerDMLogic::CreateWidgetEventTranslationSingleton(vtkMRMLScene* scene, const std::string& singletonId) { if (auto node = GetWidgetEventTranslationSingleton(scene, singletonId)) @@ -88,6 +92,7 @@ vtkMRMLLayerDMWidgetEventTranslationNode* vtkSlicerLayerDMLogic::CreateWidgetEve return vtkMRMLLayerDMWidgetEventTranslationNode::SafeDownCast(scene->AddNode(newNode)); } +//----------------------------------------------------------------------------- void vtkSlicerLayerDMLogic::CreateDefaultEventTranslation(vtkMRMLNode* node, const std::string& singletonId, const std::function& configureF) @@ -99,12 +104,20 @@ void vtkSlicerLayerDMLogic::CreateDefaultEventTranslation(vtkMRMLNode* node, SetWidgetEventTranslationNode(node, GetWidgetEventTranslationSingleton(node ? node->GetScene() : nullptr, singletonId, configureF)); } +//----------------------------------------------------------------------------- std::string vtkSlicerLayerDMLogic::GetEventTranslationRole() { return EventTranslationRole; } +//----------------------------------------------------------------------------- std::string vtkSlicerLayerDMLogic::GetDisplayRole() { return DisplayRole; } + +//----------------------------------------------------------------------------- +void vtkSlicerLayerDMLogic::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); +} diff --git a/LayerDM/Logic/vtkSlicerLayerDMLogic.h b/LayerDM/Logic/vtkSlicerLayerDMLogic.h index f9883c9..e5fcd58 100644 --- a/LayerDM/Logic/vtkSlicerLayerDMLogic.h +++ b/LayerDM/Logic/vtkSlicerLayerDMLogic.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkSlicerLayerDMLogic_h +#define __vtkSlicerLayerDMLogic_h #include "vtkSlicerLayerDMModuleLogicExport.h" @@ -16,6 +37,7 @@ class VTK_SLICER_LAYERDM_MODULE_LOGIC_EXPORT vtkSlicerLayerDMLogic : public vtkS public: static vtkSlicerLayerDMLogic* New(); vtkTypeMacro(vtkSlicerLayerDMLogic, vtkSlicerModuleLogic); + void PrintSelf(ostream& os, vtkIndent indent) override; vtkSlicerLayerDMLogic(const vtkSlicerLayerDMLogic&) = delete; void operator=(const vtkSlicerLayerDMLogic&) = delete; @@ -23,11 +45,6 @@ class VTK_SLICER_LAYERDM_MODULE_LOGIC_EXPORT vtkSlicerLayerDMLogic : public vtkS /// Registers the LayerDM MRML nodes in the scene void RegisterNodes() override; - static int GetVersionMajor(); - static int GetVersionMinor(); - static int GetVersionPatch(); - static std::string GetVersion(); - /// Attach a translation node ref to the input node static void SetWidgetEventTranslationNode(vtkMRMLNode* node, vtkMRMLLayerDMWidgetEventTranslationNode* translationNode); @@ -272,3 +289,5 @@ T* vtkSlicerLayerDMLogic::SetNthReferenceNode(vtkMRMLNode* node, T* nodeRef, con { return ModifyNodeReference(node, nodeRef, [=] { node->SetAndObserveNthNodeReferenceID(role, nthRef, nodeRef->GetID()); }); } + +#endif diff --git a/LayerDM/Logic/vtkSlicerLayerDMModuleLogicExport.h b/LayerDM/Logic/vtkSlicerLayerDMModuleLogicExport.h index 8f53192..d4292d8 100644 --- a/LayerDM/Logic/vtkSlicerLayerDMModuleLogicExport.h +++ b/LayerDM/Logic/vtkSlicerLayerDMModuleLogicExport.h @@ -1,3 +1,29 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +/// Provides the Slicer conventional export header name for this library. +/// The Slicer core build generates its own equivalent in the build tree; this file is what +/// supplies the name in the standalone build, where only the module header is generated. +#ifndef __vtkSlicerLayerDMModuleLogicExport_h +#define __vtkSlicerLayerDMModuleLogicExport_h #include "vtkSlicerLayerDMModuleLogicModule.h" + +#endif diff --git a/LayerDM/Logic/vtkSlicerLayerDMVersion.h.in b/LayerDM/Logic/vtkSlicerLayerDMVersion.h.in deleted file mode 100644 index a090244..0000000 --- a/LayerDM/Logic/vtkSlicerLayerDMVersion.h.in +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include - -static constexpr int vtkSlicerLayerDM_VERSION_MAJOR { @LayerDM_VERSION_MAJOR@ }; -static constexpr int vtkSlicerLayerDM_VERSION_MINOR { @LayerDM_VERSION_MINOR@ }; -static constexpr int vtkSlicerLayerDM_VERSION_PATCH { @LayerDM_VERSION_PATCH@ }; -static const std::string vtkSlicerLayerDM_VERSION { "@LayerDM_VERSION@" }; diff --git a/LayerDM/Logic/wheel.cmake b/LayerDM/Logic/wheel.cmake index f845da4..bd3166b 100644 --- a/LayerDM/Logic/wheel.cmake +++ b/LayerDM/Logic/wheel.cmake @@ -1,21 +1,8 @@ -set(LayerDM_VERSION "${SKBUILD_PROJECT_VERSION}") -string(REPLACE "." ";" version_components "${LayerDM_VERSION}") # this creates a list -list(GET version_components 0 LayerDM_VERSION_MAJOR) -list(GET version_components 1 LayerDM_VERSION_MINOR) -list(GET version_components 2 LayerDM_VERSION_PATCH) - -configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/vtkSlicerLayerDMVersion.h.in" - "${CMAKE_CURRENT_BINARY_DIR}/vtkSlicerLayerDMVersion.h" - @ONLY -) - set(classes vtkSlicerLayerDMLogic ) set(headers - "${CMAKE_CURRENT_BINARY_DIR}/vtkSlicerLayerDMVersion.h" vtkSlicerLayerDMModuleLogicExport.h ) diff --git a/LayerDM/MRML/vtkMRMLLayerDMNodeReferenceObserver.cxx b/LayerDM/MRML/vtkMRMLLayerDMNodeReferenceObserver.cxx index 4cf98d7..af638f7 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMNodeReferenceObserver.cxx +++ b/LayerDM/MRML/vtkMRMLLayerDMNodeReferenceObserver.cxx @@ -1,3 +1,23 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMNodeReferenceObserver.h" // LayerDM includes @@ -8,8 +28,12 @@ #include #include +// STD includes +#include + namespace { +//----------------------------------------------------------------------------- /// Simple class to expose the content of the vtkMRMLNodeReference ToNode / Role. /// Not meant to be used in the scene nor exposed externally. class vtkMRMLNodeReferenceFacade : public vtkMRMLNode @@ -37,35 +61,54 @@ class vtkMRMLNodeReferenceFacade : public vtkMRMLNode static vtkMRMLNodeReference* CastCallData(void* callData) { return static_cast(callData); } }; +//----------------------------------------------------------------------------- vtkMRMLNodeNewMacro(vtkMRMLNodeReferenceFacade); } // namespace +namespace +{ +//----------------------------------------------------------------------------- +/// Add the reference to the list if it is not already there. +/// The reference lists are unordered, so uniqueness is not provided by the container itself. +void InsertUniqueRef(std::vector& refs, const vtkMRMLLayerDMNodeReferenceObserver::RefT& ref) +{ + if (std::find(refs.begin(), refs.end(), ref) == refs.end()) + { + refs.emplace_back(ref); + } +} +} // namespace + +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMNodeReferenceObserver); +//----------------------------------------------------------------------------- void vtkMRMLLayerDMNodeReferenceObserver::SetReferenceModifiedCallBack(const CallBackT& modifiedCallback) { - m_onRefModified = modifiedCallback; + this->ReferenceModifiedCallback = modifiedCallback; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMNodeReferenceObserver::SetScene(vtkMRMLScene* scene) { - if (m_scene == scene) + if (this->Scene == scene) { return; } - this->m_obs->UpdateObserver(m_scene, scene, { vtkMRMLScene::NodeAddedEvent, vtkMRMLScene::NodeRemovedEvent }); - this->m_scene = scene; + this->Observer->UpdateObservation(this->Scene, scene, { vtkMRMLScene::NodeAddedEvent, vtkMRMLScene::NodeRemovedEvent }); + this->Scene = scene; this->UpdateFromScene(); } +//----------------------------------------------------------------------------- vtkMRMLLayerDMNodeReferenceObserver::vtkMRMLLayerDMNodeReferenceObserver() - : m_obs(vtkSmartPointer::New()) + : Observer(vtkSmartPointer::New()) { - m_obs->SetUpdateCallback( + this->Observer->SetUpdateCallback( [this](vtkObject* obj, unsigned long eventId, void* callData) { - if (obj == this->m_scene) + if (obj == this->Scene) { switch (eventId) { @@ -94,18 +137,30 @@ vtkMRMLLayerDMNodeReferenceObserver::vtkMRMLLayerDMNodeReferenceObserver() }); } -inline std::set> GetSceneNodes(vtkMRMLScene* scene) +//----------------------------------------------------------------------------- +vtkMRMLLayerDMNodeReferenceObserver::~vtkMRMLLayerDMNodeReferenceObserver() +{ + this->Observer->ClearCallback(); +} + +namespace +{ +//----------------------------------------------------------------------------- +std::set> GetSceneNodes(vtkMRMLScene* scene) { if (!scene) { return {}; } + // Traverse the collection with an iterator: vtkCollection::GetItemAsObject walks the collection from its + // first item on every call, which makes an indexed scan quadratic in the number of nodes. std::set> nodes; - for (int iNode = 0; iNode < scene->GetNumberOfNodes(); iNode++) + vtkObject* item = nullptr; + vtkCollectionSimpleIterator it; + for (scene->GetNodes()->InitTraversal(it); (item = scene->GetNodes()->GetNextItemAsObject(it));) { - auto node = vtkMRMLNode::SafeDownCast(scene->GetNodes()->GetItemAsObject(iNode)); - if (node) + if (auto node = vtkMRMLNode::SafeDownCast(item)) { nodes.insert(node); } @@ -113,7 +168,8 @@ inline std::set> GetSceneNodes(vtkMRMLScene* scene) return nodes; } -inline std::tuple>, std::vector>> GetNodesRemovedAddedFromScene( +//----------------------------------------------------------------------------- +std::tuple>, std::vector>> GetNodesRemovedAddedFromScene( vtkMRMLScene* scene, const std::set>& currentNodes) { @@ -138,10 +194,12 @@ inline std::tuple>, std::vectorm_scene, this->m_nodes); + auto [nodesRemoved, nodesAdded] = GetNodesRemovedAddedFromScene(this->Scene, this->Nodes); for (const auto& node : nodesRemoved) { this->OnNodeRemoved(node); @@ -152,9 +210,10 @@ void vtkMRMLLayerDMNodeReferenceObserver::UpdateFromScene() } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMNodeReferenceObserver::OnNodeRemoved(vtkMRMLNode* node) { - auto eraseKeyInMap = [&](std::map, std::set>& map, vtkMRMLNode* keyNode) + auto eraseKeyInMap = [&](std::map, std::vector>& map, vtkMRMLNode* keyNode) { if (map.find(keyNode) == map.end()) { @@ -170,109 +229,121 @@ void vtkMRMLLayerDMNodeReferenceObserver::OnNodeRemoved(vtkMRMLNode* node) } // Remove any observer on the node - m_obs->RemoveObserver(node); + this->Observer->RemoveObservations(node); // Erase the node from the different maps to avoid any dangling pointers - m_nodes.erase(node); - eraseKeyInMap(m_refFrom, node); - eraseKeyInMap(m_refTo, node); + this->Nodes.erase(node); + eraseKeyInMap(this->NodeFromReferences, node); + eraseKeyInMap(this->NodeToReferences, node); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMNodeReferenceObserver::OnNodeAdded(vtkMRMLNode* node) { - m_nodes.insert(node); - m_obs->UpdateObserver(nullptr, node, { vtkMRMLNode::ReferenceAddedEvent, vtkMRMLNode::ReferenceModifiedEvent, vtkMRMLNode::ReferenceRemovedEvent }); + this->Nodes.insert(node); + this->Observer->UpdateObservation(nullptr, node, { vtkMRMLNode::ReferenceAddedEvent, vtkMRMLNode::ReferenceModifiedEvent, vtkMRMLNode::ReferenceRemovedEvent }); for (const auto& [toNode, role] : GetNodeReferencesFromScene(node)) { this->OnReferenceAdded(node, toNode, role); } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMNodeReferenceObserver::OnReferenceAdded(vtkMRMLNode* fromNode, vtkMRMLNode* toNode, const std::string& role) { - m_refTo[fromNode].insert({ toNode, role }); - m_refFrom[toNode].insert({ fromNode, role }); + InsertUniqueRef(this->NodeToReferences[fromNode], { toNode, role }); + InsertUniqueRef(this->NodeFromReferences[toNode], { fromNode, role }); this->TriggerReferenceAdded(fromNode, toNode, role); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMNodeReferenceObserver::OnReferenceRemoved(vtkMRMLNode* fromNode, vtkMRMLNode* toNode, const std::string& role) { - auto eraseRefInMap = [&](std::map, std::set>& map, vtkMRMLNode* keyNode, vtkMRMLNode* valueNode) + auto eraseRefInMap = [&](std::map, std::vector>& map, vtkMRMLNode* keyNode, vtkMRMLNode* valueNode) { if (map.find(keyNode) == map.end()) { return; } - map[keyNode].erase({ valueNode, role }); + auto& refs = map[keyNode]; + refs.erase(std::remove(refs.begin(), refs.end(), RefT{ valueNode, role }), refs.end()); if (map[keyNode].empty()) { map.erase(keyNode); } }; - eraseRefInMap(m_refTo, fromNode, toNode); - eraseRefInMap(m_refFrom, toNode, fromNode); + eraseRefInMap(this->NodeToReferences, fromNode, toNode); + eraseRefInMap(this->NodeFromReferences, toNode, fromNode); this->TriggerReferenceRemoved(fromNode, toNode, role); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMNodeReferenceObserver::RemoveOutdatedReferences(vtkMRMLNode* fromNode) { auto sceneRefs = GetNodeReferencesFromScene(fromNode); - for (const auto& ref : GetNodeToReferences(fromNode)) + for (const auto& ref : this->GetNodeToReferences(fromNode)) { - if (sceneRefs.find(ref) == sceneRefs.end()) + if (std::find(sceneRefs.begin(), sceneRefs.end(), ref) == sceneRefs.end()) { this->OnReferenceRemoved(fromNode, std::get<0>(ref), std::get<1>(ref)); } } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMNodeReferenceObserver::OnReferenceModified(vtkMRMLNode* fromNode, vtkMRMLNode* toNode, const std::string& role) { this->RemoveOutdatedReferences(fromNode); this->OnReferenceAdded(fromNode, toNode, role); } -std::set vtkMRMLLayerDMNodeReferenceObserver::GetNodeToReferences(vtkMRMLNode* node) const +//----------------------------------------------------------------------------- +std::vector vtkMRMLLayerDMNodeReferenceObserver::GetNodeToReferences(vtkMRMLNode* node) const { - if (const auto it = m_refTo.find(node); it != m_refTo.end()) + if (const auto it = this->NodeToReferences.find(node); it != this->NodeToReferences.end()) { return it->second; } return {}; } -std::set vtkMRMLLayerDMNodeReferenceObserver::GetNodeFromReferences(vtkMRMLNode* node) const +//----------------------------------------------------------------------------- +std::vector vtkMRMLLayerDMNodeReferenceObserver::GetNodeFromReferences(vtkMRMLNode* node) const { - if (const auto it = m_refFrom.find(node); it != m_refFrom.end()) + if (const auto it = this->NodeFromReferences.find(node); it != this->NodeFromReferences.end()) { return it->second; } return {}; } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMNodeReferenceObserver::GetReferenceToSize() const { - return static_cast(m_refTo.size()); + return static_cast(this->NodeToReferences.size()); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMNodeReferenceObserver::GetReferenceFromSize() const { - return static_cast(m_refFrom.size()); + return static_cast(this->NodeFromReferences.size()); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMNodeReferenceObserver::GetNumberOfNodes() const { - return static_cast(m_nodes.size()); + return static_cast(this->Nodes.size()); } -std::set vtkMRMLLayerDMNodeReferenceObserver::GetNodeReferencesFromScene(vtkMRMLNode* node) +//----------------------------------------------------------------------------- +std::vector vtkMRMLLayerDMNodeReferenceObserver::GetNodeReferencesFromScene(vtkMRMLNode* node) { if (!node) { return {}; } - std::set references; + std::vector references; std::vector roles; node->GetNodeReferenceRoles(roles); for (const auto& role : roles) @@ -280,22 +351,25 @@ std::set vtkMRMLLayerDMNodeReferenceO for (int iNode = 0; iNode < node->GetNumberOfNodeReferences(role.c_str()); iNode++) { auto toNode = node->GetNthNodeReference(role.c_str(), iNode); - references.insert({ toNode, role }); + InsertUniqueRef(references, { toNode, role }); } } return references; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMNodeReferenceObserver::TriggerReferenceAdded(vtkMRMLNode* fromNode, vtkMRMLNode* toNode, const std::string& role) const { - TriggerCallback(m_onRefModified, fromNode, toNode, role, ReferenceAddedEvent); + TriggerCallback(this->ReferenceModifiedCallback, fromNode, toNode, role, ReferenceAddedEvent); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMNodeReferenceObserver::TriggerReferenceRemoved(vtkMRMLNode* fromNode, vtkMRMLNode* toNode, const std::string& role) const { - TriggerCallback(m_onRefModified, fromNode, toNode, role, ReferenceRemovedEvent); + TriggerCallback(this->ReferenceModifiedCallback, fromNode, toNode, role, ReferenceRemovedEvent); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMNodeReferenceObserver::TriggerCallback(const CallBackT& callback, vtkMRMLNode* fromNode, vtkMRMLNode* toNode, const std::string& role, int eventType) { if (!callback || !fromNode || !toNode) @@ -304,3 +378,14 @@ void vtkMRMLLayerDMNodeReferenceObserver::TriggerCallback(const CallBackT& callb } callback(fromNode, toNode, role, eventType); } + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMNodeReferenceObserver::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Scene: " << (this->Scene ? "set" : "(none)") << std::endl; + os << indent << "Number of observed nodes: " << this->Nodes.size() << std::endl; + os << indent << "Number of nodes with outgoing references: " << this->NodeToReferences.size() << std::endl; + os << indent << "Number of nodes with incoming references: " << this->NodeFromReferences.size() << std::endl; + os << indent << "Reference modified callback: " << (this->ReferenceModifiedCallback ? "set" : "(none)") << std::endl; +} diff --git a/LayerDM/MRML/vtkMRMLLayerDMNodeReferenceObserver.h b/LayerDM/MRML/vtkMRMLLayerDMNodeReferenceObserver.h index d0e0c4f..98c198d 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMNodeReferenceObserver.h +++ b/LayerDM/MRML/vtkMRMLLayerDMNodeReferenceObserver.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMNodeReferenceObserver_h +#define __vtkMRMLLayerDMNodeReferenceObserver_h // LayerDM includes #include "vtkSlicerLayerDMModuleMRMLExport.h" @@ -12,6 +33,7 @@ #include #include #include +#include class vtkMRMLNode; class vtkMRMLLayerDMObjectEventObserver; @@ -23,6 +45,17 @@ class vtkMRMLScene; class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMNodeReferenceObserver : public vtkObject { public: + /// Reference to a node with a given role. + /// + /// The node is held by a weak pointer, unlike the other LayerDM containers, because these lists can outlive + /// the node: OnNodeRemoved drops the owning key of the map holding the opposite direction of the reference + /// without notifying the nodes referencing it, so their lists keep the reference until they are updated. + /// Reading such an entry must therefore check the node before using it. The lists are unordered for the same + /// reason: a weak pointer nulls itself in place, which would silently change the value of an element of an + /// ordered container and break its ordering. + /// + /// \sa NodeToReferences + /// \sa NodeFromReferences using RefT = std::tuple, std::string>; enum Event @@ -32,11 +65,13 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMNodeReferenceObserver }; static vtkMRMLLayerDMNodeReferenceObserver* New(); + vtkTypeMacro(vtkMRMLLayerDMNodeReferenceObserver, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; /// @{ /// Get references to / from node - std::set GetNodeToReferences(vtkMRMLNode* node) const; - std::set GetNodeFromReferences(vtkMRMLNode* node) const; + std::vector GetNodeToReferences(vtkMRMLNode* node) const; + std::vector GetNodeFromReferences(vtkMRMLNode* node) const; /// @} /// @{ @@ -49,8 +84,8 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMNodeReferenceObserver /// @} /// @{ - /// Set the callback triggerred when a reference from a node to another node with a given role is added / removed. - /// If the callbacks are defined before the scene is set to the object, the callbacks will be triggerred for existing nodes in the scene. + /// Set the callback triggered when a reference from a node to another node with a given role is added / removed. + /// If the callbacks are defined before the scene is set to the object, the callbacks will be triggered for existing nodes in the scene. using CallBackT = std::function; void SetReferenceModifiedCallBack(const CallBackT& modifiedCallback); /// @} @@ -60,7 +95,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMNodeReferenceObserver protected: vtkMRMLLayerDMNodeReferenceObserver(); - ~vtkMRMLLayerDMNodeReferenceObserver() override = default; + ~vtkMRMLLayerDMNodeReferenceObserver() override; private: vtkMRMLLayerDMNodeReferenceObserver(const vtkMRMLLayerDMNodeReferenceObserver&); @@ -74,16 +109,28 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMNodeReferenceObserver void RemoveOutdatedReferences(vtkMRMLNode* fromNode); void OnReferenceModified(vtkMRMLNode* fromNode, vtkMRMLNode* toNode, const std::string& role); - static std::set GetNodeReferencesFromScene(vtkMRMLNode* node); + static std::vector GetNodeReferencesFromScene(vtkMRMLNode* node); void TriggerReferenceAdded(vtkMRMLNode* fromNode, vtkMRMLNode* toNode, const std::string& role) const; void TriggerReferenceRemoved(vtkMRMLNode* fromNode, vtkMRMLNode* toNode, const std::string& role) const; static void TriggerCallback(const CallBackT& callback, vtkMRMLNode* fromNode, vtkMRMLNode* toNode, const std::string& role, int eventType); - vtkWeakPointer m_scene{}; - vtkSmartPointer m_obs{}; - std::map, std::set> m_refTo{}; - std::map, std::set> m_refFrom{}; - std::set> m_nodes{}; + vtkWeakPointer Scene; + vtkSmartPointer Observer; + + /// @{ + /// References from and to each observed node. + /// + /// The keys own their node, which keeps every node referenced by a RefT of the opposite map alive. + /// + /// \sa RefT + /// \sa OnNodeRemoved + std::map, std::vector> NodeToReferences; + std::map, std::vector> NodeFromReferences; + /// @} + + std::set> Nodes; - CallBackT m_onRefModified{}; + CallBackT ReferenceModifiedCallback; }; + +#endif diff --git a/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserver.cxx b/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserver.cxx index d2effc8..28bae6a 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserver.cxx +++ b/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserver.cxx @@ -1,11 +1,35 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMObjectEventObserver.h" // VTK includes #include #include +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMObjectEventObserver); +namespace +{ +//----------------------------------------------------------------------------- template struct Overloaded : Ts... { @@ -14,145 +38,213 @@ struct Overloaded : Ts... template Overloaded(Ts...) -> Overloaded; +} // namespace +//----------------------------------------------------------------------------- vtkMRMLLayerDMObjectEventObserver::vtkMRMLLayerDMObjectEventObserver() - : m_updateCommand(vtkSmartPointer::New()) - , m_isBlocked(false) + : UpdateCommand(vtkSmartPointer::New()) + , Blocked(false) { - this->m_updateCommand->SetClientData(this); - this->m_updateCommand->SetCallback( + this->UpdateCommand->SetClientData(this); + this->UpdateCommand->SetCallback( [](vtkObject* caller, unsigned long eid, void* clientData, void* callData) { auto client = static_cast(clientData); - if (client->m_isBlocked) + + if (eid == vtkCommand::DeleteEvent) { + client->OnObjectDeleted(caller); return; } - try + if (client->Blocked) { - // Dispatch to callback depending on current std variant content - std::visit(Overloaded{ [&](const std::function& f) { f(caller); }, - [&](const std::function& f) { f(caller, eid); }, - [&](const std::function& f) { f(caller, eid, callData); } }, - client->m_callback); - } - catch (const std::bad_function_call&) - { - // Ignore unset function callbacks + return; } + + client->InvokeCallback(caller, eid, callData); }); } +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMObjectEventObserver::InvokeCallback(vtkObject* obj, unsigned long eventId, void* callData) +{ + try + { + // Dispatch to callback depending on current std variant content + std::visit(Overloaded{ [&](const std::function& f) { f(obj); }, + [&](const std::function& f) { f(obj, eventId); }, + [&](const std::function& f) { f(obj, eventId, callData); } }, + this->Callback); + } + catch (const std::bad_function_call&) + { + // Ignore unset function callbacks + } +} + +//----------------------------------------------------------------------------- vtkMRMLLayerDMObjectEventObserver::~vtkMRMLLayerDMObjectEventObserver() { - for (const auto& obs : m_obsMap) + for (const auto& [object, eventTags] : this->ObservedEventsMap) { - if (obs.first) + for (const auto& [event, tag] : eventTags) { - for (auto& event : obs.second) - { - obs.first->RemoveObserver(event); - } + object->RemoveObserver(tag); } } } -bool vtkMRMLLayerDMObjectEventObserver::UpdateObserver(vtkObject* prevObj, vtkObject* obj, unsigned long event) +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMObjectEventObserver::UpdateObservation(vtkObject* prevObj, vtkObject* obj, unsigned long event) { - return this->UpdateObserver(prevObj, obj, std::vector{ event }); + return this->UpdateObservation(prevObj, obj, std::vector{ event }); } -bool vtkMRMLLayerDMObjectEventObserver::UpdateObserver(vtkObject* prevObj, vtkObject* obj, const std::vector& events) +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMObjectEventObserver::UpdateObservation(vtkObject* prevObj, vtkObject* obj, const std::vector& events) { if (prevObj == obj) { return false; } - this->RemoveObserver(prevObj); + this->RemoveObservations(prevObj); for (const auto& event : events) { - this->AddObserver(obj, event); + this->AddObservation(obj, event); } return true; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMObjectEventObserver::SetUpdateCallback(const std::function& callback) { - this->m_callback = callback; + this->Callback = callback; } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMObjectEventObserver::SetBlocked(bool isBlocked) { - bool wasBlocked = this->m_isBlocked; - this->m_isBlocked = isBlocked; + bool wasBlocked = this->Blocked; + this->Blocked = isBlocked; return wasBlocked; } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMObjectEventObserver::IsBlocked() const { - return this->m_isBlocked; + return this->Blocked; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMObjectEventObserver::SetUpdateCallback(const std::function& callback) { - this->m_callback = callback; + this->Callback = callback; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMObjectEventObserver::SetUpdateCallback(const std::function& callback) { - this->m_callback = callback; + this->Callback = callback; +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMObjectEventObserver::ClearCallback() +{ + this->Callback = std::function{}; } -void vtkMRMLLayerDMObjectEventObserver::AddObserver(vtkObject* node, unsigned long event) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMObjectEventObserver::AddObservation(vtkObject* node, unsigned long event) { if (!node) { return; } - if (this->m_obsMap.find(node) == std::end(this->m_obsMap)) + auto& observedEvents = this->ObservedEventsMap[node]; + + // Always observe the object destruction, so that the object can be removed from the observed map while its + // address is still valid. + if (observedEvents.find(vtkCommand::DeleteEvent) == std::end(observedEvents)) + { + observedEvents[vtkCommand::DeleteEvent] = node->AddObserver(vtkCommand::DeleteEvent, this->UpdateCommand); + } + + if (event == vtkCommand::DeleteEvent) { - this->m_obsMap[node] = std::set{}; + this->DeleteEventObservers.insert(node); + return; } - if (this->m_obsMap[node].find(event) != std::end(this->m_obsMap[node])) + if (observedEvents.find(event) != std::end(observedEvents)) { return; } - this->m_obsMap[node].insert(node->AddObserver(event, this->m_updateCommand)); + observedEvents[event] = node->AddObserver(event, this->UpdateCommand); } -void vtkMRMLLayerDMObjectEventObserver::RemoveObserver(vtkObject* node) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMObjectEventObserver::OnObjectDeleted(vtkObject* node) { - if (!node || this->m_obsMap.find(node) == std::end(this->m_obsMap)) + if (!node) { return; } - for (auto& event : m_obsMap[node]) + const bool notifyOwner = this->DeleteEventObservers.erase(node) > 0; + this->ObservedEventsMap.erase(node); + if (notifyOwner) { - node->RemoveObserver(event); + this->InvokeCallback(node, vtkCommand::DeleteEvent, nullptr); } +} - this->m_obsMap.erase(node); +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMObjectEventObserver::RemoveObservations(vtkObject* node) +{ + if (!node) + { + return; + } + + this->DeleteEventObservers.erase(node); + + if (const auto found = this->ObservedEventsMap.find(node); found != std::end(this->ObservedEventsMap)) + { + for (const auto& [event, tag] : found->second) + { + node->RemoveObserver(tag); + } + this->ObservedEventsMap.erase(found); + } } +//----------------------------------------------------------------------------- vtkMRMLLayerDMObjectEventObserver::UpdateGuard::UpdateGuard(vtkMRMLLayerDMObjectEventObserver* obs) - : m_obs(obs) + : Observer(obs) { - if (m_obs) + if (this->Observer) { - m_wasBlocked = m_obs->SetBlocked(true); + this->WasBlocked = this->Observer->SetBlocked(true); } } +//----------------------------------------------------------------------------- vtkMRMLLayerDMObjectEventObserver::UpdateGuard::~UpdateGuard() { - if (m_obs) + if (this->Observer) { - m_obs->SetBlocked(m_wasBlocked); + this->Observer->SetBlocked(this->WasBlocked); } } + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMObjectEventObserver::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Blocked: " << (this->Blocked ? "true" : "false") << std::endl; + os << indent << "Number of observed objects: " << this->ObservedEventsMap.size() << std::endl; +} diff --git a/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserver.h b/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserver.h index 95b091e..5a2e49c 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserver.h +++ b/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserver.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMObjectEventObserver_h +#define __vtkMRMLLayerDMObjectEventObserver_h // LayerDM includes #include "vtkSlicerLayerDMModuleMRMLExport.h" @@ -28,6 +49,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMObjectEventObserver : struct UpdateGuard; static vtkMRMLLayerDMObjectEventObserver* New(); vtkTypeMacro(vtkMRMLLayerDMObjectEventObserver, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; /// @{ /// Remove previous monitored events from \param prevObj and observe events from the \param obj @@ -35,13 +57,13 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMObjectEventObserver : /// On event triggered, calls the update set by \sa SetUpdateCallback. /// /// \warning prevObj is not mutated by this call. To update the pointer, a manual set is required after update. - bool UpdateObserver(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent); - bool UpdateObserver(vtkObject* prevObj, vtkObject* obj, const std::vector& events); + bool UpdateObservation(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent); + bool UpdateObservation(vtkObject* prevObj, vtkObject* obj, const std::vector& events); /// @} /// Remove observers attached to the input object. - /// Use \sa UpdateObserver to update the observed events for a new object (RemoveObserver is then called automatically). - void RemoveObserver(vtkObject* obj); + /// Use \sa UpdateObservation to update the observed events for a new object (RemoveObservations is then called automatically). + void RemoveObservations(vtkObject* obj); /// @{ /// Set the callback triggered when one of the observed objects and event is invoked. @@ -50,6 +72,9 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMObjectEventObserver : void SetUpdateCallback(const std::function& callback); /// @} + /// Forget the update callback. + void ClearCallback(); + /// Set update callback blocked. /// @return previous blocked state. bool SetBlocked(bool isBlocked); @@ -63,8 +88,8 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMObjectEventObserver : ~UpdateGuard(); private: - vtkMRMLLayerDMObjectEventObserver* m_obs; - bool m_wasBlocked{}; + vtkMRMLLayerDMObjectEventObserver* Observer; + bool WasBlocked{}; }; protected: @@ -72,14 +97,23 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMObjectEventObserver : ~vtkMRMLLayerDMObjectEventObserver() override; private: - void AddObserver(vtkObject* obj, unsigned long event); + void AddObservation(vtkObject* obj, unsigned long event); - vtkSmartPointer m_updateCommand{}; - std::map, std::set> m_obsMap{}; + /// Dispatch an observed event to the update callback. + void InvokeCallback(vtkObject* obj, unsigned long eventId, void* callData); + /// Called when an observed object invokes vtkCommand::DeleteEvent. + /// Forgets the object, then forwards the event to the update callback if the object was observed for it. + void OnObjectDeleted(vtkObject* obj); + + vtkSmartPointer UpdateCommand; + std::map> ObservedEventsMap; + std::set DeleteEventObservers; std::variant, std::function, std::function> - m_callback; - bool m_isBlocked; + Callback; + bool Blocked; }; + +#endif diff --git a/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserverScripted.cxx b/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserverScripted.cxx index 55a49fd..e01d39c 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserverScripted.cxx +++ b/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserverScripted.cxx @@ -1,12 +1,34 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMObjectEventObserverScripted.h" #include "vtkMRMLLayerDMPythonUtil.h" #include +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMObjectEventObserverScripted); +//----------------------------------------------------------------------------- vtkMRMLLayerDMObjectEventObserverScripted::vtkMRMLLayerDMObjectEventObserverScripted() - : m_object(nullptr) + : Object(nullptr) { this->SetUpdateCallback( [this](vtkObject* node, unsigned long eventId, void* callData) -> void @@ -17,29 +39,39 @@ vtkMRMLLayerDMObjectEventObserverScripted::vtkMRMLLayerDMObjectEventObserverScri } vtkPythonScopeGilEnsurer gilEnsurer; - if (auto result = vtkMRMLLayerDMPythonUtil::CallPythonObject(this->m_object, vtkMRMLLayerDMPythonUtil::ToPyArgs(node, eventId, callData))) + if (auto result = vtkMRMLLayerDMPythonUtil::CallPythonObject(this->Object, vtkMRMLLayerDMPythonUtil::ToPyArgs(node, eventId, callData))) { Py_DECREF(result); } else { - auto errorMsg = std::string(__func__) + ": Failed to call : " + vtkMRMLLayerDMPythonUtil::GetObjectStr(this->m_object) + ":"; + auto errorMsg = std::string(__func__) + ": Failed to call : " + vtkMRMLLayerDMPythonUtil::GetObjectStr(this->Object) + ":"; vtkMRMLLayerDMPythonUtil::PrintErrorTraceback(this, errorMsg); } }); } +//----------------------------------------------------------------------------- vtkMRMLLayerDMObjectEventObserverScripted::~vtkMRMLLayerDMObjectEventObserverScripted() { - vtkMRMLLayerDMPythonUtil::DeletePythonObject(&this->m_object); + vtkMRMLLayerDMPythonUtil::DeletePythonObject(&this->Object); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMObjectEventObserverScripted::SetPythonCallback(PyObject* object) { - vtkMRMLLayerDMPythonUtil::SetPythonObject(&this->m_object, object); + vtkMRMLLayerDMPythonUtil::SetPythonObject(&this->Object, object); } +//----------------------------------------------------------------------------- PyObject* vtkMRMLLayerDMObjectEventObserverScripted::CastCallData(PyObject* object, int vtkType) { return vtkMRMLLayerDMPythonUtil::CastCallData(object, vtkType); } + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMObjectEventObserverScripted::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Python object: " << (this->Object ? "set" : "(none)") << std::endl; +} diff --git a/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserverScripted.h b/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserverScripted.h index d4eb088..e79983b 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserverScripted.h +++ b/LayerDM/MRML/vtkMRMLLayerDMObjectEventObserverScripted.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMObjectEventObserverScripted_h +#define __vtkMRMLLayerDMObjectEventObserverScripted_h // LayerDM includes #include "vtkMRMLLayerDMObjectEventObserver.h" @@ -17,6 +38,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMObjectEventObserverScr public: static vtkMRMLLayerDMObjectEventObserverScripted* New(); vtkTypeMacro(vtkMRMLLayerDMObjectEventObserverScripted, vtkMRMLLayerDMObjectEventObserver); + void PrintSelf(ostream& os, vtkIndent indent) override; /// \brief Sets the Python callable object to be invoked on events. /// @@ -42,5 +64,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMObjectEventObserverScr ~vtkMRMLLayerDMObjectEventObserverScripted() override; private: - PyObject* m_object; + PyObject* Object; }; + +#endif diff --git a/LayerDM/MRML/vtkMRMLLayerDMPythonUtil.cxx b/LayerDM/MRML/vtkMRMLLayerDMPythonUtil.cxx index ad2c7d6..a4aca26 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMPythonUtil.cxx +++ b/LayerDM/MRML/vtkMRMLLayerDMPythonUtil.cxx @@ -1,28 +1,53 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMPythonUtil.h" #include +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMPythonUtil); vtkMRMLLayerDMPythonUtil::vtkMRMLLayerDMPythonUtil() = default; vtkMRMLLayerDMPythonUtil::~vtkMRMLLayerDMPythonUtil() = default; +//----------------------------------------------------------------------------- PyObject* vtkMRMLLayerDMPythonUtil::ToPyObject(vtkObjectBase* obj) { return vtkPythonUtil::GetObjectFromPointer(obj); } +//----------------------------------------------------------------------------- PyObject* vtkMRMLLayerDMPythonUtil::ToPyObject(unsigned long value) { return PyLong_FromUnsignedLong(value); } +//----------------------------------------------------------------------------- PyObject* vtkMRMLLayerDMPythonUtil::ToPyObject(const std::string& value) { return PyUnicode_FromString(value.c_str()); } +//----------------------------------------------------------------------------- PyObject* vtkMRMLLayerDMPythonUtil::RawPtrToPython(void* ptr) { if (ptr) @@ -30,11 +55,12 @@ PyObject* vtkMRMLLayerDMPythonUtil::RawPtrToPython(void* ptr) return PyCapsule_New(ptr, nullptr, nullptr); } - // Return borrowed reference to Py_None + // Return a new reference to Py_None, as the callers take ownership of the returned object Py_INCREF(Py_None); return Py_None; } +//----------------------------------------------------------------------------- vtkSmartPyObject vtkMRMLLayerDMPythonUtil::ToPyArgs(const std::vector& pyObjs) { vtkPythonScopeGilEnsurer gilEnsurer; @@ -53,18 +79,21 @@ vtkSmartPyObject vtkMRMLLayerDMPythonUtil::ToPyArgs(const std::vector return { pyTuple }; } +//----------------------------------------------------------------------------- vtkSmartPyObject vtkMRMLLayerDMPythonUtil::ToPyArgs(vtkObjectBase* obj) { vtkPythonScopeGilEnsurer gilEnsurer; return ToPyArgs({ ToPyObject(obj) }); } +//----------------------------------------------------------------------------- vtkSmartPyObject vtkMRMLLayerDMPythonUtil::ToPyArgs(vtkObject* obj, unsigned long eventId, void* callData) { vtkPythonScopeGilEnsurer gilEnsurer; return ToPyArgs({ ToPyObject(obj), ToPyObject(eventId), RawPtrToPython(callData) }); } +//----------------------------------------------------------------------------- PyObject* vtkMRMLLayerDMPythonUtil::CastCallData(PyObject* object, int vtkType) { vtkPythonScopeGilEnsurer gilEnsurer; @@ -130,6 +159,7 @@ PyObject* vtkMRMLLayerDMPythonUtil::CastCallData(PyObject* object, int vtkType) } } +//----------------------------------------------------------------------------- PyObject* vtkMRMLLayerDMPythonUtil::CallPythonMethod(PyObject* object, const vtkSmartPyObject& pyArgs, const std::string& fName) { if (!IsValidPythonContext() || !object) @@ -138,6 +168,8 @@ PyObject* vtkMRMLLayerDMPythonUtil::CallPythonMethod(PyObject* object, const vtk } vtkPythonScopeGilEnsurer gilEnsurer; + + // PyObject_GetAttrString returns a new reference which needs to be released on every path. PyObject* method = PyObject_GetAttrString(object, fName.c_str()); if (!method) { @@ -150,12 +182,16 @@ PyObject* vtkMRMLLayerDMPythonUtil::CallPythonMethod(PyObject* object, const vtk // PyCallable_Check doesn't raise any errors. Raise called attribute isn't callable. const auto errorString = std::string("vtkMRMLLayerDMPythonUtil::") + __func__ + ": Attribute is not callable : '" + fName + "' of object : " + GetObjectStr(object); PyErr_SetString(PyExc_TypeError, errorString.c_str()); + Py_DECREF(method); return nullptr; } - return CallPythonObject(method, pyArgs); + PyObject* result = CallPythonObject(method, pyArgs); + Py_DECREF(method); + return result; } +//----------------------------------------------------------------------------- PyObject* vtkMRMLLayerDMPythonUtil::CallPythonObject(PyObject* object, const vtkSmartPyObject& pyArgs) { if (!IsValidPythonContext() || !object) @@ -175,6 +211,7 @@ PyObject* vtkMRMLLayerDMPythonUtil::CallPythonObject(PyObject* object, const vtk return PyObject_CallObject(object, pyArgs); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPythonUtil::SetPythonObject(PyObject** destObject, PyObject* object) { if (!IsValidPythonContext()) @@ -193,6 +230,7 @@ void vtkMRMLLayerDMPythonUtil::SetPythonObject(PyObject** destObject, PyObject* Py_XINCREF(*destObject); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPythonUtil::DeletePythonObject(PyObject** destObject) { if (!Py_IsInitialized()) @@ -205,6 +243,7 @@ void vtkMRMLLayerDMPythonUtil::DeletePythonObject(PyObject** destObject) *destObject = nullptr; } +//----------------------------------------------------------------------------- std::string vtkMRMLLayerDMPythonUtil::GetObjectStr(PyObject* object) { if (!Py_IsInitialized()) @@ -217,6 +256,9 @@ std::string vtkMRMLLayerDMPythonUtil::GetObjectStr(PyObject* object) return "None"; } + // This is a public static method which may be called without the GIL held. + vtkPythonScopeGilEnsurer gilEnsurer; + // Save current errors to avoid changing the current python error stack if any PyObject *type, *value, *traceback; PyErr_Fetch(&type, &value, &traceback); @@ -224,15 +266,22 @@ std::string vtkMRMLLayerDMPythonUtil::GetObjectStr(PyObject* object) std::string objectString{ "INVALID_OBJECT_STR" }; if (auto strObj = PyObject_Str(object)) { - objectString = PyUnicode_AsUTF8(strObj); + if (const char* strValue = PyUnicode_AsUTF8(strObj)) + { + objectString = strValue; + } Py_DECREF(strObj); } + // PyObject_Str or the conversion above may have raised; the fetched error is restored below. + PyErr_Clear(); + // Restore the python error stack PyErr_Restore(type, value, traceback); return objectString; } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMPythonUtil::IsValidPythonContext() { if (!Py_IsInitialized()) @@ -244,6 +293,7 @@ bool vtkMRMLLayerDMPythonUtil::IsValidPythonContext() return !PyErr_Occurred(); } +//----------------------------------------------------------------------------- std::string vtkMRMLLayerDMPythonUtil::FormatExceptionTraceback() { // Don't use IsValidPythonContext here as it checks if no error has occurred @@ -264,13 +314,26 @@ std::string vtkMRMLLayerDMPythonUtil::FormatExceptionTraceback() PyErr_Fetch(&type, &value, &traceback); PyErr_NormalizeException(&type, &value, &traceback); + // Exceptions raised from C++ have no traceback, and PyTuple_Pack cannot pack a null object. + // Every step below may also fail, in which case the traceback cannot be formatted at all. PyObject* tracebackModule = PyImport_ImportModule("traceback"); - PyObject* formatExceptionFunc = PyObject_GetAttrString(tracebackModule, "format_exception"); - PyObject* args = PyTuple_Pack(3, type, value, traceback); - PyObject* formattedList = PyObject_CallObject(formatExceptionFunc, args); - PyObject* emptyString = PyUnicode_FromString(""); - PyObject* formatted = PyUnicode_Join(emptyString, formattedList); - std::string exceptionTraceback = PyUnicode_AsUTF8(formatted); + PyObject* formatExceptionFunc = tracebackModule ? PyObject_GetAttrString(tracebackModule, "format_exception") : nullptr; + PyObject* args = formatExceptionFunc ? PyTuple_Pack(3, type, value, traceback ? traceback : Py_None) : nullptr; + PyObject* formattedList = args ? PyObject_CallObject(formatExceptionFunc, args) : nullptr; + PyObject* emptyString = formattedList ? PyUnicode_FromString("") : nullptr; + PyObject* formatted = emptyString ? PyUnicode_Join(emptyString, formattedList) : nullptr; + + std::string exceptionTraceback; + if (formatted) + { + if (const char* formattedStr = PyUnicode_AsUTF8(formatted)) + { + exceptionTraceback = formattedStr; + } + } + + // Discard any error raised while formatting so that the original error is the one restored. + PyErr_Clear(); // Cleanup PyErr_Restore(type, value, traceback); @@ -283,6 +346,7 @@ std::string vtkMRMLLayerDMPythonUtil::FormatExceptionTraceback() return exceptionTraceback; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPythonUtil::PrintErrorTraceback(const vtkObject* object, const std::string& errorMsg) { // If the traceback is not empty, print the traceback using vtkErrorMacro @@ -298,5 +362,11 @@ void vtkMRMLLayerDMPythonUtil::PrintErrorTraceback(const vtkObject* object, cons errorString += "\n"; } errorString += traceback; - vtkErrorWithObjectMacro(object, "" << traceback.c_str()); + vtkErrorWithObjectMacro(object, "" << errorString.c_str()); +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPythonUtil::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); } diff --git a/LayerDM/MRML/vtkMRMLLayerDMPythonUtil.h b/LayerDM/MRML/vtkMRMLLayerDMPythonUtil.h index 985eb6e..9a6f67f 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMPythonUtil.h +++ b/LayerDM/MRML/vtkMRMLLayerDMPythonUtil.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMPythonUtil_h +#define __vtkMRMLLayerDMPythonUtil_h #include "vtkSlicerLayerDMModuleMRMLExport.h" // VTK includes @@ -22,6 +43,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMPythonUtil : public vt public: static vtkMRMLLayerDMPythonUtil* New(); vtkTypeMacro(vtkMRMLLayerDMPythonUtil, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; /// \brief Convert a VTK object to a Python object /// \param obj VTK object base pointer to convert @@ -116,3 +138,5 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMPythonUtil : public vt vtkMRMLLayerDMPythonUtil(); ~vtkMRMLLayerDMPythonUtil() override; }; + +#endif diff --git a/LayerDM/MRML/vtkMRMLLayerDMSelectionObserver.cxx b/LayerDM/MRML/vtkMRMLLayerDMSelectionObserver.cxx index f338d9a..d7090c1 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMSelectionObserver.cxx +++ b/LayerDM/MRML/vtkMRMLLayerDMSelectionObserver.cxx @@ -1,3 +1,23 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + // LayerDM includes #include "vtkMRMLLayerDMObjectEventObserver.h" #include "vtkMRMLLayerDMSelectionObserver.h" @@ -11,15 +31,17 @@ // VTK includes #include +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMSelectionObserver); +//----------------------------------------------------------------------------- vtkMRMLLayerDMSelectionObserver::vtkMRMLLayerDMSelectionObserver() - : m_obs{ vtkSmartPointer::New() } + : Observer{ vtkSmartPointer::New() } { - m_obs->SetUpdateCallback( + this->Observer->SetUpdateCallback( [this](vtkObject* obj) { - if (obj == this->m_interactionNode || obj == this->m_selectionNode) + if (obj == this->InteractionNode || obj == this->SelectionNode) { this->Modified(); } @@ -28,11 +50,13 @@ vtkMRMLLayerDMSelectionObserver::vtkMRMLLayerDMSelectionObserver() vtkMRMLLayerDMSelectionObserver::~vtkMRMLLayerDMSelectionObserver() = default; +//----------------------------------------------------------------------------- void vtkMRMLLayerDMSelectionObserver::SetScene(vtkMRMLScene* scene) { this->UpdateNodesFromScene(scene); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMSelectionObserver::UpdateNodesFromScene(vtkMRMLScene* scene) { bool didModify{}; @@ -44,6 +68,7 @@ void vtkMRMLLayerDMSelectionObserver::UpdateNodesFromScene(vtkMRMLScene* scene) } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMSelectionObserver::UpdateNodesFromApplicationLogic(vtkMRMLApplicationLogic* logic) { bool didModify{}; @@ -55,33 +80,39 @@ void vtkMRMLLayerDMSelectionObserver::UpdateNodesFromApplicationLogic(vtkMRMLApp } } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMSelectionObserver::SetInteractionNode(vtkMRMLInteractionNode* interactionNode) { - const auto didModify = this->m_obs->UpdateObserver(m_interactionNode, interactionNode); - this->m_interactionNode = interactionNode; + const auto didModify = this->Observer->UpdateObservation(this->InteractionNode, interactionNode); + this->InteractionNode = interactionNode; return didModify; } +//----------------------------------------------------------------------------- vtkMRMLInteractionNode* vtkMRMLLayerDMSelectionObserver::GetInteractionNode() const { - return this->m_interactionNode; + return this->InteractionNode; } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMSelectionObserver::SetSelectionNode(vtkMRMLSelectionNode* selectionNode) { - const auto didModify = this->m_obs->UpdateObserver(m_selectionNode, selectionNode); - this->m_selectionNode = selectionNode; + const auto didModify = this->Observer->UpdateObservation(this->SelectionNode, selectionNode); + this->SelectionNode = selectionNode; return didModify; } +//----------------------------------------------------------------------------- vtkMRMLSelectionNode* vtkMRMLLayerDMSelectionObserver::GetSelectionNode() const { - return this->m_selectionNode; + return this->SelectionNode; } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMSelectionObserver::IsPlacing(vtkMRMLNode* node) const { - if (!node) + // A node which has not been added to a scene yet has no ID. + if (!node || !node->GetID()) { return false; } @@ -89,77 +120,93 @@ bool vtkMRMLLayerDMSelectionObserver::IsPlacing(vtkMRMLNode* node) const return this->IsPlacing() && (this->GetActivePlaceNodeID() == std::string(node->GetID())); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMSelectionObserver::IsPlacing() const { - if (!this->m_interactionNode) + if (!this->InteractionNode) { return false; } - return this->m_interactionNode->GetCurrentInteractionMode() == vtkMRMLInteractionNode::Place; + return this->InteractionNode->GetCurrentInteractionMode() == vtkMRMLInteractionNode::Place; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMSelectionObserver::StartPlace(vtkMRMLNode* node, bool isPersistent) { - if (!node || !this->m_interactionNode || !this->m_selectionNode) + if (!node || !this->InteractionNode || !this->SelectionNode) { return; } // Avoid triggering modified if the node is already in place mode - if (IsPlacing(node)) + if (this->IsPlacing(node)) { - this->m_interactionNode->SetPlaceModePersistence(isPersistent); + this->InteractionNode->SetPlaceModePersistence(isPersistent); return; } { - vtkMRMLLayerDMObjectEventObserver::UpdateGuard guard(m_obs); - this->m_selectionNode->SetActivePlaceNodeClassName(node->GetClassName()); - this->m_selectionNode->SetActivePlaceNodeID(node->GetID()); - this->m_interactionNode->SetCurrentInteractionMode(vtkMRMLInteractionNode::Place); - this->m_interactionNode->SetPlaceModePersistence(isPersistent); + vtkMRMLLayerDMObjectEventObserver::UpdateGuard guard(this->Observer); + this->SelectionNode->SetActivePlaceNodeClassName(node->GetClassName()); + this->SelectionNode->SetActivePlaceNodeID(node->GetID()); + this->InteractionNode->SetCurrentInteractionMode(vtkMRMLInteractionNode::Place); + this->InteractionNode->SetPlaceModePersistence(isPersistent); } this->Modified(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMSelectionObserver::StopPlace() const { this->SetInteractionMode(vtkMRMLInteractionNode::ViewTransform); } +//----------------------------------------------------------------------------- std::string vtkMRMLLayerDMSelectionObserver::GetActivePlaceNodeID() const { - if (!this->m_selectionNode || !this->m_selectionNode->GetActivePlaceNodeID()) + if (!this->SelectionNode || !this->SelectionNode->GetActivePlaceNodeID()) { return ""; } - return this->m_selectionNode->GetActivePlaceNodeID(); + return this->SelectionNode->GetActivePlaceNodeID(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMSelectionObserver::SetInteractionMode(int interactionMode) const { - if (!m_interactionNode) + if (!this->InteractionNode) { return; } - m_interactionNode->SetCurrentInteractionMode(interactionMode); + this->InteractionNode->SetCurrentInteractionMode(interactionMode); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMSelectionObserver::GetCurrentInteractionMode() const { - if (!this->m_interactionNode) + if (!this->InteractionNode) { return 0; } - return this->m_interactionNode->GetCurrentInteractionMode(); + return this->InteractionNode->GetCurrentInteractionMode(); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMSelectionObserver::GetPlaceModePersistence() const { - if (!m_interactionNode) + if (!this->InteractionNode) { return false; } - return m_interactionNode->GetPlaceModePersistence(); + return this->InteractionNode->GetPlaceModePersistence(); +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMSelectionObserver::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Scene: " << (this->Scene ? "set" : "(none)") << std::endl; + os << indent << "Interaction node: " << (this->InteractionNode ? "set" : "(none)") << std::endl; + os << indent << "Selection node: " << (this->SelectionNode ? "set" : "(none)") << std::endl; } diff --git a/LayerDM/MRML/vtkMRMLLayerDMSelectionObserver.h b/LayerDM/MRML/vtkMRMLLayerDMSelectionObserver.h index 0dcd844..bd77f32 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMSelectionObserver.h +++ b/LayerDM/MRML/vtkMRMLLayerDMSelectionObserver.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMSelectionObserver_h +#define __vtkMRMLLayerDMSelectionObserver_h // LayerDM includes #include "vtkSlicerLayerDMModuleMRMLExport.h" @@ -24,6 +45,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMSelectionObserver : pu public: static vtkMRMLLayerDMSelectionObserver* New(); vtkTypeMacro(vtkMRMLLayerDMSelectionObserver, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; /// @{ /// \brief Updates selection and interaction nodes from the input scene @@ -69,8 +91,10 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMSelectionObserver : pu void operator=(const vtkMRMLLayerDMSelectionObserver&) = delete; private: - vtkSmartPointer m_obs; - vtkWeakPointer m_scene; - vtkWeakPointer m_interactionNode; - vtkWeakPointer m_selectionNode; + vtkSmartPointer Observer; + vtkWeakPointer Scene; + vtkWeakPointer InteractionNode; + vtkWeakPointer SelectionNode; }; + +#endif diff --git a/LayerDM/MRML/vtkMRMLLayerDMWidgetEventTranslationNode.cxx b/LayerDM/MRML/vtkMRMLLayerDMWidgetEventTranslationNode.cxx index 09e70ee..efb8b0a 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMWidgetEventTranslationNode.cxx +++ b/LayerDM/MRML/vtkMRMLLayerDMWidgetEventTranslationNode.cxx @@ -1,3 +1,23 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMWidgetEventTranslationNode.h" // Slicer includes @@ -10,8 +30,10 @@ // STL includes #include +//----------------------------------------------------------------------------- vtkMRMLNodeNewMacro(vtkMRMLLayerDMWidgetEventTranslationNode); +//----------------------------------------------------------------------------- void vtkMRMLLayerDMWidgetEventTranslationNode::PrintSelf(ostream& os, vtkIndent indent) { Superclass::PrintSelf(os, indent); @@ -20,17 +42,22 @@ void vtkMRMLLayerDMWidgetEventTranslationNode::PrintSelf(ostream& os, vtkIndent os << ToString(this->EventMap, &nextIndent, "\n"); } -void vtkMRMLLayerDMWidgetEventTranslationNode::Copy(vtkMRMLNode* node) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMWidgetEventTranslationNode::CopyContent(vtkMRMLNode* anode, bool deepCopy /*=true*/) { - const auto other = vtkMRMLLayerDMWidgetEventTranslationNode::SafeDownCast(node); - if (!other) + MRMLNodeModifyBlocker blocker(this); + Superclass::CopyContent(anode, deepCopy); + + const auto node = vtkMRMLLayerDMWidgetEventTranslationNode::SafeDownCast(anode); + if (!node) { return; } - this->EventMap = other->EventMap; - this->Modified(); + + this->EventMap = node->EventMap; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMWidgetEventTranslationNode::ReadXMLAttributes(const char** atts) { MRMLNodeModifyBlocker blocker(this); @@ -44,6 +71,7 @@ void vtkMRMLLayerDMWidgetEventTranslationNode::ReadXMLAttributes(const char** at vtkMRMLReadXMLEndMacro(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMWidgetEventTranslationNode::WriteXML(ostream& of, int indent) { Superclass::WriteXML(of, indent); @@ -52,16 +80,19 @@ void vtkMRMLLayerDMWidgetEventTranslationNode::WriteXML(ostream& of, int indent) vtkMRMLWriteXMLEndMacro(); } +//----------------------------------------------------------------------------- const char* vtkMRMLLayerDMWidgetEventTranslationNode::GetNodeTagName() { return "WidgetEventTranslationNode"; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMWidgetEventTranslationNode::SetTranslation(int widgetState, unsigned long interactionEvent, unsigned long widgetEvent, int modifier) { this->SetTranslation(EventKey{ widgetState, interactionEvent, modifier }, widgetEvent); } +//----------------------------------------------------------------------------- unsigned long vtkMRMLLayerDMWidgetEventTranslationNode::GetEndInteractionEvent(unsigned long startInteractionEvent) { std::map endInteractionMap{ @@ -74,6 +105,7 @@ unsigned long vtkMRMLLayerDMWidgetEventTranslationNode::GetEndInteractionEvent(u return it == endInteractionMap.end() ? vtkMRMLAbstractWidget::WidgetEventNone : it->second; } +//----------------------------------------------------------------------------- unsigned long vtkMRMLLayerDMWidgetEventTranslationNode::GetClickEvent(unsigned long releaseEvent) { std::map clickMap{ @@ -86,6 +118,7 @@ unsigned long vtkMRMLLayerDMWidgetEventTranslationNode::GetClickEvent(unsigned l return it == clickMap.end() ? vtkMRMLAbstractWidget::WidgetEventNone : it->second; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMWidgetEventTranslationNode::SetTranslationClickAndDrag(int widgetState, unsigned long interactionEvent, int widgetStateDragging, @@ -99,6 +132,7 @@ void vtkMRMLLayerDMWidgetEventTranslationNode::SetTranslationClickAndDrag(int wi this->SetTranslation(widgetStateDragging, endInteractionEvent, widgetEndEvent); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMWidgetEventTranslationNode::SetTranslationKeyboard(int widgetState, const std::string& keySym, unsigned long widgetEvent, @@ -106,9 +140,10 @@ void vtkMRMLLayerDMWidgetEventTranslationNode::SetTranslationKeyboard(int widget int repeatCount, unsigned long keyEvent) { - this->SetTranslation(EventKey{ widgetState, keyEvent, modifier, EventKey::thresholdRepeatCount(repeatCount), keySym }, widgetEvent); + this->SetTranslation(EventKey{ widgetState, keyEvent, modifier, EventKey::ThresholdRepeatCount(repeatCount), keySym }, widgetEvent); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMWidgetEventTranslationNode::RemoveTranslationEvent(unsigned long widgetEvent) { int erasedCount{}; @@ -132,12 +167,14 @@ int vtkMRMLLayerDMWidgetEventTranslationNode::RemoveTranslationEvent(unsigned lo return erasedCount; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMWidgetEventTranslationNode::SetTranslation(const EventKey& key, unsigned long widgetEvent) { this->EventMap[key] = widgetEvent; this->Modified(); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMWidgetEventTranslationNode::BlockTranslationEvent(unsigned long widgetEvent, bool isBlocked) { const auto wasBlocked = this->IsWidgetEventBlocked(widgetEvent); @@ -152,6 +189,7 @@ bool vtkMRMLLayerDMWidgetEventTranslationNode::BlockTranslationEvent(unsigned lo return wasBlocked; } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMWidgetEventTranslationNode::BlockAllTranslationEvents(bool isBlocked) { const auto wasBlocked = this->IsBlocked; @@ -159,6 +197,7 @@ bool vtkMRMLLayerDMWidgetEventTranslationNode::BlockAllTranslationEvents(bool is return wasBlocked; } +//----------------------------------------------------------------------------- unsigned long vtkMRMLLayerDMWidgetEventTranslationNode::Translate(int widgetState, vtkMRMLInteractionEventData* eventData) { if (!eventData) @@ -171,24 +210,27 @@ unsigned long vtkMRMLLayerDMWidgetEventTranslationNode::Translate(int widgetStat if (eventId == vtkCommand::KeyPressEvent) { - key.repeatCount = EventKey::thresholdRepeatCount(eventData->GetKeyRepeatCount()); - key.keySym = eventData->GetKeySym(); + key.RepeatCount = EventKey::ThresholdRepeatCount(eventData->GetKeyRepeatCount()); + key.KeySym = eventData->GetKeySym(); } return this->Translate(key); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMWidgetEventTranslationNode::Clear() { this->EventMap.clear(); this->Modified(); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMWidgetEventTranslationNode::GetNumberOfTranslations() const { return static_cast(this->EventMap.size()); } +//----------------------------------------------------------------------------- unsigned long vtkMRMLLayerDMWidgetEventTranslationNode::Translate(EventKey key) const { // Early return if the translation node is blocked @@ -201,18 +243,18 @@ unsigned long vtkMRMLLayerDMWidgetEventTranslationNode::Translate(EventKey key) const auto noRelaxation = [](EventKey k) { return k; }; const auto relaxModifier = [](EventKey k) { - k.modifier = (k.modifier == vtkEvent::AnyModifier) ? vtkEvent::NoModifier : k.modifier; + k.Modifier = (k.Modifier == vtkEvent::AnyModifier) ? vtkEvent::NoModifier : k.Modifier; return k; }; const auto relaxState = [](EventKey k) { - k.widgetState = vtkMRMLAbstractWidget::WidgetStateAny; + k.WidgetState = vtkMRMLAbstractWidget::WidgetStateAny; return k; }; const auto relaxClick = [](EventKey k) { - const auto clickEvent = GetClickEvent(k.eventId); - k.eventId = clickEvent != vtkMRMLAbstractWidget::WidgetEventNone ? clickEvent : k.eventId; + const auto clickEvent = GetClickEvent(k.EventId); + k.EventId = clickEvent != vtkMRMLAbstractWidget::WidgetEventNone ? clickEvent : k.EventId; return k; }; @@ -232,11 +274,13 @@ unsigned long vtkMRMLLayerDMWidgetEventTranslationNode::Translate(EventKey key) return vtkMRMLAbstractWidget::WidgetEventNone; } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMWidgetEventTranslationNode::IsWidgetEventBlocked(unsigned long widgetEvent) const { return this->BlockedEvents.find(widgetEvent) != std::end(this->BlockedEvents); } +//----------------------------------------------------------------------------- unsigned long vtkMRMLLayerDMWidgetEventTranslationNode::GetWidgetEvent(const EventKey& key) const { const auto it = this->EventMap.find(key); @@ -244,19 +288,21 @@ unsigned long vtkMRMLLayerDMWidgetEventTranslationNode::GetWidgetEvent(const Eve return this->IsWidgetEventBlocked(widgetEvent) ? vtkMRMLAbstractWidget::WidgetEventNone : widgetEvent; } +//----------------------------------------------------------------------------- std::string vtkMRMLLayerDMWidgetEventTranslationNode::ToString(const std::pair& eventPair) { const auto& [key, widgetEvent] = eventPair; std::stringstream ss; - ss << "widgetState=" << key.widgetState << ","; - ss << "eventId=" << key.eventId << ","; - ss << "modifier=" << key.modifier << ","; - ss << "repeatCount=" << key.repeatCount << ","; - ss << "keySym=" << key.keySym << ","; + ss << "widgetState=" << key.WidgetState << ","; + ss << "eventId=" << key.EventId << ","; + ss << "modifier=" << key.Modifier << ","; + ss << "repeatCount=" << key.RepeatCount << ","; + ss << "keySym=" << key.KeySym << ","; ss << "widgetEvent=" << widgetEvent; return ss.str(); } +//----------------------------------------------------------------------------- std::string vtkMRMLLayerDMWidgetEventTranslationNode::ToString(const std::map& eventMap, const vtkIndent* indent, const std::string& eol) { std::stringstream ss; @@ -272,6 +318,7 @@ std::string vtkMRMLLayerDMWidgetEventTranslationNode::ToString(const std::map vtkMRMLLayerDMWidgetEventTranslationNode::EventMapFromString(const std::string& value) { std::stringstream ss(value); @@ -287,6 +334,7 @@ std::map vtkM return eventMap; } +//----------------------------------------------------------------------------- std::pair vtkMRMLLayerDMWidgetEventTranslationNode::EventPairFromString(const std::string& value) { // Helper function to trim the input string @@ -340,11 +388,11 @@ std::pair vtk trim(keyName); trim(val); - tryRead(keyName, "widgetState", [val, &key] { key.widgetState = std::stoi(val); }); - tryRead(keyName, "eventId", [val, &key] { key.eventId = std::stoul(val); }); - tryRead(keyName, "modifier", [val, &key] { key.modifier = std::stoi(val); }); - tryRead(keyName, "repeatCount", [val, &key] { key.repeatCount = EventKey::thresholdRepeatCount(std::stoi(val)); }); - tryRead(keyName, "keySym", [val, &key] { key.keySym = val; }); + tryRead(keyName, "widgetState", [val, &key] { key.WidgetState = std::stoi(val); }); + tryRead(keyName, "eventId", [val, &key] { key.EventId = std::stoul(val); }); + tryRead(keyName, "modifier", [val, &key] { key.Modifier = std::stoi(val); }); + tryRead(keyName, "repeatCount", [val, &key] { key.RepeatCount = EventKey::ThresholdRepeatCount(std::stoi(val)); }); + tryRead(keyName, "keySym", [val, &key] { key.KeySym = val; }); tryRead(keyName, "widgetEvent", [val, &widgetEvent] { widgetEvent = std::stoul(val); }); } return { key, widgetEvent }; diff --git a/LayerDM/MRML/vtkMRMLLayerDMWidgetEventTranslationNode.h b/LayerDM/MRML/vtkMRMLLayerDMWidgetEventTranslationNode.h index f0849b3..4c748db 100644 --- a/LayerDM/MRML/vtkMRMLLayerDMWidgetEventTranslationNode.h +++ b/LayerDM/MRML/vtkMRMLLayerDMWidgetEventTranslationNode.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMWidgetEventTranslationNode_h +#define __vtkMRMLLayerDMWidgetEventTranslationNode_h #include "vtkSlicerLayerDMModuleMRMLExport.h" @@ -29,7 +50,11 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMWidgetEventTranslation void operator=(const vtkMRMLLayerDMWidgetEventTranslationNode&) = delete; void PrintSelf(ostream& os, vtkIndent indent) override; - void Copy(vtkMRMLNode* node) override; + + /// Copy node content (excludes basic data, such as name and node references). + /// \sa vtkMRMLNode::CopyContent + vtkMRMLCopyContentMacro(vtkMRMLLayerDMWidgetEventTranslationNode); + void ReadXMLAttributes(const char** atts) override; void WriteXML(ostream& of, int indent) override; @@ -108,17 +133,20 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMWidgetEventTranslation { bool operator==(const EventKey& other) const { return this->AsTuple() == other.AsTuple(); } bool operator<(const EventKey& other) const { return this->AsTuple() < other.AsTuple(); } - std::tuple AsTuple() const { return std::make_tuple(widgetState, eventId, modifier, repeatCount, keySym); } + std::tuple AsTuple() const + { + return std::make_tuple(this->WidgetState, this->EventId, this->Modifier, this->RepeatCount, this->KeySym); + } /// Slicer event data never generate repeat counts less than 1. /// Make sure the repeat count is always greater than one - static int thresholdRepeatCount(int repeatCount) { return std::max(1, repeatCount); } + static int ThresholdRepeatCount(int repeatCount) { return std::max(1, repeatCount); } - int widgetState{ vtkMRMLAbstractWidget::WidgetStateAny }; - unsigned long eventId{ vtkCommand::NoEvent }; - int modifier{ vtkEvent::NoModifier }; - int repeatCount{ 1 }; - std::string keySym{}; + int WidgetState{ vtkMRMLAbstractWidget::WidgetStateAny }; + unsigned long EventId{ vtkCommand::NoEvent }; + int Modifier{ vtkEvent::NoModifier }; + int RepeatCount{ 1 }; + std::string KeySym; }; void SetTranslation(const EventKey& key, unsigned long widgetEvent); @@ -132,7 +160,9 @@ class VTK_SLICER_LAYERDM_MODULE_MRML_EXPORT vtkMRMLLayerDMWidgetEventTranslation static std::map EventMapFromString(const std::string& value); static std::pair EventPairFromString(const std::string& value); - std::map EventMap{}; - std::set BlockedEvents{}; + std::map EventMap; + std::set BlockedEvents; bool IsBlocked{}; }; + +#endif diff --git a/LayerDM/MRML/vtkSlicerLayerDMModuleMRMLExport.h b/LayerDM/MRML/vtkSlicerLayerDMModuleMRMLExport.h index ddb78ce..63f0808 100644 --- a/LayerDM/MRML/vtkSlicerLayerDMModuleMRMLExport.h +++ b/LayerDM/MRML/vtkSlicerLayerDMModuleMRMLExport.h @@ -1,3 +1,29 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +/// Provides the Slicer conventional export header name for this library. +/// The Slicer core build generates its own equivalent in the build tree; this file is what +/// supplies the name in the standalone build, where only the module header is generated. +#ifndef __vtkSlicerLayerDMModuleMRMLExport_h +#define __vtkSlicerLayerDMModuleMRMLExport_h #include "vtkSlicerLayerDMModuleMRMLModule.h" + +#endif diff --git a/LayerDM/MRMLDM/Python/ScriptedPipelineSceneConnector.py b/LayerDM/MRMLDM/Python/ScriptedPipelineSceneConnector.py new file mode 100644 index 0000000..e96809f --- /dev/null +++ b/LayerDM/MRMLDM/Python/ScriptedPipelineSceneConnector.py @@ -0,0 +1,108 @@ +import slicer +from slicer import ( + vtkMRMLLayerDMPipelineFactory, + vtkMRMLLayerDMPipelineScriptedCreator, +) + + +class ScriptedPipelineSceneConnector: + """ + Convenience base class connecting the scripted LayerDM pipelines of one module to the scene. + + Subclass and override: + - getPipelines: return the pipeline classes handled by the module. + - getSceneObservers: optionally return (eventId, callback) pairs to observe on the MRML scene. + - onConnected: optionally perform additional setup after connecting (for instance adding + data nodes to the scene). + + Expected usage in a scripted module: + - Instantiate the subclass at module level. + - Call connect at application startup and from the module widget's setup. connect removes any + previous connection first, so re-connecting from setup makes a module reload rebind the + reloaded pipeline classes (the LayerDM pipeline managers automatically recreate the pipelines + of a creator which was removed from the factory). + - Call disconnect from the module widget's cleanup, which is invoked by the module reload + machinery before the widget is rebuilt and when the module is unloaded. + """ + + def __init__(self, scene=None): + """ + :param scene: Optional MRML scene to connect to. If None, the slicer.mrmlScene singleton is used + when connect is called. Passing the scene explicitly avoids depending on the singleton, which + is not available in trame-slicer. + """ + self._scene = scene + self._connectedScene = None + self._pipelineCreator = None + self._sceneObserverTags = [] + + def getScene(self): + """ + Returns the scene this connector is connected to. + While disconnected, returns the scene it would connect to (the scene given at construction, + or the slicer.mrmlScene singleton). + Subclass overrides should use this instead of the slicer.mrmlScene singleton. + """ + if self._connectedScene is not None: + return self._connectedScene + return self._scene if self._scene is not None else slicer.mrmlScene + + def getPipelines(self) -> list: + """ + Override to return the pipeline classes handled by the module. + Each class should provide a TryCreatePipeline(viewNode, node) class method returning a new pipeline + instance or None. The classes are queried in order until one returns a pipeline. + """ + return [] + + def getSceneObservers(self) -> list: + """Override to return (eventId, callback) pairs to observe on the MRML scene while connected.""" + return [] + + def onConnected(self) -> None: + """Override to perform additional setup after connecting (for instance adding data nodes to the scene).""" + + def connect(self) -> None: + """ + Registers a pipeline creator for the getPipelines classes, adds the getSceneObservers observers and + calls onConnected. Any previous connection held by this instance is removed first. + + :raises TypeError: If any of the getPipelines classes doesn't implement TryCreatePipeline. + Checked eagerly, as a missing TryCreatePipeline would otherwise only fail when a node is + added to the scene, far from the actual mistake. + """ + pipelines = self.getPipelines() + for pipeline in pipelines: + if not callable(getattr(pipeline, "TryCreatePipeline", None)): + raise TypeError( + f"{pipeline.__name__} cannot be used with {type(self).__name__}: " + "it does not implement the TryCreatePipeline(viewNode, node) class method.") + + self.disconnect() + self._connectedScene = self._scene if self._scene is not None else slicer.mrmlScene + + def tryCreate(viewNode, node): + for pipeline in pipelines: + ret = pipeline.TryCreatePipeline(viewNode, node) + if ret is not None: + return ret + return None + + self._pipelineCreator = vtkMRMLLayerDMPipelineScriptedCreator() + self._pipelineCreator.SetPythonCallback(tryCreate) + vtkMRMLLayerDMPipelineFactory.GetInstance().AddPipelineCreator(self._pipelineCreator) + + for eventId, callback in self.getSceneObservers(): + self._sceneObserverTags.append(self._connectedScene.AddObserver(eventId, callback)) + + self.onConnected() + + def disconnect(self) -> None: + """Undoes the connection done by connect. Safe to call when nothing is connected.""" + if self._pipelineCreator is not None: + vtkMRMLLayerDMPipelineFactory.GetInstance().RemovePipelineCreator(self._pipelineCreator) + self._pipelineCreator = None + for tag in self._sceneObserverTags: + self._connectedScene.RemoveObserver(tag) + self._sceneObserverTags = [] + self._connectedScene = None diff --git a/LayerDM/MRMLDM/Python/__init__.py b/LayerDM/MRMLDM/Python/__init__.py index 5d121af..8aa6691 100644 --- a/LayerDM/MRMLDM/Python/__init__.py +++ b/LayerDM/MRMLDM/Python/__init__.py @@ -1,3 +1,4 @@ +from .ScriptedPipelineSceneConnector import ScriptedPipelineSceneConnector from .vtkMRMLLayerDMScriptedPipeline import vtkMRMLLayerDMScriptedPipeline -__all__ = ["vtkMRMLLayerDMScriptedPipeline"] +__all__ = ["ScriptedPipelineSceneConnector", "vtkMRMLLayerDMScriptedPipeline"] diff --git a/LayerDM/MRMLDM/Python/extension.cmake b/LayerDM/MRMLDM/Python/extension.cmake index fa885be..cfa5b68 100644 --- a/LayerDM/MRMLDM/Python/extension.cmake +++ b/LayerDM/MRMLDM/Python/extension.cmake @@ -1,5 +1,6 @@ set(LayerDM_PYTHON_SCRIPTS __init__.py + ScriptedPipelineSceneConnector.py vtkMRMLLayerDMScriptedPipeline.py ) diff --git a/LayerDM/MRMLDM/Python/vtkMRMLLayerDMScriptedPipeline.py b/LayerDM/MRMLDM/Python/vtkMRMLLayerDMScriptedPipeline.py index 944c9c9..def446a 100644 --- a/LayerDM/MRMLDM/Python/vtkMRMLLayerDMScriptedPipeline.py +++ b/LayerDM/MRMLDM/Python/vtkMRMLLayerDMScriptedPipeline.py @@ -4,7 +4,7 @@ vtkMRMLAbstractViewNode, vtkMRMLAbstractWidget, vtkMRMLInteractionEventData, - vtkMRMLLayerDMPipelineI, + vtkMRMLLayerDMPipeline, vtkMRMLLayerDMPipelineManager, vtkMRMLLayerDMScriptedPipelineBridge, vtkMRMLNode, @@ -14,25 +14,19 @@ class vtkMRMLLayerDMScriptedPipeline(vtkMRMLLayerDMScriptedPipelineBridge): - """ - Python base class for all Layer Displayable Manager pipelines. - """ + """Python base class for all Layer Displayable Manager pipelines.""" def __init__(self): self.SetPythonObject(self) @property def viewNode(self) -> vtkMRMLAbstractViewNode: - """ - Property returning the current view node on which the pipeline is attached. - """ + """Property returning the current view node on which the pipeline is attached.""" return self.GetViewNode() @property def displayNode(self) -> vtkMRMLNode: - """ - Property returning the current display node for which the pipeline was created. - """ + """Property returning the current display node for which the pipeline was created.""" return self.GetDisplayNode() def CanProcessInteractionEvent(self, eventData: vtkMRMLInteractionEventData) -> tuple[bool, float]: @@ -59,7 +53,7 @@ def GetCustomCamera(self, renderOrder: int) -> vtkCamera | None: def GetMouseCursor(self) -> int: """ Custom mouse cursor from VTK mouse cursor enum. - This value is only used if the pipeline actually processes an event and is ignore otherwise. + This value is only used if the pipeline actually processes an event and is ignored otherwise. :return: 0 by default. """ return 0 @@ -136,10 +130,10 @@ def OnRendererAdded(self, renderer: vtkRenderer | None) -> None: Triggered when the pipeline is displayed on a new renderer. default behavior: does nothing. See also: self.GetRenderer() - See also: self.ResetDisplay() + See also: self.UpdateDisplay() See also: self.RequestRender() - :param renderer: Optional instance or renderer on which the pipeline is added + :param renderer: Optional instance of renderer on which the pipeline is added """ pass @@ -149,15 +143,14 @@ def OnRendererRemoved(self, renderer: vtkRenderer) -> None: default behavior: does nothing. See also: self.GetRenderer() - :param renderer: Optional instance or renderer from which the pipeline was removed - :return: + :param renderer: Optional instance of renderer from which the pipeline was removed """ pass def OnUpdate(self, obj: vtkObject, eventId: int, callData: Any | None) -> None: """ Observer update callback. - Triggered when any object & events observed using UpdateObserver is triggered. + Triggered when any object & events observed using UpdateObservation is triggered. :param obj: vtkObject instance which triggered the callback :param eventId: Event id which triggered the callback @@ -177,14 +170,14 @@ def ProcessInteractionEvent(self, eventData: vtkMRMLInteractionEventData) -> boo def SetDisplayNode(self, displayNode: vtkMRMLNode) -> None: """ - Set the display node for the pipeline has changed (initialization). + Called when the display node of the pipeline has changed (initialization). default behavior: Stored and display node is observed for vtkCommand::ModifiedEvent. - See also: self.UpdateObserver(prevObj, newObj, eventIds) + See also: self.UpdateObservation(prevObj, newObj, eventIds) See also: self.OnUpdate(obj, eventId, callData) :param displayNode: The new instance of display node for the pipeline """ - vtkMRMLLayerDMPipelineI.SetDisplayNode(self, displayNode) + vtkMRMLLayerDMPipeline.SetDisplayNode(self, displayNode) def SetPipelineManager(self, pipelineManager: vtkMRMLLayerDMPipelineManager) -> None: """ @@ -195,14 +188,14 @@ def SetPipelineManager(self, pipelineManager: vtkMRMLLayerDMPipelineManager) -> See also: self.GetNodePipeline(node) :param pipelineManager: The instance of pipeline manager managing the current pipeline """ - vtkMRMLLayerDMPipelineI.SetPipelineManager(self, pipelineManager) + vtkMRMLLayerDMPipeline.SetPipelineManager(self, pipelineManager) def SetScene(self, scene: vtkMRMLScene) -> None: """ Set the pipeline scene (initialization). default behavior: Stores the scene for access (no active observer). """ - vtkMRMLLayerDMPipelineI.SetScene(self, scene) + vtkMRMLLayerDMPipeline.SetScene(self, scene) def SetViewNode(self, viewNode: vtkMRMLAbstractViewNode) -> None: """ @@ -210,11 +203,11 @@ def SetViewNode(self, viewNode: vtkMRMLAbstractViewNode) -> None: default behavior: Stored and view node is observed for vtkCommand::ModifiedEvent. :param viewNode: The instance of viewNode the pipeline is attached to """ - vtkMRMLLayerDMPipelineI.SetViewNode(self, viewNode) + vtkMRMLLayerDMPipeline.SetViewNode(self, viewNode) - def UpdatePipeline(self) -> None: + def UpdateFromMRML(self) -> None: """ - Triggered by self.ResetDisplay() calls + Triggered by self.UpdateDisplay() calls Override to update the representation of the pipeline in the different views. See also: self.RequestRender() diff --git a/LayerDM/MRMLDM/Python/wheel.cmake b/LayerDM/MRMLDM/Python/wheel.cmake index b119240..f821bfa 100644 --- a/LayerDM/MRMLDM/Python/wheel.cmake +++ b/LayerDM/MRMLDM/Python/wheel.cmake @@ -1,5 +1,6 @@ install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py + ${CMAKE_CURRENT_SOURCE_DIR}/ScriptedPipelineSceneConnector.py ${CMAKE_CURRENT_SOURCE_DIR}/vtkMRMLLayerDMScriptedPipeline.py DESTINATION LayerDMLib ) diff --git a/LayerDM/MRMLDM/extension.cmake b/LayerDM/MRMLDM/extension.cmake index eac3779..fd35584 100644 --- a/LayerDM/MRMLDM/extension.cmake +++ b/LayerDM/MRMLDM/extension.cmake @@ -27,15 +27,15 @@ set(${KIT}_SRCS vtkMRMLLayerDMInteractionLogic.h vtkMRMLLayerDMLayerManager.cxx vtkMRMLLayerDMLayerManager.h + vtkMRMLLayerDMPipeline.cxx + vtkMRMLLayerDMPipeline.h vtkMRMLLayerDMPipelineCallbackCreator.cxx vtkMRMLLayerDMPipelineCallbackCreator.h vtkMRMLLayerDMPipelineCreateHelper.h - vtkMRMLLayerDMPipelineCreatorI.cxx - vtkMRMLLayerDMPipelineCreatorI.h + vtkMRMLLayerDMPipelineCreator.cxx + vtkMRMLLayerDMPipelineCreator.h vtkMRMLLayerDMPipelineFactory.cxx vtkMRMLLayerDMPipelineFactory.h - vtkMRMLLayerDMPipelineI.cxx - vtkMRMLLayerDMPipelineI.h vtkMRMLLayerDMPipelineManager.cxx vtkMRMLLayerDMPipelineManager.h vtkMRMLLayerDisplayableManager.h diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMCameraSynchronizer.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMCameraSynchronizer.cxx index 2e21c2c..34b0a16 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMCameraSynchronizer.cxx +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMCameraSynchronizer.cxx @@ -1,3 +1,23 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMCameraSynchronizer.h" // Layer DM includes @@ -9,88 +29,95 @@ // VTK includes #include +#include #include #include #include // STL includes #include +#include +//----------------------------------------------------------------------------- /// \brief Abstract class for the camera strategies. /// Implements only the reset camera clipping range logic for the layer cameras. /// Other methods are expected to be implemented by deriving classes. -class CameraSynchronizeStrategy +class vtkMRMLLayerDMCameraSynchronizer::CameraSynchronizeStrategy { public: explicit CameraSynchronizeStrategy(const vtkSmartPointer& camera, std::function invokeModifiedEvent) - : m_camera(camera) - , m_invokeModifiedEvent{ std::move(invokeModifiedEvent) } + : Camera(camera) + , InvokeModifiedEvent{ std::move(invokeModifiedEvent) } { } virtual ~CameraSynchronizeStrategy() = default; virtual void UpdateCamera() = 0; protected: - vtkSmartPointer m_camera; - vtkNew m_eventObserver; - std::function m_invokeModifiedEvent; + vtkSmartPointer Camera; + vtkNew EventObserver; + std::function InvokeModifiedEvent; }; +namespace +{ +//----------------------------------------------------------------------------- /// Default camera synchronization consists in updating the camera when the first renderer active camera is updated. -class DefaultCameraSynchronizeStrategy : public CameraSynchronizeStrategy +class DefaultCameraSynchronizeStrategy : public vtkMRMLLayerDMCameraSynchronizer::CameraSynchronizeStrategy { public: explicit DefaultCameraSynchronizeStrategy(const vtkSmartPointer& camera, vtkRenderer* renderer, std::function invokeModifiedEvent) : CameraSynchronizeStrategy(camera, std::move(invokeModifiedEvent)) - , m_renderer(renderer) + , Renderer(renderer) { - this->m_eventObserver->SetUpdateCallback( + this->EventObserver->SetUpdateCallback( [this](vtkObject* object) { - if (object == this->m_renderer) + if (object == this->Renderer) { this->ObserveActiveCamera(); } this->UpdateCamera(); }); - this->m_eventObserver->UpdateObserver(nullptr, this->m_renderer, vtkCommand::ActiveCameraEvent); + this->EventObserver->UpdateObservation(nullptr, this->Renderer, vtkCommand::ActiveCameraEvent); this->ObserveActiveCamera(); } void UpdateCamera() override { - if (!this->m_observedCamera) + if (!this->ObservedCamera) { return; } // Update camera and preserve clipping range double clippingRange[2]; - this->m_camera->GetClippingRange(clippingRange); - this->m_camera->DeepCopy(this->m_observedCamera); - this->m_camera->SetClippingRange(clippingRange); - this->m_invokeModifiedEvent(); + this->Camera->GetClippingRange(clippingRange); + this->Camera->DeepCopy(this->ObservedCamera); + this->Camera->SetClippingRange(clippingRange); + this->InvokeModifiedEvent(); } private: - void ObserveActiveCamera() { this->SetObservedCamera(this->m_renderer ? this->m_renderer->GetActiveCamera() : nullptr); } + void ObserveActiveCamera() { this->SetObservedCamera(this->Renderer ? this->Renderer->GetActiveCamera() : nullptr); } void SetObservedCamera(vtkCamera* camera) { - if (this->m_observedCamera == camera) + if (this->ObservedCamera == camera) { return; } - this->m_eventObserver->UpdateObserver(this->m_observedCamera, camera); - this->m_observedCamera = camera; + this->EventObserver->UpdateObservation(this->ObservedCamera, camera); + this->ObservedCamera = camera; } - vtkWeakPointer m_renderer; - vtkWeakPointer m_observedCamera; + vtkWeakPointer Renderer; + vtkWeakPointer ObservedCamera; }; +//----------------------------------------------------------------------------- /// Synchronizes the default camera to the current slice node view configuration. /// The Slice renderer 0 camera is not configured nor modified during camera changes. /// All of its actors are set to render in 2D. @@ -100,34 +127,34 @@ class DefaultCameraSynchronizeStrategy : public CameraSynchronizeStrategy /// /// Clipping range is configured to show all actors attached to the default camera. /// If clipping is required, then it should be done inside the specific pipeline. -class SliceViewCameraSynchronizeStrategy : public CameraSynchronizeStrategy +class SliceViewCameraSynchronizeStrategy : public vtkMRMLLayerDMCameraSynchronizer::CameraSynchronizeStrategy { public: explicit SliceViewCameraSynchronizeStrategy(const vtkSmartPointer& camera, vtkMRMLSliceNode* sliceNode, std::function invokeModifiedEvent) : CameraSynchronizeStrategy(camera, std::move(invokeModifiedEvent)) - , m_sliceNode{ sliceNode } + , SliceNode{ sliceNode } { - this->m_eventObserver->SetUpdateCallback( + this->EventObserver->SetUpdateCallback( [this](vtkObject* object) { - if (object == this->m_sliceNode) + if (object == this->SliceNode) { this->UpdateCamera(); } }); - this->m_eventObserver->UpdateObserver(nullptr, this->m_sliceNode); + this->EventObserver->UpdateObservation(nullptr, this->SliceNode); } void UpdateCamera() override { - if (!this->m_sliceNode) + if (!this->SliceNode) { return; } // Compute view center - vtkMatrix4x4* xyToRas = this->m_sliceNode->GetXYToRAS(); - std::array viewCenterXY = { 0.5 * this->m_sliceNode->GetDimensions()[0], 0.5 * this->m_sliceNode->GetDimensions()[1], 0.0, 1.0 }; + vtkMatrix4x4* xyToRas = this->SliceNode->GetXYToRAS(); + std::array viewCenterXY = { 0.5 * this->SliceNode->GetDimensions()[0], 0.5 * this->SliceNode->GetDimensions()[1], 0.0, 1.0 }; std::array viewCenterRAS = {}; xyToRas->MultiplyPoint(viewCenterXY.data(), viewCenterRAS.data()); @@ -139,107 +166,119 @@ class SliceViewCameraSynchronizeStrategy : public CameraSynchronizeStrategy } // Parallel projection and scale - this->m_camera->ParallelProjectionOn(); - this->m_camera->SetParallelScale(0.5 * this->m_sliceNode->GetFieldOfView()[1]); + this->Camera->ParallelProjectionOn(); + this->Camera->SetParallelScale(0.5 * this->SliceNode->GetFieldOfView()[1]); // Set focal point - this->m_camera->SetFocalPoint(viewCenterRAS.data()); + this->Camera->SetFocalPoint(viewCenterRAS.data()); // View directions - vtkMatrix4x4* sliceToRAS = this->m_sliceNode->GetSliceToRAS(); + vtkMatrix4x4* sliceToRAS = this->SliceNode->GetSliceToRAS(); std::array vRight = { sliceToRAS->GetElement(0, 0), sliceToRAS->GetElement(1, 0), sliceToRAS->GetElement(2, 0) }; std::array vUp = { sliceToRAS->GetElement(0, 1), sliceToRAS->GetElement(1, 1), sliceToRAS->GetElement(2, 1) }; - this->m_camera->SetViewUp(vUp.data()); + this->Camera->SetViewUp(vUp.data()); // Position - double d = this->m_camera->GetDistance(); + double d = this->Camera->GetDistance(); std::array normal{}; vtkMath::Cross(vRight.data(), vUp.data(), normal.data()); double position[3] = { viewCenterRAS[0] + normal[0] * d, viewCenterRAS[1] + normal[1] * d, viewCenterRAS[2] + normal[2] * d }; - this->m_camera->SetPosition(position); - this->m_invokeModifiedEvent(); + this->Camera->SetPosition(position); + this->InvokeModifiedEvent(); } private: - vtkWeakPointer m_sliceNode; + vtkWeakPointer SliceNode; }; +} // namespace +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMCameraSynchronizer); +//----------------------------------------------------------------------------- void vtkMRMLLayerDMCameraSynchronizer::SetViewNode(vtkMRMLAbstractViewNode* viewNode) { - if (this->m_viewNode == viewNode) + if (this->ViewNode == viewNode) { return; } - this->m_viewNode = viewNode; + this->ViewNode = viewNode; this->UpdateStrategy(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMCameraSynchronizer::SetDefaultCamera(const vtkSmartPointer& camera) { - if (this->m_defaultCamera == camera) + if (this->DefaultCamera == camera) { return; } - this->m_defaultCamera = camera; + this->DefaultCamera = camera; this->UpdateStrategy(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMCameraSynchronizer::SetRenderer(vtkRenderer* renderer) { - if (this->m_renderer == renderer) + if (this->Renderer == renderer) { return; } - this->m_renderer = renderer; + this->Renderer = renderer; this->UpdateStrategy(); } -vtkMRMLLayerDMCameraSynchronizer::vtkMRMLLayerDMCameraSynchronizer() - : m_defaultCamera{ nullptr } - , m_renderer{ nullptr } - , m_viewNode{ nullptr } - , m_syncStrategy{ nullptr } -{ -} +vtkMRMLLayerDMCameraSynchronizer::vtkMRMLLayerDMCameraSynchronizer() = default; vtkMRMLLayerDMCameraSynchronizer::~vtkMRMLLayerDMCameraSynchronizer() = default; +//----------------------------------------------------------------------------- void vtkMRMLLayerDMCameraSynchronizer::UpdateStrategy() { - if (!this->m_defaultCamera || !this->m_renderer) + if (!this->DefaultCamera || !this->Renderer) { - this->m_syncStrategy = nullptr; + this->SynchronizeStrategy = nullptr; return; } const auto invokeModifiedEvent = [this] { - if (this->m_isBlocked) + if (this->IsBlocked) { return; } this->Modified(); }; - if (auto sliceNode = vtkMRMLSliceNode::SafeDownCast(this->m_viewNode)) + if (auto sliceNode = vtkMRMLSliceNode::SafeDownCast(this->ViewNode)) { - this->m_syncStrategy = std::make_unique(this->m_defaultCamera, sliceNode, invokeModifiedEvent); + this->SynchronizeStrategy = std::make_unique(this->DefaultCamera, sliceNode, invokeModifiedEvent); } else { - this->m_syncStrategy = std::make_unique(this->m_defaultCamera, this->m_renderer, invokeModifiedEvent); + this->SynchronizeStrategy = std::make_unique(this->DefaultCamera, this->Renderer, invokeModifiedEvent); } - this->m_syncStrategy->UpdateCamera(); + this->SynchronizeStrategy->UpdateCamera(); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMCameraSynchronizer::BlockModified(bool isBlocked) { - bool wasBlocked = this->m_isBlocked; - m_isBlocked = isBlocked; + bool wasBlocked = this->IsBlocked; + this->IsBlocked = isBlocked; return wasBlocked; } + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMCameraSynchronizer::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Blocked: " << (this->IsBlocked ? "true" : "false") << std::endl; + os << indent << "Default camera: " << (this->DefaultCamera ? "set" : "(none)") << std::endl; + os << indent << "Renderer: " << (this->Renderer ? "set" : "(none)") << std::endl; + os << indent << "View node: " << (this->ViewNode ? "set" : "(none)") << std::endl; + os << indent << "Synchronize strategy: " << (this->SynchronizeStrategy ? "set" : "(none)") << std::endl; +} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMCameraSynchronizer.h b/LayerDM/MRMLDM/vtkMRMLLayerDMCameraSynchronizer.h index f71c6d2..780b439 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMCameraSynchronizer.h +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMCameraSynchronizer.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMCameraSynchronizer_h +#define __vtkMRMLLayerDMCameraSynchronizer_h #include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" @@ -12,7 +33,6 @@ class vtkCamera; class vtkRenderer; -class CameraSynchronizeStrategy; class vtkMRMLAbstractViewNode; /// \brief Class responsible for synchronizing the camera of the different display layers. @@ -25,8 +45,11 @@ class vtkMRMLAbstractViewNode; class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMCameraSynchronizer : public vtkObject { public: + class CameraSynchronizeStrategy; + static vtkMRMLLayerDMCameraSynchronizer* New(); vtkTypeMacro(vtkMRMLLayerDMCameraSynchronizer, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; /// Set the view node for which the camera will be synchronized. void SetViewNode(vtkMRMLAbstractViewNode* viewNode); @@ -50,9 +73,11 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMCame /// Reset the internal strategy given current view node. void UpdateStrategy(); - vtkSmartPointer m_defaultCamera; - vtkWeakPointer m_renderer; - vtkWeakPointer m_viewNode; - std::unique_ptr m_syncStrategy; - bool m_isBlocked{ false }; + vtkSmartPointer DefaultCamera; + vtkWeakPointer Renderer; + vtkWeakPointer ViewNode; + std::unique_ptr SynchronizeStrategy; + bool IsBlocked{ false }; }; + +#endif diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMInteractionLogic.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMInteractionLogic.cxx index 61667c1..01b9ee2 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMInteractionLogic.cxx +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMInteractionLogic.cxx @@ -1,7 +1,27 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMInteractionLogic.h" // Layer DM includes -#include "vtkMRMLLayerDMPipelineI.h" +#include "vtkMRMLLayerDMPipeline.h" // Slicer includes #include "vtkMRMLAbstractWidget.h" @@ -10,59 +30,68 @@ // VTK includes #include +// STD includes +#include +#include +#include +#include + +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMInteractionLogic); -vtkMRMLLayerDMPipelineI* vtkMRMLLayerDMInteractionLogic::GetLastFocusedPipeline() const +//----------------------------------------------------------------------------- +vtkMRMLLayerDMPipeline* vtkMRMLLayerDMInteractionLogic::GetLastFocusedPipeline() const { - return this->m_prevFocusedPipeline; + return this->LastFocusedPipeline; } -vtkMRMLLayerDMInteractionLogic::vtkMRMLLayerDMInteractionLogic() - : m_prevFocusedPipeline{ nullptr } - , m_canProcess{} - , m_viewNode{ nullptr } -{ -} +vtkMRMLLayerDMInteractionLogic::vtkMRMLLayerDMInteractionLogic() = default; +//----------------------------------------------------------------------------- int vtkMRMLLayerDMInteractionLogic::MinWidgetState() { return vtkMRMLAbstractWidget::WidgetStateOnWidget; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMInteractionLogic::LoseFocus(vtkMRMLInteractionEventData* eventData) { - if (this->m_prevFocusedPipeline) + if (this->LastFocusedPipeline) { - this->m_prevFocusedPipeline->LoseFocus(eventData); - this->m_prevFocusedPipeline = nullptr; + this->LastFocusedPipeline->LoseFocus(eventData); + this->LastFocusedPipeline = nullptr; } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMInteractionLogic::LoseFocus() { vtkNew leaveEvent; leaveEvent->SetType(vtkCommand::LeaveEvent); - leaveEvent->SetViewNode(this->m_viewNode); + leaveEvent->SetViewNode(this->ViewNode); this->LoseFocus(leaveEvent); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMInteractionLogic::SetViewNode(vtkMRMLAbstractViewNode* viewNode) { - this->m_viewNode = viewNode; + this->ViewNode = viewNode; } -std::vector> vtkMRMLLayerDMInteractionLogic::GetCanProcessPipelines() const +//----------------------------------------------------------------------------- +std::vector> vtkMRMLLayerDMInteractionLogic::GetCanProcessPipelines() const { - return this->m_canProcess; + return this->CanProcessPipelines; } +//----------------------------------------------------------------------------- std::tuple vtkMRMLLayerDMInteractionLogic::PrioritizeCanProcessPipelines(vtkMRMLInteractionEventData* eventData) { // For each pipeline, if pipeline can process, store its state value, layer and distance to interaction - std::map> priority; + std::map> priority; double minDistance = std::numeric_limits::max(); int maxState = this->MinWidgetState(); - for (const auto& pipeline : m_pipelines) + for (const auto& pipeline : this->Pipelines) { if (pipeline->IsInteractionProcessingBlocked()) { @@ -72,7 +101,7 @@ std::tuple vtkMRMLLayerDMInteractionLogic::PrioritizeCanProcessPipe double pipelineDistance = std::numeric_limits::max(); if (pipeline->CanProcessInteractionEvent(eventData, pipelineDistance)) { - this->m_canProcess.emplace_back(pipeline); + this->CanProcessPipelines.emplace_back(pipeline); int widgetState = std::max(this->MinWidgetState(), pipeline->GetWidgetState()); minDistance = std::min(minDistance, pipelineDistance); maxState = std::max(widgetState, maxState); @@ -80,44 +109,52 @@ std::tuple vtkMRMLLayerDMInteractionLogic::PrioritizeCanProcessPipe } } // Sort can process by layer order and inverted square distance (larger layer number first and closest to interaction) - std::sort(this->m_canProcess.begin(), - this->m_canProcess.end(), - [&priority](const vtkSmartPointer& a, const vtkSmartPointer& b) { return priority[a] > priority[b]; }); + std::sort(this->CanProcessPipelines.begin(), + this->CanProcessPipelines.end(), + [&priority](const vtkSmartPointer& a, const vtkSmartPointer& b) { return priority[a] > priority[b]; }); return std::make_tuple(minDistance, maxState); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMInteractionLogic::LosePreviousFocusInCannotProcess(vtkMRMLInteractionEventData* eventData) { // Lose focus if previous focused pipeline cannot process current interaction - if (std::find(this->m_canProcess.begin(), this->m_canProcess.end(), this->m_prevFocusedPipeline) == this->m_canProcess.end()) + if (std::find(this->CanProcessPipelines.begin(), this->CanProcessPipelines.end(), this->LastFocusedPipeline) == this->CanProcessPipelines.end()) { this->LoseFocus(eventData); } } -void vtkMRMLLayerDMInteractionLogic::AddPipeline(const vtkSmartPointer& pipeline) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMInteractionLogic::AddPipeline(const vtkSmartPointer& pipeline) { - if (std::find(this->m_pipelines.begin(), this->m_pipelines.end(), pipeline) != this->m_pipelines.end()) + if (std::find(this->Pipelines.begin(), this->Pipelines.end(), pipeline) != this->Pipelines.end()) { return; } - this->m_pipelines.emplace_back(pipeline); + this->Pipelines.emplace_back(pipeline); } -void vtkMRMLLayerDMInteractionLogic::RemovePipeline(const vtkSmartPointer& pipeline) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMInteractionLogic::RemovePipeline(const vtkSmartPointer& pipeline) { - if (this->m_prevFocusedPipeline == pipeline) + if (this->LastFocusedPipeline == pipeline) { this->LoseFocus(); } - this->m_pipelines.erase(std::find(this->m_pipelines.begin(), this->m_pipelines.end(), pipeline)); + const auto pipelineIt = std::find(this->Pipelines.begin(), this->Pipelines.end(), pipeline); + if (pipelineIt != this->Pipelines.end()) + { + this->Pipelines.erase(pipelineIt); + } } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMInteractionLogic::CanProcessInteractionEvent(vtkMRMLInteractionEventData* eventData, double& distance2) { // Clear previous interaction list - this->m_canProcess.clear(); + this->CanProcessPipelines.clear(); // On leave event lose focus and early return to avoid bad pipeline state if (eventData->GetType() == vtkCommand::LeaveEvent) @@ -135,12 +172,13 @@ bool vtkMRMLLayerDMInteractionLogic::CanProcessInteractionEvent(vtkMRMLInteracti // Return lowest double value if any pipeline can process and is not idle // Otherwise, return the min distance returned by the processes. distance2 = maxState > this->MinWidgetState() ? std::numeric_limits::lowest() : minDistance; - return !this->m_canProcess.empty(); + return !this->CanProcessPipelines.empty(); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMInteractionLogic::ProcessInteractionEvent(vtkMRMLInteractionEventData* eventData) { - for (const auto& pipeline : m_canProcess) + for (const auto& pipeline : this->CanProcessPipelines) { if (pipeline->IsInteractionProcessingBlocked()) { @@ -150,11 +188,11 @@ bool vtkMRMLLayerDMInteractionLogic::ProcessInteractionEvent(vtkMRMLInteractionE // If pipeline can process, store pipeline for further interaction events if (pipeline->ProcessInteractionEvent(eventData)) { - if (pipeline != this->m_prevFocusedPipeline) + if (pipeline != this->LastFocusedPipeline) { this->LoseFocus(eventData); } - this->m_prevFocusedPipeline = pipeline; + this->LastFocusedPipeline = pipeline; return true; } } @@ -163,3 +201,13 @@ bool vtkMRMLLayerDMInteractionLogic::ProcessInteractionEvent(vtkMRMLInteractionE this->LoseFocus(eventData); return false; } + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMInteractionLogic::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Number of pipelines: " << this->Pipelines.size() << std::endl; + os << indent << "Number of pipelines able to process the last event: " << this->CanProcessPipelines.size() << std::endl; + os << indent << "Last focused pipeline: " << (this->LastFocusedPipeline ? "set" : "(none)") << std::endl; + os << indent << "View node: " << (this->ViewNode ? "set" : "(none)") << std::endl; +} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMInteractionLogic.h b/LayerDM/MRMLDM/vtkMRMLLayerDMInteractionLogic.h index 37a17f9..771ffc3 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMInteractionLogic.h +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMInteractionLogic.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMInteractionLogic_h +#define __vtkMRMLLayerDMInteractionLogic_h #include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" @@ -10,7 +31,7 @@ // STL includes #include -class vtkMRMLLayerDMPipelineI; +class vtkMRMLLayerDMPipeline; class vtkMRMLInteractionEventData; class vtkMRMLAbstractViewNode; @@ -28,15 +49,16 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMInte public: static vtkMRMLLayerDMInteractionLogic* New(); vtkTypeMacro(vtkMRMLLayerDMInteractionLogic, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; - void AddPipeline(const vtkSmartPointer& pipeline); + void AddPipeline(const vtkSmartPointer& pipeline); bool CanProcessInteractionEvent(vtkMRMLInteractionEventData* eventData, double& distance2); - std::vector> GetCanProcessPipelines() const; - vtkMRMLLayerDMPipelineI* GetLastFocusedPipeline() const; + std::vector> GetCanProcessPipelines() const; + vtkMRMLLayerDMPipeline* GetLastFocusedPipeline() const; void LoseFocus(vtkMRMLInteractionEventData* eventData); void LoseFocus(); bool ProcessInteractionEvent(vtkMRMLInteractionEventData* eventData); - void RemovePipeline(const vtkSmartPointer& pipeline); + void RemovePipeline(const vtkSmartPointer& pipeline); void SetViewNode(vtkMRMLAbstractViewNode* viewNode); protected: @@ -48,8 +70,10 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMInte std::tuple PrioritizeCanProcessPipelines(vtkMRMLInteractionEventData* eventData); void LosePreviousFocusInCannotProcess(vtkMRMLInteractionEventData* eventData); - std::vector> m_pipelines; - vtkSmartPointer m_prevFocusedPipeline; - std::vector> m_canProcess; - vtkWeakPointer m_viewNode; + std::vector> Pipelines; + vtkSmartPointer LastFocusedPipeline; + std::vector> CanProcessPipelines; + vtkWeakPointer ViewNode; }; + +#endif diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMLayerManager.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMLayerManager.cxx index b458e23..2cd1ef5 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMLayerManager.cxx +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMLayerManager.cxx @@ -1,20 +1,46 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMLayerManager.h" // Layer DM includes #include "vtkMRMLLayerDMObjectEventObserver.h" -#include "vtkMRMLLayerDMPipelineI.h" +#include "vtkMRMLLayerDMPipeline.h" // VTK includes #include #include +#include #include #include #include #include +// STD includes +#include + +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMLayerManager); -bool vtkMRMLLayerDMLayerManager::AddPipelineLayers(vtkMRMLLayerDMPipelineI* pipeline) +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMLayerManager::AddPipelineLayers(vtkMRMLLayerDMPipeline* pipeline) { if (!pipeline) { @@ -26,61 +52,68 @@ bool vtkMRMLLayerDMLayerManager::AddPipelineLayers(vtkMRMLLayerDMPipelineI* pipe auto key = std::make_tuple(order, GetCameraId(pipeline->GetCustomCamera(order))); if (!this->ContainsLayerKey(key)) { - this->m_pipelineLayers[key] = {}; + this->PipelineLayers[key] = {}; } - this->m_pipelineLayers[key].emplace(pipeline); + this->PipelineLayers[key].emplace(pipeline); } return true; } -void vtkMRMLLayerDMLayerManager::AddPipeline(vtkMRMLLayerDMPipelineI* pipeline) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMLayerManager::AddPipeline(vtkMRMLLayerDMPipeline* pipeline) { if (!pipeline) { return; } - this->m_obs->UpdateObserver(nullptr, pipeline, vtkMRMLLayerDMPipelineI::RenderGroupingModified); + this->Observer->UpdateObservation(nullptr, pipeline, { vtkMRMLLayerDMPipeline::RenderGroupingModified, vtkCommand::DeleteEvent }); this->AddPipelineLayers(pipeline); this->UpdateLayers(); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMLayerManager::GetNumberOfDistinctLayers() const { - return static_cast(this->m_pipelineLayers.size()); + return static_cast(this->PipelineLayers.size()); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMLayerManager::GetNumberOfManagedLayers() const { return this->GetNumberOfDistinctLayers() - 1; } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMLayerManager::GetNumberOfRenderers() const { - return static_cast(this->m_renderers.size()); + return static_cast(this->Renderers.size()); } -void vtkMRMLLayerDMLayerManager::RemovePipelineLayers(vtkMRMLLayerDMPipelineI* pipeline) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMLayerManager::RemovePipelineLayers(vtkMRMLLayerDMPipeline* pipeline) { - for (auto& [key, pipelines] : m_pipelineLayers) + for (auto& [key, pipelines] : this->PipelineLayers) { pipelines.erase(pipeline); } } -void vtkMRMLLayerDMLayerManager::RemovePipeline(vtkMRMLLayerDMPipelineI* pipeline) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMLayerManager::RemovePipeline(vtkMRMLLayerDMPipeline* pipeline) { if (!pipeline) { return; } - this->m_obs->UpdateObserver(pipeline, nullptr); + this->Observer->UpdateObservation(pipeline, nullptr); this->RemovePipelineRenderer(pipeline); this->RemovePipelineLayers(pipeline); this->UpdateLayers(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::ResetCameraClippingRange() const { // Reset first renderer clipping range @@ -90,52 +123,73 @@ void vtkMRMLLayerDMLayerManager::ResetCameraClippingRange() const } // Reset the managed renderers grouped by common cameras - for (const auto& pair : m_cameraRendererMap) + for (const auto& pair : this->CameraRendererMap) { this->ResetRenderersCameraClippingRange(pair.second, this->ComputeRenderersVisibleBounds(pair.second)); } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::SetRenderWindow(vtkRenderWindow* renderWindow) { - if (this->m_renderWindow == renderWindow) + if (this->RenderWindow == renderWindow) { return; } this->RemoveAllLayers(); - this->m_renderWindow = renderWindow; + this->RenderWindow = renderWindow; this->UpdateLayers(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::SetDefaultCamera(const vtkSmartPointer& camera) { - if (this->m_defaultCamera == camera) + if (this->DefaultCamera == camera) { return; } - this->m_defaultCamera = camera; + this->DefaultCamera = camera; this->UpdateLayers(); } +//----------------------------------------------------------------------------- vtkMRMLLayerDMLayerManager::vtkMRMLLayerDMLayerManager() - : m_emptyPipeline(vtkSmartPointer::New()) - , m_obs(vtkSmartPointer::New()) + : EmptyPipeline(vtkSmartPointer::New()) + , Observer(vtkSmartPointer::New()) { - this->m_obs->SetUpdateCallback( - [this](vtkObject* obj) + this->Observer->SetUpdateCallback( + [this](vtkObject* obj, unsigned long eventId) { - if (auto pipeline = vtkMRMLLayerDMPipelineI::SafeDownCast(obj)) + auto pipeline = vtkMRMLLayerDMPipeline::SafeDownCast(obj); + if (!pipeline) + { + return; + } + + if (eventId == vtkCommand::DeleteEvent) { - this->RemovePipelineLayers(pipeline); - this->AddPipelineLayers(pipeline); - this->UpdateLayers(); + this->RemovePipeline(pipeline); + return; } + + this->RemovePipelineLayers(pipeline); + this->AddPipelineLayers(pipeline); + this->UpdateLayers(); }); - this->AddPipeline(this->m_emptyPipeline); + this->AddPipeline(this->EmptyPipeline); +} + +//----------------------------------------------------------------------------- +vtkMRMLLayerDMLayerManager::~vtkMRMLLayerDMLayerManager() +{ + // Releasing the pipelines below destroys them, which would otherwise re-enter this object through the + // deletion forwarded to the update callback while its members are being destroyed. + this->Observer->ClearCallback(); } +//----------------------------------------------------------------------------- vtkRenderer* vtkMRMLLayerDMLayerManager::GetRendererMatchingKey(const LayerKey& key) { // If key index matches the default layer, return the render window's first renderer @@ -151,18 +205,20 @@ vtkRenderer* vtkMRMLLayerDMLayerManager::GetRendererMatchingKey(const LayerKey& { return nullptr; } - return this->m_renderers[rendererIndex]; + return this->Renderers[rendererIndex]; } +//----------------------------------------------------------------------------- vtkRenderer* vtkMRMLLayerDMLayerManager::GetDefaultRenderer() const { - if (!this->m_renderWindow) + if (!this->RenderWindow) { return nullptr; } - return this->m_renderWindow->GetRenderers()->GetFirstRenderer(); + return this->RenderWindow->GetRenderers()->GetFirstRenderer(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::AddMissingLayers() { while (this->GetNumberOfRenderers() < this->GetNumberOfManagedLayers()) @@ -171,12 +227,13 @@ void vtkMRMLLayerDMLayerManager::AddMissingLayers() // Events handling is done using the DM mechanism. auto renderer = vtkSmartPointer::New(); renderer->InteractiveOff(); - this->m_renderWindow->AddRenderer(renderer); - this->m_renderers.emplace_back(renderer); + this->RenderWindow->AddRenderer(renderer); + this->Renderers.emplace_back(renderer); } } -std::array vtkMRMLLayerDMLayerManager::ComputeRenderersVisibleBounds(const std::set>& renderers) +//----------------------------------------------------------------------------- +std::array vtkMRMLLayerDMLayerManager::ComputeRenderersVisibleBounds(const std::vector>& renderers) { vtkBoundingBox bbox; @@ -194,11 +251,13 @@ std::array vtkMRMLLayerDMLayerManager::ComputeRenderersVisibleBounds( return bounds; } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMLayerManager::ContainsLayerKey(const LayerKey& key) { - return this->m_pipelineLayers.find(key) != this->m_pipelineLayers.end(); + return this->PipelineLayers.find(key) != this->PipelineLayers.end(); } +//----------------------------------------------------------------------------- std::uintptr_t vtkMRMLLayerDMLayerManager::GetCameraId(vtkCamera* camera) { if (!camera) @@ -208,28 +267,27 @@ std::uintptr_t vtkMRMLLayerDMLayerManager::GetCameraId(vtkCamera* camera) return reinterpret_cast(camera); } -vtkCamera* vtkMRMLLayerDMLayerManager::GetCameraForLayer(const LayerKey& key, const std::set>& pipelines) const +//----------------------------------------------------------------------------- +vtkCamera* vtkMRMLLayerDMLayerManager::GetCameraForLayer(const LayerKey& key, const std::set& pipelines) const { if (const auto cameraId = std::get<1>(key); cameraId == 0) { - return this->m_defaultCamera; + return this->DefaultCamera; } - for (const auto& pipeline : pipelines) + if (pipelines.empty()) { - if (pipeline) - { - return pipeline->GetCustomCamera(std::get<0>(key)); - } + return nullptr; } - return nullptr; + return (*pipelines.begin())->GetCustomCamera(std::get<0>(key)); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMLayerManager::GetKeyIndex(const LayerKey& key) const { int index = 0; - for (const auto& pair : m_pipelineLayers) + for (const auto& pair : this->PipelineLayers) { if (pair.first == key) { @@ -240,20 +298,24 @@ int vtkMRMLLayerDMLayerManager::GetKeyIndex(const LayerKey& key) const return -1; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::RemoveAllLayers() { - for (const auto& renderer : m_renderers) + // Iterate over a copy as RemoveRenderer erases from this->Renderers + const auto renderers = this->Renderers; + for (const auto& renderer : renderers) { this->RemoveRenderer(renderer); } this->UpdateRenderWindowNumberOfLayers(); - this->m_renderers.clear(); + this->Renderers.clear(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::RemoveAllPipelineRenderers() { // if the render window is null, notify pipelines - for (const auto& [key, pipelines] : m_pipelineLayers) + for (const auto& [key, pipelines] : this->PipelineLayers) { for (const auto& pipeline : pipelines) { @@ -262,7 +324,8 @@ void vtkMRMLLayerDMLayerManager::RemoveAllPipelineRenderers() } } -void vtkMRMLLayerDMLayerManager::RemovePipelineRenderer(vtkMRMLLayerDMPipelineI* pipeline) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMLayerManager::RemovePipelineRenderer(vtkMRMLLayerDMPipeline* pipeline) { if (pipeline) { @@ -270,45 +333,51 @@ void vtkMRMLLayerDMLayerManager::RemovePipelineRenderer(vtkMRMLLayerDMPipelineI* } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::RemoveOutdatedLayers() { while (this->GetNumberOfRenderers() && (this->GetNumberOfRenderers() > this->GetNumberOfManagedLayers())) { - this->RemoveRenderer(this->m_renderers[this->GetNumberOfRenderers() - 1]); + this->RemoveRenderer(this->Renderers[this->GetNumberOfRenderers() - 1]); } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::RemoveOutdatedPipelines() { - // Remove pipelines which have been garbage collected - for (auto& [key, pipelines] : m_pipelineLayers) + // Remove the layers which no longer contain any pipeline. Destroyed pipelines are removed from their layers + // when they are destroyed, so only empty layers are left to clean up here. + // \sa vtkMRMLLayerDMObjectEventObserver::UpdateObservation + for (auto layerIt = this->PipelineLayers.begin(); layerIt != this->PipelineLayers.end();) { - for (const auto& pipeline : pipelines) + if (layerIt->second.empty()) { - if (!pipeline) - { - pipelines.erase(pipeline); - } + layerIt = this->PipelineLayers.erase(layerIt); } - - if (pipelines.empty()) + else { - this->m_pipelineLayers.erase(key); + ++layerIt; } } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::RemoveRenderer(const vtkSmartPointer& renderer) { - if (this->m_renderWindow && this->m_renderWindow->HasRenderer(renderer)) + if (this->RenderWindow && this->RenderWindow->HasRenderer(renderer)) { - this->m_renderWindow->RemoveRenderer(renderer); + this->RenderWindow->RemoveRenderer(renderer); } - this->m_renderers.erase(std::find(this->m_renderers.begin(), this->m_renderers.end(), renderer)); + const auto rendererIt = std::find(this->Renderers.begin(), this->Renderers.end(), renderer); + if (rendererIt != this->Renderers.end()) + { + this->Renderers.erase(rendererIt); + } } -void vtkMRMLLayerDMLayerManager::ResetRenderersCameraClippingRange(const std::set>& renderers, const std::array& bounds) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMLayerManager::ResetRenderersCameraClippingRange(const std::vector>& renderers, const std::array& bounds) { for (const auto& renderer : renderers) { @@ -320,22 +389,20 @@ void vtkMRMLLayerDMLayerManager::ResetRenderersCameraClippingRange(const std::se } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::SynchronizePipelineRenderers() { - std::map> pipelineRenderers; - std::map> pipelineOrders; + std::map> pipelineRenderers; + std::map> pipelineOrders; - for (const auto& [key, pipelines] : m_pipelineLayers) + for (const auto& [key, pipelines] : this->PipelineLayers) { auto renderer = this->GetRendererMatchingKey(key); auto order = std::get<0>(key); for (const auto& pipeline : pipelines) { - if (pipeline) - { - pipelineRenderers[pipeline.GetPointer()].push_back(renderer); - pipelineOrders[pipeline.GetPointer()].push_back(order); - } + pipelineRenderers[pipeline].push_back(renderer); + pipelineOrders[pipeline].push_back(order); } } @@ -345,30 +412,35 @@ void vtkMRMLLayerDMLayerManager::SynchronizePipelineRenderers() } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::UpdateRenderWindowNumberOfLayers() const { - if (!this->m_renderWindow) + if (!this->RenderWindow) { return; } - // Synchronize the render window number of layers with its actual number of renderers - int numberOfRenderers = this->m_renderWindow->GetRenderers()->GetNumberOfItems(); + // Synchronize the render window number of layers with its actual number of renderers. + // Traverse the collection with an iterator: vtkCollection::GetItemAsObject walks the collection from its + // first item on every call. int iMax = 0; - for (int iRenderer = 0; iRenderer < numberOfRenderers; iRenderer++) + vtkObject* item = nullptr; + vtkCollectionSimpleIterator it; + for (this->RenderWindow->GetRenderers()->InitTraversal(it); (item = this->RenderWindow->GetRenderers()->GetNextItemAsObject(it));) { - if (auto renderer = vtkRenderer::SafeDownCast(this->m_renderWindow->GetRenderers()->GetItemAsObject(iRenderer))) + if (auto renderer = vtkRenderer::SafeDownCast(item)) { iMax = std::max(iMax, renderer->GetLayer()); } } - this->m_renderWindow->SetNumberOfLayers(iMax + 1); + this->RenderWindow->SetNumberOfLayers(iMax + 1); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::UpdateLayers() { - if (!this->m_renderWindow) + if (!this->RenderWindow) { this->RemoveAllPipelineRenderers(); return; @@ -383,33 +455,46 @@ void vtkMRMLLayerDMLayerManager::UpdateLayers() this->UpdateRenderWindowNumberOfLayers(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::UpdateRendererLayerOrdering() const { // Managed layers are always ordered from layer 1 to the number of managed renderers for (int iRenderer = 0; iRenderer < this->GetNumberOfRenderers(); iRenderer++) { - this->m_renderers[iRenderer]->SetLayer(iRenderer + 1); + this->Renderers[iRenderer]->SetLayer(iRenderer + 1); } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMLayerManager::UpdateRendererCamera() { // Set the camera for the managed renderers // Layer 0 is unmanaged and its camera is left unchanged by the layer manager // Pipelines with no explicit camera map to the default camera // Pipelines with custom camera are grouped and use their cameras - this->m_cameraRendererMap.clear(); + this->CameraRendererMap.clear(); int iRenderer = -1; - for (const auto& pair : m_pipelineLayers) + for (const auto& pair : this->PipelineLayers) { if (iRenderer >= 0 && iRenderer < this->GetNumberOfRenderers()) { auto camera = this->GetCameraForLayer(pair.first, pair.second); - this->m_renderers[iRenderer]->SetActiveCamera(camera); - this->m_cameraRendererMap[camera].emplace(this->m_renderers[iRenderer]); + this->Renderers[iRenderer]->SetActiveCamera(camera); + this->CameraRendererMap[GetCameraId(camera)].emplace_back(this->Renderers[iRenderer]); } iRenderer++; } } + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMLayerManager::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Number of layers: " << this->PipelineLayers.size() << std::endl; + os << indent << "Number of renderers: " << this->Renderers.size() << std::endl; + os << indent << "Number of cameras: " << this->CameraRendererMap.size() << std::endl; + os << indent << "Render window: " << (this->RenderWindow ? "set" : "(none)") << std::endl; + os << indent << "Default camera: " << (this->DefaultCamera ? "set" : "(none)") << std::endl; +} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMLayerManager.h b/LayerDM/MRMLDM/vtkMRMLLayerDMLayerManager.h index 23efc26..2196b51 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMLayerManager.h +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMLayerManager.h @@ -1,8 +1,29 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMLayerManager_h +#define __vtkMRMLLayerDMLayerManager_h #include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" -#include "vtkMRMLLayerDMPipelineI.h" +#include "vtkMRMLLayerDMPipeline.h" // VTK includes #include @@ -16,7 +37,7 @@ #include #include -class vtkMRMLLayerDMPipelineI; +class vtkMRMLLayerDMPipeline; class vtkRenderWindow; class vtkRenderer; class vtkCamera; @@ -40,11 +61,12 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMLaye static vtkMRMLLayerDMLayerManager* New(); vtkTypeMacro(vtkMRMLLayerDMLayerManager, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; /// Adds the pipeline to the layers. /// May change an update of the layer ordering. /// Will trigger the SetRenderer call on the pipeline when it's added to its layer. - void AddPipeline(vtkMRMLLayerDMPipelineI* pipeline); + void AddPipeline(vtkMRMLLayerDMPipeline* pipeline); int GetNumberOfDistinctLayers() const; int GetNumberOfManagedLayers() const; @@ -54,7 +76,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMLaye /// Removes the pipeline from the layers. /// May change the layer ordering if pipeline was the last one of its current renderer. - void RemovePipeline(vtkMRMLLayerDMPipelineI* pipeline); + void RemovePipeline(vtkMRMLLayerDMPipeline* pipeline); /// Iterates over the renderers and resets their clipping range to visible bounds void ResetCameraClippingRange() const; @@ -68,52 +90,55 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMLaye protected: vtkMRMLLayerDMLayerManager(); - ~vtkMRMLLayerDMLayerManager() override = default; + ~vtkMRMLLayerDMLayerManager() override; private: vtkRenderer* GetRendererMatchingKey(const LayerKey& key); vtkRenderer* GetDefaultRenderer() const; void AddMissingLayers(); - static std::array ComputeRenderersVisibleBounds(const std::set>& renderers); + static std::array ComputeRenderersVisibleBounds(const std::vector>& renderers); bool ContainsLayerKey(const LayerKey& key); static std::uintptr_t GetCameraId(vtkCamera* camera); - vtkCamera* GetCameraForLayer(const LayerKey& key, const std::set>& pipelines) const; + vtkCamera* GetCameraForLayer(const LayerKey& key, const std::set& pipelines) const; int GetKeyIndex(const LayerKey& key) const; void RemoveAllLayers(); void RemoveAllPipelineRenderers(); - static void RemovePipelineRenderer(vtkMRMLLayerDMPipelineI* pipeline); + static void RemovePipelineRenderer(vtkMRMLLayerDMPipeline* pipeline); void RemoveOutdatedLayers(); void RemoveOutdatedPipelines(); void RemoveRenderer(const vtkSmartPointer& renderer); - static void ResetRenderersCameraClippingRange(const std::set>& renderers, const std::array& bounds); + static void ResetRenderersCameraClippingRange(const std::vector>& renderers, const std::array& bounds); void SynchronizePipelineRenderers(); void UpdateRenderWindowNumberOfLayers() const; void UpdateLayers(); void UpdateRendererLayerOrdering() const; void UpdateRendererCamera(); - bool AddPipelineLayers(vtkMRMLLayerDMPipelineI* pipeline); - void RemovePipelineLayers(vtkMRMLLayerDMPipelineI* pipeline); + bool AddPipelineLayers(vtkMRMLLayerDMPipeline* pipeline); + void RemovePipelineLayers(vtkMRMLLayerDMPipeline* pipeline); // Map of pipeline layers ordered by ascending - std::map>> m_pipelineLayers; + std::map> PipelineLayers; - /// Pipeline observer listening for \sa vtkMRMLLayerDMPipelineI::RenderGroupingModified events. - vtkSmartPointer m_obs; + // Pipeline observer listening for \sa vtkMRMLLayerDMPipeline::RenderGroupingModified events and pipeline + // destruction. + vtkSmartPointer Observer; // Placeholder empty pipeline with target layer = 0 and camera sync to layer 0 for default renderer - vtkSmartPointer m_emptyPipeline; + vtkSmartPointer EmptyPipeline; // Pointer to the current render window - vtkWeakPointer m_renderWindow; + vtkWeakPointer RenderWindow; // Pointer to the default camera - vtkSmartPointer m_defaultCamera; + vtkSmartPointer DefaultCamera; // Renderers managed by the layer manager - std::vector> m_renderers; + std::vector> Renderers; - // Camera to renderer map - std::map, std::set>> m_cameraRendererMap; + // Camera to renderer map grouped by the camera they are synchronized on. + std::map>> CameraRendererMap; }; + +#endif diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipeline.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMPipeline.cxx new file mode 100644 index 0000000..eb1f731 --- /dev/null +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipeline.cxx @@ -0,0 +1,433 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#include "vtkMRMLLayerDMPipeline.h" + +// Layer DM includes +#include "vtkMRMLAbstractWidget.h" +#include "vtkMRMLLayerDMPipelineManager.h" +#include "vtkMRMLScene.h" +#include "vtkMRMLLayerDMObjectEventObserver.h" + +// VTK includes +#include +#include + +// STD includes +#include + +//----------------------------------------------------------------------------- +vtkStandardNewMacro(vtkMRMLLayerDMPipeline); + +void vtkMRMLLayerDMPipeline::UpdateFromMRML() {} + +void vtkMRMLLayerDMPipeline::OnRendererRemoved(vtkRenderer* renderer) {} + +void vtkMRMLLayerDMPipeline::OnRendererAdded(vtkRenderer* renderer) {} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::SetDisplayNode(vtkMRMLNode* displayNode) +{ + this->UpdateObservation(this->DisplayNode, displayNode); + this->DisplayNode = displayNode; +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::UpdateDisplay() +{ + if (this->IsUpdateDisplayBlocked || !this->ViewNode) + { + return; + } + + // Make sure to avoid looping display update during processing + this->BlockUpdateDisplay(true); + this->UpdateFromMRML(); + this->RequestRender(); + this->BlockUpdateDisplay(false); +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::SetViewNode(vtkMRMLAbstractViewNode* viewNode) +{ + this->UpdateObservation(this->ViewNode, viewNode); + this->ViewNode = viewNode; +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipeline::BlockUpdateDisplay(bool isBlocked) +{ + if (this->Frozen) + { + return true; + } + + bool prev = this->IsUpdateDisplayBlocked; + this->IsUpdateDisplayBlocked = isBlocked; + return prev; +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipeline::BlockInteractionProcessing(bool isBlocked) +{ + if (this->Frozen) + { + return true; + } + + const auto prev = this->InteractionProcessingBlocked; + this->InteractionProcessingBlocked = isBlocked; + return prev; +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipeline::IsInteractionProcessingBlocked() const +{ + return this->InteractionProcessingBlocked; +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipeline::CanProcessInteractionEvent(vtkMRMLInteractionEventData* eventData, double& distance2) +{ + return false; +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipeline::ProcessInteractionEvent(vtkMRMLInteractionEventData* eventData) +{ + return false; +} + +//----------------------------------------------------------------------------- +int vtkMRMLLayerDMPipeline::GetMouseCursor() const +{ + return 0; +} + +//----------------------------------------------------------------------------- +int vtkMRMLLayerDMPipeline::GetWidgetState() const +{ + return vtkMRMLAbstractWidget::WidgetStateIdle; +} + +void vtkMRMLLayerDMPipeline::LoseFocus(vtkMRMLInteractionEventData* eventData) {} + +void vtkMRMLLayerDMPipeline::OnDefaultCameraModified(vtkCamera* camera) {} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::OnReferenceToDisplayNodeAdded(vtkMRMLNode* fromNode, const std::string& role) +{ + this->OnUpdate(this->GetDisplayNode(), vtkMRMLNode::ReferenceAddedEvent, nullptr); +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::OnReferenceToDisplayNodeRemoved(vtkMRMLNode* fromNode, const std::string& role) +{ + this->OnUpdate(this->GetDisplayNode(), vtkMRMLNode::ReferenceRemovedEvent, nullptr); +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipeline::UpdateObservation(vtkObject* prevObj, vtkObject* obj, unsigned long event) const +{ + return this->Observer->UpdateObservation(prevObj, obj, event); +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipeline::UpdateObservation(vtkObject* prevObj, vtkObject* obj, const std::vector& events) const +{ + return this->Observer->UpdateObservation(prevObj, obj, events); +} + +//----------------------------------------------------------------------------- +unsigned int vtkMRMLLayerDMPipeline::GetRenderOrder() const +{ + return 0; +} + +//----------------------------------------------------------------------------- +std::vector vtkMRMLLayerDMPipeline::GetRenderers() const +{ + std::vector renderers; + for (const auto& [_, renderer] : this->RenderersMap) + { + renderers.emplace_back(renderer); + } + return renderers; +} + +//----------------------------------------------------------------------------- +std::vector vtkMRMLLayerDMPipeline::GetRenderOrders() const +{ + return { this->GetRenderOrder() }; +} + +//----------------------------------------------------------------------------- +vtkCamera* vtkMRMLLayerDMPipeline::GetCustomCamera() const +{ + return nullptr; +} + +//----------------------------------------------------------------------------- +vtkCamera* vtkMRMLLayerDMPipeline::GetCustomCamera(unsigned int renderOrder) const +{ + return this->GetCustomCamera(); +} + +//----------------------------------------------------------------------------- +unsigned int vtkMRMLLayerDMPipeline::GetMaxRenderOrder() const +{ + const auto renderOrders = this->GetRenderOrders(); + if (renderOrders.empty()) + { + return 0; + } + return *std::max_element(renderOrders.begin(), renderOrders.end()); +} + +//----------------------------------------------------------------------------- +unsigned int vtkMRMLLayerDMPipeline::GetVtkRendererOrder(const vtkRenderer* renderer) const +{ + if (!renderer) + { + return 0; + } + + for (const auto& [order, mappedRenderers] : this->RenderersMap) + { + if (mappedRenderers.GetPointer() == renderer) + { + return order; + } + } + return 0; +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::SetRenderers(const std::vector& renderers, const std::vector& renderOrders) +{ + if (this->RenderersMatchPipelineRenderers(renderers, renderOrders)) + { + return; + } + + if (renderers.size() != renderOrders.size()) + { + const std::string viewId = this->GetViewNode() ? this->GetViewNode()->GetID() : "null"; + const std::string displayId = this->GetDisplayNode() ? this->GetDisplayNode()->GetID() : "null"; + vtkErrorMacro("Renderer / render order mismatch for DM pipeline ViewNode: " << viewId << ", DisplayNode: " << displayId); + return; + } + + // Remove the previous renderers + for (const auto& [order, renderer] : this->RenderersMap) + { + this->OnRendererRemoved(renderer); + } + this->RenderersMap.clear(); + + // Early return if not currently added to any renderer + if (renderers.empty()) + { + return; + } + + // Add the new renderers + for (size_t i = 0; i < renderers.size(); ++i) + { + this->RenderersMap[renderOrders[i]] = renderers[i]; + this->OnRendererAdded(renderers[i]); + } + this->UpdateDisplay(); +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipeline::RenderersMatchPipelineRenderers(const std::vector& renderers, const std::vector& renderOrders) +{ + if (renderers.size() != this->RenderersMap.size() || renderOrders.size() != this->RenderersMap.size()) + { + return false; + } + + for (size_t i = 0; i < renderOrders.size(); ++i) + { + const auto expectedOrder = renderOrders[i]; + const auto expectedRenderer = renderers[i]; + + if (const auto it = this->RenderersMap.find(expectedOrder); it == this->RenderersMap.end() || it->second.GetPointer() != expectedRenderer) + { + return false; + } + } + return true; +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::SetRenderer(vtkRenderer* renderer) +{ + this->SetRenderers({ renderer }, { this->GetRenderOrder() }); +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::SetScene(vtkMRMLScene* scene) +{ + this->Scene = scene; +} + +//----------------------------------------------------------------------------- +vtkMRMLLayerDMPipeline* vtkMRMLLayerDMPipeline::GetNodePipeline(vtkMRMLNode* node) const +{ + if (!this->PipelineManager) + { + return nullptr; + } + return this->PipelineManager->GetNodePipeline(node); +} + +//----------------------------------------------------------------------------- +vtkMRMLLayerDMPipelineManager* vtkMRMLLayerDMPipeline::GetPipelineManager() const +{ + return this->PipelineManager; +} + +//----------------------------------------------------------------------------- +vtkMRMLAbstractViewNode* vtkMRMLLayerDMPipeline::GetViewNode() const +{ + return this->ViewNode; +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipeline::BlockUpdateObserver(bool isBlocked) const +{ + if (this->Frozen) + { + return true; + } + + return this->Observer->SetBlocked(isBlocked); +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipeline::IsUpdateObserverBlocked() const +{ + return this->Observer->IsBlocked(); +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::SetFrozen(bool isFrozen) +{ + if (this->Frozen == isFrozen) + { + return; + } + + // Block states are only updated when the pipeline is not frozen. + // Unfreeze to update all before saving the frozen state. + this->Frozen = false; + this->BlockInteractionProcessing(isFrozen); + this->BlockUpdateDisplay(isFrozen); + this->BlockUpdateObserver(isFrozen); + this->Frozen = isFrozen; +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipeline::IsFrozen() const +{ + return this->Frozen; +} + +//----------------------------------------------------------------------------- +vtkMRMLNode* vtkMRMLLayerDMPipeline::GetDisplayNode() const +{ + return this->DisplayNode; +} + +//----------------------------------------------------------------------------- +vtkRenderer* vtkMRMLLayerDMPipeline::GetRenderer() const +{ + if (this->RenderersMap.empty()) + { + return nullptr; + } + return this->RenderersMap.begin()->second; +} + +//----------------------------------------------------------------------------- +vtkRenderer* vtkMRMLLayerDMPipeline::GetRenderer(unsigned int renderOrder) const +{ + const auto it = this->RenderersMap.find(renderOrder); + if (it == std::end(this->RenderersMap)) + { + return nullptr; + } + return it->second; +} + +//----------------------------------------------------------------------------- +vtkMRMLScene* vtkMRMLLayerDMPipeline::GetScene() const +{ + return this->Scene; +} + +void vtkMRMLLayerDMPipeline::OnUpdate(vtkObject* obj, unsigned long eventId, void* callData) {} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::RemoveObservations(vtkObject* prevObj) const +{ + this->Observer->RemoveObservations(prevObj); +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::RequestRender() const +{ + if (this->PipelineManager) + { + this->PipelineManager->RequestRender(); + } +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::SetPipelineManager(vtkMRMLLayerDMPipelineManager* pipelineManager) +{ + this->PipelineManager = pipelineManager; +} + +//----------------------------------------------------------------------------- +vtkMRMLLayerDMPipeline::vtkMRMLLayerDMPipeline() + : IsUpdateDisplayBlocked{ false } + , Frozen{ false } + , InteractionProcessingBlocked{ false } + , Observer(vtkSmartPointer::New()) +{ + this->Observer->SetUpdateCallback([this](vtkObject* obj, unsigned long eventId, void* callData) { this->OnUpdate(obj, eventId, callData); }); +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipeline::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "View node: " << (this->ViewNode ? "set" : "(none)") << std::endl; + os << indent << "Display node: " << (this->DisplayNode ? "set" : "(none)") << std::endl; + os << indent << "Scene: " << (this->Scene ? "set" : "(none)") << std::endl; + os << indent << "Pipeline manager: " << (this->PipelineManager ? "set" : "(none)") << std::endl; + os << indent << "Number of renderers: " << this->RenderersMap.size() << std::endl; + os << indent << "Update display blocked: " << (this->IsUpdateDisplayBlocked ? "true" : "false") << std::endl; + os << indent << "Frozen: " << (this->Frozen ? "true" : "false") << std::endl; + os << indent << "Interaction processing blocked: " << (this->InteractionProcessingBlocked ? "true" : "false") << std::endl; +} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineI.h b/LayerDM/MRMLDM/vtkMRMLLayerDMPipeline.h similarity index 78% rename from LayerDM/MRMLDM/vtkMRMLLayerDMPipelineI.h rename to LayerDM/MRMLDM/vtkMRMLLayerDMPipeline.h index e00760d..5870ee0 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineI.h +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipeline.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMPipeline_h +#define __vtkMRMLLayerDMPipeline_h #include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" @@ -12,7 +33,7 @@ class vtkCamera; class vtkMRMLAbstractViewNode; class vtkMRMLInteractionEventData; -class vtkMRMLLayerDMPipelineI; +class vtkMRMLLayerDMPipeline; class vtkMRMLLayerDMPipelineManager; class vtkMRMLNode; class vtkMRMLScene; @@ -22,18 +43,19 @@ class vtkRenderer; /// \brief Interface for the layered displayable manager pipelines. /// /// Contains empty implementation and default behavior for the different API calls. -/// Implementation can be limited to \sa UpdatePipeline and reactivity on node changes for pure +/// Implementation can be limited to \sa UpdateFromMRML and reactivity on node changes for pure /// display pipelines. /// /// Widget pipelines should also implement the \sa CanProcessInteractionEvent and \sa ProcessInteractionEvent /// methods. /// /// A python main class is available from \sa vtkMRMLLayerDMScriptedPipeline.py -class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipelineI : public vtkObject +class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipeline : public vtkObject { public: - static vtkMRMLLayerDMPipelineI* New(); - vtkTypeMacro(vtkMRMLLayerDMPipelineI, vtkObject); + static vtkMRMLLayerDMPipeline* New(); + vtkTypeMacro(vtkMRMLLayerDMPipeline, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; enum Events { @@ -117,7 +139,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe /// Set the display node for the pipeline has changed (initialization). /// default behavior: Stored and display node is observed for vtkCommand::ModifiedEvent. - /// \sa UpdateObserver + /// \sa UpdateObservation /// \sa OnUpdate virtual void SetDisplayNode(vtkMRMLNode* displayNode); @@ -135,12 +157,12 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe /// \param viewNode: The instance of viewNode the pipeline is attached to virtual void SetViewNode(vtkMRMLAbstractViewNode* viewNode); - /// Triggered on \sa ResetDisplay calls + /// Triggered on \sa UpdateDisplay calls /// default behavior: does nothing. - virtual void UpdatePipeline(); + virtual void UpdateFromMRML(); - /// If \param isBlocked is true, \sa UpdatePipeline is not called during \sa ResetDisplay. - bool BlockResetDisplay(bool isBlocked); + /// If \param isBlocked is true, \sa UpdateFromMRML is not called during \sa UpdateDisplay. + bool BlockUpdateDisplay(bool isBlocked); /// @{ /// If \param isBlocked is true, blocks \sa CanProcessInteractionEvent and \sa ProcessInteractionEvent to be called. @@ -156,11 +178,11 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe /// @} /// @{ - /// If \param isFrozen is true, blocks all reactiveness from the pipeline (ResetDisplay, Interaction and Update). + /// If \param isFrozen is true, blocks all reactiveness from the pipeline (UpdateDisplay, Interaction and Update). /// Reactiveness cannot be toggled back on unless the pipeline is unfrozen first. /// Used to deactivate pipelines during removal. /// - /// \sa BlockResetDisplay \sa BlockInteractionProcessing \sa BlockUpdateObserver + /// \sa BlockUpdateDisplay \sa BlockInteractionProcessing \sa BlockUpdateObserver void SetFrozen(bool isFrozen); bool IsFrozen() const; /// @} @@ -171,7 +193,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe /// Returns the pipeline associated with the input node. /// Delegates to \sa vtkMRMLLayerDMPipelineManager::GetNodePipeline. /// nullptr if not found or pipelineManager instance is nullptr. - vtkMRMLLayerDMPipelineI* GetNodePipeline(vtkMRMLNode* node) const; + vtkMRMLLayerDMPipeline* GetNodePipeline(vtkMRMLNode* node) const; /// Returns the instance of pipeline manager which created the pipeline. vtkMRMLLayerDMPipelineManager* GetPipelineManager() const; @@ -205,26 +227,26 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe /// On event triggered, calls \sa OnUpdate /// /// \warning prevObj is not mutated by this call. To update the pointer, a manual set is required after update. - bool UpdateObserver(vtkObject* prevObj, vtkObject* obj, const std::vector& events) const; - bool UpdateObserver(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent) const; + bool UpdateObservation(vtkObject* prevObj, vtkObject* obj, const std::vector& events) const; + bool UpdateObservation(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent) const; /// @} /// Remove all observed events for the input object. - /// For updating the observer, use \sa UpdateObserver instead. + /// For updating the observer, use \sa UpdateObservation instead. /// /// \warning prevObj is not mutated by this call. - void RemoveObserver(vtkObject* prevObj) const; + void RemoveObservations(vtkObject* prevObj) const; /// Request rendering and camera clipping reset. /// Calls are delegated to \sa vtkMRMLLayerDMPipelineManager::RequestRender. /// - /// \sa ResetDisplay + /// \sa UpdateDisplay void RequestRender() const; - /// Resets the pipeline display and request a new render \sa RequestRender. - /// Delegates actual work to \sa UpdatePipeline. + /// Updates the pipeline display and requests a new render \sa RequestRender. + /// Delegates actual work to \sa UpdateFromMRML. /// Called the first time after pipeline initialization. - void ResetDisplay(); + void UpdateDisplay(); /// Set the new renderers. /// Triggers \sa OnRendererAdded and \sa OnRendererRemoved. @@ -239,23 +261,25 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe void SetRenderer(vtkRenderer* renderer); protected: - vtkMRMLLayerDMPipelineI(); - ~vtkMRMLLayerDMPipelineI() override = default; + vtkMRMLLayerDMPipeline(); + ~vtkMRMLLayerDMPipeline() override = default; /// Observer update callback. - /// Triggered when any object & events observed using UpdateObserver is triggered. + /// Triggered when any object & events observed using UpdateObservation is triggered. virtual void OnUpdate(vtkObject* obj, unsigned long eventId, void* callData); private: bool RenderersMatchPipelineRenderers(const std::vector& renderers, const std::vector& renderOrders); - vtkWeakPointer m_viewNode; - vtkWeakPointer m_displayNode; - std::map> m_renderersMap; - bool m_isResetDisplayBlocked; - bool m_isFrozen; - bool m_isInteractionProcessingBlocked; - vtkSmartPointer m_obs; - vtkWeakPointer m_pipelineManager; - vtkWeakPointer m_scene; + vtkWeakPointer ViewNode; + vtkWeakPointer DisplayNode; + std::map> RenderersMap; + bool IsUpdateDisplayBlocked; + bool Frozen; + bool InteractionProcessingBlocked; + vtkSmartPointer Observer; + vtkWeakPointer PipelineManager; + vtkWeakPointer Scene; }; + +#endif diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCallbackCreator.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCallbackCreator.cxx index fd63aae..32d552d 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCallbackCreator.cxx +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCallbackCreator.cxx @@ -1,23 +1,53 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMPipelineCallbackCreator.h" // Layer DM includes -#include "vtkMRMLLayerDMPipelineI.h" +#include "vtkMRMLLayerDMPipeline.h" // VTK includes #include #include +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMPipelineCallbackCreator); -vtkSmartPointer vtkMRMLLayerDMPipelineCallbackCreator::CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) const +//----------------------------------------------------------------------------- +vtkSmartPointer vtkMRMLLayerDMPipelineCallbackCreator::CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) const { - return this->m_callback(viewNode, node); + return this->Callback(viewNode, node); } -void vtkMRMLLayerDMPipelineCallbackCreator::SetCallback(const std::function(vtkMRMLAbstractViewNode*, vtkMRMLNode*)>& callback) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipelineCallbackCreator::SetCallback(const std::function(vtkMRMLAbstractViewNode*, vtkMRMLNode*)>& callback) { - this->m_callback = callback; + this->Callback = callback; } vtkMRMLLayerDMPipelineCallbackCreator::vtkMRMLLayerDMPipelineCallbackCreator() - : m_callback([](vtkMRMLAbstractViewNode*, vtkMRMLNode*) { return nullptr; }) {}; + : Callback([](vtkMRMLAbstractViewNode*, vtkMRMLNode*) { return nullptr; }) {}; + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipelineCallbackCreator::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Callback: " << (this->Callback ? "set" : "(none)") << std::endl; +} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCallbackCreator.h b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCallbackCreator.h index c21e40b..54a58be 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCallbackCreator.h +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCallbackCreator.h @@ -1,8 +1,29 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMPipelineCallbackCreator_h +#define __vtkMRMLLayerDMPipelineCallbackCreator_h #include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" -#include "vtkMRMLLayerDMPipelineCreatorI.h" +#include "vtkMRMLLayerDMPipelineCreator.h" // STL includes #include @@ -12,19 +33,22 @@ /// /// \sa vtkMRMLLayerDMPipelineScriptedCreator /// \sa vtkMRMLLayerDMPipelineFactory::AddPipelineCreator -class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipelineCallbackCreator : public vtkMRMLLayerDMPipelineCreatorI +class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipelineCallbackCreator : public vtkMRMLLayerDMPipelineCreator { public: static vtkMRMLLayerDMPipelineCallbackCreator* New(); - vtkTypeMacro(vtkMRMLLayerDMPipelineCallbackCreator, vtkMRMLLayerDMPipelineCreatorI); + vtkTypeMacro(vtkMRMLLayerDMPipelineCallbackCreator, vtkMRMLLayerDMPipelineCreator); + void PrintSelf(ostream& os, vtkIndent indent) override; - vtkSmartPointer CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) const override; - void SetCallback(const std::function(vtkMRMLAbstractViewNode*, vtkMRMLNode*)>& callback); + vtkSmartPointer CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) const override; + void SetCallback(const std::function(vtkMRMLAbstractViewNode*, vtkMRMLNode*)>& callback); protected: vtkMRMLLayerDMPipelineCallbackCreator(); ~vtkMRMLLayerDMPipelineCallbackCreator() override = default; private: - std::function(vtkMRMLAbstractViewNode*, vtkMRMLNode*)> m_callback; + std::function(vtkMRMLAbstractViewNode*, vtkMRMLNode*)> Callback; }; + +#endif diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreateHelper.h b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreateHelper.h index 61f74ff..83e32e2 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreateHelper.h +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreateHelper.h @@ -1,7 +1,28 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMPipelineCreateHelper_h +#define __vtkMRMLLayerDMPipelineCreateHelper_h // Layer DM includes -#include "vtkMRMLLayerDMPipelineI.h" +#include "vtkMRMLLayerDMPipeline.h" // VTK includes #include @@ -12,7 +33,7 @@ namespace layer_dm /// Supports variadic calls with triplets /// \sa TryCreate template -vtkSmartPointer TryCreateForView(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) +vtkSmartPointer TryCreateForView(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) { if (TExpView::SafeDownCast(viewNode) && TExpNode::SafeDownCast(node)) { @@ -32,7 +53,7 @@ vtkSmartPointer TryCreateForView(vtkMRMLAbstractViewNod /// Supports variadic calls with triplets /// \sa TryCreateForView template -vtkSmartPointer TryCreate(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) +vtkSmartPointer TryCreate(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) { if (auto pipeline = TryCreateForView(viewNode, node)) { @@ -45,3 +66,5 @@ vtkSmartPointer TryCreate(vtkMRMLAbstractViewNode* view return nullptr; } }; // namespace layer_dm + +#endif diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreator.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreator.cxx new file mode 100644 index 0000000..61f80d9 --- /dev/null +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreator.cxx @@ -0,0 +1,41 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#include "vtkMRMLLayerDMPipelineCreator.h" + +// VTK includes +#include +#include + +//----------------------------------------------------------------------------- +vtkStandardNewMacro(vtkMRMLLayerDMPipelineCreator); + +//----------------------------------------------------------------------------- +vtkSmartPointer vtkMRMLLayerDMPipelineCreator::CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) const +{ + return {}; +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipelineCreator::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Priority: " << this->Priority << std::endl; +} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreator.h b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreator.h new file mode 100644 index 0000000..316ddb7 --- /dev/null +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreator.h @@ -0,0 +1,61 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMPipelineCreator_h +#define __vtkMRMLLayerDMPipelineCreator_h + +#include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" + +#include "vtkMRMLLayerDMPipeline.h" + +// VTK includes +#include + +class vtkMRMLAbstractViewNode; +class vtkMRMLNode; + +/// \brief Interface responsible for creating new pipelines given input pairs of viewNode and node. +/// +/// \sa vtkMRMLLayerDMPipelineCallbackCreator +/// \sa vtkMRMLLayerDMPipelineFactory::AddPipelineCreator +class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipelineCreator : public vtkObject +{ +public: + static vtkMRMLLayerDMPipelineCreator* New(); + vtkTypeMacro(vtkMRMLLayerDMPipelineCreator, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; + + virtual vtkSmartPointer CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) const; + + /// @{ + /// \brief Get/Set the priority of creation of the pipeline creator. + /// Pipelines with higher priority will be allowed to handle creation first (allowing to supplant existing creators). + vtkGetMacro(Priority, int); + vtkSetMacro(Priority, int); + /// @} +protected: + vtkMRMLLayerDMPipelineCreator() = default; + ~vtkMRMLLayerDMPipelineCreator() override = default; + +private: + int Priority = 0; +}; + +#endif diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreatorI.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreatorI.cxx deleted file mode 100644 index 26d36de..0000000 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreatorI.cxx +++ /dev/null @@ -1,12 +0,0 @@ -#include "vtkMRMLLayerDMPipelineCreatorI.h" - -// VTK includes -#include -#include - -vtkStandardNewMacro(vtkMRMLLayerDMPipelineCreatorI); - -vtkSmartPointer vtkMRMLLayerDMPipelineCreatorI::CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) const -{ - return {}; -} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreatorI.h b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreatorI.h deleted file mode 100644 index 6750f3d..0000000 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineCreatorI.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" - -#include "vtkMRMLLayerDMPipelineI.h" - -// VTK includes -#include - -class vtkMRMLAbstractViewNode; -class vtkMRMLNode; - -/// \brief Interface responsible for creating new pipelines given input pairs of viewNode and node. -/// -/// \sa vtkMRMLLayerDMPipelineCallbackCreator -/// \sa vtkMRMLLayerDMPipelineFactory::AddPipelineCreator -class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipelineCreatorI : public vtkObject -{ -public: - static vtkMRMLLayerDMPipelineCreatorI* New(); - vtkTypeMacro(vtkMRMLLayerDMPipelineCreatorI, vtkObject); - - virtual vtkSmartPointer CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) const; - - /// @{ - /// \brief Get/Set the priority of creation of the pipeline creator. - /// Pipelines with higher priority will be allowed to handle creation first (allowing to supplant existing creators). - vtkGetMacro(Priority, int); - vtkSetMacro(Priority, int); - /// @} -protected: - vtkMRMLLayerDMPipelineCreatorI() = default; - ~vtkMRMLLayerDMPipelineCreatorI() override = default; - -private: - int Priority = 0; -}; diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineFactory.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineFactory.cxx index 9004ef7..8954179 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineFactory.cxx +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineFactory.cxx @@ -1,38 +1,71 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMPipelineFactory.h" // Layer DM includes -#include "vtkMRMLLayerDMPipelineCreatorI.h" +#include "vtkMRMLLayerDMPipelineCreator.h" #include "vtkMRMLLayerDMPipelineCallbackCreator.h" -#include "vtkMRMLLayerDMPipelineI.h" +#include "vtkMRMLLayerDMPipeline.h" #include "vtkMRMLLayerDMObjectEventObserver.h" // VTK includes #include #include +// STD includes +#include + +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMPipelineFactory); +//----------------------------------------------------------------------------- vtkSmartPointer vtkMRMLLayerDMPipelineFactory::GetInstance() { static vtkSmartPointer instance = vtkSmartPointer::New(); return instance; } -void vtkMRMLLayerDMPipelineFactory::AddPipelineCreator(const vtkSmartPointer& creator) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipelineFactory::AddPipelineCreator(const vtkSmartPointer& creator) { + if (!creator) + { + vtkErrorMacro("AddPipelineCreator: the pipeline creator is null."); + return; + } + if (this->ContainsPipelineCreator(creator)) { return; } - this->m_obs->UpdateObserver(nullptr, creator); - this->m_pipelineCreators.emplace_back(creator); + this->Observer->UpdateObservation(nullptr, creator); + this->PipelineCreators.emplace_back(creator); this->SortPipelineCreators(); this->InvokeEvent(vtkCommand::ModifiedEvent); } -vtkSmartPointer vtkMRMLLayerDMPipelineFactory::AddPipelineCreator( - const std::function(vtkMRMLAbstractViewNode*, vtkMRMLNode*)>& creatorCallBack, +//----------------------------------------------------------------------------- +vtkSmartPointer vtkMRMLLayerDMPipelineFactory::AddPipelineCreator( + const std::function(vtkMRMLAbstractViewNode*, vtkMRMLNode*)>& creatorCallBack, int priority) { auto creator = vtkSmartPointer::New(); @@ -42,34 +75,47 @@ vtkSmartPointer vtkMRMLLayerDMPipelineFactory::A return creator; } -void vtkMRMLLayerDMPipelineFactory::RemovePipelineCreator(const vtkSmartPointer& creator) +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipelineFactory::RemovePipelineCreator(const vtkSmartPointer& creator) { - this->m_obs->RemoveObserver(creator); - size_t prevSize = this->m_pipelineCreators.size(); - this->m_pipelineCreators.erase(std::remove_if(this->m_pipelineCreators.begin(), - this->m_pipelineCreators.end(), - [creator](const vtkSmartPointer& value) { return value == creator; }), - this->m_pipelineCreators.end()); - if (this->m_pipelineCreators.size() != prevSize) + this->Observer->RemoveObservations(creator); + size_t prevSize = this->PipelineCreators.size(); + this->PipelineCreators.erase(std::remove_if(this->PipelineCreators.begin(), + this->PipelineCreators.end(), + [creator](const vtkSmartPointer& value) { return value == creator; }), + this->PipelineCreators.end()); + if (this->PipelineCreators.size() != prevSize) { this->InvokeEvent(vtkCommand::ModifiedEvent); } } -bool vtkMRMLLayerDMPipelineFactory::ContainsPipelineCreator(const vtkSmartPointer& creator) const +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipelineFactory::ContainsPipelineCreator(const vtkSmartPointer& creator) const { - return std::find(this->m_pipelineCreators.begin(), this->m_pipelineCreators.end(), creator) != this->m_pipelineCreators.end(); + return std::find(this->PipelineCreators.begin(), this->PipelineCreators.end(), creator) != this->PipelineCreators.end(); } -vtkSmartPointer vtkMRMLLayerDMPipelineFactory::CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) +//----------------------------------------------------------------------------- +vtkSmartPointer vtkMRMLLayerDMPipelineFactory::CreatePipeline(vtkMRMLAbstractViewNode* viewNode, + vtkMRMLNode* node, + vtkSmartPointer* creator) { - for (const auto& ctor : m_pipelineCreators) + if (creator) + { + *creator = nullptr; + } + for (const auto& ctor : this->PipelineCreators) { if (auto created = ctor->CreatePipeline(viewNode, node)) { - this->m_lastView = viewNode; - this->m_lastNode = node; - this->m_lastPipeline = created; + this->LastView = viewNode; + this->LastNode = node; + this->LastPipeline = created; + if (creator) + { + *creator = ctor; + } this->InvokeEvent(PipelineAboutToBeCreatedEvent); return created; } @@ -78,45 +124,59 @@ vtkSmartPointer vtkMRMLLayerDMPipelineFactory::CreatePi return {}; } +//----------------------------------------------------------------------------- +vtkSmartPointer vtkMRMLLayerDMPipelineFactory::CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) +{ + return CreatePipeline(viewNode, node, nullptr); +} + +//----------------------------------------------------------------------------- vtkMRMLAbstractViewNode* vtkMRMLLayerDMPipelineFactory::GetLastViewNode() const { - return this->m_lastView; + return this->LastView; } +//----------------------------------------------------------------------------- vtkMRMLNode* vtkMRMLLayerDMPipelineFactory::GetLastNode() const { - { - return this->m_lastNode; - } + return this->LastNode; } -vtkMRMLLayerDMPipelineI* vtkMRMLLayerDMPipelineFactory::GetLastPipeline() const +//----------------------------------------------------------------------------- +vtkMRMLLayerDMPipeline* vtkMRMLLayerDMPipelineFactory::GetLastPipeline() const { - { - return this->m_lastPipeline; - } + return this->LastPipeline; } vtkMRMLLayerDMPipelineFactory::vtkMRMLLayerDMPipelineFactory() - : m_obs(vtkSmartPointer::New()) - , m_lastView(nullptr) - , m_lastNode(nullptr) - , m_lastPipeline(nullptr) + : Observer(vtkSmartPointer::New()) { - m_obs->SetUpdateCallback([this](vtkObject* node) { this->SortPipelineCreators(); }); + this->Observer->SetUpdateCallback([this](vtkObject* node) { this->SortPipelineCreators(); }); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineFactory::SortPipelineCreators() { - std::sort(std::begin(m_pipelineCreators), - std::end(m_pipelineCreators), - [](const vtkSmartPointer& a, const vtkSmartPointer& b) + std::sort(std::begin(this->PipelineCreators), + std::end(this->PipelineCreators), + [](const vtkSmartPointer& a, const vtkSmartPointer& b) { + // Order null creators last if (!a || !b) { - return true; + return a && !b; } return a->GetPriority() > b->GetPriority(); }); } + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipelineFactory::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Number of pipeline creators: " << this->PipelineCreators.size() << std::endl; + os << indent << "Last view node: " << (this->LastView ? "set" : "(none)") << std::endl; + os << indent << "Last node: " << (this->LastNode ? "set" : "(none)") << std::endl; + os << indent << "Last pipeline: " << (this->LastPipeline ? "set" : "(none)") << std::endl; +} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineFactory.h b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineFactory.h index 136920b..53cb8ad 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineFactory.h +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineFactory.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMPipelineFactory_h +#define __vtkMRMLLayerDMPipelineFactory_h #include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" @@ -14,13 +35,13 @@ class vtkMRMLAbstractViewNode; class vtkMRMLLayerDMObjectEventObserver; -class vtkMRMLLayerDMPipelineCreatorI; -class vtkMRMLLayerDMPipelineI; +class vtkMRMLLayerDMPipelineCreator; +class vtkMRMLLayerDMPipeline; class vtkMRMLNode; /// \brief Class responsible for creating new pipelines given input viewNode and Node pairs. /// -/// Delegates creation to its list of \sa vtkMRMLLayerDMPipelineCreatorI. +/// Delegates creation to its list of \sa vtkMRMLLayerDMPipelineCreator. /// Early returns when a first creator capable of handling the input is found. class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipelineFactory : public vtkObject { @@ -33,6 +54,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe static vtkMRMLLayerDMPipelineFactory* New(); vtkTypeMacro(vtkMRMLLayerDMPipelineFactory, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; /// \brief Singleton instance of the factory used by the displayable manager static vtkSmartPointer GetInstance(); @@ -40,38 +62,42 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe /// \brief Add the input creator to the list of creators. /// If the factory already contains the creator, does nothing. /// Invokes vtkCommand::ModifiedEvent if the factory is modified. - void AddPipelineCreator(const vtkSmartPointer& creator); + void AddPipelineCreator(const vtkSmartPointer& creator); /// Convenience method to add creator callback /// Delegates to \sa vtkMRMLLayerDMPipelineCallbackCreator and returns the creator instance. /// /// \param priority: Creator priority value (default=0) higher indicates priority on handling the creation call - vtkSmartPointer AddPipelineCreator( - const std::function(vtkMRMLAbstractViewNode*, vtkMRMLNode*)>& creatorCallBack, + vtkSmartPointer AddPipelineCreator( + const std::function(vtkMRMLAbstractViewNode*, vtkMRMLNode*)>& creatorCallBack, int priority = 0); /// \brief Remove the input creator from the list of creators. /// If the factory doesn't contain the creator, does nothing. /// Invokes vtkCommand::ModifiedEvent if the factory is modified. - void RemovePipelineCreator(const vtkSmartPointer& creator); + void RemovePipelineCreator(const vtkSmartPointer& creator); /// true if the given creator is contained in the factory, false otherwise. - bool ContainsPipelineCreator(const vtkSmartPointer& creator) const; + bool ContainsPipelineCreator(const vtkSmartPointer& creator) const; + /// @{ /// Tries to create a new pipeline given input viewNode and node by iterating on its creators. /// Returns nullptr if no creator was able to create a pipeline. + /// If creator is not nullptr, it is set to the creator that created the pipeline. /// Invokes PipelineAboutToBeCreatedEvent before returning the newly created pipeline instance. /// \sa GetLastViewNode /// \sa GetLastNode /// \sa GetLastPipeline - vtkSmartPointer CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node); + vtkSmartPointer CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node, vtkSmartPointer* creator); + vtkSmartPointer CreatePipeline(vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node); + /// @} /// @{ /// Get the last pipeline created by the factory. /// Values are valid when the PipelineAboutToBeCreatedEvent event is triggered. vtkMRMLAbstractViewNode* GetLastViewNode() const; vtkMRMLNode* GetLastNode() const; - vtkMRMLLayerDMPipelineI* GetLastPipeline() const; + vtkMRMLLayerDMPipeline* GetLastPipeline() const; /// @} protected: @@ -83,9 +109,11 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe /// Updated when new creators are added / removed or when creators modified events are triggered. void SortPipelineCreators(); - std::vector> m_pipelineCreators; - vtkSmartPointer m_obs; - vtkWeakPointer m_lastView; - vtkWeakPointer m_lastNode; - vtkWeakPointer m_lastPipeline; + std::vector> PipelineCreators; + vtkSmartPointer Observer; + vtkWeakPointer LastView; + vtkWeakPointer LastNode; + vtkWeakPointer LastPipeline; }; + +#endif diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineI.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineI.cxx deleted file mode 100644 index 08a01de..0000000 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineI.cxx +++ /dev/null @@ -1,359 +0,0 @@ -#include "vtkMRMLLayerDMPipelineI.h" - -// Layer DM includes -#include "vtkMRMLAbstractWidget.h" -#include "vtkMRMLLayerDMPipelineManager.h" -#include "vtkMRMLScene.h" -#include "vtkMRMLLayerDMObjectEventObserver.h" - -// VTK includes -#include -#include - -vtkStandardNewMacro(vtkMRMLLayerDMPipelineI); - -void vtkMRMLLayerDMPipelineI::UpdatePipeline() {} - -void vtkMRMLLayerDMPipelineI::OnRendererRemoved(vtkRenderer* renderer) {} - -void vtkMRMLLayerDMPipelineI::OnRendererAdded(vtkRenderer* renderer) {} - -void vtkMRMLLayerDMPipelineI::SetDisplayNode(vtkMRMLNode* displayNode) -{ - this->UpdateObserver(this->m_displayNode, displayNode); - this->m_displayNode = displayNode; -} - -void vtkMRMLLayerDMPipelineI::ResetDisplay() -{ - if (this->m_isResetDisplayBlocked || !this->m_viewNode) - { - return; - } - - // Make sure to avoid looping reset display during processing - this->BlockResetDisplay(true); - this->UpdatePipeline(); - this->RequestRender(); - this->BlockResetDisplay(false); -} - -void vtkMRMLLayerDMPipelineI::SetViewNode(vtkMRMLAbstractViewNode* viewNode) -{ - this->UpdateObserver(this->m_viewNode, viewNode); - this->m_viewNode = viewNode; -} - -bool vtkMRMLLayerDMPipelineI::BlockResetDisplay(bool isBlocked) -{ - if (this->m_isFrozen) - { - return true; - } - - bool prev = this->m_isResetDisplayBlocked; - this->m_isResetDisplayBlocked = isBlocked; - return prev; -} - -bool vtkMRMLLayerDMPipelineI::BlockInteractionProcessing(bool isBlocked) -{ - if (this->m_isFrozen) - { - return true; - } - - const auto prev = this->m_isInteractionProcessingBlocked; - this->m_isInteractionProcessingBlocked = isBlocked; - return prev; -} - -bool vtkMRMLLayerDMPipelineI::IsInteractionProcessingBlocked() const -{ - return this->m_isInteractionProcessingBlocked; -} - -bool vtkMRMLLayerDMPipelineI::CanProcessInteractionEvent(vtkMRMLInteractionEventData* eventData, double& distance2) -{ - return false; -} - -bool vtkMRMLLayerDMPipelineI::ProcessInteractionEvent(vtkMRMLInteractionEventData* eventData) -{ - return false; -} - -int vtkMRMLLayerDMPipelineI::GetMouseCursor() const -{ - return 0; -} - -int vtkMRMLLayerDMPipelineI::GetWidgetState() const -{ - return vtkMRMLAbstractWidget::WidgetStateIdle; -} - -void vtkMRMLLayerDMPipelineI::LoseFocus(vtkMRMLInteractionEventData* eventData) {} - -void vtkMRMLLayerDMPipelineI::OnDefaultCameraModified(vtkCamera* camera) {} - -void vtkMRMLLayerDMPipelineI::OnReferenceToDisplayNodeAdded(vtkMRMLNode* fromNode, const std::string& role) -{ - this->OnUpdate(this->GetDisplayNode(), vtkMRMLNode::ReferenceAddedEvent, nullptr); -} - -void vtkMRMLLayerDMPipelineI::OnReferenceToDisplayNodeRemoved(vtkMRMLNode* fromNode, const std::string& role) -{ - this->OnUpdate(this->GetDisplayNode(), vtkMRMLNode::ReferenceRemovedEvent, nullptr); -} - -bool vtkMRMLLayerDMPipelineI::UpdateObserver(vtkObject* prevObj, vtkObject* obj, unsigned long event) const -{ - return this->m_obs->UpdateObserver(prevObj, obj, event); -} - -bool vtkMRMLLayerDMPipelineI::UpdateObserver(vtkObject* prevObj, vtkObject* obj, const std::vector& events) const -{ - return this->m_obs->UpdateObserver(prevObj, obj, events); -} - -unsigned int vtkMRMLLayerDMPipelineI::GetRenderOrder() const -{ - return 0; -} - -std::vector vtkMRMLLayerDMPipelineI::GetRenderers() const -{ - std::vector renderers; - for (const auto& [_, renderer] : this->m_renderersMap) - { - renderers.emplace_back(renderer); - } - return renderers; -} - -std::vector vtkMRMLLayerDMPipelineI::GetRenderOrders() const -{ - return { this->GetRenderOrder() }; -} - -vtkCamera* vtkMRMLLayerDMPipelineI::GetCustomCamera() const -{ - return nullptr; -} - -vtkCamera* vtkMRMLLayerDMPipelineI::GetCustomCamera(unsigned int renderOrder) const -{ - return GetCustomCamera(); -} - -unsigned int vtkMRMLLayerDMPipelineI::GetMaxRenderOrder() const -{ - const auto renderOrders = GetRenderOrders(); - if (renderOrders.empty()) - { - return 0; - } - return *std::max_element(renderOrders.begin(), renderOrders.end()); -} - -unsigned int vtkMRMLLayerDMPipelineI::GetVtkRendererOrder(const vtkRenderer* renderer) const -{ - if (!renderer) - { - return 0; - } - - for (const auto& [order, mappedRenderers] : this->m_renderersMap) - { - if (mappedRenderers.GetPointer() == renderer) - { - return order; - } - } - return 0; -} - -void vtkMRMLLayerDMPipelineI::SetRenderers(const std::vector& renderers, const std::vector& renderOrders) -{ - if (this->RenderersMatchPipelineRenderers(renderers, renderOrders)) - { - return; - } - - if (renderers.size() != renderOrders.size()) - { - const std::string viewId = this->GetViewNode() ? this->GetViewNode()->GetID() : "null"; - const std::string displayId = this->GetDisplayNode() ? this->GetDisplayNode()->GetID() : "null"; - vtkErrorMacro("Renderer / render order mismatch for DM pipeline ViewNode: " << viewId << ", DisplayNode: " << displayId); - return; - } - - // Remove the previous renderers - for (const auto& [order, renderer] : this->m_renderersMap) - { - this->OnRendererRemoved(renderer); - } - this->m_renderersMap.clear(); - - // Early return if not currently added to any renderer - if (renderers.empty()) - { - return; - } - - // Add the new renderers - for (size_t i = 0; i < renderers.size(); ++i) - { - this->m_renderersMap[renderOrders[i]] = renderers[i]; - this->OnRendererAdded(renderers[i]); - } - this->ResetDisplay(); -} - -bool vtkMRMLLayerDMPipelineI::RenderersMatchPipelineRenderers(const std::vector& renderers, const std::vector& renderOrders) -{ - if (renderers.size() != this->m_renderersMap.size() || renderOrders.size() != this->m_renderersMap.size()) - { - return false; - } - - for (size_t i = 0; i < renderOrders.size(); ++i) - { - const auto expectedOrder = renderOrders[i]; - const auto expectedRenderer = renderers[i]; - - if (const auto it = this->m_renderersMap.find(expectedOrder); it == this->m_renderersMap.end() || it->second.GetPointer() != expectedRenderer) - { - return false; - } - } - return true; -} - -void vtkMRMLLayerDMPipelineI::SetRenderer(vtkRenderer* renderer) -{ - this->SetRenderers({ renderer }, { this->GetRenderOrder() }); -} - -void vtkMRMLLayerDMPipelineI::SetScene(vtkMRMLScene* scene) -{ - this->m_scene = scene; -} - -vtkMRMLLayerDMPipelineI* vtkMRMLLayerDMPipelineI::GetNodePipeline(vtkMRMLNode* node) const -{ - if (!this->m_pipelineManager) - { - return nullptr; - } - return this->m_pipelineManager->GetNodePipeline(node); -} - -vtkMRMLLayerDMPipelineManager* vtkMRMLLayerDMPipelineI::GetPipelineManager() const -{ - return m_pipelineManager; -} - -vtkMRMLAbstractViewNode* vtkMRMLLayerDMPipelineI::GetViewNode() const -{ - return this->m_viewNode; -} - -bool vtkMRMLLayerDMPipelineI::BlockUpdateObserver(bool isBlocked) const -{ - if (this->m_isFrozen) - { - return true; - } - - return this->m_obs->SetBlocked(isBlocked); -} - -bool vtkMRMLLayerDMPipelineI::IsUpdateObserverBlocked() const -{ - return this->m_obs->IsBlocked(); -} - -void vtkMRMLLayerDMPipelineI::SetFrozen(bool isFrozen) -{ - if (this->m_isFrozen == isFrozen) - { - return; - } - - // Block states are only updated when the pipeline is not frozen. - // Unfreeze to update all before saving the frozen state. - this->m_isFrozen = false; - this->BlockInteractionProcessing(isFrozen); - this->BlockResetDisplay(isFrozen); - this->BlockUpdateObserver(isFrozen); - this->m_isFrozen = isFrozen; -} - -bool vtkMRMLLayerDMPipelineI::IsFrozen() const -{ - return this->m_isFrozen; -} - -vtkMRMLNode* vtkMRMLLayerDMPipelineI::GetDisplayNode() const -{ - return this->m_displayNode; -} - -vtkRenderer* vtkMRMLLayerDMPipelineI::GetRenderer() const -{ - if (this->m_renderersMap.empty()) - { - return nullptr; - } - return this->m_renderersMap.begin()->second; -} - -vtkRenderer* vtkMRMLLayerDMPipelineI::GetRenderer(unsigned int renderOrder) const -{ - const auto it = this->m_renderersMap.find(renderOrder); - if (it == std::end(this->m_renderersMap)) - { - return nullptr; - } - return it->second; -} - -vtkMRMLScene* vtkMRMLLayerDMPipelineI::GetScene() const -{ - return this->m_scene; -} - -void vtkMRMLLayerDMPipelineI::OnUpdate(vtkObject* obj, unsigned long eventId, void* callData) {} - -void vtkMRMLLayerDMPipelineI::RemoveObserver(vtkObject* prevObj) const -{ - this->m_obs->RemoveObserver(prevObj); -} - -void vtkMRMLLayerDMPipelineI::RequestRender() const -{ - if (this->m_pipelineManager) - { - this->m_pipelineManager->RequestRender(); - } -} - -void vtkMRMLLayerDMPipelineI::SetPipelineManager(vtkMRMLLayerDMPipelineManager* pipelineManager) -{ - this->m_pipelineManager = pipelineManager; -} - -vtkMRMLLayerDMPipelineI::vtkMRMLLayerDMPipelineI() - : m_viewNode{ nullptr } - , m_displayNode{ nullptr } - , m_renderersMap{} - , m_isResetDisplayBlocked{ false } - , m_isFrozen{ false } - , m_isInteractionProcessingBlocked{ false } - , m_obs(vtkSmartPointer::New()) - , m_pipelineManager(nullptr) -{ - this->m_obs->SetUpdateCallback([this](vtkObject* obj, unsigned long eventId, void* callData) { this->OnUpdate(obj, eventId, callData); }); -} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineManager.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineManager.cxx index 4252eab..ecc8a54 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineManager.cxx +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineManager.cxx @@ -1,3 +1,23 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMPipelineManager.h" // Layer DM includes @@ -6,100 +26,131 @@ #include "vtkMRMLLayerDMLayerManager.h" #include "vtkMRMLLayerDMNodeReferenceObserver.h" #include "vtkMRMLLayerDMObjectEventObserver.h" +#include "vtkMRMLLayerDMPipelineCreator.h" #include "vtkMRMLLayerDMPipelineFactory.h" -#include "vtkMRMLLayerDMPipelineI.h" +#include "vtkMRMLLayerDMPipeline.h" // Slicer includes #include "vtkMRMLAbstractViewNode.h" #include "vtkMRMLScene.h" +// VTK includes +#include + // VTK includes #include #include #include #include +// STD includes +#include + +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMPipelineManager); -/// Helper struct to block reset display and reset display once when deleting -struct ResetPipelineDisplayOnceGuard +//----------------------------------------------------------------------------- +/// Helper struct to block display updates and update display once when deleting +struct UpdatePipelineDisplayOnceGuard { - explicit ResetPipelineDisplayOnceGuard(vtkSmartPointer pipeline) - : m_pipeline{ std::move(pipeline) } + explicit UpdatePipelineDisplayOnceGuard(vtkSmartPointer pipeline) + : Pipeline{ std::move(pipeline) } { - if (m_pipeline) + if (this->Pipeline) { - m_wasBlocked = m_pipeline->BlockResetDisplay(true); + this->WasBlocked = this->Pipeline->BlockUpdateDisplay(true); } } - ~ResetPipelineDisplayOnceGuard() + ~UpdatePipelineDisplayOnceGuard() { - if (m_pipeline) + if (this->Pipeline) { - m_pipeline->BlockResetDisplay(m_wasBlocked); - m_pipeline->ResetDisplay(); + this->Pipeline->BlockUpdateDisplay(this->WasBlocked); + this->Pipeline->UpdateDisplay(); } } - vtkSmartPointer m_pipeline; - bool m_wasBlocked{}; + vtkSmartPointer Pipeline; + bool WasBlocked{}; }; +//----------------------------------------------------------------------------- /// Helper struct to block rendering and request render once when deleting struct RequestRenderOnceGuard { explicit RequestRenderOnceGuard(vtkMRMLLayerDMPipelineManager& pipelineManager) - : m_pipelineManager{ pipelineManager } + : PipelineManager{ pipelineManager } { - m_wasBlocked = m_pipelineManager.BlockRequestRender(true); + this->WasBlocked = this->PipelineManager.BlockRequestRender(true); } ~RequestRenderOnceGuard() { - m_pipelineManager.BlockRequestRender(m_wasBlocked); - m_pipelineManager.RequestRender(); + this->PipelineManager.BlockRequestRender(this->WasBlocked); + this->PipelineManager.RequestRender(); } - vtkMRMLLayerDMPipelineManager& m_pipelineManager; - bool m_wasBlocked{}; + vtkMRMLLayerDMPipelineManager& PipelineManager; + bool WasBlocked{}; }; +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMPipelineManager::CreatePipelineForNode(vtkMRMLNode* displayNode) { // Early return if manager is not yet created - if (!this->m_factory || !this->m_viewNode) + if (!this->Factory || !this->ViewNode) { return false; } - auto pipeline = this->m_factory->CreatePipeline(this->m_viewNode, displayNode); + vtkSmartPointer creator; + auto pipeline = this->Factory->CreatePipeline(this->ViewNode, displayNode, &creator); if (!pipeline) { return false; } RequestRenderOnceGuard renderGuard{ *this }; - ResetPipelineDisplayOnceGuard resetPipelineGuard{ pipeline }; - pipeline->SetViewNode(this->m_viewNode); + UpdatePipelineDisplayOnceGuard updatePipelineGuard{ pipeline }; + pipeline->SetViewNode(this->ViewNode); pipeline->SetPipelineManager(this); - pipeline->SetScene(this->m_scene); - pipeline->SetViewNode(this->m_viewNode); + pipeline->SetScene(this->Scene); + pipeline->SetViewNode(this->ViewNode); pipeline->SetDisplayNode(displayNode); - pipeline->OnDefaultCameraModified(this->m_defaultCamera); - this->m_pipelineMap[displayNode] = pipeline; - this->m_layerManager->AddPipeline(pipeline); - this->m_interactionLogic->AddPipeline(pipeline); + pipeline->OnDefaultCameraModified(this->DefaultCamera); + this->PipelineMap[displayNode] = pipeline; + this->PipelineCreatorMap[displayNode] = creator; + this->EventObserver->UpdateObservation(nullptr, displayNode, vtkCommand::DeleteEvent); + this->LayerManager->AddPipeline(pipeline); + this->InteractionLogic->AddPipeline(pipeline); this->UpdatePipeline(pipeline); this->InvokeEvent(vtkCommand::ModifiedEvent); return true; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::ClearDisplayableNodes() { - this->m_pipelineMap.clear(); + RequestRenderOnceGuard renderGuard{ *this }; + + for (const auto& [node, pipeline] : this->PipelineMap) + { + if (!pipeline) + { + continue; + } + pipeline->SetFrozen(true); + this->InteractionLogic->RemovePipeline(pipeline); + this->LayerManager->RemovePipeline(pipeline); + this->EventObserver->RemoveObservations(node); + } + + this->PipelineMap.clear(); + this->PipelineCreatorMap.clear(); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMPipelineManager::AddNode(vtkMRMLNode* node) { if (auto pipeline = this->GetNodePipeline(node)) @@ -110,15 +161,17 @@ bool vtkMRMLLayerDMPipelineManager::AddNode(vtkMRMLNode* node) return this->CreatePipelineForNode(node); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::UpdateAllPipelines() { RequestRenderOnceGuard renderGuard{ *this }; - for (const auto& pipeline : m_pipelineMap) + for (const auto& pipeline : this->PipelineMap) { this->UpdatePipeline(pipeline.second); } } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMPipelineManager::RemovePipeline(vtkMRMLNode* displayNode) { auto pipeline = this->GetNodePipeline(displayNode); @@ -130,124 +183,135 @@ bool vtkMRMLLayerDMPipelineManager::RemovePipeline(vtkMRMLNode* displayNode) RequestRenderOnceGuard renderGuard{ *this }; pipeline->SetFrozen(true); // Let interaction logic process the removal first if the pipeline needs to lose focus. - this->m_interactionLogic->RemovePipeline(pipeline); - this->m_layerManager->RemovePipeline(pipeline); - this->m_pipelineMap.erase(displayNode); + this->InteractionLogic->RemovePipeline(pipeline); + this->LayerManager->RemovePipeline(pipeline); + this->EventObserver->RemoveObservations(displayNode); + this->PipelineMap.erase(displayNode); + this->PipelineCreatorMap.erase(displayNode); this->InvokeEvent(vtkCommand::ModifiedEvent); return true; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::SetRenderWindow(vtkRenderWindow* renderWindow) { // Observe window resize updates (bound to default camera changed update for representations which depend on the camera / display properties) - this->m_eventObs->UpdateObserver(this->m_renderWindow, renderWindow, vtkCommand::WindowResizeEvent); - this->m_renderWindow = renderWindow; - this->m_layerManager->SetRenderWindow(renderWindow); + this->EventObserver->UpdateObservation(this->RenderWindow, renderWindow, vtkCommand::WindowResizeEvent); + this->RenderWindow = renderWindow; + this->LayerManager->SetRenderWindow(renderWindow); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::SetViewNode(vtkMRMLAbstractViewNode* viewNode) { - if (this->m_viewNode == viewNode) + if (this->ViewNode == viewNode) { return; } - this->m_viewNode = viewNode; - this->m_cameraSync->SetViewNode(viewNode); - this->m_interactionLogic->SetViewNode(viewNode); + this->ViewNode = viewNode; + this->CameraSynchronizer->SetViewNode(viewNode); + this->InteractionLogic->SetViewNode(viewNode); this->UpdateAllPipelines(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::SetFactory(const vtkSmartPointer& factory) { - if (this->m_factory == factory) + if (this->Factory == factory) { return; } - this->m_eventObs->UpdateObserver(this->m_factory, factory); - this->m_factory = factory; + this->EventObserver->UpdateObservation(this->Factory, factory); + this->Factory = factory; this->UpdateFromScene(); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMPipelineManager::GetMouseCursor() const { - auto lastFocused = this->m_interactionLogic->GetLastFocusedPipeline(); + auto lastFocused = this->InteractionLogic->GetLastFocusedPipeline(); return lastFocused ? lastFocused->GetMouseCursor() : VTK_CURSOR_DEFAULT; } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMPipelineManager::CanProcessInteractionEvent(vtkMRMLInteractionEventData* eventData, double& distance2) const { - return this->m_interactionLogic->CanProcessInteractionEvent(eventData, distance2); + return this->InteractionLogic->CanProcessInteractionEvent(eventData, distance2); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::LoseFocus(vtkMRMLInteractionEventData* eventData) const { - this->m_interactionLogic->LoseFocus(eventData); + this->InteractionLogic->LoseFocus(eventData); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::LoseFocus() const { - this->m_interactionLogic->LoseFocus(); + this->InteractionLogic->LoseFocus(); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMPipelineManager::ProcessInteractionEvent(vtkMRMLInteractionEventData* eventData) const { - return this->m_interactionLogic->ProcessInteractionEvent(eventData); + return this->InteractionLogic->ProcessInteractionEvent(eventData); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMPipelineManager::RemoveNode(vtkMRMLNode* node) { return this->RemovePipeline(node); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::ResetCameraClippingRange() const { // Block camera sync update triggers during clipping range refresh - const auto wasBlocked = this->m_cameraSync->BlockModified(true); - this->m_layerManager->ResetCameraClippingRange(); - this->m_cameraSync->BlockModified(wasBlocked); + const auto wasBlocked = this->CameraSynchronizer->BlockModified(true); + this->LayerManager->ResetCameraClippingRange(); + this->CameraSynchronizer->BlockModified(wasBlocked); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::RequestRender() { - if (this->m_isRequestRenderBlocked || !this->m_renderWindow) + if (this->IsRequestRenderBlocked || !this->RenderWindow) { return; } this->BlockRequestRender(true); this->ResetCameraClippingRange(); - this->m_requestRender(); + this->RequestRenderCallback(); this->BlockRequestRender(false); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::OnDefaultCameraModified() { RequestRenderOnceGuard renderGuard{ *this }; - for (const auto& pipeline : this->m_pipelineMap) + for (const auto& pipeline : this->PipelineMap) { - pipeline.second->OnDefaultCameraModified(this->m_defaultCamera); + pipeline.second->OnDefaultCameraModified(this->DefaultCamera); } } +//----------------------------------------------------------------------------- vtkMRMLLayerDMPipelineManager::vtkMRMLLayerDMPipelineManager() - : m_factory{ nullptr } - , m_layerManager(vtkSmartPointer::New()) - , m_cameraSync(vtkSmartPointer::New()) - , m_interactionLogic(vtkSmartPointer::New()) - , m_eventObs(vtkSmartPointer::New()) - , m_defaultCamera(vtkSmartPointer::New()) - , m_nodeRefObs{ vtkSmartPointer::New() } - , m_viewNode{ nullptr } - , m_scene{ nullptr } - , m_pipelineMap{} - , m_requestRender{ [] {} } -{ - this->m_cameraSync->SetDefaultCamera(this->m_defaultCamera); - this->m_layerManager->SetDefaultCamera(this->m_defaultCamera); - - this->m_nodeRefObs->SetReferenceModifiedCallBack( + : LayerManager(vtkSmartPointer::New()) + , CameraSynchronizer(vtkSmartPointer::New()) + , InteractionLogic(vtkSmartPointer::New()) + , EventObserver(vtkSmartPointer::New()) + , DefaultCamera(vtkSmartPointer::New()) + , NodeReferenceObserver{ vtkSmartPointer::New() } + , RequestRenderCallback{ [] {} } +{ + this->CameraSynchronizer->SetDefaultCamera(this->DefaultCamera); + this->LayerManager->SetDefaultCamera(this->DefaultCamera); + + this->NodeReferenceObserver->SetReferenceModifiedCallBack( [this](vtkMRMLNode* fromNode, vtkMRMLNode* toNode, const std::string& role, int eventType) { auto pipeline = this->GetNodePipeline(toNode); @@ -265,119 +329,163 @@ vtkMRMLLayerDMPipelineManager::vtkMRMLLayerDMPipelineManager() } }); - this->m_eventObs->SetUpdateCallback( - [this](vtkObject* obj) + this->EventObserver->SetUpdateCallback( + [this](vtkObject* obj, unsigned long eventId) { - if (obj == this->m_factory) + if (eventId == vtkCommand::DeleteEvent) + { + if (auto node = vtkMRMLNode::SafeDownCast(obj)) + { + this->RemovePipeline(node); + } + return; + } + + if (obj == this->Factory) { this->UpdateFromScene(); } - if (obj == this->m_cameraSync || obj == this->m_renderWindow) + if (obj == this->CameraSynchronizer || obj == this->RenderWindow) { this->OnDefaultCameraModified(); } }); // Monitor camera updates - this->m_eventObs->UpdateObserver(nullptr, this->m_cameraSync); + this->EventObserver->UpdateObservation(nullptr, this->CameraSynchronizer); } -void vtkMRMLLayerDMPipelineManager::UpdatePipeline(const vtkSmartPointer& pipeline) const +//----------------------------------------------------------------------------- +vtkMRMLLayerDMPipelineManager::~vtkMRMLLayerDMPipelineManager() +{ + this->EventObserver->ClearCallback(); +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipelineManager::UpdatePipeline(const vtkSmartPointer& pipeline) const { if (!pipeline) { return; } - ResetPipelineDisplayOnceGuard resetPipelineGuard{ pipeline }; - pipeline->SetViewNode(this->m_viewNode); + UpdatePipelineDisplayOnceGuard updatePipelineGuard{ pipeline }; + pipeline->SetViewNode(this->ViewNode); } -vtkSmartPointer vtkMRMLLayerDMPipelineManager::GetNodePipeline(vtkMRMLNode* node) const +//----------------------------------------------------------------------------- +vtkSmartPointer vtkMRMLLayerDMPipelineManager::GetNodePipeline(vtkMRMLNode* node) const { - const auto found = this->m_pipelineMap.find(node); - if (found == std::end(this->m_pipelineMap)) + const auto found = this->PipelineMap.find(node); + if (found == std::end(this->PipelineMap)) { return {}; } return found->second; } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMPipelineManager::GetNumberOfPipelines() const { - return this->m_pipelineMap.size(); + return static_cast(this->PipelineMap.size()); } -vtkMRMLLayerDMPipelineI* vtkMRMLLayerDMPipelineManager::GetNthPipeline(int iPipeline) const +//----------------------------------------------------------------------------- +vtkMRMLLayerDMPipeline* vtkMRMLLayerDMPipelineManager::GetNthPipeline(int iPipeline) const { - if (iPipeline < 0 || iPipeline >= this->m_pipelineMap.size()) + if (iPipeline < 0 || iPipeline >= this->GetNumberOfPipelines()) { return nullptr; } - return std::next(this->m_pipelineMap.begin(), iPipeline)->second; + return std::next(this->PipelineMap.begin(), iPipeline)->second; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::SetRenderer(vtkRenderer* renderer) const { // Pass the renderer to the camera sync - this->m_cameraSync->SetRenderer(renderer); + this->CameraSynchronizer->SetRenderer(renderer); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::SetRequestRender(const std::function& requestRender) { - this->m_requestRender = requestRender; + this->RequestRenderCallback = requestRender; this->UpdateAllPipelines(); } +//----------------------------------------------------------------------------- vtkCamera* vtkMRMLLayerDMPipelineManager::GetDefaultCamera() const { - return this->m_defaultCamera; + return this->DefaultCamera; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::RemoveOutdatedPipelines() { - if (!this->m_scene) + if (!this->Scene) { return; } - std::vector> outdatedPipelines; - for (const auto& pipe : m_pipelineMap) + std::vector outdatedPipelines; + for (const auto& [node, pipeline] : this->PipelineMap) { - if (!pipe.first || !this->m_scene->GetNodeByID(pipe.first->GetID())) + if (!this->Scene->GetNodeByID(node->GetID()) || this->IsPipelineCreatorOutdated(node)) { - outdatedPipelines.emplace_back(pipe.first); + outdatedPipelines.emplace_back(node); } } - for (const auto& pipe : outdatedPipelines) + for (const auto& node : outdatedPipelines) + { + this->RemovePipeline(node); + } +} + +//----------------------------------------------------------------------------- +bool vtkMRMLLayerDMPipelineManager::IsPipelineCreatorOutdated(vtkMRMLNode* node) const +{ + const auto found = this->PipelineCreatorMap.find(node); + if (found == std::end(this->PipelineCreatorMap)) { - this->RemovePipeline(pipe); + return false; } + return !found->second || !this->Factory || !this->Factory->ContainsPipelineCreator(found->second.GetPointer()); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::AddMissingPipelines() { - if (!this->m_scene) + if (!this->Scene) { return; } - int nNodes = this->m_scene->GetNumberOfNodes(); - for (int iNode = 0; iNode < nNodes; iNode++) + // Use GetNextItemAsObject during iteration to avoid quadratic iteration of GetItemAsObject + std::vector> sceneNodes; + vtkObject* item = nullptr; + vtkCollectionSimpleIterator it; + for (this->Scene->GetNodes()->InitTraversal(it); (item = this->Scene->GetNodes()->GetNextItemAsObject(it));) { - if (auto node = vtkMRMLNode::SafeDownCast(this->m_scene->GetNodes()->GetItemAsObject(iNode))) + if (auto node = vtkMRMLNode::SafeDownCast(item)) { - this->AddNode(node); + sceneNodes.emplace_back(node); } } + + for (const auto& node : sceneNodes) + { + this->AddNode(node); + } } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::UpdateFromScene() { - if (!this->m_scene) + if (!this->Scene) { return; } @@ -387,24 +495,37 @@ void vtkMRMLLayerDMPipelineManager::UpdateFromScene() this->AddMissingPipelines(); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMPipelineManager::BlockRequestRender(bool isBlocked) { - const auto wasBlocked = this->m_isRequestRenderBlocked; - this->m_isRequestRenderBlocked = isBlocked; + const auto wasBlocked = this->IsRequestRenderBlocked; + this->IsRequestRenderBlocked = isBlocked; return wasBlocked; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineManager::SetScene(vtkMRMLScene* scene) { - if (this->m_scene == scene) + if (this->Scene == scene) { return; } - this->m_scene = scene; - this->m_nodeRefObs->SetScene(scene); - for (const auto& [node, pipeline] : m_pipelineMap) + this->Scene = scene; + this->NodeReferenceObserver->SetScene(scene); + for (const auto& [node, pipeline] : this->PipelineMap) { pipeline->SetScene(scene); } } + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipelineManager::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Number of pipelines: " << this->PipelineMap.size() << std::endl; + os << indent << "View node: " << (this->ViewNode ? "set" : "(none)") << std::endl; + os << indent << "Scene: " << (this->Scene ? "set" : "(none)") << std::endl; + os << indent << "Render window: " << (this->RenderWindow ? "set" : "(none)") << std::endl; + os << indent << "Request render blocked: " << (this->IsRequestRenderBlocked ? "true" : "false") << std::endl; +} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineManager.h b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineManager.h index 3e6639b..ee1a394 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineManager.h +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineManager.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMPipelineManager_h +#define __vtkMRMLLayerDMPipelineManager_h #include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" @@ -20,9 +41,9 @@ class vtkMRMLLayerDMInteractionLogic; class vtkMRMLLayerDMLayerManager; class vtkMRMLLayerDMNodeReferenceObserver; class vtkMRMLLayerDMObjectEventObserver; -class vtkMRMLLayerDMPipelineCreatorI; +class vtkMRMLLayerDMPipelineCreator; class vtkMRMLLayerDMPipelineFactory; -class vtkMRMLLayerDMPipelineI; +class vtkMRMLLayerDMPipeline; class vtkMRMLNode; class vtkMRMLScene; class vtkRenderWindow; @@ -37,6 +58,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe public: static vtkMRMLLayerDMPipelineManager* New(); vtkTypeMacro(vtkMRMLLayerDMPipelineManager, vtkObject); + void PrintSelf(ostream& os, vtkIndent indent) override; /// Add a new node to the pipeline manager. /// If no pipeline exist for the input display node and the \sa vtkMRMLLayerDMPipelineFactory can create @@ -62,15 +84,16 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe int GetMouseCursor() const; /// Returns the pipeline associated with the input display node if any. - vtkSmartPointer GetNodePipeline(vtkMRMLNode* node) const; + vtkSmartPointer GetNodePipeline(vtkMRMLNode* node) const; /// Returns the number of pipelines currently managed by the pipeline manager int GetNumberOfPipelines() const; - /// Returns the list of currently managed display nodes of the pipeline manager. + /// Returns the nth pipeline managed by the pipeline manager, nullptr if the index is out of range. /// /// \sa GetNodePipeline - vtkMRMLLayerDMPipelineI* GetNthPipeline(int iPipeline) const; + /// \sa GetNumberOfPipelines + vtkMRMLLayerDMPipeline* GetNthPipeline(int iPipeline) const; /// @{ /// Makes the latest pipeline lose focus @@ -131,35 +154,44 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe protected: vtkMRMLLayerDMPipelineManager(); - ~vtkMRMLLayerDMPipelineManager() override = default; + ~vtkMRMLLayerDMPipelineManager() override; private: /// Notify pipelines that the default camera has changed. void OnDefaultCameraModified(); /// Update the input pipeline and reset its display. - void UpdatePipeline(const vtkSmartPointer& pipeline) const; + void UpdatePipeline(const vtkSmartPointer& pipeline) const; - /// Remove pipelines with nodes not present in the scene anymore. + /// Remove pipelines with nodes not present in the scene anymore, as well as pipelines + /// whose creator has been removed from the factory (their nodes are then handled again + /// by \sa AddMissingPipelines and may be recreated by the remaining creators). void RemoveOutdatedPipelines(); /// Add pipelines for nodes not currently handled by the pipeline manager. void AddMissingPipelines(); - vtkSmartPointer m_factory; - vtkSmartPointer m_layerManager; - vtkSmartPointer m_cameraSync; - vtkSmartPointer m_interactionLogic; - vtkSmartPointer m_eventObs; - vtkSmartPointer m_defaultCamera; - vtkSmartPointer m_nodeRefObs; + /// true if the pipeline associated with the input node was created by a creator which has + /// since been removed from the factory (or destroyed). + bool IsPipelineCreatorOutdated(vtkMRMLNode* node) const; - vtkWeakPointer m_viewNode; - vtkWeakPointer m_scene; - vtkWeakPointer m_renderWindow; + vtkSmartPointer Factory; + vtkSmartPointer LayerManager; + vtkSmartPointer CameraSynchronizer; + vtkSmartPointer InteractionLogic; + vtkSmartPointer EventObserver; + vtkSmartPointer DefaultCamera; + vtkSmartPointer NodeReferenceObserver; - std::map, vtkSmartPointer> m_pipelineMap; - std::function m_requestRender; + vtkWeakPointer ViewNode; + vtkWeakPointer Scene; + vtkWeakPointer RenderWindow; - bool m_isRequestRenderBlocked{ false }; + std::map> PipelineMap; + std::map> PipelineCreatorMap; + std::function RequestRenderCallback; + + bool IsRequestRenderBlocked{ false }; }; + +#endif diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineScriptedCreator.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineScriptedCreator.cxx index 6d0697c..c148290 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineScriptedCreator.cxx +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineScriptedCreator.cxx @@ -1,7 +1,27 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMPipelineScriptedCreator.h" // Layer DM includes -#include "vtkMRMLLayerDMPipelineI.h" +#include "vtkMRMLLayerDMPipeline.h" #include "vtkMRMLLayerDMPythonUtil.h" // Slicer includes @@ -12,13 +32,15 @@ #include #include +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMPipelineScriptedCreator); +//----------------------------------------------------------------------------- vtkMRMLLayerDMPipelineScriptedCreator::vtkMRMLLayerDMPipelineScriptedCreator() - : m_object(nullptr) + : Object(nullptr) { this->SetCallback( - [this](vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) -> vtkSmartPointer + [this](vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) -> vtkSmartPointer { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) { @@ -27,23 +49,32 @@ vtkMRMLLayerDMPipelineScriptedCreator::vtkMRMLLayerDMPipelineScriptedCreator() vtkPythonScopeGilEnsurer gilEnsurer; PyObject* result = vtkMRMLLayerDMPythonUtil::CallPythonObject( - this->m_object, vtkMRMLLayerDMPythonUtil::ToPyArgs({ vtkMRMLLayerDMPythonUtil::ToPyObject(viewNode), vtkMRMLLayerDMPythonUtil::ToPyObject(node) })); + this->Object, vtkMRMLLayerDMPythonUtil::ToPyArgs({ vtkMRMLLayerDMPythonUtil::ToPyObject(viewNode), vtkMRMLLayerDMPythonUtil::ToPyObject(node) })); if (!result) { - auto errorMsg = std::string(__func__) + ": Failed to call : " + vtkMRMLLayerDMPythonUtil::GetObjectStr(this->m_object) + ":"; + auto errorMsg = std::string(__func__) + ": Failed to call : " + vtkMRMLLayerDMPythonUtil::GetObjectStr(this->Object) + ":"; vtkMRMLLayerDMPythonUtil::PrintErrorTraceback(this, errorMsg); return nullptr; } - return vtkMRMLLayerDMPipelineI::SafeDownCast(vtkPythonUtil::GetPointerFromObject(result, "vtkMRMLLayerDMPipelineI")); + return vtkMRMLLayerDMPipeline::SafeDownCast(vtkPythonUtil::GetPointerFromObject(result, "vtkMRMLLayerDMPipeline")); }); } +//----------------------------------------------------------------------------- vtkMRMLLayerDMPipelineScriptedCreator::~vtkMRMLLayerDMPipelineScriptedCreator() { - vtkMRMLLayerDMPythonUtil::DeletePythonObject(&this->m_object); + vtkMRMLLayerDMPythonUtil::DeletePythonObject(&this->Object); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMPipelineScriptedCreator::SetPythonCallback(PyObject* object) { - vtkMRMLLayerDMPythonUtil::SetPythonObject(&this->m_object, object); + vtkMRMLLayerDMPythonUtil::SetPythonObject(&this->Object, object); +} + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMPipelineScriptedCreator::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Python object: " << (this->Object ? "set" : "(none)") << std::endl; } diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineScriptedCreator.h b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineScriptedCreator.h index daf6ca4..7c253f1 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineScriptedCreator.h +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMPipelineScriptedCreator.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMPipelineScriptedCreator_h +#define __vtkMRMLLayerDMPipelineScriptedCreator_h #include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" @@ -15,6 +36,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe static vtkMRMLLayerDMPipelineScriptedCreator* New(); vtkTypeMacro(vtkMRMLLayerDMPipelineScriptedCreator, vtkMRMLLayerDMPipelineCallbackCreator); + void PrintSelf(ostream& os, vtkIndent indent) override; void SetPythonCallback(PyObject* object); protected: @@ -22,5 +44,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPipe ~vtkMRMLLayerDMPipelineScriptedCreator() override; private: - PyObject* m_object; + PyObject* Object; }; + +#endif diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMScriptedPipelineBridge.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDMScriptedPipelineBridge.cxx index 190c75e..73bca5c 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMScriptedPipelineBridge.cxx +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMScriptedPipelineBridge.cxx @@ -1,3 +1,23 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDMScriptedPipelineBridge.h" // Layer DM includes @@ -15,9 +35,11 @@ #include #include +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDMScriptedPipelineBridge); -void vtkMRMLLayerDMScriptedPipelineBridge::UpdatePipeline() +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMScriptedPipelineBridge::UpdateFromMRML() { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) { @@ -27,21 +49,25 @@ void vtkMRMLLayerDMScriptedPipelineBridge::UpdatePipeline() this->CallPythonMethod({}, __func__, true); } +//----------------------------------------------------------------------------- PyObject* vtkMRMLLayerDMScriptedPipelineBridge::CastCallData(PyObject* object, int vtkType) { return vtkMRMLLayerDMPythonUtil::CastCallData(object, vtkType); } +//----------------------------------------------------------------------------- vtkMRMLLayerDMScriptedPipelineBridge::vtkMRMLLayerDMScriptedPipelineBridge() - : m_object{ nullptr } + : Object{ nullptr } { } +//----------------------------------------------------------------------------- vtkMRMLLayerDMScriptedPipelineBridge::~vtkMRMLLayerDMScriptedPipelineBridge() { - vtkMRMLLayerDMPythonUtil::DeletePythonObject(&this->m_object); + vtkMRMLLayerDMPythonUtil::DeletePythonObject(&this->Object); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMScriptedPipelineBridge::CanProcessInteractionEvent(vtkMRMLInteractionEventData* eventData, double& distance2) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -60,13 +86,15 @@ bool vtkMRMLLayerDMScriptedPipelineBridge::CanProcessInteractionEvent(vtkMRMLInt } Py_DECREF(result); - // Unpack error or unexpected return type - PyErr_SetString(PyExc_TypeError, "Expected a tuple[bool, float] return type"); + // Clear errors and report unexpected Python error (avoid silently making the Python context invalid) + PyErr_Clear(); + vtkErrorMacro(<< "CanProcessInteractionEvent: expected a tuple[bool, float] return type from " << vtkMRMLLayerDMPythonUtil::GetObjectStr(this->Object)); } return false; } +//----------------------------------------------------------------------------- vtkCamera* vtkMRMLLayerDMScriptedPipelineBridge::GetCustomCamera(unsigned int renderOrder) const { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -81,13 +109,17 @@ vtkCamera* vtkMRMLLayerDMScriptedPipelineBridge::GetCustomCamera(unsigned int re { if (result != Py_None) { - return vtkCamera::SafeDownCast(vtkPythonUtil::GetPointerFromObject(result, "vtkCamera")); + // The camera is owned by the Python pipeline, the wrapper reference is not needed past this point. + vtkCamera* camera = vtkCamera::SafeDownCast(vtkPythonUtil::GetPointerFromObject(result, "vtkCamera")); + Py_DECREF(result); + return camera; } Py_DECREF(result); } return Superclass::GetCustomCamera(renderOrder); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMScriptedPipelineBridge::GetMouseCursor() const { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -103,6 +135,7 @@ int vtkMRMLLayerDMScriptedPipelineBridge::GetMouseCursor() const return Superclass::GetMouseCursor(); } +//----------------------------------------------------------------------------- unsigned int vtkMRMLLayerDMScriptedPipelineBridge::GetRenderOrder() const { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -118,6 +151,7 @@ unsigned int vtkMRMLLayerDMScriptedPipelineBridge::GetRenderOrder() const return Superclass::GetRenderOrder(); } +//----------------------------------------------------------------------------- std::vector vtkMRMLLayerDMScriptedPipelineBridge::GetRenderOrders() const { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -151,6 +185,7 @@ std::vector vtkMRMLLayerDMScriptedPipelineBridge::GetRenderOrders( return Superclass::GetRenderOrders(); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMScriptedPipelineBridge::GetWidgetState() const { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -166,6 +201,7 @@ int vtkMRMLLayerDMScriptedPipelineBridge::GetWidgetState() const return Superclass::GetWidgetState(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::LoseFocus(vtkMRMLInteractionEventData* eventData) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -177,6 +213,7 @@ void vtkMRMLLayerDMScriptedPipelineBridge::LoseFocus(vtkMRMLInteractionEventData this->CallPythonMethod(vtkMRMLLayerDMPythonUtil::ToPyArgs(eventData), __func__, true); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::OnDefaultCameraModified(vtkCamera* camera) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -188,11 +225,13 @@ void vtkMRMLLayerDMScriptedPipelineBridge::OnDefaultCameraModified(vtkCamera* ca this->CallPythonMethod(vtkMRMLLayerDMPythonUtil::ToPyArgs(camera), __func__, true); } +//----------------------------------------------------------------------------- inline vtkSmartPyObject ToPyArgs(vtkMRMLNode* fromNode, const std::string& s) { return vtkMRMLLayerDMPythonUtil::ToPyArgs({ vtkMRMLLayerDMPythonUtil::ToPyObject(fromNode), vtkMRMLLayerDMPythonUtil::ToPyObject(s) }); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::OnReferenceToDisplayNodeAdded(vtkMRMLNode* fromNode, const std::string& role) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -203,6 +242,7 @@ void vtkMRMLLayerDMScriptedPipelineBridge::OnReferenceToDisplayNodeAdded(vtkMRML this->CallPythonMethod(ToPyArgs(fromNode, role), __func__, true); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::OnReferenceToDisplayNodeRemoved(vtkMRMLNode* fromNode, const std::string& role) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -213,6 +253,7 @@ void vtkMRMLLayerDMScriptedPipelineBridge::OnReferenceToDisplayNodeRemoved(vtkMR this->CallPythonMethod(ToPyArgs(fromNode, role), __func__, true); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::OnRendererAdded(vtkRenderer* renderer) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -224,6 +265,7 @@ void vtkMRMLLayerDMScriptedPipelineBridge::OnRendererAdded(vtkRenderer* renderer this->CallPythonMethod(vtkMRMLLayerDMPythonUtil::ToPyArgs(renderer), __func__, true); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::OnRendererRemoved(vtkRenderer* renderer) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -235,6 +277,7 @@ void vtkMRMLLayerDMScriptedPipelineBridge::OnRendererRemoved(vtkRenderer* render this->CallPythonMethod(vtkMRMLLayerDMPythonUtil::ToPyArgs(renderer), __func__, true); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDMScriptedPipelineBridge::ProcessInteractionEvent(vtkMRMLInteractionEventData* eventData) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -245,13 +288,15 @@ bool vtkMRMLLayerDMScriptedPipelineBridge::ProcessInteractionEvent(vtkMRMLIntera vtkPythonScopeGilEnsurer gilEnsurer; if (auto result = this->CallPythonMethod(vtkMRMLLayerDMPythonUtil::ToPyArgs(eventData), __func__, false)) { - bool wasProcessed = result == Py_True; + // Accept any truthy return value, not just the True singleton. + const bool wasProcessed = PyObject_IsTrue(result) == 1; Py_DECREF(result); return wasProcessed; } return false; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::SetDisplayNode(vtkMRMLNode* displayNode) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -263,6 +308,7 @@ void vtkMRMLLayerDMScriptedPipelineBridge::SetDisplayNode(vtkMRMLNode* displayNo this->CallPythonMethod(vtkMRMLLayerDMPythonUtil::ToPyArgs(displayNode), __func__, true); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::SetViewNode(vtkMRMLAbstractViewNode* viewNode) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -274,6 +320,7 @@ void vtkMRMLLayerDMScriptedPipelineBridge::SetViewNode(vtkMRMLAbstractViewNode* this->CallPythonMethod(vtkMRMLLayerDMPythonUtil::ToPyArgs(viewNode), __func__, true); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::SetScene(vtkMRMLScene* scene) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -285,6 +332,7 @@ void vtkMRMLLayerDMScriptedPipelineBridge::SetScene(vtkMRMLScene* scene) this->CallPythonMethod(vtkMRMLLayerDMPythonUtil::ToPyArgs(scene), __func__, true); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::SetPipelineManager(vtkMRMLLayerDMPipelineManager* pipelineManager) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -296,11 +344,13 @@ void vtkMRMLLayerDMScriptedPipelineBridge::SetPipelineManager(vtkMRMLLayerDMPipe this->CallPythonMethod(vtkMRMLLayerDMPythonUtil::ToPyArgs(pipelineManager), __func__, true); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::SetPythonObject(PyObject* object) { - vtkMRMLLayerDMPythonUtil::SetPythonObject(&this->m_object, object); + vtkMRMLLayerDMPythonUtil::SetPythonObject(&this->Object, object); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDMScriptedPipelineBridge::OnUpdate(vtkObject* obj, unsigned long eventId, void* callData) { if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext()) @@ -312,13 +362,17 @@ void vtkMRMLLayerDMScriptedPipelineBridge::OnUpdate(vtkObject* obj, unsigned lon this->CallPythonMethod(vtkMRMLLayerDMPythonUtil::ToPyArgs(obj, eventId, callData), __func__, true); } +//----------------------------------------------------------------------------- PyObject* vtkMRMLLayerDMScriptedPipelineBridge::CallPythonMethod(const vtkSmartPyObject& pyArgs, const std::string& fName, bool decrementResult) const { - auto result = vtkMRMLLayerDMPythonUtil::CallPythonMethod(this->m_object, pyArgs, fName); + // The result may be decremented here and the error path inspects Python objects, so hold the + // GIL for the whole method rather than relying on every caller to do it. + vtkPythonScopeGilEnsurer gilEnsurer; + auto result = vtkMRMLLayerDMPythonUtil::CallPythonMethod(this->Object, pyArgs, fName); if (!result) { - std::string errorMsg = "Failed to call : " + fName + " : of object : " + vtkMRMLLayerDMPythonUtil::GetObjectStr(this->m_object) + ":"; + std::string errorMsg = "Failed to call : " + fName + " : of object : " + vtkMRMLLayerDMPythonUtil::GetObjectStr(this->Object) + ":"; vtkMRMLLayerDMPythonUtil::PrintErrorTraceback(this, errorMsg); return nullptr; } @@ -331,6 +385,7 @@ PyObject* vtkMRMLLayerDMScriptedPipelineBridge::CallPythonMethod(const vtkSmartP return result; } +//----------------------------------------------------------------------------- int vtkMRMLLayerDMScriptedPipelineBridge::CastToIntAndDecrement(PyObject* result) const { if (!result) @@ -341,3 +396,10 @@ int vtkMRMLLayerDMScriptedPipelineBridge::CastToIntAndDecrement(PyObject* result Py_DECREF(result); return value; } + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDMScriptedPipelineBridge::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Python object: " << (this->Object ? "set" : "(none)") << std::endl; +} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDMScriptedPipelineBridge.h b/LayerDM/MRMLDM/vtkMRMLLayerDMScriptedPipelineBridge.h index d97a377..eafe91b 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDMScriptedPipelineBridge.h +++ b/LayerDM/MRMLDM/vtkMRMLLayerDMScriptedPipelineBridge.h @@ -1,24 +1,46 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDMScriptedPipelineBridge_h +#define __vtkMRMLLayerDMScriptedPipelineBridge_h #include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" -#include "vtkMRMLLayerDMPipelineI.h" +#include "vtkMRMLLayerDMPipeline.h" // VTK includes #include class vtkSmartPyObject; -/// \brief Python bridge for vtkMRMLLayerDMPipelineI. +/// \brief Python bridge for vtkMRMLLayerDMPipeline. /// Delegates calls to the pipeline to its underlying python object. /// -/// \sa vtkMRMLLayerDMPipelineI +/// \sa vtkMRMLLayerDMPipeline /// \sa vtkMRMLLayerDMScriptedPipeline -class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMScriptedPipelineBridge : public vtkMRMLLayerDMPipelineI +class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMScriptedPipelineBridge : public vtkMRMLLayerDMPipeline { public: static vtkMRMLLayerDMScriptedPipelineBridge* New(); - vtkTypeMacro(vtkMRMLLayerDMScriptedPipelineBridge, vtkMRMLLayerDMPipelineI); + vtkTypeMacro(vtkMRMLLayerDMScriptedPipelineBridge, vtkMRMLLayerDMPipeline); + void PrintSelf(ostream& os, vtkIndent indent) override; bool CanProcessInteractionEvent(vtkMRMLInteractionEventData* eventData, double& distance2) override; vtkCamera* GetCustomCamera(unsigned int renderOrder) const override; @@ -38,7 +60,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMScri void SetScene(vtkMRMLScene* scene) override; void SetPipelineManager(vtkMRMLLayerDMPipelineManager* pipelineManager) override; void SetPythonObject(PyObject* object); - void UpdatePipeline() override; + void UpdateFromMRML() override; static PyObject* CastCallData(PyObject* object, int vtkType); protected: @@ -51,5 +73,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMScri PyObject* CallPythonMethod(const vtkSmartPyObject& pyArgs, const std::string& fName, bool decrementResult) const; int CastToIntAndDecrement(PyObject* result) const; - PyObject* m_object; + PyObject* Object; }; + +#endif diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDisplayableManager.cxx b/LayerDM/MRMLDM/vtkMRMLLayerDisplayableManager.cxx index 5ea6cc7..e0858d5 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDisplayableManager.cxx +++ b/LayerDM/MRMLDM/vtkMRMLLayerDisplayableManager.cxx @@ -1,6 +1,27 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + #include "vtkMRMLLayerDisplayableManager.h" // Layer DM includes +#include "vtkMRMLLayerDMPipeline.h" #include "vtkMRMLLayerDMPipelineFactory.h" #include "vtkMRMLLayerDMPipelineManager.h" @@ -19,39 +40,41 @@ // STD includes #include +//----------------------------------------------------------------------------- vtkStandardNewMacro(vtkMRMLLayerDisplayableManager); -vtkMRMLLayerDisplayableManager::vtkMRMLLayerDisplayableManager() - : m_pipelineManager(nullptr) -{ -} +vtkMRMLLayerDisplayableManager::vtkMRMLLayerDisplayableManager() = default; +//----------------------------------------------------------------------------- bool vtkMRMLLayerDisplayableManager::CanProcessInteractionEvent(vtkMRMLInteractionEventData* eventData, double& distance2) { - if (!this->m_pipelineManager) + if (!this->PipelineManager) { return false; } - return this->m_pipelineManager->CanProcessInteractionEvent(eventData, distance2); + return this->PipelineManager->CanProcessInteractionEvent(eventData, distance2); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDisplayableManager::ProcessInteractionEvent(vtkMRMLInteractionEventData* eventData) { - if (!this->m_pipelineManager) + if (!this->PipelineManager) { return false; } - return this->m_pipelineManager->ProcessInteractionEvent(eventData); + return this->PipelineManager->ProcessInteractionEvent(eventData); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::RegisterInDefaultViews() { vtkMRMLLayerDisplayableManager::RegisterInFactory(vtkMRMLSliceViewDisplayableManagerFactory::GetInstance()); vtkMRMLLayerDisplayableManager::RegisterInFactory(vtkMRMLThreeDViewDisplayableManagerFactory::GetInstance()); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::RegisterInFactory(vtkMRMLDisplayableManagerFactory* factory) { if (!factory || vtkMRMLLayerDisplayableManager::IsRegisteredInFactory(factory)) @@ -63,6 +86,7 @@ void vtkMRMLLayerDisplayableManager::RegisterInFactory(vtkMRMLDisplayableManager factory->RegisterDisplayableManager(dm->GetClassName()); } +//----------------------------------------------------------------------------- bool vtkMRMLLayerDisplayableManager::IsRegisteredInFactory(vtkMRMLDisplayableManagerFactory* factory) { if (!factory) @@ -74,89 +98,103 @@ bool vtkMRMLLayerDisplayableManager::IsRegisteredInFactory(vtkMRMLDisplayableMan return factory->IsDisplayableManagerRegistered(dm->GetClassName()); } -vtkSmartPointer vtkMRMLLayerDisplayableManager::GetNodePipeline(vtkMRMLNode* node) const +//----------------------------------------------------------------------------- +vtkSmartPointer vtkMRMLLayerDisplayableManager::GetNodePipeline(vtkMRMLNode* node) const { - return m_pipelineManager->GetNodePipeline(node); + if (!this->PipelineManager) + { + return nullptr; + } + return this->PipelineManager->GetNodePipeline(node); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::OnMRMLSceneStartBatchProcess() { - if (!this->m_pipelineManager) + if (!this->PipelineManager) { return; } - this->m_pipelineManager->BlockRequestRender(true); + this->PipelineManager->BlockRequestRender(true); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::OnMRMLSceneEndBatchProcess() { - if (!this->m_pipelineManager) + if (!this->PipelineManager) { return; } - this->m_pipelineManager->BlockRequestRender(false); - this->m_pipelineManager->RequestRender(); + this->PipelineManager->BlockRequestRender(false); + this->PipelineManager->RequestRender(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::OnMRMLSceneNodeAdded(vtkMRMLNode* node) { - if (!this->m_pipelineManager) + if (!this->PipelineManager) { return; } - this->m_pipelineManager->AddNode(node); + this->PipelineManager->AddNode(node); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::OnMRMLSceneNodeRemoved(vtkMRMLNode* node) { - if (!this->m_pipelineManager) + if (!this->PipelineManager) { return; } - this->m_pipelineManager->RemoveNode(node); + this->PipelineManager->RemoveNode(node); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::UnobserveMRMLScene() { - if (!this->m_pipelineManager) + if (!this->PipelineManager) { return; } - this->m_pipelineManager->ClearDisplayableNodes(); + this->PipelineManager->ClearDisplayableNodes(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::UpdateFromMRML() { this->SetUpdateFromMRMLRequested(false); - if (!this->m_pipelineManager) + if (!this->PipelineManager) { return; } - this->m_pipelineManager->SetScene(this->GetMRMLScene()); - this->m_pipelineManager->UpdateFromScene(); + this->PipelineManager->SetScene(this->GetMRMLScene()); + this->PipelineManager->UpdateFromScene(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::OnMRMLDisplayableNodeModifiedEvent(vtkObject* caller) { auto viewNode = vtkMRMLAbstractViewNode::SafeDownCast(caller); - if (!viewNode || !this->m_pipelineManager) + if (!viewNode || !this->PipelineManager) { return; } - this->m_pipelineManager->SetViewNode(viewNode); + this->PipelineManager->SetViewNode(viewNode); } +//----------------------------------------------------------------------------- int vtkMRMLLayerDisplayableManager::GetMouseCursor() { - if (!this->m_pipelineManager) + if (!this->PipelineManager) { return vtkMRMLAbstractDisplayableManager::GetMouseCursor(); } - return this->m_pipelineManager->GetMouseCursor(); + return this->PipelineManager->GetMouseCursor(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::Create() { vtkRenderer* renderer = this->GetRenderer(); @@ -166,43 +204,46 @@ void vtkMRMLLayerDisplayableManager::Create() return; } - if (!this->m_pipelineManager) + if (!this->PipelineManager) { - this->m_pipelineManager = vtkSmartPointer::New(); + this->PipelineManager = vtkSmartPointer::New(); } - this->m_pipelineManager->SetRenderWindow(renderer->GetRenderWindow()); - this->m_pipelineManager->SetRenderer(renderer); - this->m_pipelineManager->SetFactory(vtkMRMLLayerDMPipelineFactory::GetInstance()); - this->m_pipelineManager->SetScene(this->GetMRMLScene()); - this->m_pipelineManager->SetViewNode(vtkMRMLAbstractViewNode::SafeDownCast(this->GetMRMLDisplayableNode())); - this->m_pipelineManager->SetRequestRender([this] { this->RequestRender(); }); + this->PipelineManager->SetRenderWindow(renderer->GetRenderWindow()); + this->PipelineManager->SetRenderer(renderer); + this->PipelineManager->SetFactory(vtkMRMLLayerDMPipelineFactory::GetInstance()); + this->PipelineManager->SetScene(this->GetMRMLScene()); + this->PipelineManager->SetViewNode(vtkMRMLAbstractViewNode::SafeDownCast(this->GetMRMLDisplayableNode())); + this->PipelineManager->SetRequestRender([this] { this->RequestRender(); }); // Make sure the DM is up to date with the current scene state this->UpdateFromMRML(); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::SetRenderer(vtkRenderer* newRenderer) { Superclass::SetRenderer(newRenderer); - if (!this->m_pipelineManager) + if (!this->PipelineManager) { return; } - this->m_pipelineManager->SetRenderWindow(newRenderer ? newRenderer->GetRenderWindow() : nullptr); - this->m_pipelineManager->SetRenderer(newRenderer); + this->PipelineManager->SetRenderWindow(newRenderer ? newRenderer->GetRenderWindow() : nullptr); + this->PipelineManager->SetRenderer(newRenderer); } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::SetHasFocus(bool hasFocus, vtkMRMLInteractionEventData* eventData) { - Superclass ::SetHasFocus(hasFocus, eventData); - if (this->m_pipelineManager && !hasFocus) + Superclass::SetHasFocus(hasFocus, eventData); + if (this->PipelineManager && !hasFocus) { - this->m_pipelineManager->LoseFocus(eventData); + this->PipelineManager->LoseFocus(eventData); } } +//----------------------------------------------------------------------------- vtkSmartPointer vtkMRMLLayerDisplayableManager::RenderWindowBufferToImage(vtkRenderWindow* window) { auto imageData = vtkSmartPointer::New(); @@ -210,6 +251,7 @@ vtkSmartPointer vtkMRMLLayerDisplayableManager::RenderWindowBuffer return imageData; } +//----------------------------------------------------------------------------- void vtkMRMLLayerDisplayableManager::RenderWindowBufferToImage(vtkRenderWindow* window, const vtkSmartPointer& imageData) { if (!window || !imageData) @@ -241,3 +283,10 @@ void vtkMRMLLayerDisplayableManager::RenderWindowBufferToImage(vtkRenderWindow* // Free the pixel buffer allocated by VTK delete[] pixels; } + +//----------------------------------------------------------------------------- +void vtkMRMLLayerDisplayableManager::PrintSelf(ostream& os, vtkIndent indent) +{ + this->Superclass::PrintSelf(os, indent); + os << indent << "Pipeline manager: " << (this->PipelineManager ? "set" : "(none)") << std::endl; +} diff --git a/LayerDM/MRMLDM/vtkMRMLLayerDisplayableManager.h b/LayerDM/MRMLDM/vtkMRMLLayerDisplayableManager.h index 993f585..c4e2275 100644 --- a/LayerDM/MRMLDM/vtkMRMLLayerDisplayableManager.h +++ b/LayerDM/MRMLDM/vtkMRMLLayerDisplayableManager.h @@ -1,4 +1,25 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +#ifndef __vtkMRMLLayerDisplayableManager_h +#define __vtkMRMLLayerDisplayableManager_h #include "vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h" @@ -10,7 +31,7 @@ class vtkImageData; class vtkMRMLDisplayableManagerFactory; -class vtkMRMLLayerDMPipelineI; +class vtkMRMLLayerDMPipeline; class vtkMRMLLayerDMPipelineManager; class vtkRenderWindow; @@ -25,6 +46,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDispla public: static vtkMRMLLayerDisplayableManager* New(); vtkTypeMacro(vtkMRMLLayerDisplayableManager, vtkMRMLAbstractDisplayableManager); + void PrintSelf(ostream& os, vtkIndent indent) override; bool CanProcessInteractionEvent(vtkMRMLInteractionEventData* eventData, double& distance2) override; bool ProcessInteractionEvent(vtkMRMLInteractionEventData* eventData) override; @@ -44,7 +66,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDispla /// Pipelines are completely managed by the displayable manager and any dependencies can be /// injected when creating the pipeline through the pipeline creator mechanism. /// Runtime access logic shouldn't be necessary outside the LayerDM layer. - vtkSmartPointer GetNodePipeline(vtkMRMLNode* node) const; + vtkSmartPointer GetNodePipeline(vtkMRMLNode* node) const; /// @{ /// Utility function to get the content of the render window image as buffer @@ -77,5 +99,7 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDispla void SetHasFocus(bool hasFocus, vtkMRMLInteractionEventData* eventData) override; private: - vtkSmartPointer m_pipelineManager; + vtkSmartPointer PipelineManager; }; + +#endif diff --git a/LayerDM/MRMLDM/vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h b/LayerDM/MRMLDM/vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h index 44ca3e5..8f82921 100644 --- a/LayerDM/MRMLDM/vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h +++ b/LayerDM/MRMLDM/vtkSlicerLayerDMModuleMRMLDisplayableManagerExport.h @@ -1,3 +1,29 @@ -#pragma once +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + +/// Provides the Slicer conventional export header name for this library. +/// The Slicer core build generates its own equivalent in the build tree; this file is what +/// supplies the name in the standalone build, where only the module header is generated. +#ifndef __vtkSlicerLayerDMModuleMRMLDisplayableManagerExport_h +#define __vtkSlicerLayerDMModuleMRMLDisplayableManagerExport_h #include "vtkSlicerLayerDMModuleMRMLDisplayableManagerModule.h" + +#endif diff --git a/LayerDM/MRMLDM/vtkSlicerLayerDMModuleMRMLDisplayableManagerModule.h.in b/LayerDM/MRMLDM/vtkSlicerLayerDMModuleMRMLDisplayableManagerModule.h.in index c8bdae4..7ff0242 100644 --- a/LayerDM/MRMLDM/vtkSlicerLayerDMModuleMRMLDisplayableManagerModule.h.in +++ b/LayerDM/MRMLDM/vtkSlicerLayerDMModuleMRMLDisplayableManagerModule.h.in @@ -3,10 +3,10 @@ /// The vtkSlicerLayerDMModuleMRMLDisplayableManagerModule captures some system differences between Unix /// and Windows operating systems, and enables auto init for the VTK module. -#ifndef __vtkSlicerLayerDMModuleMRMLDisplayableManagerExport_h -#define __vtkSlicerLayerDMModuleMRMLDisplayableManagerExport_h +#ifndef __vtkSlicerLayerDMModuleMRMLDisplayableManagerModule_h +#define __vtkSlicerLayerDMModuleMRMLDisplayableManagerModule_h -#if defined(_WIN32) && !defined(MRMLLayerDM_STATIC) +#if defined(_WIN32) && !defined(vtkSlicerLayerDMModuleMRMLDisplayableManager_STATIC) # if defined(vtkSlicerLayerDMModuleMRMLDisplayableManager_EXPORTS) # define VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT __declspec(dllexport) # else diff --git a/LayerDM/MRMLDM/wheel.cmake b/LayerDM/MRMLDM/wheel.cmake index 6d5e427..887ce6d 100644 --- a/LayerDM/MRMLDM/wheel.cmake +++ b/LayerDM/MRMLDM/wheel.cmake @@ -2,10 +2,10 @@ set(classes vtkMRMLLayerDMCameraSynchronizer vtkMRMLLayerDMInteractionLogic vtkMRMLLayerDMLayerManager + vtkMRMLLayerDMPipeline vtkMRMLLayerDMPipelineCallbackCreator - vtkMRMLLayerDMPipelineCreatorI + vtkMRMLLayerDMPipelineCreator vtkMRMLLayerDMPipelineFactory - vtkMRMLLayerDMPipelineI vtkMRMLLayerDMPipelineManager vtkMRMLLayerDisplayableManager vtkMRMLLayerDMPipelineScriptedCreator diff --git a/LayerDM/Testing/Cxx/CMakeLists.txt b/LayerDM/Testing/Cxx/CMakeLists.txt index a0e3737..6aaff6f 100644 --- a/LayerDM/Testing/Cxx/CMakeLists.txt +++ b/LayerDM/Testing/Cxx/CMakeLists.txt @@ -1,42 +1,32 @@ -set(KIT ${MODULE_NAME}) +set(KIT qSlicer${MODULE_NAME}Module) -list(APPEND QT_REQUIRED_COMPONENTS Core Test) -if(Slicer_REQUIRED_QT_VERSION VERSION_GREATER_EQUAL "6") - find_package(Qt6 COMPONENTS ${QT_REQUIRED_COMPONENTS} QUIET) -else() - find_package(Qt5 COMPONENTS ${QT_REQUIRED_COMPONENTS} QUIET) +# NodeReferenceObserverTest builds its node references from Markups nodes. +slicer_is_loadable_builtin_module_enabled("Markups" _build_markups_module) +if(NOT _build_markups_module) + return() endif() -set(TEST_SOURCES +#----------------------------------------------------------------------------- +set(KIT_TEST_SRCS NodeReferenceObserverTest.cxx -) - -set(EXTRA_INCLUDE "vtkMRMLDebugLeaksMacro.h\"\n\#include \n\#include \n\#include \"vtkTestingOutputWindow.h") -create_test_sourcelist(Tests ${KIT}CppTests.cxx - ${TEST_SOURCES} - EXTRA_INCLUDE ${EXTRA_INCLUDE} -) + ) +#----------------------------------------------------------------------------- include_directories(${CMAKE_CURRENT_BINARY_DIR}) -ctk_add_executable_utf8(${KIT}CxxTests ${Tests} ${Tests_MOC_CXX} ${Tests_UtilityFiles}) +#----------------------------------------------------------------------------- +slicerMacroConfigureModuleCxxTestDriver( + NAME ${KIT} + SOURCES ${KIT_TEST_SRCS} + TARGET_LIBRARIES vtkSlicerMarkupsModuleMRML + WITH_VTK_DEBUG_LEAKS_CHECK + WITH_VTK_ERROR_OUTPUT_CHECK + ) + +# Configure CMake Qt automatic code generation set_target_properties(${KIT}CxxTests PROPERTIES AUTOMOC ON -) - -foreach(lib IN LISTS QT_REQUIRED_COMPONENTS) - if(Slicer_REQUIRED_QT_VERSION VERSION_GREATER_EQUAL "6") - list(APPEND MODULE_TARGET_LIBRARIES Qt6::${lib}) - else() - list(APPEND MODULE_TARGET_LIBRARIES Qt5::${lib}) - endif() -endforeach() - -target_link_libraries( - ${KIT}CxxTests - ${MODULE_TARGET_LIBRARIES} - vtkSlicerMarkupsModuleMRML -) + ) -include(SlicerMacroSimpleTest) +#----------------------------------------------------------------------------- simple_test(NodeReferenceObserverTest) diff --git a/LayerDM/Testing/Cxx/NodeReferenceObserverTest.cxx b/LayerDM/Testing/Cxx/NodeReferenceObserverTest.cxx index b5215ef..f76e9f3 100644 --- a/LayerDM/Testing/Cxx/NodeReferenceObserverTest.cxx +++ b/LayerDM/Testing/Cxx/NodeReferenceObserverTest.cxx @@ -1,3 +1,23 @@ +/*============================================================================== + + Program: 3D Slicer + + Copyright (c) Kitware SAS + + See COPYRIGHT.txt + or http://www.slicer.org/copyright/copyright.txt for details. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + +==============================================================================*/ + // LayerDM includes #include "vtkMRMLLayerDMObjectEventObserver.h" #include "vtkMRMLLayerDMNodeReferenceObserver.h" @@ -10,6 +30,9 @@ // VTK includes #include +// STD includes +#include + // CTK includes #include "vtkSlicerLayerDMLogic.h" @@ -17,6 +40,7 @@ namespace { +//----------------------------------------------------------------------------- struct Spy { @@ -42,6 +66,7 @@ struct Spy int callCount{}; }; +//----------------------------------------------------------------------------- struct Test { Test(const vtkSmartPointer& inScene = nullptr) @@ -67,6 +92,7 @@ struct Test }; } // namespace +//----------------------------------------------------------------------------- class NodeReferenceObserverTester : public QObject { Q_OBJECT @@ -237,11 +263,12 @@ private slots: QVERIFY(test.obs->GetReferenceFromSize() <= nNodes); // Verify that the references from / to the created markups is consistent with the current display node + using RefT = vtkMRMLLayerDMNodeReferenceObserver::RefT; const auto refTo = test.obs->GetNodeToReferences(markups); - QVERIFY(refTo.find({ markups->GetDisplayNode(), "display" }) != refTo.end()); + QVERIFY(std::find(refTo.begin(), refTo.end(), RefT{ markups->GetDisplayNode(), "display" }) != refTo.end()); const auto refFrom = test.obs->GetNodeFromReferences(markups->GetDisplayNode()); - QVERIFY(refFrom.find({ markups, "display" }) != refFrom.end()); + QVERIFY(std::find(refFrom.begin(), refFrom.end(), RefT{ markups, "display" }) != refFrom.end()); // Remove the markups and its display node from the scene scene->RemoveNode(markups->GetDisplayNode()); diff --git a/LayerDM/Testing/Python/CMakeLists.txt b/LayerDM/Testing/Python/CMakeLists.txt index 8d2a504..62294ee 100644 --- a/LayerDM/Testing/Python/CMakeLists.txt +++ b/LayerDM/Testing/Python/CMakeLists.txt @@ -6,12 +6,19 @@ set(EXTENSION_TEST_PYTHON_SCRIPTS InteractionLogicTest.py LayerManagerTest.py LogicTest.py - ObjectEventObserverScriptedTest.py PipelineFactoryTest.py - PipelineManagerTest.py - SelectionTest.py ) +# These tests build their nodes from Markups nodes. +slicer_is_loadable_builtin_module_enabled("Markups" _build_markups_module) +if(_build_markups_module) + list(APPEND EXTENSION_TEST_PYTHON_SCRIPTS + ObjectEventObserverScriptedTest.py + PipelineManagerTest.py + SelectionTest.py + ) +endif() + set(EXTENSION_TEST_PYTHON_RESOURCES ) diff --git a/LayerDM/Testing/Python/CameraSynchronizerTest.py b/LayerDM/Testing/Python/CameraSynchronizerTest.py index 003aae0..58384ef 100644 --- a/LayerDM/Testing/Python/CameraSynchronizerTest.py +++ b/LayerDM/Testing/Python/CameraSynchronizerTest.py @@ -1,14 +1,13 @@ from unittest.mock import MagicMock -import slicer from slicer import vtkMRMLLayerDMCameraSynchronizer, vtkMRMLSliceNode, vtkMRMLViewNode -from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest from vtk import vtkCamera, vtkCommand, vtkRenderWindow, vtkRenderer +from LayerDMTestSupport import ScriptedLoadableModuleTest + class CameraSynchronizerTest(ScriptedLoadableModuleTest): def setUp(self): - slicer.mrmlScene.Clear(0) self.renderWindow = vtkRenderWindow() self.renderer = vtkRenderer() diff --git a/LayerDM/Testing/Python/DisplayableManagerTest.py b/LayerDM/Testing/Python/DisplayableManagerTest.py index ce11159..d756328 100644 --- a/LayerDM/Testing/Python/DisplayableManagerTest.py +++ b/LayerDM/Testing/Python/DisplayableManagerTest.py @@ -1,7 +1,8 @@ -import slicer from slicer import ( + vtkMRMLApplicationLogic, + vtkMRMLDisplayableManagerGroup, vtkMRMLLayerDMPipelineFactory, - vtkMRMLLayerDMPipelineI, + vtkMRMLLayerDMPipeline, vtkMRMLLayerDMPipelineManager, vtkMRMLLayerDMPipelineScriptedCreator, vtkMRMLLayerDisplayableManager, @@ -10,15 +11,20 @@ vtkMRMLSliceViewDisplayableManagerFactory, vtkMRMLThreeDViewDisplayableManagerFactory, ) -from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest from vtk import VTK_OBJECT, vtkActor, vtkImageData, vtkObjectFactory, vtkRenderer +from LayerDMTestSupport import ( + ScriptedLoadableModuleTest, + create_render_window, + create_test_scene, + is_rendering_supported, +) from MockPipeline import MockPipeline class DisplayableManagerTest(ScriptedLoadableModuleTest): def setUp(self): - slicer.mrmlScene.Clear(0) + self.scene = create_test_scene() # Verify the DM can be created by the vtk Object Factory assert vtkObjectFactory.CreateInstance(vtkMRMLLayerDisplayableManager.__name__) is not None @@ -34,7 +40,23 @@ def setUp(self): vtkMRMLThreeDViewDisplayableManagerFactory.GetInstance() ) - self.threeDNode = slicer.mrmlScene.GetNodeByID("vtkMRMLViewNode1") + # Create a 3D view node and display it in a standalone render window. + # This is the slicer-core equivalent of the 3D view created by the 3D Slicer + # application layout manager. + self.threeDNode = self.scene.AddNewNodeByClass("vtkMRMLViewNode") + self.renderWindow, self.renderer = create_render_window() + + self.displayableManagerFactory = vtkMRMLThreeDViewDisplayableManagerFactory() + self.displayableManagerFactory.SetMRMLApplicationLogic(vtkMRMLApplicationLogic()) + vtkMRMLLayerDisplayableManager.RegisterInFactory(self.displayableManagerFactory) + + self.displayableManagerGroup = vtkMRMLDisplayableManagerGroup() + self.displayableManagerGroup.Initialize(self.displayableManagerFactory, self.renderer) + self.displayableManagerGroup.SetMRMLDisplayableNode(self.threeDNode) + self.displayableManager = self.displayableManagerGroup.GetDisplayableManagerByClassName( + vtkMRMLLayerDisplayableManager.__name__ + ) + self.node = vtkMRMLScriptedModuleNode() # Register a pipeline creator with static pipeline @@ -60,29 +82,27 @@ def onRendererAdded(renderer: vtkRenderer): self.pipeline.mockOnRendererAdded.side_effect = onRendererAdded # Trigger pipeline creation by adding a node to the scene - slicer.mrmlScene.AddNode(self.node) + self.scene.AddNode(self.node) # Verify the pipeline's actor is present in the threed view - renderWindow = slicer.app.layoutManager().viewWidget(self.threeDNode).viewWidget().renderWindow() - renderer = renderWindow.GetRenderers().GetItemAsObject(0) - assert renderer.HasViewProp(actor) + assert self.renderer.HasViewProp(actor) def test_registered_pipelines_have_access_to_scene_manager_view_and_display_node(self): # Trigger pipeline creation by adding a new model node to the scene - slicer.mrmlScene.AddNode(self.node) + self.scene.AddNode(self.node) # Check getters point to the right instances - assert self.pipeline.GetScene() == slicer.mrmlScene + assert self.pipeline.GetScene() == self.scene assert isinstance(self.pipeline.GetPipelineManager(), vtkMRMLLayerDMPipelineManager) assert self.pipeline.GetViewNode() == self.threeDNode assert self.pipeline.GetDisplayNode() == self.node def test_registered_pipelines_can_override_setters_to_scene_manager_view_and_display_node(self): # Trigger pipeline creation by adding a new model node to the scene - slicer.mrmlScene.AddNode(self.node) + self.scene.AddNode(self.node) # Check overridden setters where called - self.pipeline.mockSetScene.assert_called_with(slicer.mrmlScene) + self.pipeline.mockSetScene.assert_called_with(self.scene) self.pipeline.mockSetPipelineManager.assert_called_with(self.pipeline.GetPipelineManager()) self.pipeline.mockSetViewNode.assert_called_with(self.threeDNode) self.pipeline.mockSetDisplayNode.assert_called_with(self.node) @@ -99,8 +119,8 @@ def test_pipelines_have_access_to_sibling_pipelines(self): self.factory.AddPipelineCreator(otherCreator) # Add both nodes to the scene for pipeline creation to happen - slicer.mrmlScene.AddNode(self.node) - slicer.mrmlScene.AddNode(otherNode) + self.scene.AddNode(self.node) + self.scene.AddNode(otherNode) # Assert the first pipeline has access to the second one assert self.pipeline.GetNodePipeline(otherNode) == otherPipeline @@ -108,43 +128,47 @@ def test_pipelines_have_access_to_sibling_pipelines(self): def test_pipelines_can_add_observers_to_vtk_objects_and_events(self): def onSetScene(scene: vtkMRMLScene): assert self.pipeline.GetScene() != scene - self.pipeline.UpdateObserver(self.pipeline.GetScene(), scene, vtkMRMLScene.NodeAddedEvent) - vtkMRMLLayerDMPipelineI.SetScene(self.pipeline, scene) + self.pipeline.UpdateObservation(self.pipeline.GetScene(), scene, vtkMRMLScene.NodeAddedEvent) + vtkMRMLLayerDMPipeline.SetScene(self.pipeline, scene) # Add observer on the scene when its added self.pipeline.mockSetScene.side_effect = onSetScene - slicer.mrmlScene.AddNode(self.node) + self.scene.AddNode(self.node) self.pipeline.mockSetScene.assert_called_once() - modelNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode") + modelNode = self.scene.AddNewNodeByClass("vtkMRMLModelNode") self.pipeline.mockOnUpdate.assert_called_once() args = self.pipeline.mockOnUpdate.call_args[0] - assert args[0] == slicer.mrmlScene + assert args[0] == self.scene assert args[1] == vtkMRMLScene.NodeAddedEvent assert self.pipeline.CastCallData(args[2], VTK_OBJECT) == modelNode def test_pipelines_update_display_is_called_at_init(self): - slicer.mrmlScene.AddNode(self.node) - self.pipeline.mockUpdatePipeline.assert_called_once() + self.scene.AddNode(self.node) + self.pipeline.mockUpdateFromMRML.assert_called_once() - def test_pipelines_update_display_is_called_at_reset(self): - slicer.mrmlScene.AddNode(self.node) - self.pipeline.mockUpdatePipeline.reset_mock() + def test_pipelines_update_from_mrml_is_called_at_update_display(self): + self.scene.AddNode(self.node) + self.pipeline.mockUpdateFromMRML.reset_mock() - self.pipeline.ResetDisplay() - self.pipeline.mockUpdatePipeline.assert_called_once() + self.pipeline.UpdateDisplay() + self.pipeline.mockUpdateFromMRML.assert_called_once() def test_pipeline_exceptions_are_propagated_to_python(self): _error_msg = "Something went wrong in Python" - self.pipeline.mockUpdatePipeline.side_effect = RuntimeError(_error_msg) + self.pipeline.mockUpdateFromMRML.side_effect = RuntimeError(_error_msg) with self.assertRaises(RuntimeError) as context: - slicer.mrmlScene.AddNode(self.node) + self.scene.AddNode(self.node) assert _error_msg in str(context.exception) def test_can_render_to_image_data(self): - render_window = slicer.app.layoutManager().threeDWidget(0).threeDView().renderWindow() + if not is_rendering_supported(): + self.skipTest("No OpenGL implementation available to render offscreen") + + render_window = self.renderWindow + render_window.Render() # Direct return value call image = vtkMRMLLayerDisplayableManager.RenderWindowBufferToImage(render_window) diff --git a/LayerDM/Testing/Python/EventTranslationNodeTest.py b/LayerDM/Testing/Python/EventTranslationNodeTest.py index 2c13418..0c46c08 100644 --- a/LayerDM/Testing/Python/EventTranslationNodeTest.py +++ b/LayerDM/Testing/Python/EventTranslationNodeTest.py @@ -1,16 +1,16 @@ import os from tempfile import TemporaryDirectory -import slicer from slicer import vtkMRMLAbstractWidget, vtkMRMLInteractionEventData -from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest from vtk import vtkCommand, vtkEvent +from LayerDMTestSupport import ScriptedLoadableModuleTest, create_test_scene, load_scene, save_scene + class EventTranslationNodeTest(ScriptedLoadableModuleTest): def setUp(self): - slicer.mrmlScene.Clear(0) - self.tl_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLayerDMWidgetEventTranslationNode") + self.scene = create_test_scene() + self.tl_node = self.scene.AddNewNodeByClass("vtkMRMLLayerDMWidgetEventTranslationNode") self.no_event = vtkMRMLAbstractWidget.WidgetEventNone def save_restore_scene(self): @@ -18,11 +18,11 @@ def save_restore_scene(self): with TemporaryDirectory() as tmp_dir: scene_path = os.path.join(tmp_dir, "scene.mrb") - slicer.util.saveScene(scene_path) - slicer.mrmlScene.Clear(0) - slicer.util.loadScene(scene_path) + save_scene(self.scene, scene_path) + self.scene.Clear(0) + load_scene(self.scene, scene_path) - return slicer.mrmlScene.GetNodeByID(node_id) + return self.scene.GetNodeByID(node_id) def click_event(self, event_type, modifier=vtkEvent.AnyModifier): event_data = vtkMRMLInteractionEventData() diff --git a/LayerDM/Testing/Python/InteractionLogicTest.py b/LayerDM/Testing/Python/InteractionLogicTest.py index 4082c4b..31f67a7 100644 --- a/LayerDM/Testing/Python/InteractionLogicTest.py +++ b/LayerDM/Testing/Python/InteractionLogicTest.py @@ -1,14 +1,12 @@ -import slicer from slicer import vtkMRMLLayerDMInteractionLogic, vtkMRMLInteractionEventData, vtkMRMLAbstractWidget -from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest from vtk import reference, vtkCommand +from LayerDMTestSupport import ScriptedLoadableModuleTest from MockPipeline import MockPipeline class InteractionLogicTest(ScriptedLoadableModuleTest): def setUp(self): - slicer.mrmlScene.Clear(0) self.distance = reference(0.0) self.event = vtkMRMLInteractionEventData() self.logic = vtkMRMLLayerDMInteractionLogic() diff --git a/LayerDM/Testing/Python/LayerDMTestSupport.py b/LayerDM/Testing/Python/LayerDMTestSupport.py new file mode 100644 index 0000000..a14eebf --- /dev/null +++ b/LayerDM/Testing/Python/LayerDMTestSupport.py @@ -0,0 +1,135 @@ +""" +Helpers used by the LayerDM tests so that they can run both: + - inside the 3D Slicer application (extension testing), + - in a plain Python / Pytest / VTK / slicer-core environment (wheel testing). + +The tests build their own MRML scene with :func:`create_test_scene` instead of +using the ``slicer.mrmlScene`` singleton, which is only set inside the 3D Slicer +application. The ``ScriptedLoadableModuleTest`` helper class provided by 3D +Slicer is also only available there, so this module falls back to +``unittest.TestCase`` when it cannot be imported. +""" + +import subprocess +import sys +import textwrap +import unittest + +try: + # Only available when running inside the 3D Slicer application + from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest +except ImportError: + # unittest.TestCase is used instead so that the tests can be collected and + # run by pytest (which honors setUp/tearDown) outside of 3D Slicer. + ScriptedLoadableModuleTest = unittest.TestCase + + +def create_test_scene(): + """ + Create the MRML scene used by a test. + + On top of a plain ``vtkMRMLScene``, this: + - creates the interaction and selection singleton nodes, + - registers the LayerDM MRML nodes in the scene (which is done by the + module logic at application startup in 3D Slicer). + + The MRML node classes are registered in the scene itself, so each test needs + its own scene. The scene also keeps the logic instances it depends on alive. + """ + from slicer import vtkMRMLApplicationLogic, vtkMRMLScene, vtkSlicerLayerDMLogic + + scene = vtkMRMLScene() + + # Creates the interaction & selection singleton nodes + applicationLogic = vtkMRMLApplicationLogic() + applicationLogic.SetMRMLScene(scene) + + # Registers the LayerDM MRML nodes in the scene + moduleLogic = vtkSlicerLayerDMLogic() + moduleLogic.SetMRMLScene(scene) + moduleLogic.RegisterNodes() + + # Keep the logic instances alive for as long as the scene is used + scene.applicationLogic = applicationLogic + scene.moduleLogic = moduleLogic + return scene + + +def save_scene(scene, path) -> None: + """ + Save the input scene to the input ``.mrml`` or ``.mrb`` path. + + The scene is written directly instead of going through + ``slicer.util.saveScene``, which only saves the 3D Slicer application scene. + """ + scene.SetURL(str(path)) + scene.Commit() + + +def load_scene(scene, path) -> None: + """ + Load the input ``.mrml`` or ``.mrb`` path into the input scene. + + The scene is read directly instead of going through ``slicer.util.loadScene``, + which only loads into the 3D Slicer application scene. + """ + scene.SetURL(str(path)) + scene.Connect() + + +def create_render_window(size=(300, 300)): + """ + Create a render window which can be used to drive displayable managers. + + Return a ``(vtkRenderWindow, vtkRenderer)`` tuple. + + Note: the render window needs an interactor. Otherwise, instantiating the + displayable managers of a ``vtkMRMLDisplayableManagerGroup`` would crash. + """ + from vtk import vtkInteractorStyle, vtkRenderer, vtkRenderWindow, vtkRenderWindowInteractor + + renderWindow = vtkRenderWindow() + # Offscreen rendering avoids popping up windows when running the tests + renderWindow.SetOffScreenRendering(True) + renderer = vtkRenderer() + renderWindow.AddRenderer(renderer) + + interactor = vtkRenderWindowInteractor() + interactor.SetInteractorStyle(vtkInteractorStyle()) + renderWindow.SetInteractor(interactor) + + renderWindow.SetSize(int(size[0]), int(size[1])) + return renderWindow, renderer + + +_RENDER_PROBE_SCRIPT = """ +import vtk + +renderWindow = vtk.vtkRenderWindow() +renderWindow.SetOffScreenRendering(True) +renderer = vtk.vtkRenderer() +renderWindow.AddRenderer(renderer) +interactor = vtk.vtkRenderWindowInteractor() +interactor.SetInteractorStyle(vtk.vtkInteractorStyle()) +renderWindow.SetInteractor(interactor) +renderWindow.SetSize(64, 64) +renderWindow.Render() +""" + +_renderingSupported = None + + +def is_rendering_supported(): + global _renderingSupported + if _renderingSupported is None: + _renderingSupported = _probe_rendering() + return _renderingSupported + + +def _probe_rendering(timeout=120): + command = [sys.executable, "-c", textwrap.dedent(_RENDER_PROBE_SCRIPT)] + try: + completed = subprocess.run(command, capture_output=True, timeout=timeout, check=False) + except (OSError, subprocess.SubprocessError): + return False + return completed.returncode == 0 diff --git a/LayerDM/Testing/Python/LayerManagerTest.py b/LayerDM/Testing/Python/LayerManagerTest.py index e2f2c56..0cd4baf 100644 --- a/LayerDM/Testing/Python/LayerManagerTest.py +++ b/LayerDM/Testing/Python/LayerManagerTest.py @@ -1,11 +1,11 @@ from unittest.mock import MagicMock -import slicer from LayerDMLib import vtkMRMLLayerDMScriptedPipeline -from slicer import vtkMRMLLayerDMLayerManager, vtkMRMLLayerDMPipelineI -from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest +from slicer import vtkMRMLLayerDMLayerManager, vtkMRMLLayerDMPipeline from vtk import vtkActor, vtkCamera, vtkPolyDataMapper, vtkRenderWindow, vtkRenderer, vtkSphereSource +from LayerDMTestSupport import ScriptedLoadableModuleTest + OptionalCamera = vtkCamera | None @@ -60,7 +60,6 @@ def GetCustomCamera(self, renderOrder: int) -> OptionalCamera: class LayerManagerTest(ScriptedLoadableModuleTest): def setUp(self): - slicer.mrmlScene.Clear(0) self.renderWindow = vtkRenderWindow() self.defaultRenderer = vtkRenderer() self.renderWindow.AddRenderer(self.defaultRenderer) @@ -299,7 +298,7 @@ def test_supports_pipelines_with_dynamic_render_orders(self): assert p2.GetRenderer(10000).GetLayer() == 3 p2._renderOrders = [0, 1, 2] - p2.InvokeEvent(vtkMRMLLayerDMPipelineI.RenderGroupingModified) + p2.InvokeEvent(vtkMRMLLayerDMPipeline.RenderGroupingModified) assert p1.GetRenderer(1000).GetLayer() == 4 assert p2.GetRenderer(2).GetLayer() == 2 diff --git a/LayerDM/Testing/Python/LogicTest.py b/LayerDM/Testing/Python/LogicTest.py index 42dfc87..8972ff9 100644 --- a/LayerDM/Testing/Python/LogicTest.py +++ b/LayerDM/Testing/Python/LogicTest.py @@ -1,36 +1,36 @@ -import slicer from slicer import vtkSlicerLayerDMLogic, vtkMRMLLayerDMWidgetEventTranslationNode -from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest + +from LayerDMTestSupport import ScriptedLoadableModuleTest, create_test_scene class LogicTest(ScriptedLoadableModuleTest): def setUp(self): - slicer.mrmlScene.Clear(0) + self.scene = create_test_scene() def test_can_attach_translation_nodes_to_other_nodes(self): - node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode") + node = self.scene.AddNewNodeByClass("vtkMRMLModelNode") assert node is not None - tl_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLayerDMWidgetEventTranslationNode") + tl_node = self.scene.AddNewNodeByClass("vtkMRMLLayerDMWidgetEventTranslationNode") assert tl_node is not None vtkSlicerLayerDMLogic.SetWidgetEventTranslationNode(node, tl_node) assert vtkSlicerLayerDMLogic.GetWidgetEventTranslationNode(node) == tl_node def test_can_register_singleton_translation_nodes(self): - assert vtkSlicerLayerDMLogic.GetWidgetEventTranslationSingleton(slicer.mrmlScene, "MyTLNodeSingleton") is None - tl_node = vtkSlicerLayerDMLogic.CreateWidgetEventTranslationSingleton(slicer.mrmlScene, "MyTLNodeSingleton") + assert vtkSlicerLayerDMLogic.GetWidgetEventTranslationSingleton(self.scene, "MyTLNodeSingleton") is None + tl_node = vtkSlicerLayerDMLogic.CreateWidgetEventTranslationSingleton(self.scene, "MyTLNodeSingleton") assert tl_node is not None assert isinstance(tl_node, vtkMRMLLayerDMWidgetEventTranslationNode) assert not tl_node.GetSaveWithScene() def test_singleton_translation_nodes_are_created_once(self): - tl_node = vtkSlicerLayerDMLogic.CreateWidgetEventTranslationSingleton(slicer.mrmlScene, "MyTLNodeSingleton") - assert vtkSlicerLayerDMLogic.CreateWidgetEventTranslationSingleton(slicer.mrmlScene, "MyTLNodeSingleton") == tl_node + tl_node = vtkSlicerLayerDMLogic.CreateWidgetEventTranslationSingleton(self.scene, "MyTLNodeSingleton") + assert vtkSlicerLayerDMLogic.CreateWidgetEventTranslationSingleton(self.scene, "MyTLNodeSingleton") == tl_node def test_can_attach_singleton_tl_node(self): - node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode") + node = self.scene.AddNewNodeByClass("vtkMRMLModelNode") assert node is not None - tl_node = vtkSlicerLayerDMLogic.CreateWidgetEventTranslationSingleton(slicer.mrmlScene, "MyTLNodeSingleton") + tl_node = vtkSlicerLayerDMLogic.CreateWidgetEventTranslationSingleton(self.scene, "MyTLNodeSingleton") vtkSlicerLayerDMLogic.SetWidgetEventTranslationNode(node, tl_node) assert vtkSlicerLayerDMLogic.GetWidgetEventTranslationNode(node) == tl_node diff --git a/LayerDM/Testing/Python/MockPipeline.py b/LayerDM/Testing/Python/MockPipeline.py index 205a9fa..999b2ba 100644 --- a/LayerDM/Testing/Python/MockPipeline.py +++ b/LayerDM/Testing/Python/MockPipeline.py @@ -50,7 +50,7 @@ def __init__( self.mockSetScene = MagicMock() self.mockSetPipelineManager = MagicMock() self.mockSetRenderers = MagicMock() - self.mockUpdatePipeline = MagicMock() + self.mockUpdateFromMRML = MagicMock() def CanProcessInteractionEvent(self, eventData: vtkMRMLInteractionEventData) -> tuple[bool, float]: return self.mockCanProcess(eventData) @@ -113,5 +113,5 @@ def SetPipelineManager(self, pipelineManager: vtkMRMLLayerDMPipelineManager) -> def SetRenderers(self, renderers: list[vtkRenderer]) -> None: self.mockSetRenderers(renderers) - def UpdatePipeline(self) -> None: - self.mockUpdatePipeline() + def UpdateFromMRML(self) -> None: + self.mockUpdateFromMRML() diff --git a/LayerDM/Testing/Python/ObjectEventObserverScriptedTest.py b/LayerDM/Testing/Python/ObjectEventObserverScriptedTest.py index 52534a6..2ac99e6 100644 --- a/LayerDM/Testing/Python/ObjectEventObserverScriptedTest.py +++ b/LayerDM/Testing/Python/ObjectEventObserverScriptedTest.py @@ -1,18 +1,19 @@ -import slicer +from unittest.mock import MagicMock + from slicer import ( vtkMRMLMarkupsFiducialNode, vtkMRMLModelNode, vtkMRMLScene, vtkMRMLLayerDMObjectEventObserverScripted, ) -from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest -from unittest.mock import MagicMock from vtk import vtkCommand, VTK_OBJECT +from LayerDMTestSupport import ScriptedLoadableModuleTest, create_test_scene + class vtkMRMLLayerDMObjectEventObserverTest(ScriptedLoadableModuleTest): def setUp(self): - slicer.mrmlScene.Clear(0) + self.scene = create_test_scene() self.mock = MagicMock(return_value=None) self.observer = vtkMRMLLayerDMObjectEventObserverScripted() @@ -21,14 +22,14 @@ def setUp(self): def test_can_be_attached_to_vtk_object_modified_event(self): modelNode = vtkMRMLModelNode() - self.observer.UpdateObserver(None, modelNode, vtkCommand.ModifiedEvent) + self.observer.UpdateObservation(None, modelNode, vtkCommand.ModifiedEvent) modelNode.Modified() self.mock.assert_called_once_with(modelNode, vtkCommand.ModifiedEvent, None) def test_can_be_attached_to_multiple_events(self): markups_node = vtkMRMLMarkupsFiducialNode() - self.observer.UpdateObserver(None, markups_node, [vtkCommand.ModifiedEvent, markups_node.PointModifiedEvent]) + self.observer.UpdateObservation(None, markups_node, [vtkCommand.ModifiedEvent, markups_node.PointModifiedEvent]) markups_node.InvokeEvent(markups_node.PointModifiedEvent) self.mock.assert_called_once_with(markups_node, markups_node.PointModifiedEvent, None) @@ -40,34 +41,34 @@ def test_can_update_observer(self): m1 = vtkMRMLModelNode() m2 = vtkMRMLModelNode() - self.observer.UpdateObserver(None, m1, vtkCommand.ModifiedEvent) - self.observer.UpdateObserver(m1, m2, vtkCommand.ModifiedEvent) + self.observer.UpdateObservation(None, m1, vtkCommand.ModifiedEvent) + self.observer.UpdateObservation(m1, m2, vtkCommand.ModifiedEvent) m1.Modified() m2.Modified() self.mock.assert_called_once_with(m2, vtkCommand.ModifiedEvent, None) def test_can_remove_observer(self): modelNode = vtkMRMLModelNode() - self.observer.UpdateObserver(None, modelNode, vtkCommand.ModifiedEvent) - self.observer.RemoveObserver(modelNode) + self.observer.UpdateObservation(None, modelNode, vtkCommand.ModifiedEvent) + self.observer.RemoveObservations(modelNode) modelNode.Modified() self.mock.assert_not_called() def test_can_cast_observed_call_data(self): - self.observer.UpdateObserver(None, slicer.mrmlScene, vtkMRMLScene.NodeAddedEvent) + self.observer.UpdateObservation(None, self.scene, vtkMRMLScene.NodeAddedEvent) modelNode = vtkMRMLModelNode() - slicer.mrmlScene.AddNode(modelNode) + self.scene.AddNode(modelNode) self.mock.assert_called_once() args = self.mock.call_args[0] - assert args[0] == slicer.mrmlScene + assert args[0] == self.scene assert args[1] == vtkMRMLScene.NodeAddedEvent assert self.observer.CastCallData(args[2], VTK_OBJECT) == modelNode def test_update_can_be_blocked(self): modelNode = vtkMRMLModelNode() - self.observer.UpdateObserver(None, modelNode, vtkCommand.ModifiedEvent) + self.observer.UpdateObservation(None, modelNode, vtkCommand.ModifiedEvent) wasBlocked = self.observer.SetBlocked(True) assert self.observer.IsBlocked() assert not wasBlocked @@ -82,7 +83,7 @@ def test_update_can_be_blocked(self): def test_update_exceptions_are_propagated_to_python(self): modelNode = vtkMRMLModelNode() - self.observer.UpdateObserver(None, modelNode, vtkCommand.ModifiedEvent) + self.observer.UpdateObservation(None, modelNode, vtkCommand.ModifiedEvent) _errorMsg = "Something happened in Python" self.mock.side_effect = RuntimeError(_errorMsg) diff --git a/LayerDM/Testing/Python/PipelineFactoryTest.py b/LayerDM/Testing/Python/PipelineFactoryTest.py index a206aac..c4c28eb 100644 --- a/LayerDM/Testing/Python/PipelineFactoryTest.py +++ b/LayerDM/Testing/Python/PipelineFactoryTest.py @@ -1,28 +1,27 @@ from unittest.mock import MagicMock -import slicer from slicer import ( vtkMRMLLayerDMPipelineFactory, - vtkMRMLLayerDMPipelineI, + vtkMRMLLayerDMPipeline, vtkMRMLLayerDMPipelineScriptedCreator, - vtkMRMLLayerDMPipelineCreatorI, + vtkMRMLLayerDMPipelineCreator, vtkMRMLViewNode, vtkMRMLCameraNode ) -from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest from vtk import vtkCommand +from LayerDMTestSupport import ScriptedLoadableModuleTest + class PipelineFactoryTest(ScriptedLoadableModuleTest): def setUp(self): - slicer.mrmlScene.Clear(0) self.factory = vtkMRMLLayerDMPipelineFactory() def test_is_compatible_with_scripted_dm_creator(self): - instance = vtkMRMLLayerDMPipelineI() + instance = vtkMRMLLayerDMPipeline() creator = vtkMRMLLayerDMPipelineScriptedCreator() creator.SetPythonCallback(lambda *_: instance) - assert isinstance(creator, vtkMRMLLayerDMPipelineCreatorI) + assert isinstance(creator, vtkMRMLLayerDMPipelineCreator) self.factory.AddPipelineCreator(creator) assert self.factory.ContainsPipelineCreator(creator) @@ -34,12 +33,12 @@ def test_provides_singleton_instance(self): assert vtkMRMLLayerDMPipelineFactory.GetInstance() == vtkMRMLLayerDMPipelineFactory.GetInstance() def test_can_remove_creator_by_ref(self): - c1 = vtkMRMLLayerDMPipelineCreatorI() + c1 = vtkMRMLLayerDMPipelineCreator() self.factory.AddPipelineCreator(c1) assert self.factory.ContainsPipelineCreator(c1) - c2 = vtkMRMLLayerDMPipelineCreatorI() + c2 = vtkMRMLLayerDMPipelineCreator() assert not self.factory.ContainsPipelineCreator(c2) self.factory.AddPipelineCreator(c2) assert self.factory.ContainsPipelineCreator(c2) @@ -48,7 +47,7 @@ def test_can_remove_creator_by_ref(self): assert not self.factory.ContainsPipelineCreator(c1) def test_invokes_modified_event_on_add_remove_creator(self): - creator = vtkMRMLLayerDMPipelineCreatorI() + creator = vtkMRMLLayerDMPipelineCreator() mock = MagicMock() self.factory.AddObserver(vtkCommand.ModifiedEvent, mock) @@ -81,7 +80,7 @@ def test_iterates_on_provided_creators_until_one_can_create(self): mock.assert_called_once_with(viewNode, node) mock.reset_mock() - instance = vtkMRMLLayerDMPipelineI() + instance = vtkMRMLLayerDMPipeline() mocks[0].return_value = instance assert self.factory.CreatePipeline(viewNode, node) == instance @@ -95,7 +94,7 @@ def on_about_to_be_created(*_): mock = MagicMock() self.factory.AddObserver(self.factory.PipelineAboutToBeCreatedEvent, on_about_to_be_created) - instance = vtkMRMLLayerDMPipelineI() + instance = vtkMRMLLayerDMPipeline() creator = vtkMRMLLayerDMPipelineScriptedCreator() creator.SetPythonCallback(lambda *_: instance) @@ -108,8 +107,8 @@ def on_about_to_be_created(*_): mock.assert_called_once_with(viewNode, node, instance) def test_creators_handle_creation_by_priority(self): - i1 = vtkMRMLLayerDMPipelineI() - i2 = vtkMRMLLayerDMPipelineI() + i1 = vtkMRMLLayerDMPipeline() + i2 = vtkMRMLLayerDMPipeline() c1 = vtkMRMLLayerDMPipelineScriptedCreator() c1.SetPythonCallback(lambda *_: i1) diff --git a/LayerDM/Testing/Python/PipelineManagerTest.py b/LayerDM/Testing/Python/PipelineManagerTest.py index a0cdd9c..3772467 100644 --- a/LayerDM/Testing/Python/PipelineManagerTest.py +++ b/LayerDM/Testing/Python/PipelineManagerTest.py @@ -1,6 +1,5 @@ from unittest.mock import MagicMock -import slicer from slicer import ( vtkMRMLLayerDMPipelineFactory, vtkMRMLLayerDMPipelineManager, @@ -11,23 +10,24 @@ vtkMRMLModelNode, vtkMRMLScalarVolumeNode, ) -from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest from vtk import vtkRenderWindow, reference as ref, vtkCommand, vtkRenderer + +from LayerDMTestSupport import ScriptedLoadableModuleTest, create_test_scene from MockPipeline import MockPipeline class PipelineManagerTest(ScriptedLoadableModuleTest): def setUp(self): - slicer.mrmlScene.Clear(0) + self.scene = create_test_scene() self.renderWindow = vtkRenderWindow() self.factory = vtkMRMLLayerDMPipelineFactory() - self.viewNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLViewNode") + self.viewNode = self.scene.AddNewNodeByClass("vtkMRMLViewNode") self.pipelineManager = vtkMRMLLayerDMPipelineManager() self.pipelineManager.SetViewNode(self.viewNode) self.pipelineManager.SetFactory(self.factory) - self.pipelineManager.SetScene(slicer.mrmlScene) + self.pipelineManager.SetScene(self.scene) self.pipelineManager.SetRenderWindow(self.renderWindow) self.defaultRenderer = vtkRenderer() self.renderWindow.AddRenderer(self.defaultRenderer) @@ -75,7 +75,7 @@ def test_on_remove_node_removes_associated_pipeline(self): assert self.pipelineManager.GetNodePipeline(modelNode) is None def test_on_factory_updated_creates_pipelines_if_needed(self): - volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") + volumeNode = self.scene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") assert volumeNode assert self.pipelineManager.GetNodePipeline(volumeNode) is None @@ -85,14 +85,14 @@ def test_on_factory_updated_creates_pipelines_if_needed(self): assert self.pipelineManager.GetNodePipeline(volumeNode) is not None def test_on_scene_update_add_pipeline_as_needed(self): - modelNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode") + modelNode = self.scene.AddNewNodeByClass("vtkMRMLModelNode") self.pipelineManager.UpdateFromScene() assert self.pipelineManager.GetNodePipeline(modelNode) is not None def test_on_scene_update_remove_pipeline_as_needed(self): - modelNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode") + modelNode = self.scene.AddNewNodeByClass("vtkMRMLModelNode") self.pipelineManager.AddNode(modelNode) - slicer.mrmlScene.RemoveNode(modelNode) + self.scene.RemoveNode(modelNode) self.pipelineManager.UpdateFromScene() assert self.pipelineManager.GetNodePipeline(modelNode) is None @@ -265,29 +265,51 @@ def test_on_pipeline_removed_triggers_renderer_removed(self): m1.mockOnRendererRemoved.assert_called_once_with(self.defaultRenderer) def test_correctly_cleans_up_outdated_pipelines(self): - modelNodes = [slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode") for _ in range(5)] + modelNodes = [self.scene.AddNewNodeByClass("vtkMRMLModelNode") for _ in range(5)] for modelNode in modelNodes: self.pipelineManager.AddNode(modelNode) pipelines = [self.pipelineManager.GetNthPipeline(i_pipe) for i_pipe in range(self.pipelineManager.GetNumberOfPipelines())] modelPipelines = [p for p in pipelines if p.GetDisplayNode() in modelNodes] assert len(modelPipelines) == 5 prevNumber = self.pipelineManager.GetNumberOfPipelines() - slicer.mrmlScene.RemoveNode(modelNodes[0]) - slicer.mrmlScene.RemoveNode(modelNodes[1]) - slicer.mrmlScene.RemoveNode(modelNodes[2]) + self.scene.RemoveNode(modelNodes[0]) + self.scene.RemoveNode(modelNodes[1]) + self.scene.RemoveNode(modelNodes[2]) assert self.pipelineManager.GetNumberOfPipelines() == prevNumber self.pipelineManager.UpdateFromScene() assert self.pipelineManager.GetNumberOfPipelines() == prevNumber - 3 + def test_removes_pipeline_when_its_display_node_is_destroyed(self): + # The pipeline manager maps are keyed by the display node. A node destroyed without being removed from + # the scene first is forgotten immediately, without requiring a scene update, so that the maps never + # contain a destroyed node. + prevNumber = self.pipelineManager.GetNumberOfPipelines() + + mock = MockPipeline() + self.nextMock = mock + node = vtkMRMLMarkupsFiducialNode() + assert self.pipelineManager.AddNode(node) + assert self.pipelineManager.GetNumberOfPipelines() == prevNumber + 1 + + # Release every reference to the node. The mocks record the arguments they are called with, which would + # otherwise keep the node alive. + self.mockCreate.reset_mock() + self.mockModelCreate.reset_mock() + mock.mockSetDisplayNode.reset_mock() + self.nextMock = None + del node + + assert self.pipelineManager.GetNumberOfPipelines() == prevNumber + def test_notifies_pipelines_when_references_are_added_or_removed(self): # Create a display markups node m1 = MockPipeline() self.nextMock = m1 - dNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialDisplayNode") + dNode = self.scene.AddNewNodeByClass("vtkMRMLMarkupsFiducialDisplayNode") assert self.pipelineManager.AddNode(dNode) # Create a markups node - markups = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") + markups = self.scene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") # Set the markups node display to the previous display node and expect the pipeline to have been notified markups.SetAndObserveDisplayNodeID(dNode.GetID()) diff --git a/LayerDM/Testing/Python/SelectionTest.py b/LayerDM/Testing/Python/SelectionTest.py index 8eb3942..1845bc5 100644 --- a/LayerDM/Testing/Python/SelectionTest.py +++ b/LayerDM/Testing/Python/SelectionTest.py @@ -1,23 +1,23 @@ from unittest.mock import MagicMock -import slicer from slicer import ( vtkMRMLLayerDMSelectionObserver, vtkMRMLScene, vtkMRMLLayerDMObjectEventObserverScripted, vtkMRMLInteractionNode, ) -from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest + +from LayerDMTestSupport import ScriptedLoadableModuleTest, create_test_scene class SelectionTest(ScriptedLoadableModuleTest): def setUp(self): - slicer.mrmlScene.Clear(0) + self.scene = create_test_scene() self.selection_obs = vtkMRMLLayerDMSelectionObserver() - self.selection_obs.SetScene(slicer.mrmlScene) + self.selection_obs.SetScene(self.scene) self.mock = MagicMock(return_value=None) self._spy = vtkMRMLLayerDMObjectEventObserverScripted() - self._spy.UpdateObserver(None, self.selection_obs) + self._spy.UpdateObservation(None, self.selection_obs) self._spy.SetPythonCallback(self.mock) def test_has_access_to_singleton_nodes(self): @@ -33,7 +33,7 @@ def test_with_empty_new_scene_doesnt_initialize_singleton_nodes(self): assert obs.GetSelectionNode() is None def test_helps_in_placing_nodes(self): - markups_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") + markups_node = self.scene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") self.selection_obs.StartPlace(markups_node, True) self.mock.assert_called() diff --git a/LayerDM/pyproject.toml b/LayerDM/pyproject.toml index f572983..26dcfac 100644 --- a/LayerDM/pyproject.toml +++ b/LayerDM/pyproject.toml @@ -60,4 +60,4 @@ root = ".." git_describe_command = [ "git", "describe", "--tags", "--abbrev=0", "--match", "v[0-9]*"] # we are only interested by last tag name [project.optional-dependencies] -test = ["pytest", "virtualenv"] +test = ["pytest"] diff --git a/LayerDM/pytest.toml b/LayerDM/pytest.toml index 8d74779..8efb28c 100644 --- a/LayerDM/pytest.toml +++ b/LayerDM/pytest.toml @@ -1,5 +1,6 @@ [pytest] -markers = [ - "runtime: Run tests that only requires SlicerLayerDM runtime wheel", - "sdk: Run tests that requires both SlicerLayerDM and its SDK", -] +# Both `test_*.py` (pytest style) and `*Test.py` (3D Slicer style) modules are collected. +# LayerDM tests only require the LayerDM runtime wheel: tests requiring the SDK belong +# to the LayerDMSDK project. +python_files = ["test_*.py", "*Test.py"] +testpaths = ["tests", "Testing/Python"] diff --git a/LayerDM/qSlicerLayerDMModule.cxx b/LayerDM/qSlicerLayerDMModule.cxx index f21f697..9e6381e 100644 --- a/LayerDM/qSlicerLayerDMModule.cxx +++ b/LayerDM/qSlicerLayerDMModule.cxx @@ -2,7 +2,7 @@ Program: 3D Slicer - Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved. + Copyright (c) Kitware SAS See COPYRIGHT.txt or http://www.slicer.org/copyright/copyright.txt for details. @@ -13,6 +13,9 @@ See the License for the specific language governing permissions and limitations under the License. + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + ==============================================================================*/ #include "qSlicerLayerDMModule.h" @@ -82,7 +85,7 @@ void qSlicerLayerDMModule::setup() } //----------------------------------------------------------------------------- -qSlicerAbstractModuleRepresentation* qSlicerLayerDMModule ::createWidgetRepresentation() +qSlicerAbstractModuleRepresentation* qSlicerLayerDMModule::createWidgetRepresentation() { return nullptr; } diff --git a/LayerDM/qSlicerLayerDMModule.h b/LayerDM/qSlicerLayerDMModule.h index 9c5d0fc..203f6e9 100644 --- a/LayerDM/qSlicerLayerDMModule.h +++ b/LayerDM/qSlicerLayerDMModule.h @@ -2,7 +2,7 @@ Program: 3D Slicer - Portions (c) Copyright Brigham and Women's Hospital (BWH) All Rights Reserved. + Copyright (c) Kitware SAS See COPYRIGHT.txt or http://www.slicer.org/copyright/copyright.txt for details. @@ -13,6 +13,9 @@ See the License for the specific language governing permissions and limitations under the License. + This file was originally developed by Thibault Pelletier, Kitware SAS, + and was partially funded by ANR grants ANR-22-CE45-0034 and ANR-18-RHUS-005. + ==============================================================================*/ #ifndef __qSlicerLayerDMModule_h diff --git a/LayerDM/tests/test_runtime.py b/LayerDM/tests/test_runtime.py index 7252d4e..952a41f 100644 --- a/LayerDM/tests/test_runtime.py +++ b/LayerDM/tests/test_runtime.py @@ -1,18 +1,13 @@ -import pytest - - -@pytest.mark.runtime def test_import_layerdm(): """ This is a simple test that import the package and checks that it is found. """ - from slicer import vtkMRMLLayerDMPipelineI # noqa + from slicer import vtkMRMLLayerDMPipeline # noqa from LayerDMLib import vtkMRMLLayerDMScriptedPipeline vtkMRMLLayerDMScriptedPipeline() -@pytest.mark.runtime def test_layerdm_through_factory(): """ Test that we can instantiate a displayable manager using the slicer-core provided factories diff --git a/LayerDMSDK/pyproject.toml b/LayerDMSDK/pyproject.toml index 4c93485..4224337 100644 --- a/LayerDMSDK/pyproject.toml +++ b/LayerDMSDK/pyproject.toml @@ -60,3 +60,6 @@ git_describe_command = [ "git", "describe", "--tags", "--abbrev=0", "--match", " [project.entry-points."cmake.prefix"] SlicerLayerDM = "slicer_layer_dm_sdk.cmake" + +[project.optional-dependencies] +test = ["pytest", "virtualenv"] diff --git a/LayerDMSDK/pytest.toml b/LayerDMSDK/pytest.toml new file mode 100644 index 0000000..62241f6 --- /dev/null +++ b/LayerDMSDK/pytest.toml @@ -0,0 +1,3 @@ +[pytest] +# Tests the SDK from the point of view of a third party package building against it. +testpaths = ["tests"] diff --git a/LayerDMSDK/tests/__init__.py b/LayerDMSDK/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/LayerDM/tests/conftest.py b/LayerDMSDK/tests/conftest.py similarity index 100% rename from LayerDM/tests/conftest.py rename to LayerDMSDK/tests/conftest.py diff --git a/LayerDM/tests/packages/build_module/CMakeLists.txt b/LayerDMSDK/tests/packages/build_module/CMakeLists.txt similarity index 100% rename from LayerDM/tests/packages/build_module/CMakeLists.txt rename to LayerDMSDK/tests/packages/build_module/CMakeLists.txt diff --git a/LayerDM/tests/packages/build_module/MRML/CMakeLists.txt b/LayerDMSDK/tests/packages/build_module/MRML/CMakeLists.txt similarity index 100% rename from LayerDM/tests/packages/build_module/MRML/CMakeLists.txt rename to LayerDMSDK/tests/packages/build_module/MRML/CMakeLists.txt diff --git a/LayerDM/tests/packages/build_module/MRML/vtk.module b/LayerDMSDK/tests/packages/build_module/MRML/vtk.module similarity index 100% rename from LayerDM/tests/packages/build_module/MRML/vtk.module rename to LayerDMSDK/tests/packages/build_module/MRML/vtk.module diff --git a/LayerDM/tests/packages/build_module/MRML/vtkMRMLDummyLayerDMPipeline.cxx b/LayerDMSDK/tests/packages/build_module/MRML/vtkMRMLDummyLayerDMPipeline.cxx similarity index 100% rename from LayerDM/tests/packages/build_module/MRML/vtkMRMLDummyLayerDMPipeline.cxx rename to LayerDMSDK/tests/packages/build_module/MRML/vtkMRMLDummyLayerDMPipeline.cxx diff --git a/LayerDM/tests/packages/build_module/MRML/vtkMRMLDummyLayerDMPipeline.h b/LayerDMSDK/tests/packages/build_module/MRML/vtkMRMLDummyLayerDMPipeline.h similarity index 85% rename from LayerDM/tests/packages/build_module/MRML/vtkMRMLDummyLayerDMPipeline.h rename to LayerDMSDK/tests/packages/build_module/MRML/vtkMRMLDummyLayerDMPipeline.h index b181be0..01de2f6 100644 --- a/LayerDM/tests/packages/build_module/MRML/vtkMRMLDummyLayerDMPipeline.h +++ b/LayerDMSDK/tests/packages/build_module/MRML/vtkMRMLDummyLayerDMPipeline.h @@ -3,13 +3,13 @@ #include "vtkDummyMRMLLayerDMPipelineModule.h" -#include +#include -class VTK_DUMMY_MRMLLAYERDMPIPELINE_EXPORT vtkMRMLDummyLayerDMPipeline : public vtkMRMLLayerDMPipelineI +class VTK_DUMMY_MRMLLAYERDMPIPELINE_EXPORT vtkMRMLDummyLayerDMPipeline : public vtkMRMLLayerDMPipeline { public: static vtkMRMLDummyLayerDMPipeline* New(); - vtkTypeMacro(vtkMRMLDummyLayerDMPipeline, vtkMRMLLayerDMPipelineI); + vtkTypeMacro(vtkMRMLDummyLayerDMPipeline, vtkMRMLLayerDMPipeline); void PrintSelf(ostream& os, vtkIndent indent) override; protected: diff --git a/LayerDM/tests/packages/build_module/Python/slicer_layer_dm_pipeline.py b/LayerDMSDK/tests/packages/build_module/Python/slicer_layer_dm_pipeline.py similarity index 100% rename from LayerDM/tests/packages/build_module/Python/slicer_layer_dm_pipeline.py rename to LayerDMSDK/tests/packages/build_module/Python/slicer_layer_dm_pipeline.py diff --git a/LayerDM/tests/packages/build_module/main.py b/LayerDMSDK/tests/packages/build_module/main.py similarity index 87% rename from LayerDM/tests/packages/build_module/main.py rename to LayerDMSDK/tests/packages/build_module/main.py index 75582a2..01a9145 100644 --- a/LayerDM/tests/packages/build_module/main.py +++ b/LayerDMSDK/tests/packages/build_module/main.py @@ -1,4 +1,4 @@ -# This tests that we can use our custom vtkMRMLLayerDMPipelineI subclass +# This tests that we can use our custom vtkMRMLLayerDMPipeline subclass # with the vtkMRMLLayerDMPipelineScriptedCreator and vtkMRMLLayerDMPipelineFactory from slicer import ( diff --git a/LayerDM/tests/packages/build_module/pyproject.toml b/LayerDMSDK/tests/packages/build_module/pyproject.toml similarity index 100% rename from LayerDM/tests/packages/build_module/pyproject.toml rename to LayerDMSDK/tests/packages/build_module/pyproject.toml diff --git a/LayerDM/tests/test_build_module.py b/LayerDMSDK/tests/test_build_module.py similarity index 78% rename from LayerDM/tests/test_build_module.py rename to LayerDMSDK/tests/test_build_module.py index 56678a2..66ff1ac 100644 --- a/LayerDM/tests/test_build_module.py +++ b/LayerDMSDK/tests/test_build_module.py @@ -1,13 +1,15 @@ """ -This creates a venv and and build a project that consumes our SDK and check that we can import it correctly. -""" +This creates a venv and builds a project that consumes our SDK and checks that we can import it correctly. -import pytest +This tests the SDK from the point of view of a third party package building against it, +hence it is part of the SDK test suite and not of the LayerDM unittests. +""" from pathlib import Path + from .venv import VEnv -@pytest.mark.sdk + def test_build_module(virtualenv: VEnv, curdir: Path, tmp_path: Path): # To enable current project to be used we have to cut pypi index # But doing so would cause issues with dependencies, so we use the @@ -26,14 +28,14 @@ def test_build_module(virtualenv: VEnv, curdir: Path, tmp_path: Path): "--wheel-dir", tmp_path.as_posix() ) - runtime_dir = curdir.parent.resolve() + runtime_dir = (curdir.parent.parent / "LayerDM").resolve() virtualenv.module("pip", "wheel", runtime_dir.as_posix(), "--find-links", tmp_path.as_posix(), "--no-index", "--wheel-dir", tmp_path.as_posix() ) - sdk_dir = (curdir.parent.parent / "LayerDMSDK").resolve() + sdk_dir = curdir.parent.resolve() virtualenv.module("pip", "wheel", sdk_dir.as_posix(), "--find-links", tmp_path.as_posix(), "--no-index", diff --git a/LayerDM/tests/venv.py b/LayerDMSDK/tests/venv.py similarity index 100% rename from LayerDM/tests/venv.py rename to LayerDMSDK/tests/venv.py diff --git a/README.md b/README.md index bc376ea..9c388b3 100644 --- a/README.md +++ b/README.md @@ -85,3 +85,27 @@ the [getting started documentation](https://slicerlayerdisplayablemanager.readth And refer to the [examples](https://github.com/KitwareMedical/SlicerLayerDisplayableManager/tree/main/Examples/Python/ModelGlowDM/ModelGlowDM.py). + +## Testing + +Tests are split between the two projects: + +- `LayerDM` is covered by unittests that only require the LayerDM runtime wheel. They are run by the 3D Slicer test + runner when testing the extension, and by [pytest](https://docs.pytest.org) when testing the wheels. +- `LayerDMSDK` is covered by a test that builds a third party package against the SDK. Requiring the SDK, it belongs to + the SDK test suite and is out of the scope of the LayerDM unittests. + +To run them against the wheels, install the `slicer-layer-dm` wheel (and its dependencies) in a Python environment, +then: + +```bash +# LayerDM unittests (runtime wheel only) +python -m pytest LayerDM + +# LayerDMSDK tests (require the SDK wheel) +python -m pytest LayerDMSDK +``` + +Note: `LayerDM/Testing/Python` drives displayable managers in render windows and renders offscreen. No X server is +required: with no `DISPLAY`, VTK falls back to the EGL backend and renders with software GL. A display is only used if +one is already available. diff --git a/extension.cmake b/extension.cmake index d07db48..abb8d01 100644 --- a/extension.cmake +++ b/extension.cmake @@ -12,16 +12,6 @@ set(EXTENSION_DEPENDS "NA") # Specified as a list or "NA" if no dependencies find_package(Slicer REQUIRED) include(${Slicer_USE_FILE}) -#----------------------------------------------------------------------------- -# Extension version (auto updated by semantic version) -set(LayerDM_VERSION "v1.2.5") - -string(SUBSTRING "${LayerDM_VERSION}" 1 -1 LayerDM_VERSION) # remove first character -string(REPLACE "." ";" version_components "${LayerDM_VERSION}") # this creates a list -list(GET version_components 0 LayerDM_VERSION_MAJOR) -list(GET version_components 1 LayerDM_VERSION_MINOR) -list(GET version_components 2 LayerDM_VERSION_PATCH) - #----------------------------------------------------------------------------- # Extension modules add_subdirectory(LayerDM) diff --git a/releaserc.toml b/releaserc.toml index 91a9173..a2ad25e 100644 --- a/releaserc.toml +++ b/releaserc.toml @@ -1,5 +1,2 @@ [semantic_release] tag_format = "v{version}" -version_variables = [ - "extension.cmake:LayerDM_VERSION:tf" -]