Repository: holoviz/panel

Issue:
VTK - vtkFollower don't follow the camera in Panel
Hi,

I tried to use a vtkFollower object, I set it to the active camera then I passed the vtkRenderWindow to Panel, the object is render in the dashboard, but is not following the camera.
I guess a new camera is created in the process (and don't tell the vtkFollower object to follow it) ?

vtkFollower :
"vtkFollower is a subclass of vtkActor that always follows its specified camera. More specifically it will not change its position or scale, but it will continually update its orientation so that it is right side up and facing the camera."


#### ALL software version info
- OS: Windows 10 Pro (64-bit) version 1909
- Python: 3.8.3
- Chrome: Version 83.0.4103.97 (Official Build) (64-bit)
- Panel: 0.9.5
- Bokeh: 2.0.1

#### Description of expected behavior and the observed behavior
##### Expected behavior:
The vtkFollower (the label/text "Origin" in the example) follow the camera and face it always.
##### Observed behavior:
The vtkFollower (the label/text "Origin" in the example) is render as a fix actor and does not follow the camera .

#### Complete, minimal, self-contained example code that reproduces the issue

```python
# fmt: off
## The 3 followings line are a temp solution until I upgrate to bokeh 2.0.2 (now it 2.0.1) and won't be necessary later
from bokeh.settings import settings  # noqa isort:skip
settings.resources = 'cdn'  # noqa isort:skip
settings.resources = 'inline'  # noqa isort:skip
# fmt: on
import panel as pn
import vtk

# Create the arrow and the associated mapper and actor.
arrow = vtk.vtkArrowSource()
arrowMapper = vtk.vtkPolyDataMapper()
arrowMapper.SetInputConnection(arrow.GetOutputPort())
arrowActor = vtk.vtkActor()
arrowActor.SetMapper(arrowMapper)

# Create the 3D text and the associated mapper and follower (a type of actor).  
# Position the text so it is displayed over the origin of the arrow.
atext = vtk.vtkVectorText()
atext.SetText("Origin")
textMapper = vtk.vtkPolyDataMapper()
textMapper.SetInputConnection(atext.GetOutputPort())
textActor = vtk.vtkFollower()
textActor.SetMapper(textMapper)
textActor.SetScale(0.2, 0.2, 0.2)
textActor.AddPosition(0, 0, 0)

# Create the Renderer, RenderWindow, and RenderWindowInteractor.
renderer = vtk.vtkRenderer()
renderWindow = vtk.vtkRenderWindow()
renderWindow.AddRenderer(renderer)

# Add the actors to the renderer.
renderer.AddActor(arrowActor)
renderer.AddActor(textActor)
renderer.GetActiveCamera().SetPosition(4, 0, 0)

# Set the camera of the follower.
textActor.SetCamera(renderer.GetActiveCamera())


# Test Panel
dash = pn.pane.VTK(renderWindow)
dash.show()

# Test VTK
# interactor = vtk.vtkRenderWindowInteractor()
# interactor.SetRenderWindow(renderWindow)
# renderWindow.Render()
# interactor.Start()

```

#### Screenshots or screencasts of the bug in action
##### In Panel
![image](https://user-images.githubusercontent.com/29143941/84641799-40cb0b80-af04-11ea-890f-69a6dc60c0b5.png)

##### In VTK
![image](https://user-images.githubusercontent.com/29143941/84641990-87b90100-af04-11ea-8280-54a2ef3f886c.png)



Discussion:
Interesting, I didn't expected something will be displayed since I never thought of supporting this.
However in the next panel version vtkFollower will be clearly not supported:
![image](https://user-images.githubusercontent.com/18531147/84652001-28122400-af0b-11ea-96e7-a7539a02742f.png)

To support this there must be an equivalent in [ vtk.js](https://kitware.github.io/vtk-js/).
I'll open a feature request there

@xavArtley Thank you.
I think it's a good feature to have since it's very useful for labeling in the 3d space.
@jourdain do you think a vtkFollower would be a good feature request for vtkjs?
Yes please do on the bug tracker. Thx

Resolving patch:
diff --git a/panel/pane/vtk/synchronizable_deserializer.py b/panel/pane/vtk/synchronizable_deserializer.py
--- a/panel/pane/vtk/synchronizable_deserializer.py
+++ b/panel/pane/vtk/synchronizable_deserializer.py
@@ -12,7 +12,7 @@
     "SetUseGradientOpacity": None,
     "SetRGBTransferFunction": "SetColor",
 }
-WRAP_ID_RE = re.compile("instance:\${([^}]+)}")
+WRAP_ID_RE = re.compile(r"instance:\${([^}]+)}")
 ARRAY_TYPES = {
     'Int8Array': vtk.vtkCharArray,
     'Uint8Array': vtk.vtkUnsignedCharArray,
@@ -170,6 +170,7 @@ def make_type_handlers():
         'vtkGlyph3DMapper': generic_builder,
         'vtkProperty': generic_builder,
         'vtkActor': generic_builder,
+        'vtkFollower': generic_builder,
         'vtkColorTransferFunction': color_fun_builder,
         'vtkPiecewiseFunction': piecewise_fun_builder,
         'vtkTexture': generic_builder,
diff --git a/panel/pane/vtk/synchronizable_serializer.py b/panel/pane/vtk/synchronizable_serializer.py
--- a/panel/pane/vtk/synchronizable_serializer.py
+++ b/panel/pane/vtk/synchronizable_serializer.py
@@ -239,6 +239,7 @@ def initializeSerializers():
     registerInstanceSerializer('vtkImageSlice', genericProp3DSerializer)
     registerInstanceSerializer('vtkVolume', genericProp3DSerializer)
     registerInstanceSerializer('vtkOpenGLActor', genericActorSerializer)
+    registerInstanceSerializer('vtkFollower', genericActorSerializer)
     registerInstanceSerializer('vtkPVLODActor', genericActorSerializer)
     
 
@@ -575,6 +576,16 @@ def genericActorSerializer(parent, actor, actorId, context, depth):
         'forceOpaque': actor.GetForceOpaque(),
         'forceTranslucent': actor.GetForceTranslucent()
     })
+
+    if actor.IsA('vtkFollower'):
+        camera = actor.GetCamera()
+        cameraId = context.getReferenceId(camera)
+        cameraInstance = serializeInstance(
+            actor, camera, cameraId, context, depth + 1)
+        if cameraInstance:
+            instance['dependencies'].append(cameraInstance)
+            instance['calls'].append(['setCamera', [wrapId(cameraId)]])
+
     return instance
 
 # -----------------------------------------------------------------------------


Test patch:
diff --git a/panel/tests/pane/test_vtk.py b/panel/tests/pane/test_vtk.py
--- a/panel/tests/pane/test_vtk.py
+++ b/panel/tests/pane/test_vtk.py
@@ -32,13 +32,27 @@
 
 
 def make_render_window():
+
+    #cone actor
     cone = vtk.vtkConeSource()
     coneMapper = vtk.vtkPolyDataMapper()
     coneMapper.SetInputConnection(cone.GetOutputPort())
     coneActor = vtk.vtkActor()
     coneActor.SetMapper(coneMapper)
+
+    #text actor following camera
+    text = vtk.vtkVectorText()
+    text.SetText("Origin")
+    textMapper = vtk.vtkPolyDataMapper()
+    textMapper.SetInputConnection(text.GetOutputPort())
+    textActor = vtk.vtkFollower()
+    textActor.SetMapper(textMapper)
+
     ren = vtk.vtkRenderer()
     ren.AddActor(coneActor)
+    ren.AddActor(textActor)
+    textActor.SetCamera(ren.GetActiveCamera())
+
     renWin = vtk.vtkRenderWindow()
     renWin.AddRenderer(ren)
     return renWin
@@ -52,7 +66,7 @@ def pyvista_render_window():
     globe = examples.load_globe() #test texture
     head = examples.download_head() #test volume
     uniform = examples.load_uniform() #test structured grid
-    
+
     scalars=sphere.points[:, 2]
     sphere._add_point_array(scalars, 'test', set_active=True) #allow to test scalars
 
@@ -60,7 +74,7 @@ def pyvista_render_window():
 
     #test datasetmapper
     threshed = uniform.threshold_percent([0.15, 0.50], invert=True)
-    bodies = threshed.split_bodies() 
+    bodies = threshed.split_bodies()
     mapper = vtk.vtkCompositePolyDataMapper2()
     mapper.SetInputDataObject(0, bodies)
     multiblock = vtk.vtkActor()
@@ -126,7 +140,7 @@ def test_vtkjs_pane(document, comm, tmp_path):
     assert isinstance(model, VTKJSPlot)
     assert pane_from_url._models[model.ref['id']][0] is model
     assert isinstance(model.data, string_types)
-    
+
     with BytesIO(base64.b64decode(model.data.encode())) as in_memory:
         with ZipFile(in_memory) as zf:
             filenames = zf.namelist()
@@ -158,10 +172,10 @@ def test_vtk_pane_from_renwin(document, comm):
 
     # Check array release when actor are removed from scene
     ctx = pane._contexts[model.id]
-    assert len(ctx.dataArrayCache.keys()) == 3
+    assert len(ctx.dataArrayCache.keys()) == 5
     pane.remove_all_actors()
     # Default : 20s before removing arrays
-    assert len(ctx.dataArrayCache.keys()) == 3
+    assert len(ctx.dataArrayCache.keys()) == 5
     # Force 0s for removing arrays
     ctx.checkForArraysToRelease(0)
     assert len(ctx.dataArrayCache.keys()) == 0
@@ -181,14 +195,14 @@ def test_vtk_serialize_on_instantiation(document, comm, tmp_path):
     assert isinstance(model, VTKSynchronizedPlot)
 
     pane.param.trigger('object')
-    
+
     # test export to file
     tmpfile = os.path.join(*tmp_path.joinpath('scene').parts)
     exported_file = pane.export_scene(filename=tmpfile)
     assert exported_file.endswith('.synch')
 
     # test import from file
-    imported_pane = VTK.import_scene(filename=exported_file, 
+    imported_pane = VTK.import_scene(filename=exported_file,
                                      synchronizable=False)
     assert isinstance(imported_pane, VTKRenderWindow)
 
@@ -197,7 +211,7 @@ def test_vtk_serialize_on_instantiation(document, comm, tmp_path):
 def test_vtk_sync_helpers(document, comm):
     renWin1 = make_render_window()
     renWin2 = make_render_window()
-    
+
     # Create 2 panes to compare each other
     pane1 = VTK(renWin1)
     pane2 = VTK(renWin2)
@@ -213,23 +227,23 @@ def test_vtk_sync_helpers(document, comm):
     assert isinstance(model2, VTKSynchronizedPlot)
 
     # Actors getter
-    assert len(pane1.actors) == 1
-    assert len(pane2.actors) == 1
+    assert len(pane1.actors) == 2
+    assert len(pane2.actors) == 2
     assert pane1.actors[0] is not pane2.actors[0]
 
     # Actors add
     pane1.add_actors(pane2.actors)
-    assert len(pane1.actors) == 2
-    assert pane1.actors[1] is pane2.actors[0]
+    assert len(pane1.actors) == 4
+    assert pane1.actors[3] is pane2.actors[1]
 
     # Actors remove
     save_actor = pane1.actors[0]
     pane1.remove_actors([pane1.actors[0]])
-    assert pane1.actors[0] is pane2.actors[0]
+    assert pane1.actors[2] is pane2.actors[1]
 
     # Actors remove all
     pane1.add_actors([save_actor])
-    assert len(pane1.actors) == 2
+    assert len(pane1.actors) == 4
     pane1.remove_all_actors()
     assert len(pane1.actors) == 0
 
@@ -263,7 +277,7 @@ def test_vtk_pane_more_complex(document, comm, tmp_path):
     assert pane._models[model.ref['id']][0] is model
 
     colorbars_infered = pane.construct_colorbars().object
-    
+
     assert len(colorbars_infered.below) == 2 # infer only actor color bars
     assert all(isinstance(cb, ColorBar) for cb in colorbars_infered.below)
 
@@ -293,7 +307,7 @@ def test_vtk_pane_more_complex(document, comm, tmp_path):
     # (TODO test if the scene imported is identical to the one exported)
     imported_scene = VTK.import_scene(filename=exported_file)
     assert isinstance(imported_scene, VTKRenderWindowSynchronized)
-    
+
     # Cleanup
     pane._cleanup(model)
     assert pane._contexts == {}