Skip to content

Commit 7e0da92

Browse files
committed
feat(listeners): initial support with static definition
1 parent f9da1a3 commit 7e0da92

File tree

5 files changed

+181
-27
lines changed

5 files changed

+181
-27
lines changed

examples/vtk/widgets_plane.py

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33

44
from trame.app import get_server
55
from trame.ui.html import DivLayout
6-
from trame.widgets import html, client, vtk as vtk_widgets
6+
from trame.widgets import html, client
77
from trame_vtklocal.widgets import vtklocal
8+
from trame.decorators import TrameApp, change
89

910
# Required for vtk factory
1011
import vtkmodules.vtkRenderingOpenGL2 # noqa
1112
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleSwitch # noqa
1213

1314
from vtkmodules.vtkCommonColor import vtkNamedColors
14-
from vtkmodules.vtkCommonCore import vtkCommand
1515
from vtkmodules.vtkCommonDataModel import vtkPlane
1616
from vtkmodules.vtkFiltersCore import vtkClipPolyData
1717
from vtkmodules.vtkFiltersSources import vtkSphereSource
@@ -29,8 +29,6 @@
2929
vtkRenderer,
3030
)
3131

32-
WASM = True # "USE_WASM" in os.environ
33-
3432

3533
def create_vtk_pipeline(file_to_load):
3634
colors = vtkNamedColors()
@@ -97,38 +95,25 @@ def create_vtk_pipeline(file_to_load):
9795
)
9896
rep.SetNormal(plane.GetNormal())
9997
rep.SetOrigin(plane.GetOrigin())
100-
print(input_bounds)
10198

10299
plane_widget = vtkImplicitPlaneWidget2()
103100
plane_widget.SetInteractor(iren)
104101
plane_widget.SetRepresentation(rep)
105102

106-
if not WASM:
107-
my_callback = IPWCallback(plane)
108-
plane_widget.AddObserver(vtkCommand.InteractionEvent, my_callback)
109-
110103
renderer.ResetCamera(input_bounds)
111104
ren_win.Render()
112105

113106
plane_widget.On()
114107

115-
return ren_win, plane_widget
116-
117-
118-
class IPWCallback:
119-
def __init__(self, plane):
120-
self.plane = plane
121-
122-
def __call__(self, caller, ev):
123-
rep = caller.GetRepresentation()
124-
rep.GetPlane(self.plane)
108+
return ren_win, plane_widget, plane
125109

126110

127111
# -----------------------------------------------------------------------------
128112
# GUI
129113
# -----------------------------------------------------------------------------
130114

131115

116+
@TrameApp()
132117
class App:
133118
def __init__(self, server=None):
134119
self.server = get_server(server, client_type="vue3")
@@ -138,21 +123,59 @@ def __init__(self, server=None):
138123

139124
self.server.cli.add_argument("--data")
140125
args, _ = self.server.cli.parse_known_args()
141-
self.render_window, self.widget = create_vtk_pipeline(args.data)
126+
self.render_window, self.widget, self.plane = create_vtk_pipeline(args.data)
127+
142128
self.html_view = None
143129
self.ui = self._ui()
144130

131+
def register_widget_listeners(self):
132+
self.html_view.register_widget(self.widget)
133+
134+
# extract wasm ids
135+
widget_id = self.html_view.get_wasm_id(self.widget)
136+
rep_id = self.html_view.get_wasm_id(self.widget.representation)
137+
138+
# init state vars and listener properties
139+
self.server.state.plane_widget = None
140+
self.html_view.listeners = (
141+
"wasm_listeners",
142+
{
143+
widget_id: {
144+
"InteractionEvent": {
145+
"plane_widget": {
146+
rep_id: {
147+
"Normal": "normal",
148+
"Origin": "origin",
149+
}
150+
}
151+
}
152+
}
153+
},
154+
)
155+
156+
@change("plane_widget")
157+
def _on_widget_update(self, plane_widget, **_):
158+
if plane_widget is None:
159+
return
160+
161+
# update cutting plane
162+
self.plane.SetNormal(plane_widget.get("normal"))
163+
self.plane.SetOrigin(plane_widget.get("origin"))
164+
165+
# prevent requesting geometry too often
166+
self.html_view.render_throttle()
167+
145168
def _ui(self):
146169
with DivLayout(self.server) as layout:
147170
client.Style("body { margin: 0; }")
148171
with html.Div(
149172
style="position: absolute; left: 0; top: 0; width: 100vw; height: 100vh;"
150173
):
151-
if WASM:
152-
self.html_view = vtklocal.LocalView(self.render_window)
153-
self.html_view.register_widget(self.widget)
154-
else:
155-
self.html_view = vtk_widgets.VtkRemoteView(self.render_window)
174+
self.html_view = vtklocal.LocalView(
175+
self.render_window,
176+
throttle_rate=20,
177+
)
178+
self.register_widget_listeners()
156179

157180
return layout
158181

trame_vtklocal/utils/__init__.py

Whitespace-only changes.

trame_vtklocal/utils/throttle.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import asyncio
2+
3+
4+
class Throttle:
5+
"""
6+
Helper class that wrap a function with a given max execution rate.
7+
By default the rate is set to execute no more than once a second.
8+
9+
:param fn: the function to call.
10+
:type fn: function
11+
12+
:param ts: Number of seconds to wait before the next execution.
13+
:type ts: float
14+
"""
15+
16+
def __init__(self, fn, ts=1):
17+
self._ts = ts
18+
self._fn = fn
19+
self._requests = 0
20+
self._pending = False
21+
self._last_args = []
22+
self._last_kwargs = {}
23+
24+
@property
25+
def rate(self):
26+
"""Number of maximum executions per second"""
27+
return 1.0 / self._ts
28+
29+
@rate.setter
30+
def rate(self, rate):
31+
"""Update the maximum number of executions per seconds"""
32+
self._ts = 1.0 / rate
33+
34+
@property
35+
def delta_t(self):
36+
"""Number of seconds to wait between execution"""
37+
return self._ts
38+
39+
@delta_t.setter
40+
def delta_t(self, seconds):
41+
"""Update the number of seconds to wait between execution"""
42+
self._ts = seconds
43+
44+
async def _trottle(self):
45+
self._pending = True
46+
if self._requests:
47+
self._fn(*self._last_args, **self._last_kwargs)
48+
self._requests = 0
49+
50+
await asyncio.sleep(self._ts)
51+
if self._requests > 0:
52+
await self._trottle()
53+
self._pending = False
54+
55+
def __call__(self, *args, **kwargs):
56+
"""Function call wrapper that will throttle the actual function provided at construction"""
57+
self._requests += 1
58+
self._last_args = args
59+
self._last_kwargs = kwargs
60+
61+
if not self._pending:
62+
asyncio.create_task(self._trottle())

trame_vtklocal/widgets/vtklocal.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import base64
55
from pathlib import Path
66
from trame_client.widgets.core import AbstractElement
7-
from .. import module
7+
from trame_vtklocal import module
8+
from trame_vtklocal.utils.throttle import Throttle
89

910
try:
1011
import zlib # noqa
@@ -46,7 +47,7 @@ def get_version():
4647
class LocalView(HtmlElement):
4748
_next_id = 0
4849

49-
def __init__(self, render_window, **kwargs):
50+
def __init__(self, render_window, throttle_rate=10, **kwargs):
5051
super().__init__(
5152
"vtk-local",
5253
**kwargs,
@@ -70,6 +71,7 @@ def __init__(self, render_window, **kwargs):
7071
self._attr_names += [
7172
("cache_size", "cacheSize"),
7273
("eager_sync", "eagerSync"),
74+
"listeners", # only processed at mount time for now
7375
]
7476
self._event_names += [
7577
"updated",
@@ -78,25 +80,34 @@ def __init__(self, render_window, **kwargs):
7880
("camera", "camera"),
7981
]
8082

83+
# Generate throttle update function
84+
self.render_throttle = Throttle(self.update)
85+
self.render_throttle.rate = throttle_rate
86+
8187
@property
8288
def api(self):
89+
"""Return API from helper"""
8390
return module.get_helper(self.server).api
8491

8592
@property
8693
def object_manager(self):
94+
"""Return object_manager"""
8795
return self.api.vtk_object_manager
8896

8997
def update(self, push_camera=False):
98+
"""Sync view by pushing updates to client"""
9099
self.api.update(push_camera=push_camera)
91100
self.server.js_call(self.__ref, "update")
92101

93102
def register_widget(self, w):
103+
"""Register external element (i.e. widget) into the scene so it can be managed"""
94104
if w not in self.__registered_obj:
95105
self.api.register_widget(self._render_window, w)
96106
self.__registered_obj.append(w)
97107
self.api.update()
98108

99109
def uregister_widgets(self):
110+
"""Unregister external element (i.e. widget) from the scene so it can removed from tracking"""
100111
for w in self.__registered_obj:
101112
self.api.unregister(self._render_window, w)
102113

@@ -136,6 +147,7 @@ def export(self, format="zip", **kwargs):
136147
return zip_buffer.getvalue()
137148

138149
def reset_camera(self, renderer_or_render_window=None, **kwargs):
150+
"""Reset camera by making the call on the client side"""
139151
if renderer_or_render_window is None:
140152
renderer_or_render_window = self._render_window
141153

@@ -150,7 +162,9 @@ def reset_camera(self, renderer_or_render_window=None, **kwargs):
150162

151163
@property
152164
def ref_name(self):
165+
"""Return the assigned name as a vue.js ref"""
153166
return self.__ref
154167

155168
def get_wasm_id(self, vtk_object):
169+
"""Return vtkObject id used within WASM scene manager"""
156170
return self.object_manager.GetId(vtk_object)

vue-components/src/components/VtkLocal.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
import { inject, ref, unref, onMounted, onBeforeUnmount } from "vue";
22
import { createModule } from "../utils";
33

4+
function idToState(sceneManager, cid) {
5+
sceneManager.updateStateFromObject(cid);
6+
return sceneManager.getState(cid);
7+
}
8+
9+
function createExtractCallback(trame, sceneManager, extractInfo) {
10+
return function () {
11+
for (const [name, objIds] of Object.entries(extractInfo)) {
12+
const value = {};
13+
for (const [objId, props] of Object.entries(objIds)) {
14+
const state = idToState(sceneManager, objId);
15+
if (typeof props === "string") {
16+
value[props] = state;
17+
} else {
18+
// extract and remap
19+
for (const [k, v] of Object.entries(props)) {
20+
value[v] = state[k];
21+
}
22+
}
23+
}
24+
trame.state.set(name, value);
25+
}
26+
};
27+
}
28+
429
export default {
530
emits: ["updated", "memory-vtk", "memory-arrays", "camera"],
631
props: {
@@ -18,6 +43,22 @@ export default {
1843
wsClient: {
1944
type: Object,
2045
},
46+
listeners: {
47+
type: Object,
48+
// {
49+
// cid: {
50+
// ModifiedEvent: {
51+
// varName: {
52+
// objId: {
53+
// "PropName": "keyInJS_varName",
54+
// "PropName2": "key2InJS_varName",
55+
// },
56+
// objId: "keyInJS_fullState",
57+
// },
58+
// },
59+
// }
60+
// }
61+
},
2162
},
2263
setup(props, { emit }) {
2364
const trame = inject("trame");
@@ -265,6 +306,20 @@ export default {
265306
]);
266307
}
267308

309+
// Other listeners
310+
for (const [cid, eventMap] of Object.entries(props.listeners || {})) {
311+
for (const [eventName, extractInfo] of Object.entries(eventMap || {})) {
312+
observerTags.push([
313+
cid,
314+
sceneManager.addObserver(
315+
cid,
316+
eventName,
317+
createExtractCallback(trame, sceneManager, extractInfo)
318+
),
319+
]);
320+
}
321+
}
322+
268323
sceneManager.startEventLoop(props.renderWindow);
269324
if (resizeObserver) {
270325
resizeObserver.observe(unref(container));

0 commit comments

Comments
 (0)