From ba4b386025c3a0bf91fcac0404e90bd3c20fd46d Mon Sep 17 00:00:00 2001 From: "Maarten A. Breddels" Date: Tue, 29 Oct 2019 12:42:58 +0100 Subject: [PATCH 1/4] feat: allow controls to be a pythreejs Controls object --- ipyvolume/widgets.py | 4 ++++ js/src/figure.ts | 55 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/ipyvolume/widgets.py b/ipyvolume/widgets.py index 39f2e267..ece3dd69 100644 --- a/ipyvolume/widgets.py +++ b/ipyvolume/widgets.py @@ -266,6 +266,10 @@ def _default_camera(self): z = 2 * np.tan(45.0 / 2.0 * np.pi / 180) / np.tan(self.camera_fov / 2.0 * np.pi / 180) return pythreejs.PerspectiveCamera(fov=self.camera_fov, position=(0, 0, z), width=400, height=500) + controls = traitlets.Instance( + pythreejs.Controls, allow_none=True, help='A :any:`pythreejs.Controls` instance to control the camera' + ).tag(sync=True, **widgets.widget_serialization) + scene = traitlets.Instance(pythreejs.Scene, allow_none=True).tag(sync=True, **widgets.widget_serialization) @traitlets.default('scene') diff --git a/js/src/figure.ts b/js/src/figure.ts index fda432aa..c9cad3b6 100644 --- a/js/src/figure.ts +++ b/js/src/figure.ts @@ -106,6 +106,7 @@ class FigureModel extends widgets.DOMWidgetModel { volumes: { deserialize: widgets.unpack_models }, camera: { deserialize: widgets.unpack_models }, scene: { deserialize: widgets.unpack_models }, + controls: { deserialize: widgets.unpack_models }, }; defaults() { return {...super.defaults(), @@ -237,6 +238,7 @@ class FigureView extends widgets.DOMWidgetView { selector: any; last_tick_selection: d3.Selection; model: FigureModel; + control_external: any = null; // helper methods for testing/debugging debug_readPixel(x, y) { const buffer = new Uint8Array(4); @@ -750,6 +752,9 @@ class FigureView extends widgets.DOMWidgetView { this.mouse_trail = []; // list of x, y positions this.select_overlay = null; // lasso or sth else? + // setup controls, 2 builtin custom controls, or an external + // pythreejs control + this.control_trackball = new THREE.TrackballControls(this.camera, this.renderer.domElement); this.control_orbit = new THREE.OrbitControls(this.camera, this.renderer.domElement); this.control_trackball.dynamicDampingFactor = 1.; @@ -762,6 +767,42 @@ class FigureView extends widgets.DOMWidgetView { this.control_trackball.rotateSpeed = 0.5; this.control_trackball.zoomSpeed = 3.; + const update_angles_bound = this.update_angles.bind(this); + const update_bound = this.update.bind(this); + + this.control_trackball.addEventListener("end", update_angles_bound); + this.control_orbit.addEventListener("end", update_angles_bound); + this.control_trackball.addEventListener("change", update_bound); + this.control_orbit.addEventListener("change", update_bound); + + const sync_controls_external = () => { + const controls = this.model.get("controls"); + const controls_previous = (this.model.previousAttributes as any).controls; + // first remove previous event handlers + if (controls_previous) { + const control_external = controls_previous.obj; + control_external.removeEventListener("end", update_angles_bound); + control_external.removeEventListener("change", update_bound); + control_external.dispose(); + } + // and add new event handlers + if (controls) { + // get the threejs object + this.control_external = controls.obj; + this.control_external.addEventListener("end", update_angles_bound); + this.control_external.addEventListener("change", update_bound); + this.control_external.connectEvents(this.el); // custom pythreejs method + } else { + this.control_external = null; + } + this.update_mouse_mode(); + }; + + sync_controls_external(); + this.model.on("change:controls", () => { + sync_controls_external(); + }); + window.addEventListener("deviceorientation", this.on_orientationchange.bind(this), false); const render_size = this.getRenderSize(); @@ -929,11 +970,6 @@ class FigureView extends widgets.DOMWidgetView { this.model.on("change:tf", this.tf_set, this); this.listenTo(this.model, "msg:custom", this.custom_msg.bind(this)); - this.control_trackball.addEventListener("end", this.update_angles.bind(this)); - this.control_orbit.addEventListener("end", this.update_angles.bind(this)); - this.control_trackball.addEventListener("change", this.update.bind(this)); - this.control_orbit.addEventListener("change", this.update.bind(this)); - this.renderer.domElement.addEventListener("resize", this.on_canvas_resize.bind(this), false); this.update(); @@ -987,8 +1023,13 @@ class FigureView extends widgets.DOMWidgetView { update_mouse_mode() { const normal_mode = this.model.get("mouse_mode") === "normal"; - this.control_trackball.enabled = this.model.get("camera_control") === "trackball" && normal_mode; - this.control_orbit.enabled = this.model.get("camera_control") === "orbit" && normal_mode; + if (this.model.get("controls")) { + this.control_trackball.enabled = false; + this.control_orbit.enabled = false; + } else { + this.control_trackball.enabled = this.model.get("camera_control") === "trackball" && normal_mode; + this.control_orbit.enabled = this.model.get("camera_control") === "orbit" && normal_mode; + } } mousewheel(e) { From e129a6b8caeb4f08f0059af5b5d754cdf6bb43aa Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 29 Oct 2019 12:45:16 +0100 Subject: [PATCH 2/4] feat: enable panning for controls --- js/src/figure.ts | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/js/src/figure.ts b/js/src/figure.ts index c9cad3b6..39868b8e 100644 --- a/js/src/figure.ts +++ b/js/src/figure.ts @@ -515,15 +515,17 @@ class FigureView extends widgets.DOMWidgetView { this.model.get("camera").on("change", () => { // the threejs' lookAt ignore the quaternion, and uses the up vector // we manually set it ourselve - const up = new THREE.Vector3(0, 1, 0); - up.applyQuaternion(this.camera.quaternion); - this.camera.up = up; - this.camera.lookAt(0, 0, 0); - // TODO: shouldn't we do the same with the orbit control? - this.control_trackball.position0 = this.camera.position.clone(); - this.control_trackball.up0 = this.camera.up.clone(); - // TODO: if we implement figure.look_at, we should update control's target as well - this.update(); + if (!this.control_external) { + const up = new THREE.Vector3(0, 1, 0); + up.applyQuaternion(this.camera.quaternion); + this.camera.up = up; + this.camera.lookAt(0, 0, 0); + // TODO: shouldn't we do the same with the orbit control? + this.control_trackball.position0 = this.camera.position.clone(); + this.control_trackball.up0 = this.camera.up.clone(); + // TODO: if we implement figure.look_at, we should update control's target as well + this.update(); + } }); } else { this.camera = new THREE.PerspectiveCamera(46, 1, NEAR, FAR); @@ -1651,10 +1653,18 @@ class FigureView extends widgets.DOMWidgetView { _real_update() { this.control_trackball.handleResize(); + if (this.control_external) { + this.control_external.update(); + // it's very likely the controller will update the camera, so we sync it to the kernel + this.camera.ipymodel.syncToModel(true); + } + this._update_requested = false; // since the threejs animation system can update the camera, - // make sure we keep looking at the center - this.camera.lookAt(0, 0, 0); + // make sure we keep looking at the center (only for ipyvolume's own control) + if (!this.control_external) { + this.camera.lookAt(0, 0, 0); + } this.renderer.setClearColor(this.get_style_color("background-color")); this.x_axis.visible = this.get_style("axes.x.visible axes.visible"); From 5362e3858e78a48026b497b9c15f54ccdd887345 Mon Sep 17 00:00:00 2001 From: "Maarten A. Breddels" Date: Tue, 29 Oct 2019 14:55:44 +0100 Subject: [PATCH 3/4] docs: pythreejs integration ddescription --- docs/source/index.rst | 1 + docs/source/pythreejs.ipynb | 155 ++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 docs/source/pythreejs.ipynb diff --git a/docs/source/index.rst b/docs/source/index.rst index f6b88328..2c4164ef 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -136,6 +136,7 @@ Contents animation api vr + pythreejs Changelog diff --git a/docs/source/pythreejs.ipynb b/docs/source/pythreejs.ipynb new file mode 100644 index 00000000..fc8e9993 --- /dev/null +++ b/docs/source/pythreejs.ipynb @@ -0,0 +1,155 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Integration with pythreejs\n", + "ipyvolume uses parts of pythreejs, giving a lot of flexibility to tweak the visualizations or behaviour.\n", + "## Materials\n", + "The Scatter object has a `material` and `line_material` object, which both are a ShaderMaterial pythreejs object: `https://pythreejs.readthedocs.io/en/stable/api/materials/ShaderMaterial_autogen.html`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "import numpy as np\n", + "import ipyvolume as ipv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# a scatter plot\n", + "x, y, z = np.random.normal(size=(3, 100))\n", + "fig = ipv.figure()\n", + "scatter = ipv.scatter(x, y, z, marker='box')\n", + "scatter.connected = True # draw connecting lines\n", + "ipv.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using `scatter.material` we can tweak the material setting:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scatter.material.visible = False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or even connect a toggle button to a `line_material` property." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "toggle_lines = widgets.ToggleButton(description=\"Show lines\")\n", + "widgets.jslink((scatter.line_material, 'visible'), (toggle_lines, 'value'))\n", + "toggle_lines" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Controls\n", + "ipyvolume has builtin controls. For more flexibility, a Controls class from https://pythreejs.readthedocs.io/en/stable/api/controls/index.html can be contructed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pythreejs\n", + "import ipyvolume as ipv\n", + "import numpy as np\n", + "fig = ipv.figure()\n", + "scatter = ipv.scatter(x, y, z, marker='box')\n", + "ipv.show()\n", + "\n", + "control = pythreejs.OrbitControls(controlling=fig.camera)\n", + "# assigning to fig.controls will overwrite the builtin controls\n", + "fig.controls = control\n", + "control.autoRotate = True\n", + "# the controls does not update itself, but if we toggle this setting, ipyvolume will update the controls\n", + "fig.render_continuous = True\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "control.autoRotate = True\n", + "toggle_rotate = widgets.ToggleButton(description=\"Rotate\")\n", + "widgets.jslink((control, 'autoRotate'), (toggle_rotate, 'value'))\n", + "toggle_rotate" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Camera\n", + "The camera property of ipyvolume is by default a PerspectiveCamera, but other cameras should also work: https://pythreejs.readthedocs.io/en/stable/api/cameras/index.html\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "text = widgets.Text()\n", + "widgets.jslink((fig.camera, 'position'), (text, 'value'))\n", + "text" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 5b8c728149a91c1224c5490f06aa153c99935978 Mon Sep 17 00:00:00 2001 From: "Maarten A. Breddels" Date: Tue, 29 Oct 2019 14:56:07 +0100 Subject: [PATCH 4/4] docs: fix jupyter_sphinx version --- requirements_rtd.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_rtd.txt b/requirements_rtd.txt index bff50a45..b4881da4 100644 --- a/requirements_rtd.txt +++ b/requirements_rtd.txt @@ -1,4 +1,4 @@ -jupyter_sphinx>=0.1.4 +jupyter_sphinx==0.1.4 #git+https://github.com/maartenbreddels/jupyter-sphinx@ipywidgets7require_d ipyvolume==0.5.1 ipywidgets>=7.4.0