ThreeGeometryBuilder

Source

TrackballControls.js
/**
* @author Eberhard Graether / http://egraether.com/
* @author Mark Lundin / http://mark-lundin.com
* @author Simone Manini / http://daron1337.github.io
* @author Luca Antiga / http://lantiga.github.io
*/
import THREE from 'three';

THREE.TrackballControls = function(object, domElement) {
var _this = this;
var STATE = {
NONE: -1,
ROTATE: 0,
ZOOM: 1,
PAN: 2,
TOUCH_ROTATE: 3,
TOUCH_ZOOM_PAN: 4,
};

this.object = object;
this.domElement = domElement !== undefined ? domElement : document;

// API

this.enabled = true;

this.screen = { left: 0, top: 0, width: 0, height: 0 };

this.rotateSpeed = 1.0;
this.zoomSpeed = 1.2;
this.panSpeed = 0.3;

this.noRotate = false;
this.noZoom = false;
this.noPan = false;

this.staticMoving = false;
this.dynamicDampingFactor = 0.2;

this.minDistance = 0;
this.maxDistance = Infinity;

this.keys = [65 /*A*/, 83 /*S*/, 68 /*D*/];

// internals

this.target = new THREE.Vector3();

const EPS = 0.000001;

const lastPosition = new THREE.Vector3(),
_eye = new THREE.Vector3(),
_zoomStart = new THREE.Vector2(),
_zoomEnd = new THREE.Vector2(),
_movePrev = new THREE.Vector2(),
_moveCurr = new THREE.Vector2(),
_panStart = new THREE.Vector2(),
_panEnd = new THREE.Vector2(),
_lastAxis = new THREE.Vector3();

let _state = STATE.NONE,
_prevState = STATE.NONE,
_lastAngle = 0,
_touchZoomDistanceStart = 0,
_touchZoomDistanceEnd = 0;

// for reset

this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.up0 = this.object.up.clone();

// events

const changeEvent = { type: 'change' };
const startEvent = { type: 'start' };
const endEvent = { type: 'end' };

// methods

this.handleResize = function() {
if (this.domElement === document) {
this.screen.left = 0;
this.screen.top = 0;
this.screen.width = window.innerWidth;
this.screen.height = window.innerHeight;
} else {
const box = this.domElement.getBoundingClientRect();
// adjustments come from similar code in the jquery offset() function
const d = this.domElement.ownerDocument.documentElement;
this.screen.left = box.left + window.pageXOffset - d.clientLeft;
this.screen.top = box.top + window.pageYOffset - d.clientTop;
this.screen.width = box.width;
this.screen.height = box.height;
}
};

this.handleEvent = function(event) {
if (typeof this[event.type] === 'function') {
this[event.type](event);
}
};

const getMouseOnScreen = (function() {
var vector = new THREE.Vector2();

return (pageX, pageY) => {
vector.set(
(pageX - _this.screen.left) / _this.screen.width,
(pageY - _this.screen.top) / _this.screen.height
);

return vector;
};
})();

const getMouseOnCircle = (function() {
var vector = new THREE.Vector2();

return (pageX, pageY) => {
vector.set(
(pageX - _this.screen.width * 0.5 - _this.screen.left) /
(_this.screen.width * 0.5),
(_this.screen.height + 2 * (_this.screen.top - pageY)) /
_this.screen.width // screen.width intentional
);

return vector;
};
})();

this.rotateCamera = (function() {
var axis = new THREE.Vector3(),
quaternion = new THREE.Quaternion(),
eyeDirection = new THREE.Vector3(),
objectUpDirection = new THREE.Vector3(),
objectSidewaysDirection = new THREE.Vector3(),
moveDirection = new THREE.Vector3(),
angle;

return function rotateCamera() {
moveDirection.set(
_moveCurr.x - _movePrev.x,
_moveCurr.y - _movePrev.y,
0
);
angle = moveDirection.length();

if (angle) {
_eye.copy(_this.object.position).sub(_this.target);

eyeDirection.copy(_eye).normalize();
objectUpDirection.copy(_this.object.up).normalize();
objectSidewaysDirection
.crossVectors(objectUpDirection, eyeDirection)
.normalize();

objectUpDirection.setLength(_moveCurr.y - _movePrev.y);
objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x);

moveDirection.copy(objectUpDirection.add(objectSidewaysDirection));

axis.crossVectors(moveDirection, _eye).normalize();

angle *= _this.rotateSpeed;
quaternion.setFromAxisAngle(axis, angle);

_eye.applyQuaternion(quaternion);
_this.object.up.applyQuaternion(quaternion);

_lastAxis.copy(axis);
_lastAngle = angle;
} else if (!_this.staticMoving && _lastAngle) {
_lastAngle *= Math.sqrt(1.0 - _this.dynamicDampingFactor);
_eye.copy(_this.object.position).sub(_this.target);
quaternion.setFromAxisAngle(_lastAxis, _lastAngle);
_eye.applyQuaternion(quaternion);
_this.object.up.applyQuaternion(quaternion);
}

_movePrev.copy(_moveCurr);
};
})();

this.zoomCamera = function() {
var factor;

if (_state === STATE.TOUCH_ZOOM_PAN) {
factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
_touchZoomDistanceStart = _touchZoomDistanceEnd;
_eye.multiplyScalar(factor);
} else {
factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * _this.zoomSpeed;

if (factor !== 1.0 && factor > 0.0) {
_eye.multiplyScalar(factor);

if (_this.staticMoving) {
_zoomStart.copy(_zoomEnd);
} else {
_zoomStart.y +=
(_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor;
}
}
}
};

this.panCamera = (function() {
var mouseChange = new THREE.Vector2(),
objectUp = new THREE.Vector3(),
pan = new THREE.Vector3();

return function panCamera() {
mouseChange.copy(_panEnd).sub(_panStart);

if (mouseChange.lengthSq()) {
mouseChange.multiplyScalar(_eye.length() * _this.panSpeed);

pan
.copy(_eye)
.cross(_this.object.up)
.setLength(mouseChange.x);
pan.add(objectUp.copy(_this.object.up).setLength(mouseChange.y));

_this.object.position.add(pan);
_this.target.add(pan);

if (_this.staticMoving) {
_panStart.copy(_panEnd);
} else {
_panStart.add(
mouseChange
.subVectors(_panEnd, _panStart)
.multiplyScalar(_this.dynamicDampingFactor)
);
}
}
};
})();

this.checkDistances = function() {
if (!_this.noZoom || !_this.noPan) {
if (_eye.lengthSq() > _this.maxDistance * _this.maxDistance) {
_this.object.position.addVectors(
_this.target,
_eye.setLength(_this.maxDistance)
);
_zoomStart.copy(_zoomEnd);
}

if (_eye.lengthSq() < _this.minDistance * _this.minDistance) {
_this.object.position.addVectors(
_this.target,
_eye.setLength(_this.minDistance)
);
_zoomStart.copy(_zoomEnd);
}
}
};

this.update = function() {
_eye.subVectors(_this.object.position, _this.target);

if (!_this.noRotate) {
_this.rotateCamera();
}

if (!_this.noZoom) {
_this.zoomCamera();
}

if (!_this.noPan) {
_this.panCamera();
}

_this.object.position.addVectors(_this.target, _eye);

_this.checkDistances();

_this.object.lookAt(_this.target);

if (lastPosition.distanceToSquared(_this.object.position) > EPS) {
_this.dispatchEvent(changeEvent);

lastPosition.copy(_this.object.position);
}
};

this.reset = function() {
_state = STATE.NONE;
_prevState = STATE.NONE;

_this.target.copy(_this.target0);
_this.object.position.copy(_this.position0);
_this.object.up.copy(_this.up0);

_eye.subVectors(_this.object.position, _this.target);

_this.object.lookAt(_this.target);

_this.dispatchEvent(changeEvent);

lastPosition.copy(_this.object.position);
};

this.resetCamera = function(center, radius) {
_state = STATE.NONE;
_prevState = STATE.NONE;

// Magic Number
// 45 is the camera view angle
// 1/cos(45/2) = 2.669 => 2.7
const direction = new THREE.Vector3()
.subVectors(_this.object.position, _this.target)
.normalize()
.multiplyScalar(2.7 * radius);

_zoomStart.set(0, 0);
_zoomEnd.set(0, 0);

_this.target.copy(center);
_this.object.position.copy(center);
_this.object.position.add(direction);

_eye.subVectors(_this.object.position, _this.target);

_this.object.lookAt(_this.target);

_this.dispatchEvent(changeEvent);

lastPosition.copy(_this.object.position);
};

// listeners

function keydown(event) {
if (_this.enabled === false) {
return;
}

window.removeEventListener('keydown', keydown);

_prevState = _state;

if (_state !== STATE.NONE) {
return;
} else if (event.keyCode === _this.keys[STATE.ROTATE] && !_this.noRotate) {
_state = STATE.ROTATE;
} else if (event.keyCode === _this.keys[STATE.ZOOM] && !_this.noZoom) {
_state = STATE.ZOOM;
} else if (event.keyCode === _this.keys[STATE.PAN] && !_this.noPan) {
_state = STATE.PAN;
}
}

function keyup(event) {
if (_this.enabled === false) {
return;
}

_state = _prevState;

window.addEventListener('keydown', keydown, false);
}

function mousemove(event) {
if (_this.enabled === false) {
return;
}

event.preventDefault();
event.stopPropagation();

if (_state === STATE.ROTATE && !_this.noRotate) {
_movePrev.copy(_moveCurr);
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));
} else if (_state === STATE.ZOOM && !_this.noZoom) {
_zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY));
} else if (_state === STATE.PAN && !_this.noPan) {
_panEnd.copy(getMouseOnScreen(event.pageX, event.pageY));
}
}

function mouseup(event) {
if (_this.enabled === false) {
return;
}

event.preventDefault();
event.stopPropagation();

_state = STATE.NONE;

document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', mouseup);
_this.dispatchEvent(endEvent);
}

function mousedown(event) {
if (_this.enabled === false) {
return;
}

event.preventDefault();
event.stopPropagation();

if (_state === STATE.NONE) {
_state = event.button;
}

if (_state === STATE.ROTATE && !_this.noRotate) {
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));
_movePrev.copy(_moveCurr);
} else if (_state === STATE.ZOOM && !_this.noZoom) {
_zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY));
_zoomEnd.copy(_zoomStart);
} else if (_state === STATE.PAN && !_this.noPan) {
_panStart.copy(getMouseOnScreen(event.pageX, event.pageY));
_panEnd.copy(_panStart);
}

document.addEventListener('mousemove', mousemove, false);
document.addEventListener('mouseup', mouseup, false);

_this.dispatchEvent(startEvent);
}

function mousewheel(event) {
if (_this.enabled === false) {
return;
}

event.preventDefault();
event.stopPropagation();

let delta = 0;

if (event.wheelDelta) {
// WebKit / Opera / Explorer 9

delta = event.wheelDelta / 40;
} else if (event.detail) {
// Firefox

delta = -event.detail / 3;
}

_zoomStart.y += delta * 0.01;
_this.dispatchEvent(startEvent);
_this.dispatchEvent(endEvent);
}

function touchstart(event) {
if (_this.enabled === false) {
return;
}

switch (event.touches.length) {
case 1:
_state = STATE.TOUCH_ROTATE;
_moveCurr.copy(
getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)
);
_movePrev.copy(_moveCurr);
break;

case 2:
_state = STATE.TOUCH_ZOOM_PAN;
const dx = event.touches[0].pageX - event.touches[1].pageX;
const dy = event.touches[0].pageY - event.touches[1].pageY;
_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(
dx * dx + dy * dy
);

const x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
const y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
_panStart.copy(getMouseOnScreen(x, y));
_panEnd.copy(_panStart);
break;

default:
_state = STATE.NONE;
}
_this.dispatchEvent(startEvent);
}

function touchmove(event) {
if (_this.enabled === false) {
return;
}

event.preventDefault();
event.stopPropagation();

switch (event.touches.length) {
case 1:
_movePrev.copy(_moveCurr);
_moveCurr.copy(
getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)
);
break;

case 2:
const dx = event.touches[0].pageX - event.touches[1].pageX;
const dy = event.touches[0].pageY - event.touches[1].pageY;
_touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy);

const x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
const y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
_panEnd.copy(getMouseOnScreen(x, y));
break;

default:
_state = STATE.NONE;
}
}

function touchend(event) {
if (_this.enabled === false) {
return;
}

switch (event.touches.length) {
case 1:
_movePrev.copy(_moveCurr);
_moveCurr.copy(
getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)
);
break;

case 2:
_touchZoomDistanceStart = _touchZoomDistanceEnd = 0;

const x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
const y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
_panEnd.copy(getMouseOnScreen(x, y));
_panStart.copy(_panEnd);
break;
}

_state = STATE.NONE;
_this.dispatchEvent(endEvent);
}

function contextmenu(event) {
event.preventDefault();
}

this.dispose = function() {
this.domElement.removeEventListener('contextmenu', contextmenu, false);
this.domElement.removeEventListener('mousedown', mousedown, false);
this.domElement.removeEventListener('mousewheel', mousewheel, false);
this.domElement.removeEventListener(
'MozMousePixelScroll',
mousewheel,
false
); // firefox

this.domElement.removeEventListener('touchstart', touchstart, false);
this.domElement.removeEventListener('touchend', touchend, false);
this.domElement.removeEventListener('touchmove', touchmove, false);

document.removeEventListener('mousemove', mousemove, false);
document.removeEventListener('mouseup', mouseup, false);

window.removeEventListener('keydown', keydown, false);
window.removeEventListener('keyup', keyup, false);
};

this.domElement.addEventListener('contextmenu', contextmenu, false);
this.domElement.addEventListener('mousedown', mousedown, false);
this.domElement.addEventListener('mousewheel', mousewheel, false);
this.domElement.addEventListener('MozMousePixelScroll', mousewheel, false); // firefox

this.domElement.addEventListener('touchstart', touchstart, false);
this.domElement.addEventListener('touchend', touchend, false);
this.domElement.addEventListener('touchmove', touchmove, false);

window.addEventListener('keydown', keydown, false);
window.addEventListener('keyup', keyup, false);

this.handleResize();

// force an update at start
this.update();
};

THREE.TrackballControls.prototype = Object.create(
THREE.EventDispatcher.prototype
);
THREE.TrackballControls.prototype.constructor = THREE.TrackballControls;

export default THREE.TrackballControls;
index.js
import THREE from 'three';
import TrackballControls from './TrackballControls';

// ********************************************************************
// * Convenience function to generate colors from scalar array and LUT
// *******************************************************************
function updateFieldColorBuffer(lut, fieldData, buf) {
// var buf = new Float32Array(fieldData.length * 3);
for (let i = 0; i < fieldData.length; ++i) {
const idx = i * 3,
color = lut.getColor(fieldData[i]);
buf[idx] = color[0];
buf[idx + 1] = color[1];
buf[idx + 2] = color[2];
}
return buf;
}

// *******************************************************************
// * Convenience function to generate the correct number of empties
// *******************************************************************
function updateGreyColorBuffer(buf) {
for (let i = 0; i < buf.length; ++i) {
buf[i] = 0.5;
}
return buf;
}

export default class ThreeGeometryBuilder {
constructor(lutMgr, geometryDataModel, pipelineModel, queryDataModel) {
this.meshMap = {};

this.firstSceneLoad = true;
this.lookupTableManager = lutMgr;
this.geometryDataModel = geometryDataModel;
this.pipelineModel = pipelineModel;
this.queryDataModel = queryDataModel;
this.layerMap = this.queryDataModel.originalData.Geometry.layer_map;
this.fieldMap = this.queryDataModel.originalData.CompositePipeline.fields;
this.maxSize = queryDataModel.originalData.Geometry.object_size;

// Handle pipeline color change
const updatePipeline = (pipelineQuery, envelope) => {
var size = pipelineQuery.length;

for (let i = 0; i < size; i += 2) {
const objectName = this.layerMap[pipelineQuery[i]],
fieldName = this.fieldMap[pipelineQuery[i + 1]];
// if (fieldName !== '_') {
if (fieldName) {
this.geometryDataModel.colorGeometryBy(objectName, fieldName);
this.updateObjectVisibility(objectName, true);
} else {
this.updateObjectVisibility(objectName, false);
}
}

this.queryDataModel.fetchData();
};
this.pipelineModel.onChange(updatePipeline);

// Handle data fetching
this.queryDataModel.onDataChange((data, envelope) => {
if (data.scene) {
this.geometryDataModel.loadScene(data.scene.data);

if (this.firstSceneLoad) {
this.firstSceneLoad = false;
updatePipeline(this.pipelineModel.getPipelineQuery());
}
}
});

// Handle LookupTable change
this.lookupTableManager.addFields(
this.queryDataModel.originalData.Geometry.ranges,
this.queryDataModel.originalData.LookupTables
);
this.lookupTableManager.onChange((data, envelope) => {
this.updateColoring(data.change, data.lut);
});

// Scene management
this.camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.camera.position.z = 50;

this.scene = new THREE.Scene();
this.scene.add(this.camera);

this.dirLight = new THREE.DirectionalLight(0xffffff);
this.dirLight.position.set(200, 200, 1000).normalize();

// this.dirLight2 = new THREE.DirectionalLight( 0xffffff );
// this.dirLight2.position.set( 200, -200, 1000).normalize();

this.camera.add(this.dirLight);
this.camera.add(this.dirLight.target);

// this.camera.add( this.dirLight2 );
// this.camera.add( this.dirLight2.target );

this.geometryBuilderSubscription = this.geometryDataModel.onGeometryReady(
(data, envelope) => {
this.updateGeometry(data);
}
);
}

destroy() {
// Remove listener
if (this.geometryBuilderSubscription) {
this.geometryBuilderSubscription.unsubscribe();
this.geometryBuilderSubscription = null;
}
}

configureRenderer(canvas) {
this.renderer = new THREE.WebGLRenderer({
canvas,
});
this.renderer.setSize(window.innerWidth, window.innerHeight);

this.controls = new TrackballControls(this.camera, canvas);
this.controls.rotateSpeed = 5.0;
this.controls.zoomSpeed = 20;
this.controls.panSpeed = 2;
this.controls.noZoom = false;
this.controls.noPan = false;
this.controls.staticMoving = true;
this.controls.dynamicDampingFactor = 0.3;

// Create a render() method that can be called from anywhere
this.render = () => {
requestAnimationFrame(this.render);
this.controls.update();
this.renderer.render(this.scene, this.camera);
};

this.queryDataModel.fetchData();
}

updateColoring(whatChanged, lookupTable) {
for (const name in this.meshMap) {
const renderInfo = this.meshMap[name];
if (renderInfo.colorArrayName === lookupTable.name) {
const colors = updateFieldColorBuffer(
lookupTable,
renderInfo.fieldData,
renderInfo.colorBuffer
);
renderInfo.mesh.geometry.addAttribute(
'color',
new THREE.BufferAttribute(colors, 3)
);
}
}
}

updateGeometry(geo) {
if (!this.meshMap.hasOwnProperty(geo.name)) {
// Create new Geometry
const pSize = this.maxSize[geo.name].points,
iSize = this.maxSize[geo.name].index,
geom = new THREE.BufferGeometry(),
material = new THREE.MeshPhongMaterial({
color: 0xdddddd,
specular: 0x444444,
shininess: 10,
side: THREE.DoubleSide,
vertexColors: THREE.VertexColors,
shading: THREE.FlatShading,
}),
colorBuffer = new Float32Array(pSize * 3);

// Add object to the scene
const sceneObject = new THREE.Mesh(geom, material);
this.scene.add(sceneObject);

// Register geometry
this.meshMap[geo.name] = {
mesh: sceneObject,
material,
colorBuffer,
};

// Allocate max size object
const pArray = new Float32Array(pSize * 3),
iArray = new Uint32Array(iSize);

for (let i = 0; i < pSize; i++) {
pArray[i] = Math.random();
}
for (let i = 0; i < iSize; i++) {
iArray[i] = i % pSize;
}

geom.addAttribute('position', new THREE.BufferAttribute(pArray, 3));
geom.setIndex(new THREE.BufferAttribute(iArray, 1));
geom.addAttribute('color', new THREE.BufferAttribute(colorBuffer, 3));

geom.computeFaceNormals();
// geom.computeVertexNormals();

this.renderer.render(this.scene, this.camera);
} else {
const renderInfo = this.meshMap[geo.name],
geometry = renderInfo.mesh.geometry;

let colors = renderInfo.colorBuffer;

if (geometry.vertices && geo.points.length !== geometry.vertices.length) {
console.log(
'******** We may have a problem here, new point count = ',
geo.points.length,
', old point count = ',
geometry.vertices.length
);
// FIXME: Allocate new color buffer here
}

// geometry.setIndex( new THREE.BufferAttribute( geo.index, 1 ) );
const index = geometry.getIndex(),
pos = geometry.getAttribute('position'),
color = geometry.getAttribute('color');

if (index) {
index.array = geo.index;
index.needsUpdate = true;
}

if (pos) {
pos.array = geo.points;
pos.needsUpdate = true;
}

geometry.computeFaceNormals();
// geometry.computeVertexNormals();
// geometry.normalizeNormals();

if (geo.hasOwnProperty('field')) {
renderInfo.colorArrayName = geo.fieldName;
renderInfo.fieldData = geo.field;
colors = updateFieldColorBuffer(
this.lookupTableManager.getLookupTable(geo.fieldName),
geo.field,
colors
);
} else {
renderInfo.colorArrayName = null;
renderInfo.fieldData = null;
colors = updateGreyColorBuffer(colors);
}

if (color) {
color.array = colors;
color.needsUpdate = true;
}

geometry.computeBoundingBox();
}
}

updateObjectVisibility(name, visibility) {
if (this.meshMap[name]) {
this.meshMap[name].mesh.visible = visibility;
}
}

resetCamera() {
// Get bounds
var bbox = new THREE.Box3();
for (const meshName in this.meshMap) {
const mesh = this.meshMap[meshName].mesh;
mesh.geometry.computeBoundingBox();
bbox
.expandByPoint(mesh.geometry.boundingBox.min)
.expandByPoint(mesh.geometry.boundingBox.max);
}
const { center, radius } = bbox.getBoundingSphere();
this.controls.resetCamera(center, radius);
}

updateSize(width, height) {
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();

this.renderer.setSize(width, height);

this.controls.handleResize();
}
}