Program Listing for File LayerManagerTest.py

Return to documentation for file (Testing/Python/LayerManagerTest.py)

import slicer
from LayerDMLib import vtkMRMLLayerDMScriptedPipeline
from slicer import vtkMRMLLayerDMLayerManager
from slicer.ScriptedLoadableModule import ScriptedLoadableModuleTest
from vtk import vtkActor, vtkCamera, vtkPolyDataMapper, vtkRenderWindow, vtkRenderer, vtkSphereSource


class Pipeline(vtkMRMLLayerDMScriptedPipeline):
    """
    Simple sphere pipeline displaying a sphere with given render order / radius and center position.
    """

    def __init__(
        self, renderOrder: int = 0, camera: vtkCamera = None, radius: float = 2, center: list[float] | None = None
    ):
        super().__init__()
        center = center or [0.0] * 3
        self._renderOrder = renderOrder
        self._camera = camera

        self._sphere = vtkSphereSource()
        self._sphere.SetRadius(radius)
        self._sphere.SetCenter(*center)
        self._sphere.Update()
        self._mapper = vtkPolyDataMapper()
        self._mapper.SetInputData(self._sphere.GetOutput())
        self._actor = vtkActor()
        self._actor.SetMapper(self._mapper)

    def OnRendererAdded(self, renderer: vtkRenderer | None) -> None:
        if not renderer or renderer.HasViewProp(self._actor):
            return

        renderer.AddViewProp(self._actor)

    def OnRendererRemoved(self, renderer: vtkRenderer) -> None:
        if not renderer or not renderer.HasViewProp(self._actor):
            return

        renderer.RemoveViewProp(self._actor)

    def GetRenderOrder(self) -> int:
        return self._renderOrder

    def GetCustomCamera(self) -> vtkCamera | None:
        return self._camera


class LayerManagerTest(ScriptedLoadableModuleTest):
    def setUp(self):
        slicer.mrmlScene.Clear(0)
        self.renderWindow = vtkRenderWindow()
        self.defaultRenderer = vtkRenderer()
        self.renderWindow.AddRenderer(self.defaultRenderer)

        self.firstCamera = vtkCamera()
        self.defaultCamera = vtkCamera()
        self.defaultRenderer.SetActiveCamera(self.firstCamera)

        self.layerManager = vtkMRMLLayerDMLayerManager()
        self.layerManager.SetRenderWindow(self.renderWindow)
        self.layerManager.SetDefaultCamera(self.defaultCamera)

    def assert_are_expected_layers(self, pipelineLists, expRenderLayers, nUnmanagedRenderers=1, expNumberOfLayers=None):
        if expNumberOfLayers is None:
            expNumberOfLayers = max(expRenderLayers) + 1

        nManaged = len([layer for layer in expRenderLayers if layer != 0])
        assert self.layerManager.GetNumberOfRenderers() == nManaged
        assert self.layerManager.GetNumberOfManagedLayers() == nManaged

        assert self.renderWindow.GetRenderers().GetNumberOfItems() == nManaged + nUnmanagedRenderers
        assert self.renderWindow.GetNumberOfLayers() == expNumberOfLayers

        actLayers = set()
        for pipelines in pipelineLists:
            # Check that all pipelines map to the same renderer
            renderers = self.get_pipelines_renderers(pipelines)
            assert len(renderers) == 1

            # Check that the renderer has the expected layer
            renderer = renderers[0]
            rendererLayer = renderer.GetLayer()
            actLayers.add(rendererLayer)

            # For layer 0 verify the renderer is the default renderer (and not the default otherwise)
            assert (rendererLayer == 0) == (renderer == self.defaultRenderer)

        assert list(actLayers) == list(expRenderLayers)

    @staticmethod
    def get_pipelines_renderers(pipelines: list[Pipeline]) -> list[vtkRenderer]:
        return list({pipeline.GetRenderer() for pipeline in pipelines})

    def configure_layer_manager_with_multiple_pipelines(self):
        # Create sphere pipelines with different centers and add them respectively to different renderers
        cameras = [None, None, None, vtkCamera()]
        renderOrders = [0, 1, 2, 3]
        radius = 2
        centers = [[0, 0, 0], [10, 10, 10], [-10, -10, -10], [0, 0, 0]]

        # Add pipelines to the layer manager
        pipelines = [Pipeline(order, cam, radius, center) for order, center, cam in zip(renderOrders, centers, cameras)]

        for pipeline in pipelines:
            self.layerManager.AddPipeline(pipeline)

        return pipelines

    def test_scripted_pipeline_can_be_instantiated(self):
        assert vtkMRMLLayerDMScriptedPipeline() is not None
        assert Pipeline() is not None

    def test_at_init_has_one_distinct_default_layer(self):
        assert self.layerManager.GetNumberOfDistinctLayers() == 1

    def test_adding_pipeline_with_default_order_doesnt_create_layers(self):
        pipelines = [Pipeline() for _ in range(5)]

        for pipeline in pipelines:
            self.layerManager.AddPipeline(pipeline)

        self.assert_are_expected_layers([pipelines], expRenderLayers=[0])

    def test_adding_pipelines_to_non_default_order_are_grouped_by_value(self):
        orderValues = [1, 1000, 2000]
        pipelineLists = [[Pipeline(order) for _ in range(3)] for order in orderValues]

        for pipelines in pipelineLists:
            for pipeline in pipelines:
                self.layerManager.AddPipeline(pipeline)

        self.assert_are_expected_layers(pipelineLists, expRenderLayers=[1, 2, 3])

    def test_removed_pipeline_layers_are_collapsed(self):
        orderValues = [1, 1000, 2000]
        pipelineLists = [[Pipeline(order) for _ in range(3)] for order in orderValues]

        for pipelines in pipelineLists:
            for pipeline in pipelines:
                self.layerManager.AddPipeline(pipeline)

        for pipeline in pipelineLists[1]:
            self.layerManager.RemovePipeline(pipeline)

        pipelineLists = [pipelineLists[0], pipelineLists[-1]]
        self.assert_are_expected_layers(pipelineLists, expRenderLayers=[1, 2])

    def test_pipelines_with_different_cameras_map_to_different_layer(self):
        cameras = [vtkCamera(), vtkCamera()]
        pipelineLists = [[Pipeline(1, camera) for _ in range(3)] for camera in cameras]

        for pipelines in pipelineLists:
            for pipeline in pipelines:
                self.layerManager.AddPipeline(pipeline)

        pipelineLists = [pipelineLists[0], pipelineLists[-1]]
        self.assert_are_expected_layers(pipelineLists, expRenderLayers=[1, 2])

    def test_managed_renderers_are_numbered_from_layer_one_onwards_regardless_of_existing_renderers(self):
        for iUnManaged in range(4):
            renderer = vtkRenderer()
            renderer.SetLayer(iUnManaged)
            self.renderWindow.AddRenderer(renderer)

        pipelines = [Pipeline(1) for _ in range(5)]
        for pipeline in pipelines:
            self.layerManager.AddPipeline(pipeline)

        self.assert_are_expected_layers([pipelines], expRenderLayers=[1], nUnmanagedRenderers=5, expNumberOfLayers=4)

    def test_cleans_up_render_window_when_changed(self):
        for _ in range(2):
            self.renderWindow.AddRenderer(vtkRenderer())

        pipelines = [Pipeline(1) for _ in range(5)]
        for pipeline in pipelines:
            self.layerManager.AddPipeline(pipeline)

        self.layerManager.SetRenderWindow(vtkRenderWindow())
        assert self.renderWindow.GetRenderers().GetNumberOfItems() == 3
        assert self.renderWindow.GetNumberOfLayers() == 1

    def test_renderers_are_set_to_correct_camera(self):
        customCam = vtkCamera()
        camLayers = [(1, None), (2, customCam), (4, None)]
        pipelineLists = [[Pipeline(layer, camera) for _ in range(3)] for layer, camera in camLayers]

        for pipelines in pipelineLists:
            for pipeline in pipelines:
                self.layerManager.AddPipeline(pipeline)

        renderers = list(self.renderWindow.GetRenderers())
        assert renderers[0].GetActiveCamera() == self.firstCamera
        assert renderers[1].GetActiveCamera() == self.defaultCamera
        assert renderers[3].GetActiveCamera() == self.defaultCamera
        assert renderers[2].GetActiveCamera() == customCam

        for pipeline in pipelineLists[0]:
            self.layerManager.RemovePipeline(pipeline)

        assert renderers[0].GetActiveCamera() == self.firstCamera
        assert renderers[1].GetActiveCamera() == customCam
        assert renderers[2].GetActiveCamera() == self.defaultCamera

    def test_renderer_cameras_are_set_to_default_camera(self):
        customCam = vtkCamera()

        camLayers = [(1, None), (2, customCam), (4, None)]
        pipelineLists = [[Pipeline(layer, camera) for _ in range(3)] for layer, camera in camLayers]

        for pipelines in pipelineLists:
            for pipeline in pipelines:
                self.layerManager.AddPipeline(pipeline)

        # The camera sync has the responsibility of updating the default camera when the first cam is updated.
        self.firstCamera.SetDistance(1)
        customCam.SetDistance(2)
        self.defaultCamera.SetDistance(3)

        renderers = list(self.renderWindow.GetRenderers())
        assert renderers[0].GetActiveCamera().GetDistance() == 1
        assert renderers[1].GetActiveCamera().GetDistance() == 3
        assert renderers[2].GetActiveCamera().GetDistance() == 2
        assert renderers[3].GetActiveCamera().GetDistance() == 3

    def test_created_renderers_are_set_to_not_interactive(self):
        pipelines = [Pipeline(1) for _ in range(5)]
        for pipeline in pipelines:
            self.layerManager.AddPipeline(pipeline)

        renderers = list(self.renderWindow.GetRenderers())
        assert not renderers[1].GetInteractive()

    def test_clips_depending_on_renderer_roi(self):
        self.configure_layer_manager_with_multiple_pipelines()

        # Reset the clipping and expect all cameras clipping range to have been updated
        renderers = list(self.renderWindow.GetRenderers())
        prev_clipping_ranges = [renderer.GetActiveCamera().GetClippingRange() for renderer in renderers]

        self.layerManager.ResetCameraClippingRange()
        next_clipping_ranges = [renderer.GetActiveCamera().GetClippingRange() for renderer in renderers]

        for prev_clipping, next_clipping in zip(prev_clipping_ranges, next_clipping_ranges):
            assert prev_clipping != next_clipping

    def test_on_reset_camera_clipping_ranges_are_valid(self):
        self.configure_layer_manager_with_multiple_pipelines()
        self.layerManager.ResetCameraClippingRange()
        renderers = list(self.renderWindow.GetRenderers())

        for renderer in renderers:
            clippingRange = renderer.GetActiveCamera().GetClippingRange()
            assert -1000 < clippingRange[0] < 1000
            assert -1000 < clippingRange[1] < 1000

    def test_reset_clipping_range_affects_unmanaged_cameras(self):
        pipelines = self.configure_layer_manager_with_multiple_pipelines()
        custom_camera = pipelines[-1].GetCustomCamera()
        assert custom_camera is not None

        prev_clipping_range = custom_camera.GetClippingRange()
        self.layerManager.ResetCameraClippingRange()

        assert custom_camera.GetClippingRange()[0] != prev_clipping_range[0]
        assert custom_camera.GetClippingRange()[1] != prev_clipping_range[1]