Skip to content

Commit 90e2556

Browse files
authored
Merge pull request #21 from pynapple-org/ctrl_grp
Create controller_group.py
2 parents 009e6ed + cd8d684 commit 90e2556

File tree

8 files changed

+150
-68
lines changed

8 files changed

+150
-68
lines changed

main_debug_qt.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from PyQt6.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton
88
import pynaviz as viz
99
import sys
10-
from pynaviz.controller import ControllerGroup
10+
from pynaviz.controller_group import ControllerGroup
1111

1212

1313
tsd1 = nap.Tsd(t=np.arange(1000), d=np.cos(np.arange(1000) * 0.1))
@@ -36,9 +36,9 @@
3636

3737
arg = [viz1.plot, viz2.plot]
3838

39-
ctrl_group = ControllerGroup(arg)
40-
# ctrl_group.add(viz1.plot.controller, viz1.plot.renderer, 0)
41-
# ctrl_group.add(viz2.plot.controller, viz2.plot.renderer, 1)
39+
ctrl_group = ControllerGroup()
40+
ctrl_group.add(viz1, 0)
41+
ctrl_group.add(viz2, 1)
4242

4343
layout.addWidget(viz1)
4444
layout.addWidget(viz2)

main_simple_glfw.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@
2727
# v = viz.TsGroupWidget(tsg)
2828
# v.show()
2929
# v = viz.TsdFrameWidget(tsdframe)
30-
# v.show()
30+
v.show()

noxfile.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
import os
2-
import shutil
3-
from pathlib import Path
4-
51
import nox
62

73

84
# nox --no-venv -s linters
95
# nox --no-venv -s tests
10-
6+
# WGPU_FORCE_OFFSCREEN=1 nox
117

128
@nox.session(name="linters")
139
def linters(session):

src/pynaviz/base_plot.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,14 @@ def animate(self):
175175
world_xmin, world_xmax, world_ymin, world_ymax = get_plot_min_max(self)
176176

177177
# X axis
178-
self.ruler_x.start_pos = world_xmin, 0, 0
179-
self.ruler_x.end_pos = world_xmax, 0, 0
178+
self.ruler_x.start_pos = world_xmin, 0, -10
179+
self.ruler_x.end_pos = world_xmax, 0, -10
180180
self.ruler_x.start_value = self.ruler_x.start_pos[0]
181181
self.ruler_x.update(self.camera, self.canvas.get_logical_size())
182182

183183
# Y axis
184-
self.ruler_y.start_pos = 0, world_ymin, 0
185-
self.ruler_y.end_pos = 0, world_ymax, 0
184+
self.ruler_y.start_pos = 0, world_ymin, -10
185+
self.ruler_y.end_pos = 0, world_ymax, -10
186186
self.ruler_y.start_value = self.ruler_y.start_pos[1]
187187
self.ruler_y.update(self.camera, self.canvas.get_logical_size())
188188

src/pynaviz/controller.py

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import threading
6+
from abc import ABC, abstractmethod
67
from typing import Callable, List, Optional, Union
78

89
import pygfx
@@ -14,54 +15,7 @@
1415
from .utils import _get_event_handle
1516

1617

17-
class ControllerGroup:
18-
"""
19-
20-
Parameters
21-
----------
22-
controllers_and_renderers : list or None
23-
controllers and renderers. Can be empty.
24-
interval : tuple of float or int
25-
The start and end of the epoch to show when initializing.
26-
27-
"""
28-
29-
def __init__(self, plots=None, interval=(0, 10)):
30-
if not isinstance(interval, (tuple, list)):
31-
raise ValueError("interval should be tuple or list")
32-
if not len(interval) == 2 and not all(
33-
[isinstance(x, (float, int)) for x in interval]
34-
):
35-
raise ValueError("interval should be a 2-tuple of float/int")
36-
if interval[0] > interval[1]:
37-
raise RuntimeError("interval start should precede interval end")
38-
39-
if plots is not None:
40-
for i, plt in enumerate(plots):
41-
plt.controller._controller_id = i
42-
self._add_update_handler(plt.renderer)
43-
plt.controller.set_xlim(*interval)
44-
45-
self.interval = interval
46-
self.plots = plots
47-
48-
def _add_update_handler(self, viewport_or_renderer: Union[Viewport, Renderer]):
49-
viewport = Viewport.from_viewport_or_renderer(viewport_or_renderer)
50-
viewport.renderer.add_event_handler(self.sync_controllers, "sync")
51-
52-
def sync_controllers(self, event):
53-
"""Sync controllers according to their rule."""
54-
# print(event)
55-
# self._update_controller_group()
56-
for plt in self.plots:
57-
if (
58-
event.controller_id != plt.controller.controller_id
59-
and plt.controller.enabled
60-
):
61-
plt.controller.sync(event)
62-
63-
64-
class CustomController(PanZoomController):
18+
class CustomController(ABC, PanZoomController):
6519
""""""
6620

6721
def __init__(
@@ -150,9 +104,6 @@ def _send_sync_event(self, update_type: str, *args, **kwargs):
150104
)
151105
)
152106

153-
def sync(self, event):
154-
pass
155-
156107
def set_xlim(self, xmin: float, xmax: float):
157108
"""Set the visible X range for an OrthographicCamera.
158109
#TODO THIS SHOULD DEPEND ON THE CURRENT SYNC STATUS
@@ -175,6 +126,10 @@ def set_view(self, xmin: float, xmax: float, ymin: float, ymax: float):
175126
self.set_xlim(xmin, xmax)
176127
self.set_ylim(ymin, ymax)
177128

129+
@abstractmethod
130+
def sync(self, event):
131+
pass
132+
178133

179134
class SpanController(CustomController):
180135
"""

src/pynaviz/controller_group.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""
2+
ControllerGroup is used to synchronize in time each canvas.
3+
"""
4+
5+
from typing import Optional, Sequence, Union
6+
7+
from pygfx import Renderer, Viewport
8+
9+
10+
class ControllerGroup:
11+
"""
12+
Manages a group of plot controllers and synchronizes them.
13+
14+
Parameters
15+
----------
16+
plots : Optional[Sequence]
17+
A sequence of plot objects (each with a `.controller` and `.renderer` attribute).
18+
Can be None or empty.
19+
interval : tuple[float | int, float | int]
20+
Start and end of the epoch (x-axis range) to show when initializing.
21+
Must be a 2-tuple with start <= end.
22+
"""
23+
24+
def __init__(self, plots: Optional[Sequence] = None, interval: tuple[Union[int, float], Union[int, float]] = (0, 10)):
25+
self._controller_group = dict()
26+
27+
# Validate interval format
28+
if not isinstance(interval, (tuple, list)):
29+
raise ValueError("`interval` must be a tuple or list.")
30+
31+
if len(interval) != 2 or not all(isinstance(x, (int, float)) for x in interval):
32+
raise ValueError("`interval` must be a 2-tuple of int or float values.")
33+
34+
if interval[0] > interval[1]:
35+
raise ValueError("`interval` start must not be greater than end.")
36+
37+
self.interval = interval
38+
39+
# Initialize controller group from given plots
40+
if plots is not None:
41+
for i, plt in enumerate(plots):
42+
plt.controller._controller_id = i
43+
self._add_update_handler(plt.renderer)
44+
plt.controller.set_xlim(*interval)
45+
self._controller_group[i] = plt.controller
46+
47+
def _add_update_handler(self, viewport_or_renderer: Union[Viewport, Renderer]):
48+
"""
49+
Registers a sync event handler on the renderer of the given viewport or renderer.
50+
"""
51+
viewport = Viewport.from_viewport_or_renderer(viewport_or_renderer)
52+
viewport.renderer.add_event_handler(self.sync_controllers, "sync")
53+
54+
def sync_controllers(self, event):
55+
"""
56+
Synchronizes all other controllers in the group when a sync event is triggered.
57+
58+
Parameters
59+
----------
60+
event : Event
61+
The sync event that contains `controller_id` and possibly data to sync.
62+
"""
63+
for id_other, ctrl in self._controller_group.items():
64+
if event.controller_id != id_other and ctrl.enabled:
65+
ctrl.sync(event)
66+
67+
def add(self, plot, controller_id: int):
68+
"""
69+
Adds a plot to the controller group.
70+
71+
Parameters
72+
----------
73+
plot : object
74+
A base or widget plot with a `.controller` and `.renderer` attribute,
75+
or a wrapper with `.plot.controller` and `.plot.renderer`.
76+
controller_id : int
77+
Unique identifier to assign to the plot's controller in the group.
78+
79+
Raises
80+
------
81+
RuntimeError
82+
If the plot doesn't have a controller/renderer or the ID already exists.
83+
"""
84+
# Attempt to extract controller and renderer
85+
if hasattr(plot, "controller") and hasattr(plot, "renderer"):
86+
controller = plot.controller
87+
renderer = plot.renderer
88+
elif hasattr(plot, "plot") and hasattr(plot.plot, "controller") and hasattr(plot.plot, "renderer"):
89+
controller = plot.plot.controller
90+
renderer = plot.plot.renderer
91+
else:
92+
raise RuntimeError("Plot object must have a controller and renderer.")
93+
94+
# Prevent duplicate controller IDs
95+
if controller_id in self._controller_group:
96+
raise RuntimeError(f"Controller ID {controller_id} already exists in the group.")
97+
98+
# Assign ID if not already assigned
99+
if controller.controller_id is None:
100+
controller.controller_id = controller_id
101+
102+
self._controller_group[controller_id] = controller
103+
self._add_update_handler(renderer)
104+
105+
def remove(self, controller_id: int):
106+
"""
107+
Removes a controller from the group by its ID.
108+
109+
Parameters
110+
----------
111+
controller_id : int
112+
The ID of the controller to remove.
113+
114+
Raises
115+
------
116+
KeyError
117+
If the controller_id is not found in the group.
118+
"""
119+
if controller_id not in self._controller_group:
120+
raise KeyError(f"Controller ID {controller_id} not found in the group.")
121+
122+
controller = self._controller_group.pop(controller_id)
123+
124+
# Optional: remove event handler if needed
125+
# This assumes controller has a reference to its renderer
126+
try:
127+
viewport = Viewport.from_viewport_or_renderer(controller.renderer)
128+
viewport.renderer.remove_event_handler(self.sync_controllers, "sync")
129+
except Exception:
130+
# Fallback: skip if removal fails (e.g., missing references)
131+
pass

src/pynaviz/gui.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
QWidget,
1515
)
1616

17-
from .controller import ControllerGroup
17+
from .controller_group import ControllerGroup
1818
from .widget_plot import (
1919
TsdFrameWidget,
2020
TsdTensorWidget,
@@ -149,7 +149,7 @@ def add_dock_widget(self, item):
149149
self.gui.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock)
150150

151151
# Adding controller and render to control group
152-
self.ctrl_group.add(widget.plot.controller, widget.plot.renderer, index)
152+
self.ctrl_group.add(widget.plot, index)
153153
self._n_dock_open += 1
154154

155155
def _create_title_bar(self):

tests/test_plot_tsd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def test_plot_tsd(dummy_tsd):
3434
image = Image.open(
3535
os.path.expanduser("tests/screenshots/test_plot_tsd.png")
3636
).convert("RGBA")
37-
except:
37+
except Exception:
3838
image = Image.open(
3939
os.path.expanduser("screenshots/test_plot_tsd.png")
4040
).convert("RGBA")

0 commit comments

Comments
 (0)