UNPKG

trip.three

Version:
374 lines (323 loc) 10.3 kB
'use strict'; const tripcore = require('trip.core'); const $ = require('trip.dom').$; const THREE = require('three'); const RawEventGenerator = require('./RawEventGenerator'); const ViewEventGenerator = require('./ViewEventGenerator'); const Trackball = require('./Trackball'); const toScreenPosition = require('./util/toScreenPosition'); class Scene extends tripcore.Scene { constructor(container, options) { super(container, options); const $container = $(container); options = (options === undefined) ? {} : options; this.rawEventGenerator = new RawEventGenerator($container); this.viewEventGenerator = new ViewEventGenerator(this); this.views = []; // Each layer has a scene const layers = [new THREE.Scene()]; layers[0].index = 0; if (options.layers) { for (var i = 1; i < options.layers; ++i) { layers.push(new THREE.Scene()); layers[layers.length - 1].index = i; } } this.layers = layers; // lights var directionalLights = []; if (!options.noLights) { directionalLights = layers.map(function(layer) { var light = new THREE.DirectionalLight(0xffffff); light.position.set(-1, -1, -1); layer.add(light); layer.add(new THREE.AmbientLight(0x222222)); light.castShadow = true; return light; }); } // renderer this.width = $container.width(); this.height = $container.height(); this.aspect = this.width/this.height; let redraw = true; const clear = options.clear === undefined ? true : options.clear; const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: !clear, }); if (clear) { const clearColor = options.clearColor === undefined ? 0xffffff : options.clearColor; const clearOpacity = options.clearOpacity === undefined ? 1 : options.clearOpacity; renderer.setClearColor(clearColor, clearOpacity); } renderer.autoClear = false; renderer.setSize(this.width, this.height); if (options.enableShadowMap) { renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFShadowMap; renderer.shadowMap.cullFace = THREE.CullFaceBack; } $container[0].appendChild(renderer.domElement); // debug // scene.add(new THREE.Mesh( // new THREE.BoxGeometry(1,1,1), // new THREE.MeshLambertMaterial({color: 0x0000ff, transparent: true, opacity: 0.5}))); // Cameras var near = options.near || 0.1; var far = options.far || 10000; this.perspectiveCamera = new THREE.PerspectiveCamera(60, this.width/this.height, near, far); this.orthographicCamera = new THREE.OrthographicCamera(-10000, 10000, 5000, -5000, near, far); let mode; if ((options.mode === undefined) || (options.mode === 'perspective')) { mode = 'perspective'; this.camera = this.perspectiveCamera; } else if (options.mode === 'orthographic') { mode = 'orthographic'; this.camera = this.orthographicCamera; } else { throw new Error('unknown mode:' + options.mode); } this.__defineSetter__('mode', (val) => { if (val === 'perspective') { mode = val; this.camera = this.perspectiveCamera; } else if (val === 'orthographic') { mode = val; this.camera = this.orthographicCamera; } else { throw new Error('unknown mode:' + mode); } }); this.__defineGetter__('mode', function() { return mode; }); this.redraw = function() { redraw = true; }; this.trackball = new Trackball(this, this.rawEventGenerator); this.trackball.setTarget({ distance: options.distance, azimuth: options.azimuth, elevation: options.elevation, }); const _this = this; function render() { let camera = _this.camera; if (redraw) { _this.emit('pre_render', camera); directionalLights.forEach(function(light) { light.position.copy(camera.position); }); renderer.clear(); renderer.render(layers[0], camera); for (var i = 1; i < layers.length; ++i) { renderer.clearDepth(); renderer.render(layers[i], camera); } _this.emit('post_render', camera); redraw = false; } } function animate() { requestAnimationFrame(animate); render(); } this.render = render; function resize() { _this.width = $container.width(); _this.height = $container.height(); _this.aspect = _this.width/_this.height; _this.trackball.updateCamera(); renderer.setSize(_this.width, _this.height); redraw = true; } this.forceResize = function() { resize(); }; this.addView = function(view) { this.views.push(view); var layer = view.layer || 0; layers[layer].add(view.sceneObject); // view.render() called on next tick view.once('post_render', () => { redraw = true; }); }; this.removeView = function(view) { var layer = view.layer || 0; var index = this.views.indexOf(view); if (index === -1) { throw new Error('view not found'); } this.views.splice(index, 1); layers[layer].remove(view.sceneObject); redraw = true; }; window.addEventListener('resize', resize, false); // Resize on the next event loop tick since a scroll bar may have been added // in the meantime. setTimeout(resize, 1); animate(); } setPerspective() { this.mode = 'perspective'; this.orthoMode = undefined; this.trackball.setTarget({ azimuth: Math.PI/4, elevation: 1.08, }); } setOrthoXPos() { this.mode = 'orthographic'; this.orthoMode = '+x'; this.trackball.setTarget({ elevation: Math.PI/2, azimuth: 0, }); } setOrthoXNeg() { this.mode = 'orthographic'; this.orthoMode = '-x'; this.trackball.setTarget({ elevation: Math.PI/2, azimuth: Math.PI, }); } setOrthoYPos() { this.mode = 'orthographic'; this.orthoMode = '+y'; this.trackball.setTarget({ elevation: Math.PI/2, azimuth: Math.PI/2, }); } setOrthoYNeg() { this.mode = 'orthographic'; this.orthoMode = '-y'; this.trackball.setTarget({ elevation: Math.PI/2, azimuth: Math.PI*3/2, }); } setOrthoZPos() { this.mode = 'orthographic'; this.orthoMode = '+z'; this.trackball.setTarget({ azimuth: 0, elevation: 0, up: new THREE.Vector3(0,1,0), }); } setOrthoZNeg() { this.mode = 'orthographic'; this.orthoMode = '-z'; this.trackball.setTarget({ azimuth: 0, elevation: Math.PI, up: new THREE.Vector3(0, 1, 0), }); } getState() { return { mode: this.mode, orthoMode: this.orthoMode, target: this.trackball.getTarget(), }; } setState(state) { this.mode = state.mode; this.orthoMode = state.orthoMode; this.trackball.setTarget(state.target); } // Poor man's zoom to extents. Check the bounds on screen // coordinates and adjust the distance to fit the layers // into the screen coords. // // Because of the non-linear perspective camera, call it twice // to avoid problems of large zoom out factors due to points // a long distance out of the screen towards observer zoomToExtents(filters) { if (!filters) { filters = {}; } if (!filters.view) { filters.view = () => { return true; }; } if (!filters.layer) { filters.layer = () => { return true; }; } // So the zoom is slightly further out // and the objetcs are not on the edges of the viewport const padFactor = 1.2; let filteredViews = this.views.filter((view) => { return filters.view(view) && filters.layer(view.layer); }); let bounds = new THREE.Box3(); filteredViews.forEach((v) => { bounds.union(new THREE.Box3().setFromObject(v.sceneObject)); }); if (bounds.isEmpty()) { console.warn('empty bounds'); return; } this.trackball.updateTarget({ lookAt: bounds.center(), }); this.trackball.updateCamera(); // render() somehow changes the camera state, not sure why this // is necessary this.render(); if (this.mode === 'perspective') { // The calculated distance will put the camera // where the field of view will enclose the radius const boundingSphere = bounds.getBoundingSphere(); const radius = boundingSphere.radius; this.trackball.updateTarget({ distance: radius*Math.tan(this.camera.fov/180*Math.PI)*padFactor, }); } else if (this.mode === 'orthographic') { // Find the screen coordinates of bounding Box corners let worldPositions = [ new THREE.Vector3(bounds.min.x, bounds.min.y, bounds.min.z), new THREE.Vector3(bounds.min.x, bounds.max.y, bounds.min.z), new THREE.Vector3(bounds.max.x, bounds.min.y, bounds.min.z), new THREE.Vector3(bounds.max.x, bounds.max.y, bounds.min.z), new THREE.Vector3(bounds.min.x, bounds.min.y, bounds.max.z), new THREE.Vector3(bounds.min.x, bounds.max.y, bounds.max.z), new THREE.Vector3(bounds.max.x, bounds.min.y, bounds.max.z), new THREE.Vector3(bounds.max.x, bounds.max.y, bounds.max.z), ]; // Distance from center let dx = -Infinity; let dy = -Infinity; let halfWidth = this.width/2; let halfHeight = this.height/2; let centerX = halfWidth; let centerY = halfHeight; for (let i = 0; i < worldPositions.length; ++i) { let screenPos = toScreenPosition( this.width, this.height, this.camera, worldPositions[i]); let dx2 = Math.abs(screenPos.x - centerX); let dy2 = Math.abs(screenPos.y - centerY); if (dx2 > dx) { dx = dx2; } if (dy2 > dy) { dy = dy2; } } let xFactor = dx/halfWidth; let yFactor = dy/halfHeight; let factor = Math.max(xFactor, yFactor)*1;2; this.trackball.updateTarget({ distance: this.trackball.currentTarget.distance*factor*padFactor, }); } } } module.exports = Scene;