Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: pythreejs controls #280

Merged
merged 4 commits into from
Mar 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ Contents
animation
api
vr
pythreejs


Changelog
Expand Down
155 changes: 155 additions & 0 deletions docs/source/pythreejs.ipynb
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions ipyvolume/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
87 changes: 69 additions & 18 deletions js/src/figure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -237,6 +238,7 @@ class FigureView extends widgets.DOMWidgetView {
selector: any;
last_tick_selection: d3.Selection<d3.BaseType, unknown, d3.BaseType, unknown>;
model: FigureModel;
control_external: any = null;
// helper methods for testing/debugging
debug_readPixel(x, y) {
const buffer = new Uint8Array(4);
Expand Down Expand Up @@ -513,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);
Expand Down Expand Up @@ -750,6 +754,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.;
Expand All @@ -762,6 +769,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();
Expand Down Expand Up @@ -929,11 +972,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();

Expand Down Expand Up @@ -987,8 +1025,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) {
Expand Down Expand Up @@ -1610,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");
Expand Down
2 changes: 1 addition & 1 deletion requirements_rtd.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down