Skip to content

Commit

Permalink
Merge pull request #280 from maartenbreddels/feat_pythreejs_controls
Browse files Browse the repository at this point in the history
Feature: pythreejs controls
  • Loading branch information
maartenbreddels authored Mar 30, 2020
2 parents b627a5e + 5b8c728 commit 982c149
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 18 deletions.
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Contents
examples
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

0 comments on commit 982c149

Please sign in to comment.