@ccp-nc/crystvis-js
Version:
A Three.js based crystallographic visualisation tool
361 lines (248 loc) • 10.6 kB
JavaScript
'use strict';
/**
* @fileoverview Selection box handling for the renderer. Taken from the THREE
* example files in
*
* 'three/examples/jsm/interactive/SelectionBox.js'
* 'three/examples/jsm/interactive/SelectionHelper.js'
*
* but slightly customised for the specific needs of this app.
*
* @module
*/
import {
Frustum,
Vector3,
Vector2
} from 'three';
import $ from 'jquery';
import {
AtomMesh
} from './primitives/index.js';
/**
* This is a class to check whether objects are in a selection area in 3D space
*/
var SelectionBox = (function() {
var frustum = new Frustum();
var center = new Vector3();
var tmpPoint = new Vector3();
var vecNear = new Vector3();
var vecTopLeft = new Vector3();
var vecTopRight = new Vector3();
var vecDownRight = new Vector3();
var vecDownLeft = new Vector3();
var vecFarTopLeft = new Vector3();
var vecFarTopRight = new Vector3();
var vecFarDownRight = new Vector3();
var vecFarDownLeft = new Vector3();
var vectemp1 = new Vector3();
var vectemp2 = new Vector3();
var vectemp3 = new Vector3();
function SelectionBox(camera, scene, deep) {
this.camera = camera;
this.scene = scene;
this.startPoint = new Vector3();
this.endPoint = new Vector3();
this.collection = [];
this.deep = deep || Number.MAX_VALUE;
}
SelectionBox.prototype.select = function(startPoint, endPoint, target = null) {
this.startPoint = startPoint || this.startPoint;
this.endPoint = endPoint || this.endPoint;
this.collection = [];
target = target || this.scene;
this.updateFrustum(this.startPoint, this.endPoint);
this.searchChildInFrustum(frustum, target);
return this.collection;
};
SelectionBox.prototype.updateFrustum = function(startPoint, endPoint) {
startPoint = startPoint || this.startPoint;
endPoint = endPoint || this.endPoint;
// Avoid invalid frustum
if (startPoint.x === endPoint.x) {
endPoint.x += Number.EPSILON;
}
if (startPoint.y === endPoint.y) {
endPoint.y += Number.EPSILON;
}
this.camera.updateProjectionMatrix();
this.camera.updateMatrixWorld();
if (this.camera.isPerspectiveCamera) {
tmpPoint.copy(startPoint);
tmpPoint.x = Math.min(startPoint.x, endPoint.x);
tmpPoint.y = Math.max(startPoint.y, endPoint.y);
endPoint.x = Math.max(startPoint.x, endPoint.x);
endPoint.y = Math.min(startPoint.y, endPoint.y);
vecNear.setFromMatrixPosition(this.camera.matrixWorld);
vecTopLeft.copy(tmpPoint);
vecTopRight.set(endPoint.x, tmpPoint.y, 0);
vecDownRight.copy(endPoint);
vecDownLeft.set(tmpPoint.x, endPoint.y, 0);
vecTopLeft.unproject(this.camera);
vecTopRight.unproject(this.camera);
vecDownRight.unproject(this.camera);
vecDownLeft.unproject(this.camera);
vectemp1.copy(vecTopLeft).sub(vecNear);
vectemp2.copy(vecTopRight).sub(vecNear);
vectemp3.copy(vecDownRight).sub(vecNear);
vectemp1.normalize();
vectemp2.normalize();
vectemp3.normalize();
vectemp1.multiplyScalar(this.deep);
vectemp2.multiplyScalar(this.deep);
vectemp3.multiplyScalar(this.deep);
vectemp1.add(vecNear);
vectemp2.add(vecNear);
vectemp3.add(vecNear);
let planes = frustum.planes;
planes[0].setFromCoplanarPoints(vecNear, vecTopLeft, vecTopRight);
planes[1].setFromCoplanarPoints(vecNear, vecTopRight, vecDownRight);
planes[2].setFromCoplanarPoints(vecDownRight, vecDownLeft, vecNear);
planes[3].setFromCoplanarPoints(vecDownLeft, vecTopLeft, vecNear);
planes[4].setFromCoplanarPoints(vecTopRight, vecDownRight, vecDownLeft);
planes[5].setFromCoplanarPoints(vectemp3, vectemp2, vectemp1);
planes[5].normal.multiplyScalar(-1);
} else if (this.camera.isOrthographicCamera) {
var left = Math.min(startPoint.x, endPoint.x);
var top = Math.max(startPoint.y, endPoint.y);
var right = Math.max(startPoint.x, endPoint.x);
var down = Math.min(startPoint.y, endPoint.y);
vecTopLeft.set(left, top, -1);
vecTopRight.set(right, top, -1);
vecDownRight.set(right, down, -1);
vecDownLeft.set(left, down, -1);
vecFarTopLeft.set(left, top, 1);
vecFarTopRight.set(right, top, 1);
vecFarDownRight.set(right, down, 1);
vecFarDownLeft.set(left, down, 1);
vecTopLeft.unproject(this.camera);
vecTopRight.unproject(this.camera);
vecDownRight.unproject(this.camera);
vecDownLeft.unproject(this.camera);
vecFarTopLeft.unproject(this.camera);
vecFarTopRight.unproject(this.camera);
vecFarDownRight.unproject(this.camera);
vecFarDownLeft.unproject(this.camera);
let planes = frustum.planes;
planes[0].setFromCoplanarPoints(vecTopLeft, vecFarTopLeft, vecFarTopRight);
planes[1].setFromCoplanarPoints(vecTopRight, vecFarTopRight, vecFarDownRight);
planes[2].setFromCoplanarPoints(vecFarDownRight, vecFarDownLeft, vecDownLeft);
planes[3].setFromCoplanarPoints(vecFarDownLeft, vecFarTopLeft, vecTopLeft);
planes[4].setFromCoplanarPoints(vecTopRight, vecDownRight, vecDownLeft);
planes[5].setFromCoplanarPoints(vecFarDownRight, vecFarTopRight, vecFarTopLeft);
planes[5].normal.multiplyScalar(-1);
} else {
console.error('THREE.SelectionBox: Unsupported camera type.');
}
};
SelectionBox.prototype.searchChildInFrustum = function(frustum, object) {
if (object instanceof AtomMesh) {
if (object.material !== undefined) {
if (object.geometry.boundingSphere === null) object.geometry.computeBoundingSphere();
center.copy(object.geometry.boundingSphere.center);
center.applyMatrix4(object.matrixWorld);
if (frustum.containsPoint(center)) {
this.collection.push(object);
}
}
}
if (object.children.length > 0) {
for (var x = 0; x < object.children.length; x++) {
this.searchChildInFrustum(frustum, object.children[x]);
}
}
};
return SelectionBox;
})();
var SelectionHelper = (function() {
function SelectionHelper(selectionBox, renderer, cssClassName) {
this.element = $('<div>');
this.element.addClass(cssClassName);
this.element.css({
'position': 'fixed',
'pointerEvents': 'none',
'display': 'none',
'border-width': '1px',
'border-style': 'solid',
'background-color': 'rgb(255, 0, 0)', // Default style
'opacity': 0.5
});
this.minBoxSize = 10.0;
this.renderer = renderer;
this.domElement = this.renderer._r.domElement;
this.domElement.parentElement.appendChild(this.element.get(0));
this.selBox = selectionBox;
this.startPoint = new Vector2();
this.pointTopLeft = new Vector2();
this.pointBottomRight = new Vector2();
this.isDown = false;
this.selectOverCallback = null;
this.domElement.addEventListener('pointerdown', function(event) {
if (event.shiftKey) {
this.isDown = true;
this.onSelectStart(event);
}
}.bind(this), false);
this.domElement.addEventListener('pointermove', function(event) {
if (this.isDown && event.shiftKey) {
this.onSelectMove(event);
}
}.bind(this), false);
this.domElement.addEventListener('pointerup', function(event) {
if (this.isDown && event.shiftKey) {
this.isDown = false;
this.onSelectOver(event);
}
}.bind(this), false);
this.domElement.addEventListener('keyup', function(event) {
if (event.key == 'Shift' && this.isDown) {
this.isDown = false;
this.onSelectOver(event);
}
}.bind(this), false);
}
SelectionHelper.prototype.onSelectStart = function(event) {
this.element.css({
'display': 'block',
'left': event.clientX + 'px',
'top': event.clientY + 'px',
'width': '0px',
'height': '0px'
});
this.startPoint.x = event.clientX;
this.startPoint.y = event.clientY;
this.pointBottomRight.x = this.startPoint.x;
this.pointBottomRight.y = this.startPoint.y;
this.pointTopLeft.x = this.startPoint.x;
this.pointTopLeft.y = this.startPoint.y;
};
SelectionHelper.prototype.onSelectMove = function(event) {
this.pointBottomRight.x = Math.max(this.startPoint.x, event.clientX);
this.pointBottomRight.y = Math.max(this.startPoint.y, event.clientY);
this.pointTopLeft.x = Math.min(this.startPoint.x, event.clientX);
this.pointTopLeft.y = Math.min(this.startPoint.y, event.clientY);
this.element.css({
'left': this.pointTopLeft.x + 'px',
'top': this.pointTopLeft.y + 'px',
'width': (this.pointBottomRight.x - this.pointTopLeft.x) + 'px',
'height': (this.pointBottomRight.y - this.pointTopLeft.y) + 'px'
});
};
SelectionHelper.prototype.onSelectOver = function() {
this.element.css('display', 'none');
if (this.selectOverCallback) {
var p1 = this.renderer.documentToWorld(this.pointBottomRight.x, this.pointBottomRight.y);
var p2 = this.renderer.documentToWorld(this.pointTopLeft.x, this.pointTopLeft.y);
// Skip if they're too close
var boxSize = Math.abs(this.pointBottomRight.x - this.pointTopLeft.x) +
Math.abs(this.pointBottomRight.y - this.pointTopLeft.y);
if (boxSize > this.minBoxSize)
this.selectOverCallback(p1, p2);
}
};
return SelectionHelper;
})();
export {
SelectionBox,
SelectionHelper
};