vizicities/src/engine/Picking.js

243 wiersze
7.4 KiB
JavaScript
Executable File

import THREE from 'three';
import {point as Point} from '../geo/Point';
import PickingScene from './PickingScene';
import throttle from 'lodash.throttle';
// TODO: Look into a way of setting this up without passing in a renderer and
// camera from the engine
// TODO: Add a basic indicator on or around the mouse pointer when it is over
// something pickable / clickable
//
// A simple transparent disc or ring at the mouse point should work to start, or
// even just changing the cursor to the CSS 'pointer' style
//
// Probably want this on mousemove with a throttled update as not to spam the
// picking method
//
// Relies upon the picking method not redrawing the scene every call due to
// the way TileLayer invalidates the picking scene
var nextId = 1;
class Picking {
constructor(world, renderer, camera) {
this._world = world;
this._renderer = renderer;
this._camera = camera;
this._raycaster = new THREE.Raycaster();
// TODO: Match this with the line width used in the picking layers
this._raycaster.linePrecision = 3;
this._pickingScene = PickingScene;
this._pickingTexture = new THREE.WebGLRenderTarget();
this._pickingTexture.texture.minFilter = THREE.LinearFilter;
this._pickingTexture.texture.generateMipmaps = false;
this._nextId = 1;
this._resizeTexture();
this._initEvents();
}
_initEvents() {
this._resizeHandler = this._resizeTexture.bind(this);
window.addEventListener('resize', this._resizeHandler, false);
this._throttledMouseMoveHandler = throttle(this._onMouseMove.bind(this), 50);
this._mouseUpHandler = this._onMouseUp.bind(this);
this._world._container.addEventListener('mouseup', this._mouseUpHandler, false);
this._world._container.addEventListener('mousemove', this._throttledMouseMoveHandler, false);
this._world.on('move', this._onWorldMove, this);
}
_onMouseUp(event) {
// Only react to main button click
if (event.button !== 0) {
return;
}
var point = Point(event.clientX - this._world._container.offsetLeft, event.clientY - this._world._container.offsetTop);
var normalisedPoint = Point(0, 0);
normalisedPoint.x = (point.x / this._width) * 2 - 1;
normalisedPoint.y = -(point.y / this._height) * 2 + 1;
this._pick(point, normalisedPoint);
}
_onMouseMove(event) {
var point = Point(event.clientX - this._world._container.offsetLeft, event.clientY - this._world._container.offsetTop);
var normalisedPoint = Point(0, 0);
normalisedPoint.x = (point.x / this._width) * 2 - 1;
normalisedPoint.y = -(point.y / this._height) * 2 + 1;
this._pick(point, normalisedPoint, true);
}
_onWorldMove() {
this._needUpdate = true;
}
// TODO: Ensure this doesn't get out of sync issue with the renderer resize
_resizeTexture() {
var size = this._renderer.getSize();
this._width = size.width;
this._height = size.height;
this._pickingTexture.setSize(this._width, this._height);
this._pixelBuffer = new Uint8Array(4 * this._width * this._height);
this._needUpdate = true;
}
// TODO: Make this only re-draw the scene if both an update is needed and the
// camera has moved since the last update
//
// Otherwise it re-draws the scene on every click due to the way LOD updates
// work in TileLayer – spamming this.add() and this.remove()
//
// TODO: Pause updates during map move / orbit / zoom as this is unlikely to
// be a point in time where the user cares for picking functionality
_update() {
if (this._needUpdate) {
var texture = this._pickingTexture;
this._renderer.render(this._pickingScene, this._camera, this._pickingTexture);
// Read the rendering texture
this._renderer.readRenderTargetPixels(texture, 0, 0, texture.width, texture.height, this._pixelBuffer);
this._needUpdate = false;
}
}
_pick(point, normalisedPoint, hover) {
this._update();
var index = point.x + (this._pickingTexture.height - point.y) * this._pickingTexture.width;
// Interpret the pixel as an ID
var id = (this._pixelBuffer[index * 4 + 2] * 255 * 255) + (this._pixelBuffer[index * 4 + 1] * 255) + (this._pixelBuffer[index * 4 + 0]);
// Skip if ID is 16646655 (white) as the background returns this
if (id === 16646655) {
if (hover) {
this._world.emit('pick-hover-reset');
} else {
this._world.emit('pick-click-reset');
}
return;
}
this._raycaster.setFromCamera(normalisedPoint, this._camera);
// Perform ray intersection on picking scene
//
// TODO: Only perform intersection test on the relevant picking mesh
var intersects = this._raycaster.intersectObjects(this._pickingScene.children, true);
var _point2d = point.clone();
var _point3d;
if (intersects.length > 0) {
_point3d = intersects[0].point.clone();
}
// Pass along as much data as possible for now until we know more about how
// people use the picking API and what the returned data should be
//
// TODO: Look into the leak potential for passing so much by reference here
// this._world.emit('pick', id, _point2d, _point3d, intersects);
// this._world.emit('pick-' + id, _point2d, _point3d, intersects);
if (hover) {
this._world.emit('pick-hover', id, _point2d, _point3d, intersects);
this._world.emit('pick-hover-' + id, _point2d, _point3d, intersects);
} else {
this._world.emit('pick-click', id, _point2d, _point3d, intersects);
this._world.emit('pick-click-' + id, _point2d, _point3d, intersects);
}
}
// Add mesh to picking scene
//
// Picking ID should already be added as an attribute
add(mesh) {
this._pickingScene.add(mesh);
this._needUpdate = true;
}
// Remove mesh from picking scene
remove(mesh) {
this._pickingScene.remove(mesh);
this._needUpdate = true;
}
// Returns next ID to use for picking
getNextId() {
return nextId++;
}
destroy() {
// TODO: Find a way to properly remove these listeners as they stay
// active at the moment
window.removeEventListener('resize', this._resizeHandler, false);
this._world._container.removeEventListener('mouseup', this._mouseUpHandler, false);
this._world._container.removeEventListener('mousemove', this._throttledMouseMoveHandler, false);
this._world.off('move', this._onWorldMove);
if (this._pickingScene.children) {
// Remove everything else in the layer
var child;
for (var i = this._pickingScene.children.length - 1; i >= 0; i--) {
child = this._pickingScene.children[i];
if (!child) {
continue;
}
this._pickingScene.remove(child);
// Probably not a good idea to dispose of geometry due to it being
// shared with the non-picking scene
// if (child.geometry) {
// // Dispose of mesh and materials
// child.geometry.dispose();
// child.geometry = null;
// }
if (child.material) {
if (child.material.map) {
child.material.map.dispose();
child.material.map = null;
}
child.material.dispose();
child.material = null;
}
}
}
this._pickingScene = null;
this._pickingTexture = null;
this._pixelBuffer = null;
this._world = null;
this._renderer = null;
this._camera = null;
}
}
// Initialise without requiring new keyword
export default function(world, renderer, camera) {
return new Picking(world, renderer, camera);
};