Appearance
VTK 3D Widgets with VTK.wasm and trame
VTK 3D widgets let you interact with the 3D view in a way that you can drive a processing filter interactively.
In the example below we cover how you can create such widget in plain VTK and how to enable it on the client within the WASM context.
The code below highlight some important region and logic to understand.
py
# Widget setup
rep = vtk.vtkImplicitPlaneRepresentation(
place_factor=1.25,
outline_translation=False,
)
rep.DrawPlaneOff()
rep.PlaceWidget(bounds)
rep.normal = plane.normal
rep.origin = plane.origin
plane_widget = vtk.vtkImplicitPlaneWidget2(
interactor=renderWindowInteractor, representation=rep
)
plane_widget.On()
62
63
64
65
66
67
68
69
70
71
72
73
74
75
63
64
65
66
67
68
69
70
71
72
73
74
75
py
with vtklocal.LocalView(
self.render_window,
throttle_rate=20,
ctx_name="wasm_view",
) as view:
self.ctrl.view_update = view.update_throttle
self.ctrl.view_reset_camera = view.reset_camera
# ---------------------------------------------------------
# Widget handling
# ---------------------------------------------------------
# => push widget to client
wasm_id = view.register_vtk_object(self.widget)
# => attach listener to widget and bind data to state
view.listeners = (
"listeners",
{
wasm_id: {
"InteractionEvent": {
"plane_widget": {
"origin": (
wasm_id,
"WidgetRepresentation",
"Origin",
),
"normal": (
wasm_id,
"WidgetRepresentation",
"Normal",
),
},
},
},
},
)
# => reserve state variable for widget update
self.state.plane_widget = None
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
py
@change("plane_widget")
def on_plane_widget(self, plane_widget, **_):
if plane_widget is None:
return
origin = plane_widget.get("origin")
normal = plane_widget.get("normal")
self.plane.SetOrigin(origin)
self.plane.SetNormal(normal)
self.ctrl.view_update()
168
169
170
171
172
173
174
175
176
177
178
179
169
170
171
172
173
174
175
176
177
178
179
py
# Should import classes from vtkmodules
# but as an example we use vtk for simplicity
import vtk
from pathlib import Path
from trame.app import TrameApp
from trame.decorators import change
from trame.ui.vuetify3 import SinglePageLayout
from trame.widgets import vuetify3, vtklocal
DATA_PATH = str(Path(__file__).with_name("star-fighter.vtp").resolve())
class Clip(TrameApp):
def __init__(self, server=None):
super().__init__(server)
self._setup_vtk()
self._build_ui()
def _setup_vtk(self):
renderer = vtk.vtkRenderer()
renderWindow = vtk.vtkRenderWindow()
renderWindow.AddRenderer(renderer)
renderWindow.OffScreenRenderingOn()
renderWindowInteractor = vtk.vtkRenderWindowInteractor()
renderWindowInteractor.SetRenderWindow(renderWindow)
renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera()
reader = vtk.vtkXMLPolyDataReader(file_name=DATA_PATH)
bounds = reader().GetBounds()
plane = vtk.vtkPlane(
origin=(
0.5 * (bounds[0] + bounds[1]),
0.5 * (bounds[2] + bounds[3]),
0.5 * (bounds[4] + bounds[5]),
)
)
clipper = vtk.vtkClipDataSet(clip_function=plane)
geometry = vtk.vtkDataSetSurfaceFilter()
ctx_mapper = vtk.vtkPolyDataMapper()
reader >> ctx_mapper
clip_mapper = vtk.vtkPolyDataMapper()
reader >> clipper >> geometry >> clip_mapper
clip_actor = vtk.vtkActor(mapper=clip_mapper)
renderer.AddActor(clip_actor)
ctx_actor = vtk.vtkActor(mapper=ctx_mapper)
renderer.AddActor(ctx_actor)
ctx_actor.property.opacity = 0.1
renderer.ResetCamera()
renderWindow.Render()
# region widget
# Widget setup
rep = vtk.vtkImplicitPlaneRepresentation(
place_factor=1.25,
outline_translation=False,
)
rep.DrawPlaneOff()
rep.PlaceWidget(bounds)
rep.normal = plane.normal
rep.origin = plane.origin
plane_widget = vtk.vtkImplicitPlaneWidget2(
interactor=renderWindowInteractor, representation=rep
)
plane_widget.On()
# endregion widget
self.plane = plane
self.render_window = renderWindow
self.actor = clip_actor
self.ctx_actor = ctx_actor
self.widget = plane_widget
def _build_ui(self):
with SinglePageLayout(self.server) as layout:
self.ui = layout
layout.title.set_text("WASM Widget")
layout.icon.click = self.ctrl.view_reset_camera
with layout.toolbar as toolbar:
toolbar.density = "compact"
vuetify3.VSpacer()
vuetify3.VCheckbox(
true_icon="mdi-eye-outline",
false_icon="mdi-eye-off-outline",
v_model=("show_ctx", True),
hide_details=True,
density="compact",
)
vuetify3.VSlider(
prepend_icon="mdi-opacity",
v_model=("opacity", 1),
min=0,
max=1,
step=0.01,
density="compact",
hide_details=True,
classes="mr-6",
)
with layout.content:
# region trameWidget
with vtklocal.LocalView(
self.render_window,
throttle_rate=20,
ctx_name="wasm_view",
) as view:
self.ctrl.view_update = view.update_throttle
self.ctrl.view_reset_camera = view.reset_camera
# ---------------------------------------------------------
# Widget handling
# ---------------------------------------------------------
# => push widget to client
wasm_id = view.register_vtk_object(self.widget)
# => attach listener to widget and bind data to state
view.listeners = (
"listeners",
{
wasm_id: {
"InteractionEvent": {
"plane_widget": {
"origin": (
wasm_id,
"WidgetRepresentation",
"Origin",
),
"normal": (
wasm_id,
"WidgetRepresentation",
"Normal",
),
},
},
},
},
)
# => reserve state variable for widget update
self.state.plane_widget = None
# endregion trameWidget
@change("show_ctx")
def on_show_ctx(self, show_ctx, **_):
self.ctx_actor.visibility = show_ctx
self.ctrl.view_update()
@change("opacity")
def on_opacity(self, opacity, **_):
self.actor.property.opacity = opacity
self.ctrl.view_update()
# region trameChange
@change("plane_widget")
def on_plane_widget(self, plane_widget, **_):
if plane_widget is None:
return
origin = plane_widget.get("origin")
normal = plane_widget.get("normal")
self.plane.SetOrigin(origin)
self.plane.SetNormal(normal)
self.ctrl.view_update()
# endregion trameChange
def main():
# region export
import sys
app = Clip()
if "--export" in sys.argv:
app.ctx.wasm_view.save("star-fighter.wazex")
app.server.start()
# endregion export
if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
txt
--extra-index-url https://wheels.vtk.org
trame>=3.9
trame-vuetify
trame-vtklocal>=0.12.3
vtk==9.5.20250531.dev0
Explanation
If we just focus on the trame integration of a 3D widget for WASM, we realize that there is 3 steps that needs to be followed.
- First you need to register the widget to
vtklocal.LocalView
instance so the class get instantiated on the client side as well with all its behavior. This is done line 126 (view.register_vtk_object(self.widget)
). - Then we need to attach a listener to that widget and bind a trame state to some internal of the vtk objects that is living on the WASM side. For that we rely on the listener property. The listener structure is a set of nested dictionaries where the root keys are the various VTK objects (WASM id of such object) on which you want to bind observers. The second layer is the name of the VTK event you want the system to listen to. The thrid layer is the trame state variable that will be update. Each trame state variable will be a JavaScript object itself where each key will be resolved via some property lookup on the WASM instance. For the final data lookup piece, you need to provide a path to the data you try to extract like the following examples
(wasm_id_of_vtk_object, PropertyName...)
. If you just put thewasm_id
, you will get the full state of the corresponding vtkObject. - Finally, on the trame state side, you need to listen to the variable so you can map to the filter and ask the widget to synchronize its data with the server.
The full working code is also available.