diff --git a/examples/reference/panes/VTK.ipynb b/examples/reference/panes/VTK.ipynb index bd1b95ac2c..04fc54869c 100644 --- a/examples/reference/panes/VTK.ipynb +++ b/examples/reference/panes/VTK.ipynb @@ -46,7 +46,9 @@ " \n", " The mouse must be over the pane to work\n", "
**Warning**: These keybindings may not work as expected in a notebook context, if they interact with already bound keys\n", - "* **``orientation_widget``** (bool): A boolean to activate/deactivate the orientation widget in the 3D pane. This widget is clickable and allows to rotate the scene in one of the orthographic projections.\n", + "* **``orientation_widget``** (bool): A boolean to activate/deactivate the orientation widget in the 3D pane.\n", + "* **``ìnteractive_orientation_widget``** (bool): If True the orientation widget is clickable and allows to rotate the scene in one of the orthographic projections.\n", + "
**Warning**: if set to True, synchronization capabilities of `VTKRenderWindowSynchronized` panes could not work.\n", "* **``object``** (object): Must be a ``vtkRenderWindow`` instance.\n", "\n", "#### Properties:\n", diff --git a/examples/reference/panes/VTKJS.ipynb b/examples/reference/panes/VTKJS.ipynb index 3e0967e29e..a0a7c704be 100644 --- a/examples/reference/panes/VTKJS.ipynb +++ b/examples/reference/panes/VTKJS.ipynb @@ -43,7 +43,8 @@ " \n", " The mouse must be over the pane to work.\n", "
**Warning**: These keybindings may not work as expected in a notebook context, if they interact with already bound keys.\n", - "* **``orientation_widget``** (bool): A boolean to activate/deactivate the orientation widget in the 3D pane. This widget is clickable and allows to rotate the scene in one of the orthographic projections.\n", + "* **``orientation_widget``** (bool): A boolean to activate/deactivate the orientation widget in the 3D pane.\n", + "* **``ìnteractive_orientation_widget``** (bool): If True the orientation widget is clickable and allows to rotate the scene in one of the orthographic projections.\n", "* **``object``** (str or object): Can be a string pointing to a local or remote file with a `.vtkjs` extension.\n", "___" ] diff --git a/panel/models/vtk.py b/panel/models/vtk.py index 77c502894d..81b4613da6 100644 --- a/panel/models/vtk.py +++ b/panel/models/vtk.py @@ -9,7 +9,7 @@ from bokeh.core.enums import enumeration from bokeh.models import HTMLBox, Model, ColorMapper -vtk_cdn = "https://unpkg.com/vtk.js@13.18.0/dist/vtk.js" +vtk_cdn = "https://unpkg.com/vtk.js@14.16.4/dist/vtk.js" class VTKAxes(Model): """ @@ -64,6 +64,8 @@ class AbstractVTKPlot(HTMLBox): orientation_widget = Bool(default=False) + interactive_orientation_widget = Bool(default=False) + width = Override(default=300) diff --git a/panel/models/vtk/util.ts b/panel/models/vtk/util.ts index a86c79f54f..f918f30484 100644 --- a/panel/models/vtk/util.ts +++ b/panel/models/vtk/util.ts @@ -1,8 +1,5 @@ import {linspace} from "@bokehjs/core/util/array" -import {Follower} from "./vtkfollower" -import {RenderWindowInteractor} from "./vtkrenderwindowinteractor" - export const ARRAY_TYPES = { uint8: Uint8Array, int8: Int8Array, @@ -30,7 +27,7 @@ if (vtk) { vtkns["CubeSource"] = vtk.Filters.Sources.vtkCubeSource vtkns["DataAccessHelper"] = vtk.IO.Core.DataAccessHelper vtkns["DataArray"] = vtk.Common.Core.vtkDataArray - vtkns["Follower"] = Follower + vtkns["Follower"] = vtk.Rendering.Core.vtkFollower vtkns["FullScreenRenderWindow"] = vtk.Rendering.Misc.vtkFullScreenRenderWindow vtkns["Glyph3DMapper"] = vtk.Rendering.Core.vtkGlyph3DMapper vtkns["HttpSceneLoader"] = vtk.IO.Core.vtkHttpSceneLoader @@ -60,7 +57,7 @@ if (vtk) { vtkns["Property"] = vtk.Rendering.Core.vtkProperty vtkns["Renderer"] = vtk.Rendering.Core.vtkRenderer vtkns["RenderWindow"] = vtk.Rendering.Core.vtkRenderWindow - vtkns["RenderWindowInteractor"] = RenderWindowInteractor + vtkns["RenderWindowInteractor"] = vtk.Rendering.Core.vtkRenderWindowInteractor vtkns["SphereMapper"] = vtk.Rendering.Core.vtkSphereMapper vtkns["SynchronizableRenderWindow"] = vtk.Rendering.Misc.vtkSynchronizableRenderWindow @@ -86,7 +83,7 @@ if (vtk) { ) vtkObjectManager.setTypeMapping( "vtkFollower", - Follower.newInstance, + vtkns.Follower.newInstance, vtkObjectManager.genericUpdater ) } diff --git a/panel/models/vtk/vtkfollower.ts b/panel/models/vtk/vtkfollower.ts deleted file mode 100644 index 0a69a19803..0000000000 --- a/panel/models/vtk/vtkfollower.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { vec3, mat4 } from 'gl-matrix' - - -export let Follower: any - -const vtk = (window as any).vtk - -if(vtk) { - const macro = vtk.macro - const vtkActor = vtk.Rendering.Core.vtkActor - - function vtkFollower(publicAPI: any, model: any) { - // Set our className - model.classHierarchy.push('vtkFollower') - - // Capture 'parentClass' api for internal use - const superClass = { ...publicAPI } - - publicAPI.getMTime = () => { - let mt = superClass.getMTime() - if (model.camera !== null) { - const time = model.camera.getMTime() - mt = time > mt ? time : mt - } - - return mt; - }; - - publicAPI.computeMatrix = () => { - // check whether or not need to rebuild the matrix - if (publicAPI.getMTime() > model.matrixMTime.getMTime()) { - mat4.identity(model.matrix) - if (model.userMatrix) { - mat4.multiply(model.matrix, model.matrix, model.userMatrix) - } - mat4.translate(model.matrix, model.matrix, model.origin) - mat4.translate(model.matrix, model.matrix, model.position) - mat4.multiply(model.matrix, model.matrix, model.rotation) - mat4.scale(model.matrix, model.matrix, model.scale) - - if (model.camera) { - // first compute our target viewUp - const vup = vec3.fromValues(model.viewUp[0], model.viewUp[1], model.viewUp[2]) - if (!model.useViewUp) { - const cvup = model.camera.getViewUp() - vec3.set(vup, cvup[0], cvup[1], cvup[2]) - } - - // compute a vpn - const vpn = vec3.create(); - if (model.camera.getParallelProjection()) { - const cvpn = model.camera.getViewPlaneNormal() - vec3.set(vpn, cvpn[0], cvpn[1], cvpn[2]) - } else { - vec3.set(vpn, model.position[0], model.position[1], model.position[2]) - const cpos = model.camera.getPosition() - const tmpv3 = vec3.fromValues(cpos[0], cpos[1], cpos[2]) - vec3.subtract(vpn, tmpv3, vpn) - vec3.normalize(vpn, vpn) - } - - // compute vright - const vright = vec3.create(); - vec3.cross(vright, vup, vpn) - vec3.normalize(vright, vright) - - // now recompute the vpn so that it is orthogonal to vup - vec3.cross(vpn, vright, vup) - vec3.normalize(vpn, vpn) - - model.followerMatrix[0] = vright[0] - model.followerMatrix[1] = vright[1] - model.followerMatrix[2] = vright[2] - - model.followerMatrix[4] = vup[0] - model.followerMatrix[5] = vup[1] - model.followerMatrix[6] = vup[2] - - model.followerMatrix[8] = vpn[0] - model.followerMatrix[9] = vpn[1] - model.followerMatrix[10] = vpn[2] - - mat4.multiply(model.matrix, model.followerMatrix, model.matrix); - } - - mat4.translate(model.matrix, model.matrix, [ - -model.origin[0], - -model.origin[1], - -model.origin[2], - ]); - mat4.transpose(model.matrix, model.matrix); - - // check for identity - model.isIdentity = false; - model.matrixMTime.modified(); - } - } - } - - // ---------------------------------------------------------------------------- - // Object factory - // ---------------------------------------------------------------------------- - - const DEFAULT_VALUES = { - viewUp: [0, 1, 0], - useViewUp: false, - camera: null, - } - - // ---------------------------------------------------------------------------- - - Follower = { - newInstance: macro.newInstance((publicAPI: any, model: any, initialValues = {}) => { - Object.assign(model, DEFAULT_VALUES, initialValues) - - // Inheritance - vtkActor.extend(publicAPI, model, initialValues) - - model.followerMatrix = mat4.create() - model.camera = vtk.Rendering.Core.vtkCamera.newInstance() - mat4.identity(model.followerMatrix) - - // Build VTK API - macro.setGet(publicAPI, model, ['useViewUp', 'camera']) - - macro.setGetArray(publicAPI, model, ['viewUp'], 3) - - // Object methods - vtkFollower(publicAPI, model); - }, 'vtkFollower') - } -} \ No newline at end of file diff --git a/panel/models/vtk/vtkjs.ts b/panel/models/vtk/vtkjs.ts index a5cd01ba6e..d2088b8e63 100644 --- a/panel/models/vtk/vtkjs.ts +++ b/panel/models/vtk/vtkjs.ts @@ -48,7 +48,7 @@ export class VTKJSPlotView extends AbstractVTKView { setTimeout(() => { if (this._axes == null && this.model.axes) this._set_axes() this._set_camera_state() - this.model.properties.camera.change.emit() + this._get_camera_state() }, 100), 100 ) diff --git a/panel/models/vtk/vtklayout.ts b/panel/models/vtk/vtklayout.ts index 2ccb9f5487..381c4c38e1 100644 --- a/panel/models/vtk/vtklayout.ts +++ b/panel/models/vtk/vtklayout.ts @@ -10,7 +10,7 @@ import {vtkns, VolumeType, majorAxis, applyStyle, CSSProperties} from "./util" import {VTKColorBar} from "./vtkcolorbar" import {VTKAxes} from "./vtkaxes" -const INFO_DIV_STYLE: CSSProperties = { +const INFO_DIV_STYLE: CSSProperties = { padding: "0px 2px 0px 2px", maxHeight: "150px", height: "auto", @@ -35,7 +35,7 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView { protected _vtk_container: HTMLDivElement protected _vtk_renwin: any protected _widgetManager: any - + initialize(): void { super.initialize() this._camera_callbacks = [] @@ -49,7 +49,7 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView { if (old_info_div) this.el.removeChild(old_info_div) if (this.model.color_mappers.length < 1) return - + const info_div = document.createElement("div") const expand_width = "350px" const collapsed_width = "30px" @@ -110,7 +110,9 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView { this.init_vtk_renwin() set_size(this._vtk_container, this.model) this.el.appendChild(this._vtk_container) - this._connect_interactions_to_model() + // update camera model state only at the end of the interaction + // with the scene (avoid bouncing events and large amount of events) + this._vtk_renwin.getInteractor().onEndAnimation(() => this._get_camera_state()) this._remove_default_key_binding() this._bind_key_events() this.plot() @@ -118,7 +120,7 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView { this.model.renderer_el = this._vtk_renwin } else { set_size(this._vtk_container, this.model) - // warning if _vtk_renwin contain controllers or other elements + // warning if _vtk_renwin contain controllers or other elements // we must attach them to the new el this.el.appendChild(this._vtk_container) } @@ -146,7 +148,7 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView { abstract init_vtk_renwin(): void abstract plot(): void //here goes the specific implementation pour all concrete model based on vtk-js - + get _vtk_camera_state(): any { const vtk_camera = this._vtk_renwin.getRenderer().getActiveCamera() let state: any @@ -159,13 +161,6 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView { delete state.flattenedDepIds delete state.managedInstanceId delete state.directionOfProjection - delete state.projectionMatrix - delete state.viewMatrix - delete state.physicalTranslation - delete state.physicalScale - delete state.physicalViewUp - delete state.physicalViewNorth - delete state.mtime } return state } @@ -228,19 +223,6 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView { }) } - _connect_interactions_to_model(): void { - // update camera model state only at the end of the interaction - // with the scene (avoid bouncing events and large amount of events) - - const update_model_camera = () => { - this._get_camera_state() - this.model.properties.camera.change.emit() - } - const interactor = this._vtk_renwin.getInteractor() - const event_list = ["LeftButtonRelease", "RightButtonRelease", "EndAnimation"] - event_list.forEach((event) => interactor['on' + event](update_model_camera)) - } - _create_orientation_widget(): void { const axes = vtkns.AxesActor.newInstance() @@ -256,10 +238,16 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView { this._orientationWidget.setViewportSize(0.15) this._orientationWidget.setMinPixelSize(75) this._orientationWidget.setMaxPixelSize(300) + if (this.model.interactive_orientation_widget) + this._make_orientation_widget_interactive() + this._orientation_widget_visibility(this.model.orientation_widget) + } + _make_orientation_widget_interactive(): void { this._widgetManager = vtkns.WidgetManager.newInstance() this._widgetManager.setRenderer(this._orientationWidget.getRenderer()) + const axes = this._orientationWidget.getActor() const widget = vtkns.InteractiveOrientationWidget.newInstance() widget.placeWidget(axes.getBounds()) widget.setBounds(axes.getBounds()) @@ -292,10 +280,11 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView { this._vtk_renwin.getRenderer().resetCameraClippingRange() this._vtk_render() + this._get_camera_state() }) - this._orientation_widget_visibility(this.model.orientation_widget) } + _delete_axes(): void { if (this._axes) { Object.keys(this._axes).forEach((key) => @@ -323,8 +312,10 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView { _orientation_widget_visibility(visibility: boolean): void { this._orientationWidget.setEnabled(visibility) - if (visibility) this._widgetManager.enablePicking() - else this._widgetManager.disablePicking() + if (this._widgetManager != null){ + if (visibility) this._widgetManager.enablePicking() + else this._widgetManager.disablePicking() + } this._vtk_render() } @@ -394,6 +385,7 @@ export namespace AbstractVTKPlot { enable_keybindings: p.Property orientation_widget: p.Property color_mappers: p.Property + interactive_orientation_widget: p.Property } } @@ -415,10 +407,11 @@ export abstract class AbstractVTKPlot extends HTMLBox { static init_AbstractVTKPlot(): void { this.define({ - axes: [ p.Instance ], - camera: [ p.Instance ], - color_mappers: [ p.Array, [] ], - orientation_widget: [ p.Boolean, false ], + axes: [ p.Instance ], + camera: [ p.Instance ], + color_mappers: [ p.Array, [] ], + orientation_widget: [ p.Boolean, false ], + interactive_orientation_widget: [ p.Boolean, false ], }) this.override({ diff --git a/panel/models/vtk/vtkrenderwindowinteractor.ts b/panel/models/vtk/vtkrenderwindowinteractor.ts deleted file mode 100644 index 18a998e35a..0000000000 --- a/panel/models/vtk/vtkrenderwindowinteractor.ts +++ /dev/null @@ -1,84 +0,0 @@ -export let RenderWindowInteractor: any - -const vtk = (window as any).vtk - -if (vtk) { - const macro = vtk.macro - const vtkRenderWindowInteractor = vtk.Rendering.Core.vtkRenderWindowInteractor - // ---------------------------------------------------------------------------- - // panelRenderWindowInteractor fix findPokeRenderer - // ---------------------------------------------------------------------------- - - function panelRenderWindowInteractor(publicAPI: any, model: any) { - // Set our className - model.classHierarchy.push("panelRenderWindowInteractor") - - publicAPI.findPokedRenderer = (x = 0, y = 0) => { - if (!model.view) { - return null - } - const rc = model.view.getRenderable().getRenderersByReference() - rc.sort((a: any, b: any) => a.getLayer() - b.getLayer()) - - let interactiveren = null - let viewportren = null - let currentRenderer = null - - let count = rc.length - while (count--) { - const aren = rc[count] - if (model.view.isInViewport(x, y, aren) && aren.getInteractive()) { - currentRenderer = aren - break - } - - if (interactiveren === null && aren.getInteractive()) { - // Save this renderer in case we can't find one in the viewport that - // is interactive. - interactiveren = aren - } - if (viewportren === null && model.view.isInViewport(x, y, aren)) { - // Save this renderer in case we can't find one in the viewport that - // is interactive. - viewportren = aren - } - } - - // We must have a value. If we found an interactive renderer before, that's - // better than a non-interactive renderer. - if (currentRenderer === null) { - currentRenderer = interactiveren - } - - // We must have a value. If we found a renderer that is in the viewport, - // that is better than any old viewport (but not as good as an interactive - // one). - if (currentRenderer === null) { - currentRenderer = viewportren - } - - // We must have a value - take anything. - if (currentRenderer == null) { - currentRenderer = rc[0] - } - - return currentRenderer - } - } - - // ---------------------------------------------------------------------------- - // Object factory - // ---------------------------------------------------------------------------- - - RenderWindowInteractor = { - newInstance: macro.newInstance( - (publicAPI: any, model: any, initialValues = {}) => { - vtkRenderWindowInteractor.extend(publicAPI, model, initialValues) - - // Object specific methods - panelRenderWindowInteractor(publicAPI, model) - }, - "panelRenderWindowInteractor" - ), - } -} diff --git a/panel/models/vtk/vtksynchronized.ts b/panel/models/vtk/vtksynchronized.ts index 4e9e77820a..cf32e84f35 100644 --- a/panel/models/vtk/vtksynchronized.ts +++ b/panel/models/vtk/vtksynchronized.ts @@ -49,7 +49,7 @@ export class VTKSynchronizedPlotView extends AbstractVTKView { CONTEXT_NAME ) } - + connect_signals(): void { super.connect_signals() this.connect(this.model.properties.arrays.change, () => @@ -64,7 +64,6 @@ export class VTKSynchronizedPlotView extends AbstractVTKView { Promise.all(this._promises).then(() => { this._sync_plot(state, () => { this._on_scene_ready() - this._connect_interactions_to_model() }) }) } @@ -87,7 +86,10 @@ export class VTKSynchronizedPlotView extends AbstractVTKView { this._decode_arrays() const state = clone(this.model.scene) Promise.all(this._promises).then(() => { - this._sync_plot(state, () => this._on_scene_ready()) + this._sync_plot(state, () => this._on_scene_ready()).then(() => { + this._set_camera_state() + this._get_camera_state() + }) }) } @@ -131,10 +133,9 @@ export class VTKSynchronizedPlotView extends AbstractVTKView { if (!this._axes) this._set_axes() this._vtk_renwin.resize() this._vtk_render() - this._set_camera_state() } - _sync_plot(state: any, onSceneReady: CallableFunction): void { + _sync_plot(state: any, onSceneReady: CallableFunction): any { // Need to ensure all promises are resolved before calling this function this._renderable = false this._promises = [] @@ -147,8 +148,7 @@ export class VTKSynchronizedPlotView extends AbstractVTKView { ) if (renderer && !this._vtk_renwin.getRenderer()) this._vtk_renwin.getRenderWindow().addRenderer(renderer) - - this._vtk_renwin + return this._vtk_renwin .getRenderWindow() .synchronize(state).then(onSceneReady) } diff --git a/panel/models/vtk/vtkvolume.ts b/panel/models/vtk/vtkvolume.ts index 1118873aa8..f7846e60b2 100644 --- a/panel/models/vtk/vtkvolume.ts +++ b/panel/models/vtk/vtkvolume.ts @@ -111,7 +111,11 @@ export class VTKVolumePlotView extends AbstractVTKView { super.render() this._create_orientation_widget() this._set_axes() - if (!this.model.camera) this._vtk_renwin.getRenderer().resetCamera() + if (!this.model.camera) + this._vtk_renwin.getRenderer().resetCamera() + else + this._set_camera_state() + this._get_camera_state() } invalidate_render(): void { diff --git a/panel/pane/vtk/vtk.py b/panel/pane/vtk/vtk.py index 43f8d617f3..b66cf8252e 100644 --- a/panel/pane/vtk/vtk.py +++ b/panel/pane/vtk/vtk.py @@ -66,6 +66,9 @@ class AbstractVTK(PaneBase): orientation_widget = param.Boolean(default=False, doc=""" Activate/Deactivate the orientation widget display.""") + interactive_orientation_widget = param.Boolean(default=True, constant=True, doc=""" + """) + def _process_param_change(self, msg): msg = super(AbstractVTK, self)._process_param_change(msg) if 'axes' in msg and msg['axes'] is not None: @@ -161,6 +164,8 @@ def __new__(self, obj, **params): if VTKRenderWindow.applies(obj, **params): return VTKRenderWindow(obj, **params) else: + if params.get('interactive_orientation_widget', False): + param.main.param.warning("""Setting interactive_orientation_widget=True will break synchronization capabilities of the pane""") return VTKRenderWindowSynchronized(obj, **params) elif VTKJS.applies(obj): return VTKJS(obj, **params) @@ -416,6 +421,9 @@ class VTKRenderWindowSynchronized(BaseVTKRenderWindow, SyncHelpers): with a custom bokeh model on javascript side """ + interactive_orientation_widget = param.Boolean(default=False, constant=True, doc=""" + """) + _one_time_reset = param.Boolean(default=False) _rename = dict(_one_time_reset='one_time_reset', @@ -502,12 +510,14 @@ def unlink_camera(self): old_camera = self.vtk_camera new_camera = vtk.vtkCamera() self.vtk_camera = new_camera + exclude_properties = ['mtime'] if self.camera is not None: for k, v in self.camera.items(): - if type(v) is list: - getattr(new_camera, 'Set' + k[0].capitalize() + k[1:])(*v) - else: - getattr(new_camera, 'Set' + k[0].capitalize() + k[1:])(v) + if k not in exclude_properties: + if type(v) is list: + getattr(new_camera, 'Set' + k[0].capitalize() + k[1:])(*v) + else: + getattr(new_camera, 'Set' + k[0].capitalize() + k[1:])(v) else: new_camera.DeepCopy(old_camera)