Skip to content

Commit 309e1b4

Browse files
committed
feat(multiRenderer): add support single render view with many renderer
1 parent 6402acf commit 309e1b4

8 files changed

Lines changed: 620 additions & 3 deletions

File tree

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import math
2+
3+
import vtk
4+
from trame.app import TrameApp, TrameComponent
5+
from trame.decorators import change
6+
from trame.ui.vuetify3 import SinglePageLayout
7+
from vtkmodules.vtkRenderingCore import vtkRenderer, vtkRenderWindow
8+
9+
from trame.widgets import html, rca
10+
from trame.widgets import vuetify3 as v3
11+
12+
13+
class ManyViewManager:
14+
def __init__(self):
15+
self._view_size = [300, 300] # assume uniform
16+
self._render_window = vtkRenderWindow()
17+
self._render_window.OffScreenRenderingOn()
18+
# self._camera = vtkCamera()
19+
self._renderers = {}
20+
self._visibility = {}
21+
self._layout = {}
22+
23+
renderWindowInteractor = vtk.vtkRenderWindowInteractor()
24+
renderWindowInteractor.SetRenderWindow(self._render_window)
25+
renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()
26+
27+
def create_renderer(self, name):
28+
if name not in self._renderers:
29+
renderer = vtkRenderer() # active_camera=self._camera
30+
self._renderers[name] = renderer
31+
self._visibility[name] = True
32+
33+
self.refresh_layout()
34+
35+
return self._renderers[name]
36+
37+
def delete_renderer(self, name):
38+
self._visibility[name] = False
39+
renderer = self._renderers.pop(name, None)
40+
if renderer:
41+
# do something with render window
42+
self._render_window.RemoveRenderer(renderer)
43+
44+
self.refresh_layout()
45+
46+
def update_visibility(self, name, visibility):
47+
self._visibility[name] = visibility
48+
self.refresh_layout()
49+
50+
def refresh_layout(self):
51+
renderers_in_layout = {}
52+
for name, visible in self._visibility.items():
53+
renderer = self._renderers.get(name)
54+
if renderer is None:
55+
continue
56+
if visible:
57+
self._render_window.AddRenderer(renderer)
58+
renderers_in_layout[name] = renderer
59+
else:
60+
self._render_window.RemoveRenderer(renderer)
61+
62+
size = len(renderers_in_layout)
63+
width_count = math.ceil(math.sqrt(size))
64+
height_count = math.ceil(size / width_count)
65+
full_size = [
66+
self._view_size[0] * width_count,
67+
self._view_size[1] * height_count,
68+
]
69+
self._render_window.SetSize(*full_size)
70+
dx = 1.0 / width_count
71+
dy = 1.0 / height_count
72+
self._layout = {}
73+
for idx, (name, renderer) in enumerate(renderers_in_layout.items()):
74+
i = idx % width_count
75+
j = int(idx / width_count)
76+
bounds = (i * dx, j * dy, (i + 1) * dx, (j + 1) * dy)
77+
renderer.SetViewport(*bounds)
78+
self._layout[name] = bounds
79+
80+
@property
81+
def layout(self):
82+
return self._layout
83+
84+
85+
class Cone(TrameComponent):
86+
COUNT = 1
87+
88+
def __init__(self, server):
89+
super().__init__(server)
90+
self.name = f"Cone {Cone.COUNT}"
91+
Cone.COUNT += 1
92+
93+
self.cone = vtk.vtkConeSource()
94+
self.mapper = vtk.vtkPolyDataMapper()
95+
self.actor = vtk.vtkActor(mapper=self.mapper)
96+
self.cone >> self.mapper
97+
98+
@property
99+
def resolution(self):
100+
return self.cone.resolution
101+
102+
@resolution.setter
103+
def resolution(self, v):
104+
self.cone.resolution = v
105+
106+
107+
class ManyViewTest(TrameApp):
108+
def __init__(self, server=None):
109+
super().__init__(server)
110+
self.cones = {}
111+
self.state.views = {}
112+
self.view_manager = ManyViewManager()
113+
self._build_ui()
114+
115+
def add_view(self):
116+
cone = Cone(self.server)
117+
self.cones[cone.name] = cone
118+
renderer = self.view_manager.create_renderer(cone.name)
119+
renderer.AddActor(cone.actor)
120+
renderer.ResetCamera()
121+
self.state.names = list(self.cones.keys())
122+
self.state.views = self.view_manager.layout
123+
self.ctx.handler.update()
124+
125+
def remove_view(self):
126+
name_to_remove = self.state.active_renderer
127+
self.view_manager.delete_renderer(name_to_remove)
128+
self.state.active_renderer = None
129+
self.cones.pop(name_to_remove, None)
130+
self.state.names = list(self.cones.keys())
131+
self.state.views = self.view_manager.layout
132+
self.ctx.handler.update()
133+
134+
@change("resolution")
135+
def _on_resolution(self, resolution, active_renderer, **_):
136+
cone = self.cones.get(active_renderer)
137+
if cone:
138+
cone.resolution = resolution
139+
self.ctx.handler.update()
140+
141+
@change("active_renderer")
142+
def _on_active_renderer(self, active_renderer, **_):
143+
cone = self.cones.get(active_renderer)
144+
if cone:
145+
self.state.resolution = cone.resolution
146+
147+
def update_size(self, name, size):
148+
print("Size update for", name, size)
149+
self.view_manager._view_size = [
150+
round(size["w"] * size["p"]),
151+
round(size["h"] * size["p"]),
152+
]
153+
self.view_manager.refresh_layout()
154+
self.ctx.handler.update()
155+
156+
def _build_ui(self):
157+
with SinglePageLayout(self.server) as self.ui:
158+
with self.ui.toolbar.clear() as toolbar:
159+
toolbar.classes = "px-4"
160+
v3.VSelect(
161+
v_model=("active_renderer", None),
162+
items=("names", []),
163+
density="compact",
164+
hide_details=True,
165+
style="max-width: 300px;",
166+
)
167+
v3.VBtn(icon="mdi-plus", click=self.add_view)
168+
v3.VBtn(icon="mdi-minus", click=self.remove_view)
169+
v3.VSlider(
170+
v_model=("resolution", 6),
171+
min=3,
172+
max=24,
173+
step=1,
174+
hide_details=True,
175+
density="compact",
176+
)
177+
v3.VSwitch(
178+
v_model=("enable_interaction", True),
179+
density="compact",
180+
hide_details=True,
181+
classes="mx-2",
182+
)
183+
with self.ui.content:
184+
# with rca.RemoteControlledArea(display="image") as view:
185+
# self.ctx.handler = view.create_view_handler(
186+
# self.view_manager._render_window,
187+
# encoder="turbo-jpeg",
188+
# )
189+
190+
# Expected
191+
with rca.ImageStream(
192+
self.view_manager._render_window,
193+
encoder="turbo-jpeg",
194+
ctx_name="handler",
195+
):
196+
with v3.VRow(classes="ma-0 pa-2"):
197+
html.Img(src=["image?.src"], height="200px")
198+
with v3.VRow(classes="mx-4"):
199+
with v3.VCol(cols=3, v_for="bounds, name in views", key="name"):
200+
with v3.VCard():
201+
v3.VCardTitle("{{ name }}")
202+
with html.Div(
203+
classes="position-relative w-100",
204+
style="aspect-ratio:16/9;",
205+
):
206+
rca.ImageRegion(
207+
enable_interaction=(
208+
"enable_interaction",
209+
True,
210+
),
211+
bounds=("bounds",),
212+
size=(self.update_size, "[name, $event]"),
213+
)
214+
215+
216+
def main():
217+
app = ManyViewTest()
218+
app.server.start()
219+
220+
221+
if __name__ == "__main__":
222+
main()

trame/widgets/rca.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from trame_rca.widgets.rca import * # noqa: F403
22

33

4-
def initialize(server):
4+
def initialize(server, **kwargs):
55
from trame_rca import module
66

7-
server.enable_module(module)
7+
server.enable_module(module, **kwargs)

trame_rca/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ def __init__(
113113
interactive_quality: Optional[int] = None,
114114
still_quality: Optional[int] = None,
115115
rca_encoder: Optional[RcaEncoder | str] = None,
116+
**_,
116117
):
117118
self._window = window_wrapper(window)
118119
self._rca_encoder = RcaEncoder(rca_encoder or RcaEncoder.JPEG)
@@ -227,6 +228,7 @@ def __init__(
227228
*,
228229
scheduler: RcaRenderScheduler = None,
229230
do_schedule_render_on_interaction=True,
231+
**_,
230232
):
231233
self._window = window_wrapper(window)
232234
if scheduler is None:

trame_rca/widgets/rca.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""RCA Widgets support both vue2 and vue3."""
22

33
import warnings
4+
from weakref import WeakKeyDictionary, WeakValueDictionary
45

56
from trame_client.widgets.core import AbstractElement
67

@@ -16,6 +17,8 @@
1617
"MediaSourceDisplayArea",
1718
"VideoDecoderDisplayArea",
1819
"RawImageDisplayArea",
20+
"ImageStream",
21+
"ImageRegion",
1922
]
2023

2124

@@ -196,3 +199,92 @@ def __init__(self, **kwargs):
196199
"origin",
197200
("image_style", "imageStyle"),
198201
]
202+
203+
204+
class ImageStream(HtmlElement):
205+
ID = 0
206+
NAMES = WeakKeyDictionary()
207+
HANDLERS = WeakValueDictionary()
208+
209+
@classmethod
210+
def _next_name(cls):
211+
cls.ID += 1
212+
return f"rca_image_stream_{cls.ID}"
213+
214+
@classmethod
215+
def _get_rw_name(cls, render_window):
216+
if render_window in cls.NAMES:
217+
return cls.NAMES[render_window]
218+
name = cls._next_name()
219+
cls.NAMES[render_window] = name
220+
return name
221+
222+
@classmethod
223+
def _get_rw_handler(
224+
cls,
225+
render_window,
226+
encoder="turbo-jpeg",
227+
target_fps=30,
228+
interactive_quality=80,
229+
still_quality=95,
230+
**kwargs,
231+
):
232+
name = cls._get_rw_name(render_window)
233+
if name in cls.HANDLERS:
234+
return cls.HANDLERS[name]
235+
236+
scheduler = RcaRenderScheduler(
237+
render_window,
238+
target_fps=target_fps,
239+
interactive_quality=interactive_quality,
240+
still_quality=still_quality,
241+
rca_encoder=encoder,
242+
**kwargs,
243+
)
244+
handler = RcaViewAdapter(
245+
render_window,
246+
name,
247+
scheduler=scheduler,
248+
**kwargs,
249+
)
250+
cls.HANDLERS[render_window] = handler
251+
return handler
252+
253+
def __init__(self, render_window, image="image", encoder="turbo-jpeg", **kwargs):
254+
super().__init__("image-stream", **kwargs)
255+
self._attr_names += [
256+
"name",
257+
("pool_size", "poolSize"),
258+
]
259+
self.render_window = render_window
260+
self.name = self._get_rw_name(render_window)
261+
self.handler = self._get_rw_handler(render_window, encoder)
262+
263+
#
264+
if self.server.running:
265+
self.server.root_server.controller.rc_area_register(self.handler)
266+
else:
267+
self.ctrl.on_server_ready.add(self._on_ready)
268+
269+
self._attributes["slot"] = f'v-slot="{{ {image}: image }}"'
270+
271+
def update(self):
272+
self.handler.update()
273+
274+
def _on_ready(self, **_):
275+
for handler in self.HANDLERS.values():
276+
self.server.root_server.controller.rc_area_register(handler)
277+
278+
279+
class ImageRegion(HtmlElement):
280+
def __init__(self, **kwargs):
281+
super().__init__("image-region", **kwargs)
282+
self._attr_names += [
283+
"bounds",
284+
("enable_interaction", "enableInteraction"),
285+
("send_mouse_move", "sendMouseMove"),
286+
("event_throttle_ms", "eventThrottleMs"),
287+
]
288+
self._event_names += [
289+
"size",
290+
]

vue-components/src/components/ImageDisplayArea.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,5 +146,5 @@ export default {
146146
this.cleanup();
147147
},
148148
inject: ['trame'],
149-
template: `<img :style="imageStyle" :src="displayURL" v-show="hasContent" draggable="false" />`,
149+
template: `<slot><img :style="imageStyle" :src="displayURL" v-show="hasContent" draggable="false" /></slot>`,
150150
};

0 commit comments

Comments
 (0)