GeezerVison is showing github.com — Customizable camera linking does not work · Issue #783 · fastplotlib/fastplotlib
Customizable camera linking does not work #783DescriptionNot sure why, pygfx demo works, fpl is doing something additional I think:
import numpy as np
import fastplotlib as fpl
import pygfx
xs = np.linspace(0, 2 * np.pi, 100)
ys = np.sin(xs)
ys_big = np.random.rand(100) * 10
# create cameras, fov=0 means Orthographic projection
camera1 = pygfx.PerspectiveCamera(fov=0)
camera2 = pygfx.PerspectiveCamera(fov=0)
# create controllers, first add the "main" camera for the subplot
controller1 = pygfx.PanZoomController(camera1)
controller2 = pygfx.PanZoomController(camera2)
# add the other camera to each controller, but only include the 'x' state, i.e. 'y' for height is not included
# this must be done only after adding the "main" cameras to the controller as done above
controller1.add_camera(camera2, include_state={"x", "width"})
controller2.add_camera(camera1, include_state={"x", "width"})
# create figure using these cameras and controllers
figure = fpl.Figure(
shape=(2, 1),
cameras=[camera1, camera2],
controllers=[controller1, controller2],
size=(700, 560)
)
figure[0, 0].add_line(np.column_stack([xs, ys_big]))
figure[1, 0].add_line(np.column_stack([xs, ys]))
print(controller1._cameras)
print(controller2._cameras)
for subplot in figure:
subplot.camera.zoom = 1.0
figure.show(maintain_aspect=False, autoscale=False)
I did make the following change to PlotArea:
Index: fastplotlib/layouts/_plot_area.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py
--- a/fastplotlib/layouts/_plot_area.py
+++ b/fastplotlib/layouts/_plot_area.py (date 1743405901011)
@@ -79,7 +79,9 @@
self._camera = camera
self._controller = controller
- self.controller.add_camera(self._camera)
+ if self.camera not in self.controller.cameras:
+ self.controller.add_camera(self._camera)
+
self.controller.register_events(
self.viewport,
)Activitykushalkolar commented on Nov 12, 2025 OK this seems to work now, I didn't change make any specific change to fpl for this 🤔 . Maybe the change I suggested is what broke things. I made some changes to auto_scale and auto_center so it works for these usecases, PR incoming. kushalkolar commented on Nov 12, 2025 I'm in another branch so I'm going to checkout a new branch of main and copy paste this:
def center_scene(self, *, zoom: float = 1.0):
"""
Auto-center the scene, does not scale.
Parameters
----------
zoom: float
apply a zoom after centering the scene
"""
if not len(self._fpl_graphics_scene.children) > 0:
return
if self.parent.__class__.__name__.endswith("Figure"):
# always use figure._subplots.ravel() in internal fastplotlib code
# otherwise if we use `for subplot in figure`, this could conflict
# with a user's iterator where they are doing `for subplot in figure` !!!
for subplot in self.parent._subplots.ravel():
# scale all cameras associated with this controller
if subplot.camera in self.controller.cameras:
# scale the cameras in other subplots w.r.t. the scene in those subplots
self._auto_center_scene(
subplot.camera,
subplot._fpl_graphics_scene,
zoom
)
else:
# just change for this plot area
# this is probably a dock area
self._auto_center_scene(self.camera, self._fpl_graphics_scene, zoom)
def _auto_center_scene(self, camera: pygfx.PerspectiveCamera, scene: pygfx.Scene, zoom: float):
camera.show_object(scene)
# camera.show_object can cause the camera width and height to increase so apply a zoom to compensate
# probably because camera.show_object uses bounding sphere
camera.zoom = zoom
def auto_scale(
self,
*, # since this is often used as an event handler, don't want to coerce maintain_aspect = True
maintain_aspect: None | bool = None,
zoom: float = 0.75,
):
"""
Auto-scale the camera w.r.t to the scene
Parameters
----------
maintain_aspect: ``None`` or bool, default ``None``
Maintain the camera aspect ratio for all dimensions. If ``None``, the aspect is left unchanged.
if ``False`` the camera is scaled to the bounding box of the current scene.
zoom: float
zoom value for the camera after auto-scaling
"""
if not len(self._fpl_graphics_scene.children) > 0:
return
self.center_scene()
if maintain_aspect is None: # if not provided keep current setting
# use the same maintain apsect for all other cameras that this controller manages
# I think this make sense for most use cases, even when the other controllers are
# only managing one or 2 axes
maintain_aspect = self.camera.maintain_aspect
if self.parent.__class__.__name__.endswith("Figure"):
# always use figure._subplots.ravel() in internal fastplotlib code
# otherwise if we use `for subplot in figure`, this could conflict
# with a user's iterator where they are doing `for subplot in figure` !!!
for subplot in self.parent._subplots.ravel():
# scale all cameras associated with this controller
if subplot.camera in self.controller.cameras:
camera = subplot.camera
self._auto_scale_scene(camera, subplot._fpl_graphics_scene, zoom, maintain_aspect)
else:
# just change for this plot area, this is probably a dock area
self._auto_scale_scene(self.camera, self._fpl_graphics_scene, zoom, maintain_aspect)
def _auto_scale_scene(self, camera: pygfx.PerspectiveCamera, scene: pygfx.Scene, zoom: float, maintain_aspect: bool):
camera.maintain_aspect = maintain_aspect
if len(scene.children) > 0:
width, height, depth = np.ptp(
scene.get_world_bounding_box(), axis=0
)
else:
width, height, depth = (1, 1, 1)
# make sure width and height are non-zero
if width < 0.01:
width = 1
if height < 0.01:
height = 1
camera.width = width
camera.height = height
camera.zoom = zoom
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|