diff --git a/examples/demo/webxr.py b/examples/demo/webxr.py new file mode 100644 index 0000000..f4b3621 --- /dev/null +++ b/examples/demo/webxr.py @@ -0,0 +1,121 @@ +import vtk + +from trame.app import TrameApp +from trame.ui.html import DivLayout +from trame.widgets import html, client, vtklocal +from trame.decorators import change + +FULL_SCREEN = "position:absolute; left:0; top:0; width:100vw; height:100vh;" +TOP_RIGHT = "position: absolute; top: 1rem; right: 1rem; z-index: 999999;" +TOP_LEFT = "position: absolute; top: 1rem; left: 1rem; z-index: 999999;" +TOP_CENTER = "position: absolute; top: 1rem; left: 50%; z-index: 999999; transform: translateX(-50%);" +OFF_SCREEN = "position: none;" + + +def create_vtk_pipeline(): + renderer = vtk.vtkWebXRRenderer() + rw = vtk.vtkWebXRRenderWindow() + rw.AddRenderer(renderer) + + # WebXR Emulator doesn't work with MultiSamples>0 + rw.SetMultiSamples(0) + + rwi = vtk.vtkWebXRRenderWindowInteractor() + rwi.SetRenderWindow(rw) + + cone = vtk.vtkConeSource() + + mapper = vtk.vtkPolyDataMapper(input_connection=cone.output_port) + actor = vtk.vtkActor(mapper=mapper) + + renderer.AddActor(actor) + renderer.background = (0.1, 0.2, 0.4) + # Reset camera to place actors a little bit higher + renderer.ResetCamera() + # Prevent camera issues when no actors are visible in the viewport + renderer.GetCullers().RemoveAllItems() + + light = vtk.vtkLight() + light.SetColor(1, 1, 1) + light.SetPosition(0, 3, 0) + light.SetIntensity(1) + light.SetLightTypeToSceneLight() + renderer.AddLight(light) + + return rw, cone, actor + + +class WasmApp(TrameApp): + def __init__(self, name=None): + super().__init__(name) + self.render_window, self.cone, self.cone_actor = create_vtk_pipeline() + self._build_ui() + self.state.cone_actor = {"position": None} + + @change("resolution") + def on_resolution_change(self, resolution, **_): + self.cone.SetResolution(int(resolution)) + self.ctrl.view_update() + + def start_xr(self): + self.ctrl.start_xr() + + def stop_xr(self): + self.ctrl.stop_xr() + + def _build_ui(self): + with DivLayout(self.server): + client.Style("body { margin: 0; }") + + with html.Div(style=TOP_CENTER): + html.Button( + "StartXR", + click=self.start_xr, + ) + html.Button( + "StopXR", + click=self.stop_xr, + ) + html.Input( + type="range", + v_model=("resolution", 6), + min=3, + max=60, + step=1, + style=TOP_LEFT, + ) + + # use style=OFF_SCREEN when not using the WebXR Emulator to hide the canvas + with html.Div(style=FULL_SCREEN): + with vtklocal.LocalView( + self.render_window, + v_if=("enable_view", True), + # use auto_resize=False when using WebXR (except when using the WebXR Emulator) + auto_resize=False, + ) as view: + view.update_throttle.rate = 20 # max update rate + self.ctrl.view_update = view.update_throttle + self.ctrl.start_xr = view.start_web_xr + self.ctrl.stop_xr = view.stop_web_xr + cone_id = view.register_vtk_object(self.cone_actor) + view.listeners = ( + "listeners", + { + cone_id: { + "ModifiedEvent": { + "cone_actor": { + "position": (cone_id, "Position"), + }, + }, + }, + }, + ) + + +def main(): + app = WasmApp() + app.server.start() + + +if __name__ == "__main__": + main() diff --git a/src/trame_vtklocal/widgets/vtklocal.py b/src/trame_vtklocal/widgets/vtklocal.py index a56cff0..9599d77 100644 --- a/src/trame_vtklocal/widgets/vtklocal.py +++ b/src/trame_vtklocal/widgets/vtklocal.py @@ -97,6 +97,9 @@ class LocalView(HtmlElement): } emit_memory (bool): Emit memory information events. By default it is skipped. + auto_resize (bool): + Enabled by default. If disabled, the render window will not + automatically resize when the canvas is resized. updated (event): Emitted after each completed client side update. memory_vtk (event): @@ -149,6 +152,7 @@ def __init__(self, render_window, throttle_rate=10, **kwargs): ("progress_enabled", "progressEnabled"), ("progress_delay", "progressDelay"), ("emit_memory", "emitMemory"), + ("auto_resize", "autoResize"), ] self._event_names += [ "updated", @@ -308,6 +312,23 @@ def reset_camera(self, renderer_or_render_window=None, **kwargs): id_to_reset_camera = self.get_wasm_id(renderer_or_render_window) self.server.js_call(self.__ref, "resetCamera", id_to_reset_camera) + def start_web_xr(self, mode=1, required_features=1, optional_features=2): + """Start WebXR session + + :param mode: 0 (inline), 1 (VR) or 2 (AR) + """ + if not is_vtk_version_newer(9, 6, 20260326): + raise NotImplementedError("You need VTK>=9.7 to use WebXR (>=9.6.20260327)") + self.server.js_call( + self.__ref, "startWebXR", mode, required_features, optional_features + ) + + def stop_web_xr(self): + """Stop WebXR session""" + if not is_vtk_version_newer(9, 6, 20260326): + raise NotImplementedError("You need VTK>=9.7 to use WebXR (>=9.6.20260327)") + self.server.js_call(self.__ref, "stopWebXR") + @property def ref_name(self): """Return the assigned name as a vue.js ref""" diff --git a/vue-components/package-lock.json b/vue-components/package-lock.json index b4e1547..a09be7a 100644 --- a/vue-components/package-lock.json +++ b/vue-components/package-lock.json @@ -8,7 +8,7 @@ "name": "vue-trame_vtklocal", "version": "0.0.0", "dependencies": { - "@kitware/vtk-wasm": "^1.7.4", + "@kitware/vtk-wasm": "^1.7.5", "jszip": "3.10.1" }, "devDependencies": { @@ -1236,9 +1236,9 @@ } }, "node_modules/@kitware/vtk-wasm": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@kitware/vtk-wasm/-/vtk-wasm-1.7.4.tgz", - "integrity": "sha512-fACObCdFWpwf1v+ViM5sqBhYMfILLRYTFz3SWtbrrv5G09fgZxftg0YfrOv1R/U7+zCRaZySBC5UEFRnkBU2ng==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@kitware/vtk-wasm/-/vtk-wasm-1.7.5.tgz", + "integrity": "sha512-5YOOjl+ZIsUEkozWESakSgaRQ8zs/K29DcNGxbp6g1nxWNIEw1WyMMnWCapE8mA1X4ED7HSl4QNFTosJLyFxIg==", "license": "Apache-2.0", "dependencies": { "js-untar": "^2.0.0", diff --git a/vue-components/package.json b/vue-components/package.json index 1dd55ef..d02e7dd 100644 --- a/vue-components/package.json +++ b/vue-components/package.json @@ -14,7 +14,7 @@ "vue": "^2.7.0 || >=3.0.0" }, "dependencies": { - "@kitware/vtk-wasm": "^1.7.4", + "@kitware/vtk-wasm": "^1.7.5", "jszip": "3.10.1" }, "devDependencies": { diff --git a/vue-components/src/components/VtkLocal.js b/vue-components/src/components/VtkLocal.js index b7b9f2a..abf2be1 100644 --- a/vue-components/src/components/VtkLocal.js +++ b/vue-components/src/components/VtkLocal.js @@ -99,6 +99,10 @@ export default { // } // } }, + autoResize: { + type: Boolean, + default: true, + } }, setup(props, { emit }) { // Create global WASM handler if missing @@ -213,7 +217,7 @@ export default { function handleMessage([event]) { if (event.type === "state") { - wasmManager.pushState(event.content); + wasmManager.patchState(event.content); } if (event.type === "blob") { wasmManager.pushHash(event.hash, event.content); @@ -243,7 +247,7 @@ export default { const h = Math.floor(height * window.devicePixelRatio + 0.5); await wasmManager.setSize(props.renderWindow, w, h); } - let resizeObserver = new ResizeObserver(resize); + let resizeObserver = props.autoResize && new ResizeObserver(resize); // Memory ----------------------------------------------------------------- @@ -307,6 +311,18 @@ export default { wasmManager.sceneManager.printSceneManagerInformation(); } + // startWebXR ---------------------------------------------------------------- + + function startWebXR(mode, requiredFeatures, optionalFeatures) { + wasmManager.sceneManager.startWebXR(mode, requiredFeatures, optionalFeatures); + } + + // stopWebXR ---------------------------------------------------------------- + + function stopWebXR() { + wasmManager.sceneManager.stopWebXR(); + } + // Life Cycles ------------------------------------------------------------ onMounted(async () => { @@ -529,6 +545,8 @@ export default { statePercent, hashPercent, wasmLoading, + startWebXR, + stopWebXR, }; }, template: `