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); };