xushiting 2 tahun lalu
induk
melakukan
7078276538

+ 21 - 0
libs/stats.js/LICENSE

@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2009-2016 stats.js authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 56 - 0
libs/stats.js/README.md

@@ -0,0 +1,56 @@
+stats.js
+========
+
+#### JavaScript Performance Monitor ####
+
+This class provides a simple info box that will help you monitor your code performance.
+
+* **FPS** Frames rendered in the last second. The higher the number the better.
+* **MS** Milliseconds needed to render a frame. The lower the number the better.
+* **MB** MBytes of allocated memory. (Run Chrome with `--enable-precise-memory-info`)
+* **CUSTOM** User-defined panel support.
+
+
+### Screenshots ###
+
+![fps.png](https://raw.githubusercontent.com/mrdoob/stats.js/master/files/fps.png)
+![ms.png](https://raw.githubusercontent.com/mrdoob/stats.js/master/files/ms.png)
+![mb.png](https://raw.githubusercontent.com/mrdoob/stats.js/master/files/mb.png)
+![custom.png](https://raw.githubusercontent.com/mrdoob/stats.js/master/files/custom.png)
+
+
+### Installation ###
+```bash
+npm install stats.js
+```
+
+### Usage ###
+
+```javascript
+var stats = new Stats();
+stats.showPanel( 1 ); // 0: fps, 1: ms, 2: mb, 3+: custom
+document.body.appendChild( stats.dom );
+
+function animate() {
+
+	stats.begin();
+
+	// monitored code goes here
+
+	stats.end();
+
+	requestAnimationFrame( animate );
+
+}
+
+requestAnimationFrame( animate );
+```
+
+
+### Bookmarklet ###
+
+You can add this code to any page using the following bookmarklet:
+
+```javascript
+javascript:(function(){var script=document.createElement('script');script.onload=function(){var stats=new Stats();document.body.appendChild(stats.dom);requestAnimationFrame(function loop(){stats.update();requestAnimationFrame(loop)});};script.src='//mrdoob.github.io/stats.js/build/stats.min.js';document.head.appendChild(script);})()
+```

+ 179 - 0
libs/stats.js/stats.js

@@ -0,0 +1,179 @@
+(function (global, factory) {
+	typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+	typeof define === 'function' && define.amd ? define(factory) :
+	(global.Stats = factory());
+}(this, (function () { 'use strict';
+
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Stats = function () {
+
+	var mode = 0;
+
+	var container = document.createElement( 'div' );
+	container.style.cssText = 'position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000';
+	container.addEventListener( 'click', function ( event ) {
+
+		event.preventDefault();
+		showPanel( ++ mode % container.children.length );
+
+	}, false );
+
+	//
+
+	function addPanel( panel ) {
+
+		container.appendChild( panel.dom );
+		return panel;
+
+	}
+
+	function showPanel( id ) {
+
+		for ( var i = 0; i < container.children.length; i ++ ) {
+
+			container.children[ i ].style.display = i === id ? 'block' : 'none';
+
+		}
+
+		mode = id;
+
+	}
+
+	//
+
+	var beginTime = ( performance || Date ).now(), prevTime = beginTime, frames = 0;
+
+	var fpsPanel = addPanel( new Stats.Panel( 'FPS', '#0ff', '#002' ) );
+	var msPanel = addPanel( new Stats.Panel( 'MS', '#0f0', '#020' ) );
+
+	if ( self.performance && self.performance.memory ) {
+
+		var memPanel = addPanel( new Stats.Panel( 'MB', '#f08', '#201' ) );
+
+	}
+
+	showPanel( 0 );
+
+	return {
+
+		REVISION: 16,
+
+		dom: container,
+
+		addPanel: addPanel,
+		showPanel: showPanel,
+
+		begin: function () {
+
+			beginTime = ( performance || Date ).now();
+
+		},
+
+		end: function () {
+
+			frames ++;
+
+			var time = ( performance || Date ).now();
+
+			msPanel.update( time - beginTime, 200 );
+
+			if ( time >= prevTime + 1000 ) {
+
+				fpsPanel.update( ( frames * 1000 ) / ( time - prevTime ), 100 );
+
+				prevTime = time;
+				frames = 0;
+
+				if ( memPanel ) {
+
+					var memory = performance.memory;
+					memPanel.update( memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576 );
+
+				}
+
+			}
+
+			return time;
+
+		},
+
+		update: function () {
+
+			beginTime = this.end();
+
+		},
+
+		// Backwards Compatibility
+
+		domElement: container,
+		setMode: showPanel
+
+	};
+
+};
+
+Stats.Panel = function ( name, fg, bg ) {
+
+	var min = Infinity, max = 0, round = Math.round;
+	var PR = round( window.devicePixelRatio || 1 );
+
+	var WIDTH = 80 * PR, HEIGHT = 48 * PR,
+			TEXT_X = 3 * PR, TEXT_Y = 2 * PR,
+			GRAPH_X = 3 * PR, GRAPH_Y = 15 * PR,
+			GRAPH_WIDTH = 74 * PR, GRAPH_HEIGHT = 30 * PR;
+
+	var canvas = document.createElement( 'canvas' );
+	canvas.width = WIDTH;
+	canvas.height = HEIGHT;
+	canvas.style.cssText = 'width:80px;height:48px';
+
+	var context = canvas.getContext( '2d' );
+	context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif';
+	context.textBaseline = 'top';
+
+	context.fillStyle = bg;
+	context.fillRect( 0, 0, WIDTH, HEIGHT );
+
+	context.fillStyle = fg;
+	context.fillText( name, TEXT_X, TEXT_Y );
+	context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );
+
+	context.fillStyle = bg;
+	context.globalAlpha = 0.9;
+	context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );
+
+	return {
+
+		dom: canvas,
+
+		update: function ( value, maxValue ) {
+
+			min = Math.min( min, value );
+			max = Math.max( max, value );
+
+			context.fillStyle = bg;
+			context.globalAlpha = 1;
+			context.fillRect( 0, 0, WIDTH, GRAPH_Y );
+			context.fillStyle = fg;
+			context.fillText( round( value ) + ' ' + name + ' (' + round( min ) + '-' + round( max ) + ')', TEXT_X, TEXT_Y );
+
+			context.drawImage( canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT );
+
+			context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT );
+
+			context.fillStyle = bg;
+			context.globalAlpha = 0.9;
+			context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round( ( 1 - ( value / maxValue ) ) * GRAPH_HEIGHT ) );
+
+		}
+
+	};
+
+};
+
+return Stats;
+
+})));

File diff ditekan karena terlalu besar
+ 5 - 0
libs/stats.js/stats.min.js


+ 171 - 0
libs/stats.js/stats.module.js

@@ -0,0 +1,171 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+var Stats = function () {
+
+	var mode = 0;
+
+	var container = document.createElement( 'div' );
+	container.style.cssText = 'position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000';
+	container.addEventListener( 'click', function ( event ) {
+
+		event.preventDefault();
+		showPanel( ++ mode % container.children.length );
+
+	}, false );
+
+	//
+
+	function addPanel( panel ) {
+
+		container.appendChild( panel.dom );
+		return panel;
+
+	}
+
+	function showPanel( id ) {
+
+		for ( var i = 0; i < container.children.length; i ++ ) {
+
+			container.children[ i ].style.display = i === id ? 'block' : 'none';
+
+		}
+
+		mode = id;
+
+	}
+
+	//
+
+	var beginTime = ( performance || Date ).now(), prevTime = beginTime, frames = 0;
+
+	var fpsPanel = addPanel( new Stats.Panel( 'FPS', '#0ff', '#002' ) );
+	var msPanel = addPanel( new Stats.Panel( 'MS', '#0f0', '#020' ) );
+
+	if ( self.performance && self.performance.memory ) {
+
+		var memPanel = addPanel( new Stats.Panel( 'MB', '#f08', '#201' ) );
+
+	}
+
+	showPanel( 0 );
+
+	return {
+
+		REVISION: 16,
+
+		dom: container,
+
+		addPanel: addPanel,
+		showPanel: showPanel,
+
+		begin: function () {
+
+			beginTime = ( performance || Date ).now();
+
+		},
+
+		end: function () {
+
+			frames ++;
+
+			var time = ( performance || Date ).now();
+
+			msPanel.update( time - beginTime, 200 );
+
+			if ( time >= prevTime + 1000 ) {
+
+				fpsPanel.update( ( frames * 1000 ) / ( time - prevTime ), 100 );
+
+				prevTime = time;
+				frames = 0;
+
+				if ( memPanel ) {
+
+					var memory = performance.memory;
+					memPanel.update( memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576 );
+
+				}
+
+			}
+
+			return time;
+
+		},
+
+		update: function () {
+
+			beginTime = this.end();
+
+		},
+
+		// Backwards Compatibility
+
+		domElement: container,
+		setMode: showPanel
+
+	};
+
+};
+
+Stats.Panel = function ( name, fg, bg ) {
+
+	var min = Infinity, max = 0, round = Math.round;
+	var PR = round( window.devicePixelRatio || 1 );
+
+	var WIDTH = 80 * PR, HEIGHT = 48 * PR,
+			TEXT_X = 3 * PR, TEXT_Y = 2 * PR,
+			GRAPH_X = 3 * PR, GRAPH_Y = 15 * PR,
+			GRAPH_WIDTH = 74 * PR, GRAPH_HEIGHT = 30 * PR;
+
+	var canvas = document.createElement( 'canvas' );
+	canvas.width = WIDTH;
+	canvas.height = HEIGHT;
+	canvas.style.cssText = 'width:80px;height:48px';
+
+	var context = canvas.getContext( '2d' );
+	context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif';
+	context.textBaseline = 'top';
+
+	context.fillStyle = bg;
+	context.fillRect( 0, 0, WIDTH, HEIGHT );
+
+	context.fillStyle = fg;
+	context.fillText( name, TEXT_X, TEXT_Y );
+	context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );
+
+	context.fillStyle = bg;
+	context.globalAlpha = 0.9;
+	context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );
+
+	return {
+
+		dom: canvas,
+
+		update: function ( value, maxValue ) {
+
+			min = Math.min( min, value );
+			max = Math.max( max, value );
+
+			context.fillStyle = bg;
+			context.globalAlpha = 1;
+			context.fillRect( 0, 0, WIDTH, GRAPH_Y );
+			context.fillStyle = fg;
+			context.fillText( round( value ) + ' ' + name + ' (' + round( min ) + '-' + round( max ) + ')', TEXT_X, TEXT_Y );
+
+			context.drawImage( canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT );
+
+			context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT );
+
+			context.fillStyle = bg;
+			context.globalAlpha = 0.9;
+			context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round( ( 1 - ( value / maxValue ) ) * GRAPH_HEIGHT ) );
+
+		}
+
+	};
+
+};
+
+export default Stats;

+ 146 - 0
src/TextSprite.js

@@ -0,0 +1,146 @@
+
+
+// /**
+//  * adapted from http://stemkoski.github.io/Three.js/Sprite-Text-Labels.html
+//  */
+
+import * as THREE from "../libs/three.js/build/three.module.js";
+
+export class TextSprite extends THREE.Object3D{
+	
+	constructor(text){
+		super();
+
+		let texture = new THREE.Texture();
+		texture.minFilter = THREE.LinearFilter;
+		texture.magFilter = THREE.LinearFilter;
+		let spriteMaterial = new THREE.SpriteMaterial({
+			map: texture,
+			depthTest: false,
+			depthWrite: false});
+
+		this.texture = texture;
+
+		this.material = spriteMaterial;
+		//this.material = getRawMaterial(texture);
+		this.sprite = new THREE.Sprite(this.material);
+		this.add(this.sprite);
+
+		this.borderThickness = 4;
+		this.fontface = 'Arial';
+		this.fontsize = 28;
+		this.borderColor = { r: 0, g: 0, b: 0, a: 1.0 };
+		this.backgroundColor = { r: 255, g: 255, b: 255, a: 1.0 };
+		this.textColor = {r: 255, g: 255, b: 255, a: 1.0};
+		this.text = '';
+
+		this.setText(text);
+	}
+
+	setText(text){
+		if (this.text !== text){
+			this.text = text;
+
+			this.update();
+		}
+	}
+
+	setTextColor(color){
+		this.textColor = color;
+
+		this.update();
+	}
+
+	setBorderColor(color){
+		this.borderColor = color;
+
+		this.update();
+	}
+
+	setBackgroundColor(color){
+		this.backgroundColor = color;
+
+		this.update();
+	}
+
+	update(){
+		let canvas = document.createElement('canvas');
+		let context = canvas.getContext('2d');
+		context.font = 'Bold ' + this.fontsize + 'px ' + this.fontface;
+
+		// get size data (height depends only on font size)
+		let metrics = context.measureText(this.text);
+		let textWidth = metrics.width;
+		let margin = 5;
+		let spriteWidth = 2 * margin + textWidth + 2 * this.borderThickness;
+		let spriteHeight = this.fontsize * 1.4 + 2 * this.borderThickness;
+
+		context.canvas.width = spriteWidth;
+		context.canvas.height = spriteHeight;
+		context.font = 'Bold ' + this.fontsize + 'px ' + this.fontface;
+
+		// background color
+		context.fillStyle = 'rgba(' + this.backgroundColor.r + ',' + this.backgroundColor.g + ',' +
+			this.backgroundColor.b + ',' + this.backgroundColor.a + ')';
+		// border color
+		context.strokeStyle = 'rgba(' + this.borderColor.r + ',' + this.borderColor.g + ',' +
+			this.borderColor.b + ',' + this.borderColor.a + ')';
+
+		context.lineWidth = this.borderThickness;
+		this.roundRect(context, this.borderThickness / 2, this.borderThickness / 2,
+			textWidth + this.borderThickness + 2 * margin, this.fontsize * 1.4 + this.borderThickness, 6);
+
+		// text color
+		context.strokeStyle = 'rgba(0, 0, 0, 1.0)';
+		context.strokeText(this.text, this.borderThickness + margin, this.fontsize + this.borderThickness);
+
+		context.fillStyle = 'rgba(' + this.textColor.r + ',' + this.textColor.g + ',' +
+			this.textColor.b + ',' + this.textColor.a + ')';
+		context.fillText(this.text, this.borderThickness + margin, this.fontsize + this.borderThickness);
+
+		let texture = new THREE.Texture(canvas);
+		texture.minFilter = THREE.LinearFilter;
+		texture.magFilter = THREE.LinearFilter;
+		texture.needsUpdate = true;
+		//this.material.needsUpdate = true;
+
+		// { // screen-space sprite
+		// 	let [screenWidth, screenHeight] = [1620, 937];
+
+		// 	let uniforms = this.sprite.material.uniforms;
+		// 	let aspect = spriteHeight / spriteWidth;
+		// 	let factor = 0.5;
+
+		// 	let w = spriteWidth / screenWidth;
+		// 	let h = spriteHeight / screenHeight;
+
+		// 	uniforms.uScale.value = [2 * w, 2 * h];
+		// 	//uniforms.uScale.value = [factor * 1, factor * aspect];
+		//	this.sprite.material.uniforms.map.value = texture;
+		// }
+
+		this.sprite.material.map = texture;
+		this.texture = texture;
+
+		this.sprite.scale.set(spriteWidth * 0.01, spriteHeight * 0.01, 1.0);
+	}
+
+	roundRect(ctx, x, y, w, h, r){
+		ctx.beginPath();
+		ctx.moveTo(x + r, y);
+		ctx.lineTo(x + w - r, y);
+		ctx.quadraticCurveTo(x + w, y, x + w, y + r);
+		ctx.lineTo(x + w, y + h - r);
+		ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
+		ctx.lineTo(x + r, y + h);
+		ctx.quadraticCurveTo(x, y + h, x, y + h - r);
+		ctx.lineTo(x, y + r);
+		ctx.quadraticCurveTo(x, y, x + r, y);
+		ctx.closePath();
+		ctx.fill();
+		ctx.stroke();
+	}
+
+}
+
+

+ 106 - 0
src/custom/ExtendEventDispatcher.js

@@ -0,0 +1,106 @@
+import * as THREE from "../../libs/three.js/build/three.module.js";
+
+export class ExtendEventDispatcher extends THREE.EventDispatcher {
+	constructor () {
+		super();
+	}
+
+    addEventListener(type, listener, importance=0){    //add importance
+        if ( this._listeners === undefined ) this._listeners = {};
+    
+            const listeners = this._listeners;
+    
+            if ( listeners[ type ] === undefined ) {
+    
+                listeners[ type ] = [];
+    
+            }
+    
+            if ( !listeners[ type ].some(e=>e.listener == listener )  ) { 
+                //listeners[ type ].push( listener );
+                listeners[type].push({ listener,  importance});
+                listeners[type] = listeners[type].sort((e,a)=> a.importance - e.importance)//add
+            }
+    }
+    
+    hasEventListener(type, listener){
+        if ( this._listeners === undefined ) return false;
+    
+        const listeners = this._listeners;
+    
+        return listeners[ type ] !== undefined &&  listeners[ type ].some(e=>e.listener == listener )  
+    }
+    
+    removeEventListener(type, listener){
+        if ( this._listeners === undefined ) return;
+    
+        const listeners = this._listeners;
+        const listenerArray = listeners[ type ];
+    
+        if ( listenerArray !== undefined ) {
+    
+            /* const index = listenerArray.indexOf( listener );
+    
+            if ( index !== - 1 ) {
+    
+                listenerArray.splice( index, 1 );
+    
+            } */
+    
+            let item = listenerArray.find(e=>e.listener == listener)
+            item && listenerArray.splice(listenerArray.indexOf(item), 1);
+    
+        }
+    }
+    
+    removeEventListeners(type){  //add
+        if(this._listeners && this._listeners[type] !== undefined){
+            delete this._listeners[type];
+        }
+    }
+    
+    removeAllListeners(){  //add
+        this._listeners = {};
+    }
+    
+    dispatchEvent(event){ 
+        if(typeof event == 'string'){//add
+                event = {type:event}
+            }
+            if ( this._listeners === undefined ) return;
+    
+            const listeners = this._listeners;
+            const listenerArray = listeners[ event.type ];
+    
+            if ( listenerArray !== undefined ) {
+    
+                event.target = this;
+    
+                // Make a copy, in case listeners are removed while iterating.
+                 
+                for(let {listener} of listenerArray.slice(0)){
+                    let result = listener.call(this, event);   //add stopContinue
+                    if(result && result.stopContinue){
+                        break
+                    }
+                }
+    
+            }
+    }
+    
+    traverse(callback){ 
+            let result = callback( this );
+            if(result && result.stopContinue){//xzw add
+                return 
+            }
+                 
+            const children = this.children;
+    
+            for ( let i = 0, l = children.length; i < l; i ++ ) {
+    
+                children[ i ].traverse( callback );
+    
+            }
+    }
+    
+};

+ 119 - 0
src/custom/note.txt

@@ -0,0 +1,119 @@
+libs/stats.js 这个文件夹里的内容不在这个工程里
+libs/three.js 这个文件夹里的内容基本上都改过,但是暂时不管,先处理potree
+libs/three.js/build/three.module.js   
+	WebGLRenderer对象里,setSize方法复原了,修改了src\objetcs\tool\Compass.js里renderer.setSize和src\viewer\viewerBase.js里renderer.setSize,换成了renderer.setPixelRatio
+	event.total 我暂时复原了
+
+src/EventDispatcher.js复原了
+potree自己的类恢复到继承EventDispatcher.js了,自定义的类继承的是SelfEventDispatcher.js(扩展后的THREE.EventDispatcher.js)
+src/Actions.js复原了
+src/Annotation.js				复原了
+src/defines.js 					先缓缓,好修改
+src/Enum.js						复原了
+src/Features.js					先缓缓
+src/KeyCodes.js 					复原了
+src/LRU.js 						复原了
+src/PointCloudOctree.js 			先缓缓
+src/PointCloudOctreeGeometry.js			先缓缓
+src/PointCloudTree.js 			复原了
+src/PoTree.js 					先缓缓
+src/Potree_update_visibility.js 	复原了
+src/PotreeRenderer.js 			先缓缓,自定义??
+src/utils.js 					复原了
+src/exporter/DXFExporter.js		复原了
+src/exporter/GeoJSONExporter.js	复原了
+src/loader/BinaryLoader.js		复原了
+src/loader/GeoPackageLoader.js	复原了
+src/loader/POCLoader.js			先缓缓
+src/loader/PointAttribute.js		先缓缓,复原后出错了,很奇怪
+src/loader/ShapefileLoader.js	复原了
+src/materials/EyeDomeLightingMaterial.js		先缓缓
+src/materials/PointCloudMaterial.js			先缓缓
+src/materials/shaders/edl.fs					先缓缓
+src/materials/shaders/pointcloud.fs			先缓缓
+src/materials/shaders/pointcloud.vs			先缓缓
+src/modules/CameraAnimation/CameraAnimation.js			先缓缓
+src/modules/Images360/Images360.js						先缓缓
+src/modules/loader/2.0/DecoderWorker.js					复原了
+src/modules/loader/2.0/DecoderWorker_brotli.js			复原了
+src/modules/loader/2.0/OctreeGeometry.js					复原了
+src/modules/loader/2.0/OctreeLoader.js					复原了
+src/modules/OrientedImages/OrientedImageControls.js		复原了
+src/modules/OrientedImages/OrientedImages.js				复原了
+src/navigation/DeviceOrientationControls.js				复原了
+src/navigation/EarthControls.js							先缓缓,好修改
+src/navigation/FirstPersonControls.js					先缓缓
+src/navigation/InputHandler.js							先缓缓
+src/navigation/OrbitControls.js							先缓缓
+src/navigation/VRControls.js								复原了,加了个debugger,验证一下
+src/tools/create_github_page.js							复原了
+src/tools/create_icons_page.js							复原了
+src/tools/create_potree_page.js							复原了
+src/utils/PointCloudSM.js								复原了,发现没用到
+src/viewer/EDLRenderer.js								先缓缓
+src/viewer/HierarchicalSlider.js							复原了
+src/viewer/HQSplatRenderer.js							先缓缓
+src/viewer/LoadProject.js								先缓缓
+src/viewer/map.js										先缓缓
+src/viewer/potree.css									先缓缓,到时候找赖志彬修改
+src/viewer/PotreeRenderer.js								复原了,发现没用到
+src/viewer/profile.js									复原了
+src/viewer/SaveProject.js								先缓缓
+src/viewer/Scene.js										先缓缓
+src/viewer/sidebar.html									先缓缓
+src/viewer/sidebar.js									先缓缓
+src/viewer/View.js										先缓缓
+src/viewer/viewer.js										先缓缓
+src/viewer/PropertyPanels/AreaPanel.js					先缓缓
+src/viewer/PropertyPanels/CameraAnimationPanel.js		先缓缓
+src/viewer/PropertyPanels/DistancePanel.js				先缓缓
+src/viewer/PropertyPanels/MeasurePanel.js				先缓缓
+src/viewer/PropertyPanels/PointPanel.js					先缓缓
+src/viewer/PropertyPanels/PropertiesPanel.js				复原了
+src/viewer/PropertyPanels/VolumePanel.js					复原了
+src/workers/EptZstandardDecoder_preamble.js				复原了
+/**************************************************************************************************************************************/
+src/settings.js          										额外添加的
+src/start.js             										额外添加的
+src/custom/*             										额外添加的
+src/materials/BasicMaterial.js             						额外添加的
+src/materials/DepthBasicMaterial.js             				额外添加的
+src/materials/ModelTextureMaterial.js             				额外添加的
+src/materials/postprocessing/*             						额外添加的
+src/materials/shaders/basicTextured.fs             				额外添加的
+src/materials/shaders/basicTextured.vs             				额外添加的
+src/materials/shaders/copyCubeMap.fs             				额外添加的
+src/materials/shaders/copyCubeMap.vs             				额外添加的
+src/materials/shaders/depthBasic.fs             				额外添加的
+src/materials/shaders/depthBasic.vs             				额外添加的
+src/materials/shaders/otherShaders.js            				额外添加的
+src/modules/clipModel/*            								额外添加的
+src/modules/datasetAlignment/*            						额外添加的
+src/modules/mergeModel/*            							额外添加的
+src/modules/panoEdit/*            								额外添加的
+src/modules/Particles/*            								额外添加的
+src/modules/route/*            									额外添加的
+src/modules/siteModel/*            								额外添加的
+src/modules/CameraAnimation/CamAniEditor.js            			额外添加的
+src/modules/Images360/DepthImageSampler.js            			额外添加的
+src/modules/Images360/Panorama.js            					额外添加的
+src/modules/tile/*		            							额外添加的
+src/objects/*		            								额外添加的
+src/utils/browser.js            								额外添加的
+src/utils/cameraLight.js            							额外添加的
+src/utils/Common.js            									额外添加的
+src/utils/CursorDeal.js            								额外添加的
+src/utils/DrawUtil.js            								额外添加的
+src/utils/file.js            									额外添加的
+src/utils/History.js            								额外添加的
+src/utils/math.js            									额外添加的
+src/utils/MathLight.js            								额外添加的
+src/utils/request.js            								额外添加的
+src/utils/searchRings.js            							额外添加的
+src/utils/SplitScreen.js            							额外添加的
+src/utils/SplitScreen4Views.js            						额外添加的
+src/utils/transitions.js            							额外添加的
+src/utils/UnitConvert.js            							额外添加的
+src/viewer/viewerBase.js            							额外添加的
+src/viewer/Viewport.js            								额外添加的
+src/viewer/map/*            									额外添加的

+ 995 - 0
src/custom/potree.shim.js

@@ -0,0 +1,995 @@
+// Potree.prototype.addEventListener = function(type, listener, importance=0){ 
+
+// }
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import math from "../utils/math.js";
+import {Utils} from "../utils.js";
+import {BinaryLoader} from "../loader/BinaryLoader.js";
+// import {Features} from "../Features.js";
+import {PointAttribute,PointAttributeTypes} from "../loader/PointAttributes.js";
+import {ProfileWindow} from "../viewer/profile.js";
+import {XHRFactory} from "../XHRFactory.js";
+import {ClipTask, ClipMethod} from "../defines.js";
+import {Box3Helper} from "../utils/Box3Helper.js";
+import {KeyCodes} from "../KeyCodes.js";
+
+Utils.loadSkybox = function(path) {
+    let parent = new THREE.Object3D("skybox_root");
+
+    let camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 100000);
+    if(!window.axisYup)  camera.up.set(0, 0, 1);
+    let scene = new THREE.Scene();
+
+    let format = '.jpg';
+    let urls = [
+        path + 'px' + format, path + 'nx' + format,
+        path + 'py' + format, path + 'ny' + format,
+        path + 'pz' + format, path + 'nz' + format
+    ];
+
+    let materialArray = [];
+    {
+        for (let i = 0; i < 6; i++) {
+            let material = new THREE.MeshBasicMaterial({
+                map: null,
+                side: THREE.BackSide,
+                depthTest: false,
+                depthWrite: false,
+                color: 0x424556
+            });
+
+            materialArray.push(material);
+
+            let loader = new THREE.TextureLoader();
+            loader.load(urls[i],
+                function loaded (texture) {
+                    material.map = texture;
+                    material.needsUpdate = true;
+                    material.color.setHex(0xffffff);
+                }, function progress (xhr) {
+                    // console.log( (xhr.loaded / xhr.total * 100) + '% loaded' );
+                }, function error (xhr) {
+                    console.log('An error happened', xhr);
+                }
+            );
+        }
+    }
+
+    let skyGeometry = new THREE.CubeGeometry(700, 700, 700);
+    let skybox = new THREE.Mesh(skyGeometry, materialArray);
+
+    scene.add(skybox);
+
+    scene.traverse(n => n.frustumCulled = false);
+
+    // z up
+    scene.rotation.x = Math.PI / 2;
+
+    parent.children.push(camera);
+    camera.parent = parent;
+
+    return {camera, scene, parent};
+};
+
+Utils.getMousePointCloudIntersection = function(viewport, mouse, pointer, camera, viewer, pointclouds, pickParams = {} ) {
+    if(!pointclouds || pointclouds.length == 0)return
+    
+    let renderer = viewer.renderer;
+    
+      
+    if(viewport){ //转换到类似整个画面时
+        
+         /*let mouseInViewport = Utils.convertNDCToScreenPosition(pointer, null, viewport.resolution.x, viewport.resolution.y)
+    
+        pickParams.x = mouseInViewport.x   //mouse.x / viewport.width;
+        pickParams.y = mouseInViewport.y //renderer.domElement.clientHeight - mouse.y / viewport.height;  */
+        pickParams.x = mouse.x;
+        pickParams.y = viewport.resolution.y - mouse.y;
+    }else{ 
+        pickParams.x = mouse.x;
+        pickParams.y = renderer.domElement.clientHeight - mouse.y; 
+    } 
+     
+    //console.log('getMousePointCloudIntersection')
+    
+
+     
+    
+    /* if(!raycaster){
+        raycaster = new THREE.Raycaster();
+        raycaster.setFromCamera(pointer, camera); 
+    }  */
+    
+    let raycaster = new THREE.Raycaster();
+    raycaster.setFromCamera(pointer, camera); 
+    let ray = raycaster.ray;
+    
+    let selectedPointcloud = null;
+    let closestDistance = Infinity;
+    let closestIntersection = null;
+    let closestPoint = null;
+    
+    
+    let density
+    let sizeType
+    let size = new Map()  
+    if(pickParams.isMeasuring || Potree.settings.displayMode == 'showPanos'){ //测量或全景模式提高精准度
+        density = Potree.settings.pointDensity 
+        Potree.settings.pointDensity = 'magnifier' 
+        
+        pointclouds.forEach(e=>{//因为全景模式的pointSizeType是fixed所以要还原下
+            size.set(e, e.temp.pointSize)    
+            sizeType = e.material.pointSizeType  
+            e.material.pointSizeType = Potree.config.material.pointSizeType 
+             
+            e.changePointSize(Potree.config.material.realPointSize*2, true)//更改点云大小到能铺满为止,否则容易识别不到
+        }) 
+        Potree.updatePointClouds(pointclouds,  camera, viewport.resolution );
+    }else{
+        if(viewer.viewports.filter(e=>!e.noPointcloud && e.active).length>1 || pickParams.cameraChanged){
+            viewport.beforeRender && viewport.beforeRender()
+            Potree.updatePointClouds(pointclouds,  camera, viewport.resolution ); //不加这句的话hover久了会不准 因node是错的
+            //但依旧需要camera真的移动到那个位置才能加载出点云
+        }
+        
+    }
+    
+    
+     
+    
+    
+    
+    
+    let allPointclouds = [] 
+    for(let pointcloud of pointclouds){ 
+        
+        let point = pointcloud.pick(viewer, viewport, camera, ray, pickParams );
+        
+      
+        
+        if(!point){
+            continue;
+        }
+        allPointclouds.push(pointcloud)
+        
+        
+        let distance = camera.position.distanceTo(point.position);
+
+        if (distance < closestDistance) {
+            closestDistance = distance;
+            selectedPointcloud = pointcloud;
+            closestIntersection = point.position;
+            closestPoint = point;
+        }
+    }
+
+
+    if(pickParams.isMeasuring || Potree.settings.displayMode == 'showPanos'){
+        Potree.settings.pointDensity = density
+        
+        pointclouds.forEach(e=>{
+            e.material.pointSizeType = sizeType
+            e.changePointSize(size.get(e))
+            
+        })
+    }else{
+        /* if(viewer.viewports.filter(e=>!e.noPointcloud).length>1){
+            viewport.afterRender && viewport.afterRender() 
+        } */
+    }
+
+
+    if (selectedPointcloud) {
+        return {
+            location: closestIntersection,
+            distance: closestDistance,
+            pointcloud: selectedPointcloud,
+            point: closestPoint,
+            pointclouds: allPointclouds, //add 
+            normal: new THREE.Vector3().fromArray(closestPoint.normal )//add
+        };
+    } else {
+        return null;
+    }
+    
+};
+
+Utils.renderTargetToDataUrl = function(renderTarget, width, height, renderer, compressRatio = 0.7){
+    let pixelCount = width * height;
+    let buffer = new Uint8Array(4 * pixelCount);
+
+    renderer.readRenderTargetPixels(renderTarget, 0, 0, width, height, buffer);
+    var dataUrl = Utils.pixelsArrayToDataUrl(buffer, width, height, compressRatio)
+    return dataUrl
+    
+} 
+
+Utils.mouseToRay = function(pointer, camera  ){
+ 
+    let vector = new THREE.Vector3(pointer.x, pointer.y, 1); 
+    let origin = new THREE.Vector3(pointer.x, pointer.y, -1); //不能用camera.position,在orbitCamera时不准
+    vector.unproject(camera);
+    origin.unproject(camera);
+    let direction = new THREE.Vector3().subVectors(vector, origin).normalize();
+
+    let ray = new THREE.Ray(origin, direction);
+
+    return ray;
+}
+
+Utils.getPos2d = function(point, camera, dom, viewport){//获取一个三维坐标对应屏幕中的二维坐标
+    var pos
+    if(math.closeTo(camera.position, point, 1e-5) ){ //和相机位置重合时显示会四处飘,看是要改成一直显示中间还是隐藏?
+        pos = new THREE.Vector3(0,0,1.5); //1.5是为了不可见
+    }else{ 
+        pos = point.clone().project(camera)	//比之前hotspot的计算方式写得简单  project用于3转2(求法同shader); unproject用于2转3 :new r.Vector3(e.x, e.y, -1).unproject(this.camera);
+    }
+    
+    
+    var x,y,left,top;
+    x = (pos.x + 1) / 2 * dom.clientWidth * viewport.width;
+    y = (1 - (pos.y + 1) / 2) * dom.clientHeight * viewport.height; 
+    left = viewport.left * dom.clientWidth;
+    top = (1- viewport.bottom - viewport.height) * dom.clientHeight;
+     
+
+    var inSight = pos.x <= 1 &&  pos.x >= -1    //是否在屏幕中   
+                && pos.x <= 1 &&  pos.y >= -1 
+ 
+
+    return {
+        pos:  new THREE.Vector2(left+x,top+y) ,// 屏幕像素坐标
+        vector:  pos,   //(范围 -1 ~ 1)
+        trueSide : pos.z<1, //trueSide为false时,即使在屏幕范围内可见,也是反方向的另一个不可以被渲染的点   参见Tag.update
+        inSight : inSight,	//在屏幕范围内可见,
+        posInViewport: new THREE.Vector2(x,y)
+    };
+} 
+
+Utils.screenPass = new function () {
+	this.screenScene = new THREE.Scene();
+	this.screenQuad = new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2, 1));
+	this.screenQuad.material.depthTest = true;
+	this.screenQuad.material.depthWrite = true;
+	this.screenQuad.material.transparent = true;
+	this.screenScene.add(this.screenQuad);
+	this.camera = new THREE.Camera();
+
+	this.render = function (renderer, material, target) {
+		this.screenQuad.material = material;
+
+		if (typeof target === 'undefined') {
+			renderer.render(this.screenScene, this.camera);
+		} else {
+            renderer.setRenderTarget(target)
+			renderer.render(this.screenScene, this.camera);
+		}
+	};
+}();
+
+//add
+Utils.computePointcloudsBound = function(pointclouds){
+    var boundingBox = new THREE.Box3();
+    pointclouds.forEach(pointcloud=>{
+        pointcloud.updateBound()
+        boundingBox.union(pointcloud.bound)
+    })
+    var boundSize = boundingBox.getSize(new THREE.Vector3)
+    var center = boundingBox.getCenter(new THREE.Vector3)
+    return {boundSize, center, boundingBox}
+}
+
+
+Utils.convertScreenPositionToNDC = function(pointer, mouse, width, height) { 
+    return pointer = pointer || new THREE.Vector2,
+        pointer.x = mouse.x / width * 2 - 1,
+        pointer.y = 2 * -(mouse.y / height) + 1,
+        pointer 
+}
+
+Utils.convertNDCToScreenPosition = function(pointer, mouse, width, height) { 
+    return mouse = mouse || new THREE.Vector2,
+        mouse.x = Math.round((pointer.x + 1 ) / 2 * width),
+        mouse.y = Math.round(-(pointer.y - 1 ) / 2 * height),
+        mouse 
+}
+
+Utils.getOrthoCameraMoveVec = function(pointerDelta, camera ){//获取当camera为Ortho型时 屏幕点1 到 屏幕点2 的三维距离
+     
+    let cameraViewWidth = camera.right / camera.zoom  
+    let cameraViewHeight = camera.top / camera.zoom
+    let moveVec = new THREE.Vector3; 
+    moveVec.set( pointerDelta.x * cameraViewWidth  , pointerDelta.y * cameraViewHeight  , 0).applyQuaternion(camera.quaternion)  
+    return moveVec              
+}
+
+Utils.VectorFactory = {
+    fromArray : function(t) {
+        if (t) {
+            if (t.length < 2 || t.length > 3)
+                console.error("Wrong number of ordinates for a point!");
+            return 3 === t.length ? (new THREE.Vector3).fromArray(t) : (new THREE.Vector2).fromArray(t)
+        }
+    },
+    fromArray3 : function(t) {
+        if (t) {
+            if (3 !== t.length)
+                console.error("Wrong number of ordinates for a point!");
+            return (new THREE.Vector3).fromArray(t)
+        }
+    },
+    fromArray2 : function(t) {
+        if (t) {
+            if (2 !== t.length)
+                console.error("Wrong number of ordinates for a point!");
+            return (new THREE.Vector2).fromArray(t)
+        }
+    },
+    toString : function(t) {
+        return t.x.toFixed(8) + "," + t.y.toFixed(8) + "," + t.z.toFixed(3)
+    }
+
+}
+ 
+Utils.QuaternionFactory = {
+    rot90 : (new THREE.Quaternion).setFromAxisAngle(new THREE.Vector3(0,0,1), THREE.Math.degToRad(-90)),
+    fromArray : function(t) {
+        if (t) {
+            if (4 !== t.length)
+                console.error("Wrong number of ordinates for a quaternion!");
+            return new THREE.Quaternion(t[1],t[2],t[3],t[0]).multiply(this.rot90)
+        }
+    }
+    ,
+    toArray : function(t) {
+        if (t) {
+            var e = t.clone().multiply(a).toArray();
+            return [e[3], e[0], e[1], e[2]]
+        }
+    }
+    ,
+    fromLonLat : function(t) {
+        if (t)
+            return (new THREE.Quaternion).setFromEuler(new THREE.Euler(t.lon,t.lat,0))
+    }
+    ,
+    toLonLat : function(t) {
+        if (t) {
+            var e = (new THREE.Euler).setFromQuaternion(t);
+            return {
+                lon: e.x,
+                lat: e.y
+            }
+        }
+    }
+    
+    
+}
+ 
+Utils.datasetPosTransform = function(o={}){  
+ 
+    let pointcloud = o.pointcloud || viewer.scene.pointclouds.find(e=>e.dataset_id == o.datasetId)
+    let tranMatrix 
+    if(pointcloud){ 
+        if(Potree.settings.editType == 'merge'){
+            tranMatrix = o.fromDataset ? pointcloud.matrixWorld : new THREE.Matrix4().copy(pointcloud.matrixWorld).invert() 
+        }else{
+            tranMatrix = o.fromDataset ? pointcloud.transformMatrix : pointcloud.transformInvMatrix 
+        }
+    }else{ 
+        if(Potree.settings.intersectOnObjs){
+            let object = o.object || viewer.objs.children.find(e=>e.dataset_id == o.datasetId)
+            if(object){
+                tranMatrix = o.fromDataset ? object.matrixWorld : new THREE.Matrix4().copy(object.matrixWorld).invert() 
+            }  
+        } 
+    } 
+    if(tranMatrix){
+        return (new THREE.Vector3).copy(o.position).applyMatrix4(tranMatrix)
+    }else{
+        if(o.datasetId != void 0){
+            console.error(`datasetPosTransform找不到datasetId为${o.datasetId}的数据集或模型,请检查数据, 模型未创建或删除`)
+            //很可能是旧的热点,需要删除
+        }
+    }
+    
+}
+
+Utils.datasetRotTransform = function(o={}){
+    let pointcloud = o.pointcloud || viewer.scene.pointclouds.find(e=>e.dataset_id == o.datasetId)
+    if(pointcloud){
+        var matrix, newMatrix, result
+        
+        if(o.rotation){
+            matrix = new THREE.Matrix4().makeRotationFromEuler(o.rotation)
+        }else if(o.quaternion){
+            matrix = new THREE.Matrix4().makeRotationFromQuaternion(o.quaternion) 
+        }else if(o.matrix){
+            matrix = o.matrix.clone()
+        }else{
+            return
+        } 
+        let rotateMatrix = o.fromDataset ? pointcloud.rotateMatrix : pointcloud.rotateInvMatrix
+        newMatrix = new THREE.Matrix4().multiplyMatrices(rotateMatrix, matrix  ) 
+         
+        if(o.getRotation){
+            result = new THREE.Euler().setFromRotationMatrix(newMatrix)
+        }else if(o.getQuaternion){
+            result = new THREE.Quaternion().setFromRotationMatrix(newMatrix)
+        }else if(o.getMatrix){
+            result = newMatrix
+        }
+        
+        return result
+        
+    }
+
+    
+}
+
+Utils.isInsideFrustum = function(bounding, camera){// boundingBox在视野范围内有可见部分
+    let frustumMatrix = new THREE.Matrix4
+    frustumMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)
+    
+    let frustum = new THREE.Frustum();
+    frustum.setFromProjectionMatrix(frustumMatrix) 
+     
+    if(bounding instanceof THREE.Sphere){
+        return frustum.intersectsSphere(bounding)  
+    }else{
+        return frustum.intersectsBox(bounding)  
+    }
+}
+
+Utils.isInsideBox = function(object,  boxMatrixInverse){//object可以是点或者bounding, box原为1*1*1,但可能形变
+    let frustum = new THREE.Frustum();
+    frustum.setFromProjectionMatrix(boxMatrixInverse) 
+     
+    if(object instanceof THREE.Box3){
+        return frustum.intersectsSphere(object)  
+    }else if(object instanceof Array){//点合集,先求Sphere setFromPoints
+        let sphere = new THREE.Sphere()
+        sphere.setFromPoints(object) 
+        return this.isInsideBox(sphere, boxMatrixInverse)
+
+    }else if(object instanceof THREE.Sphere){
+        return frustum.intersectsSphere(object)  
+    }else if(object instanceof THREE.Vector3){
+        return frustum.containsPoint(object)  
+    }
+
+    /* containsPoint: ƒ containsPoint( point ) 
+    intersectsBox: ƒ intersectsBox( box )
+    intersectsObject: ƒ intersectsObject( object )//geo
+    intersectsSphere: ƒ intersectsSphere( sphere )
+    intersectsSprite: ƒ intersectsSprite( sprite )
+     */
+
+} 
+
+Utils.getIntersect = function (camera, meshes, pointer, raycaster) {
+    //获取鼠标和meshes交点
+    camera.updateMatrixWorld()
+    if(!raycaster){//getMouseIntersect
+        raycaster = new THREE.Raycaster()
+        var origin = new THREE.Vector3(pointer.x, pointer.y, -1).unproject(camera),
+        end = new THREE.Vector3(pointer.x, pointer.y, 1).unproject(camera)
+        var dir = end.sub(origin).normalize()
+        raycaster.set(origin, dir)
+    } 
+    
+    meshes.forEach(e=>{ 
+        raycaster.layers.enable(math.getBaseLog(e.layers.mask,2)) 
+    }) 
+    var n = raycaster.intersectObjects(meshes)
+    if (0 === n.length) return null
+    return n[0]
+} 
+
+BinaryLoader.prototype.load = function(node){
+    if (node.loaded) {
+        return;
+    }
+
+    let url = node.getURL();
+
+    if (this.version.equalOrHigher('1.4')) {
+        url += '.bin';
+    }
+    url += '?m='+node.pcoGeometry.timeStamp //add
+    
+    let xhr = XHRFactory.createXMLHttpRequest();
+    xhr.open('GET', url, true);
+    xhr.responseType = 'arraybuffer';
+    xhr.overrideMimeType('text/plain; charset=x-user-defined');
+    xhr.onreadystatechange = () => {
+        if (xhr.readyState === 4) {
+            if((xhr.status === 200 || xhr.status === 0) &&  xhr.response !== null){
+                let buffer = xhr.response;
+                this.parse(node, buffer);
+            } else {
+                //console.error(`Failed to load file! HTTP status: ${xhr.status}, file: ${url}`);
+                throw new Error(`Failed to load file! HTTP status: ${xhr.status}, file: ${url}`);
+            }
+        }
+    };
+    
+    try {
+        xhr.send(null);
+    } catch (e) {
+        console.log('fehler beim laden der punktwolke: ' + e);
+    }
+}
+
+PointAttribute.RGBA_PACKED = new PointAttribute("rgba", PointAttributeTypes.DATA_TYPE_INT8, 4);
+
+PointAttribute.INTENSITY = new PointAttribute("intensity", PointAttributeTypes.DATA_TYPE_UINT16, 1);
+
+PointAttribute.CLASSIFICATION = new PointAttribute("classification", PointAttributeTypes.DATA_TYPE_UINT8, 1);
+
+PointAttribute.GPS_TIME = new PointAttribute("gps-time", PointAttributeTypes.DATA_TYPE_DOUBLE, 1);
+
+ProfileWindow.prototype.initTHREE = function(){
+    this.renderer = new THREE.WebGLRenderer({alpha: true, premultipliedAlpha: false});
+    this.renderer.setClearColor(0x000000, 0);
+    this.renderer.setSize(10, 10);
+    this.renderer.autoClear = false;
+    this.renderArea.append($(this.renderer.domElement));
+    this.renderer.domElement.tabIndex = '2222';
+    $(this.renderer.domElement).css('width', '100%');
+    $(this.renderer.domElement).css('height', '100%');
+
+
+    {
+        let gl = this.renderer.getContext();
+
+        if(gl.createVertexArray == null){
+            let extVAO = gl.getExtension('OES_vertex_array_object');
+
+            if(!extVAO){
+                throw new Error("OES_vertex_array_object extension not supported");
+            }
+
+            gl.createVertexArray = extVAO.createVertexArrayOES.bind(extVAO);
+            gl.bindVertexArray = extVAO.bindVertexArrayOES.bind(extVAO);
+        }
+        
+    }
+
+    this.camera = new THREE.OrthographicCamera(-1000, 1000, 1000, -1000, -1000, 1000);
+    this.camera.up.set(0, 0, 1);
+    this.camera.rotation.order = "ZXY";
+    this.camera.rotation.x = Math.PI / 2.0;
+
+
+    this.scene = new THREE.Scene();
+    this.profileScene = new THREE.Scene();
+
+    let sg = new THREE.SphereGeometry(1, 16, 16);
+    let sm = new THREE.MeshNormalMaterial();
+    this.pickSphere = new THREE.Mesh(sg, sm);
+    this.scene.add(this.pickSphere);
+
+    this.viewerPickSphere = new THREE.Mesh(sg, sm);
+}
+
+
+Potree.updatePointClouds =  function(pointclouds,camera, areaSize /* renderer */){
+ 
+	for (let pointcloud of pointclouds) {
+		let start = performance.now();
+
+		for (let profileRequest of pointcloud.profileRequests) {
+			profileRequest.update();
+
+			let duration = performance.now() - start;
+			if(duration > 5){
+				break;
+			}
+		}
+
+		let duration = performance.now() - start;
+	}
+    
+    
+	let result = Potree.updateVisibility(pointclouds, camera, areaSize  );
+
+	for (let pointcloud of pointclouds) { 
+		//pointcloud.updateMaterial(pointcloud.material, pointcloud.visibleNodes, camera, renderer);//转移到渲染时
+		pointcloud.updateVisibleBounds();
+	}
+
+	exports.lru.freeMemory();//即Potree.lru 能看到所有在加载的node
+
+	return result;
+};
+
+
+
+Potree.updateVisibilityStructures = function(pointclouds, camera, areaSize) {
+	let frustums = [];
+	let camObjPositions = [];
+	let priorityQueue = new BinaryHeap(function (x) { return 1 / x.weight; });
+
+	for (let i = 0; i < pointclouds.length; i++) {
+		let pointcloud = pointclouds[i];
+
+		if (!pointcloud.initialized()) {
+			continue;
+		}
+
+		pointcloud.numVisibleNodes = 0;
+		pointcloud.numVisiblePoints = 0;
+		pointcloud.deepestVisibleLevel = 0;
+		pointcloud.visibleNodes = [];
+		pointcloud.visibleGeometry = [];
+
+		// frustum in object space
+		camera.updateMatrixWorld();
+		let frustum = new THREE.Frustum();
+		let viewI = camera.matrixWorldInverse;
+		let world = pointcloud.matrixWorld;
+		
+		// use close near plane for frustum intersection
+		let frustumCam = camera.clone();
+		frustumCam.near = Math.min(camera.near, 0.1);
+		frustumCam.updateProjectionMatrix();
+		let proj = camera.projectionMatrix;
+
+		let fm = new THREE.Matrix4().multiply(proj).multiply(viewI).multiply(world);
+		frustum.setFromProjectionMatrix(fm);
+		frustums.push(frustum);
+
+		// camera position in object space
+		let view = camera.matrixWorld;
+		let worldI = world.clone().invert();
+		let camMatrixObject = new THREE.Matrix4().multiply(worldI).multiply(view);
+		let camObjPos = new THREE.Vector3().setFromMatrixPosition(camMatrixObject);
+		camObjPositions.push(camObjPos);
+        
+        // 因漫游模式而隐藏的话 依旧需要加入visibleNodes,因为pick需要
+        
+                                                    /*  viewer.getObjVisiByReason(pointcloud, 'datasetSelection') */
+		if (pointcloud.visible || pointcloud.unvisibleReasons && pointcloud.unvisibleReasons.length == 1 && pointcloud.unvisibleReasons[0].reason == 'displayMode'   &&  pointcloud.root !== null) {//改 visible -> 
+			priorityQueue.push({pointcloud: i, node: pointcloud.root, weight: Number.MAX_VALUE});
+		}
+
+		// hide all previously visible nodes
+		// if(pointcloud.root instanceof PointCloudOctreeNode){
+		//	pointcloud.hideDescendants(pointcloud.root.sceneNode);
+		// }
+		if (pointcloud.root.isTreeNode()) {
+			pointcloud.hideDescendants(pointcloud.root.sceneNode);
+		}
+
+		for (let j = 0; j < pointcloud.boundingBoxNodes.length; j++) {
+			pointcloud.boundingBoxNodes[j].visible = false;
+		}
+	}
+
+	return {
+		'frustums': frustums,
+		'camObjPositions': camObjPositions,
+		'priorityQueue': priorityQueue
+	};
+};
+
+
+Potree.updateVisibility = function(pointclouds, camera, areaSize){
+
+	let numVisibleNodes = 0;
+	let numVisiblePoints = 0;
+
+	let numVisiblePointsInPointclouds = new Map(pointclouds.map(pc => [pc, 0]));
+
+	let visibleNodes = [];
+	let visibleGeometry = [];
+	let unloadedGeometry = [];
+
+	let lowestSpacing = Infinity;
+
+	// calculate object space frustum and cam pos and setup priority queue
+	let s = Potree.updateVisibilityStructures(pointclouds, camera, areaSize);//得到相机可见范围
+	let frustums = s.frustums;
+	let camObjPositions = s.camObjPositions;
+	let priorityQueue = s.priorityQueue;
+
+	let loadedToGPUThisFrame = 0;
+	
+	let domWidth = areaSize.x; //renderer.domElement.clientWidth;
+	let domHeight = areaSize.y;//renderer.domElement.clientHeight;
+
+	// check if pointcloud has been transformed
+	// some code will only be executed if changes have been detected
+	if(!Potree._pointcloudTransformVersion){
+		Potree._pointcloudTransformVersion = new Map();
+	}
+	let pointcloudTransformVersion = Potree._pointcloudTransformVersion;
+	for(let pointcloud of pointclouds){
+
+		if(!viewer.getObjVisiByReason(pointcloud, 'datasetSelection')){//改 visible ->  
+			continue;
+		}
+
+		pointcloud.updateMatrixWorld();
+
+		if(!pointcloudTransformVersion.has(pointcloud)){
+			pointcloudTransformVersion.set(pointcloud, {number: 0, transform: pointcloud.matrixWorld.clone()});
+		}else{
+			let version = pointcloudTransformVersion.get(pointcloud);
+
+			if(!version.transform.equals(pointcloud.matrixWorld)){
+				version.number++;
+				version.transform.copy(pointcloud.matrixWorld);
+
+				pointcloud.dispatchEvent({
+					type: "transformation_changed",
+					target: pointcloud
+				});
+			}
+		}
+	}
+
+	while (priorityQueue.size() > 0) {
+		let element = priorityQueue.pop();//其实是拿第一个, 再把最后一个放到前面
+         
+		let node = element.node;
+		let parent = element.parent;
+		let pointcloud = pointclouds[element.pointcloud];
+
+		// { // restrict to certain nodes for debugging
+		//	let allowedNodes = ["r", "r0", "r4"];
+		//	if(!allowedNodes.includes(node.name)){
+		//		continue;
+		//	}
+		// }
+
+		let box = node.getBoundingBox();
+		let frustum = frustums[element.pointcloud];
+		let camObjPos = camObjPositions[element.pointcloud];
+
+		let insideFrustum = frustum.intersectsBox(box);
+		let maxLevel = pointcloud.maxLevel == void 0 ? Infinity : pointcloud.maxLevel;
+		let level = node.getLevel();
+		let visible = insideFrustum;
+		visible = visible && !(numVisiblePoints + node.getNumPoints() > Potree.pointBudget);
+		visible = visible && !(numVisiblePointsInPointclouds.get(pointcloud) + node.getNumPoints() > pointcloud.pointBudget);
+		visible = visible && level <= maxLevel; //< 改为 <=
+		//visible = visible || node.getLevel() <= 2;
+
+		let clipBoxes = pointcloud.material.clipBoxes;
+		if(true && clipBoxes.length > 0){
+
+			//node.debug = false;
+
+			let numIntersecting = 0;
+			let numIntersectionVolumes = 0;
+
+			//if(node.name === "r60"){
+			//	var a = 10;
+			//}
+
+			for(let clipBox of clipBoxes){
+
+				let pcWorldInverse = pointcloud.matrixWorld.clone().invert();
+				let toPCObject = pcWorldInverse.multiply(clipBox.box.matrixWorld);
+
+				let px = new THREE.Vector3(+0.5, 0, 0).applyMatrix4(pcWorldInverse);
+				let nx = new THREE.Vector3(-0.5, 0, 0).applyMatrix4(pcWorldInverse);
+				let py = new THREE.Vector3(0, +0.5, 0).applyMatrix4(pcWorldInverse);
+				let ny = new THREE.Vector3(0, -0.5, 0).applyMatrix4(pcWorldInverse);
+				let pz = new THREE.Vector3(0, 0, +0.5).applyMatrix4(pcWorldInverse);
+				let nz = new THREE.Vector3(0, 0, -0.5).applyMatrix4(pcWorldInverse);
+
+				let pxN = new THREE.Vector3().subVectors(nx, px).normalize();
+				let nxN = pxN.clone().multiplyScalar(-1);
+				let pyN = new THREE.Vector3().subVectors(ny, py).normalize();
+				let nyN = pyN.clone().multiplyScalar(-1);
+				let pzN = new THREE.Vector3().subVectors(nz, pz).normalize();
+				let nzN = pzN.clone().multiplyScalar(-1);
+
+				let pxPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(pxN, px);
+				let nxPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(nxN, nx);
+				let pyPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(pyN, py);
+				let nyPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(nyN, ny);
+				let pzPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(pzN, pz);
+				let nzPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(nzN, nz);
+
+				//if(window.debugdraw !== undefined && window.debugdraw === true && node.name === "r60"){
+
+				//	Potree.utils.debugPlane(viewer.scene.scene, pxPlane, 1, 0xFF0000);
+				//	Potree.utils.debugPlane(viewer.scene.scene, nxPlane, 1, 0x990000);
+				//	Potree.utils.debugPlane(viewer.scene.scene, pyPlane, 1, 0x00FF00);
+				//	Potree.utils.debugPlane(viewer.scene.scene, nyPlane, 1, 0x009900);
+				//	Potree.utils.debugPlane(viewer.scene.scene, pzPlane, 1, 0x0000FF);
+				//	Potree.utils.debugPlane(viewer.scene.scene, nzPlane, 1, 0x000099);
+
+				//	Potree.utils.debugBox(viewer.scene.scene, box, new THREE.Matrix4(), 0x00FF00);
+				//	Potree.utils.debugBox(viewer.scene.scene, box, pointcloud.matrixWorld, 0xFF0000);
+				//	Potree.utils.debugBox(viewer.scene.scene, clipBox.box.boundingBox, clipBox.box.matrixWorld, 0xFF0000);
+
+				//	window.debugdraw = false;
+				//}
+
+				let frustum = new THREE.Frustum(pxPlane, nxPlane, pyPlane, nyPlane, pzPlane, nzPlane);
+				let intersects = frustum.intersectsBox(box);
+
+				if(intersects){
+					numIntersecting++;
+				}
+				numIntersectionVolumes++;
+			}
+
+			let insideAny = numIntersecting > 0;
+			let insideAll = numIntersecting === numIntersectionVolumes;
+
+			if(pointcloud.material.clipTask === ClipTask.SHOW_INSIDE){
+				if(pointcloud.material.clipMethod === ClipMethod.INSIDE_ANY && insideAny){
+					//node.debug = true
+				}else if(pointcloud.material.clipMethod === ClipMethod.INSIDE_ALL && insideAll){
+					//node.debug = true;
+				}else{
+					visible = false;
+				}
+			} else if(pointcloud.material.clipTask === ClipTask.SHOW_OUTSIDE){
+				//if(pointcloud.material.clipMethod === ClipMethod.INSIDE_ANY && !insideAny){
+				//	//visible = true;
+				//	let a = 10;
+				//}else if(pointcloud.material.clipMethod === ClipMethod.INSIDE_ALL && !insideAll){
+				//	//visible = true;
+				//	let a = 20;
+				//}else{
+				//	visible = false;
+				//}
+			}
+			
+
+		}
+
+		// visible = ["r", "r0", "r06", "r060"].includes(node.name);
+		// visible = ["r"].includes(node.name);
+
+		if (node.spacing) {
+			lowestSpacing = Math.min(lowestSpacing, node.spacing);
+		} else if (node.geometryNode && node.geometryNode.spacing) {
+			lowestSpacing = Math.min(lowestSpacing, node.geometryNode.spacing);
+		}
+
+		if (numVisiblePoints + node.getNumPoints() > Potree.pointBudget) {
+			break;
+		}
+
+		if (!visible) {
+			continue;
+		}
+
+		// TODO: not used, same as the declaration?
+		// numVisibleNodes++;
+		numVisiblePoints += node.getNumPoints();
+		let numVisiblePointsInPointcloud = numVisiblePointsInPointclouds.get(pointcloud);
+		numVisiblePointsInPointclouds.set(pointcloud, numVisiblePointsInPointcloud + node.getNumPoints());
+
+		pointcloud.numVisibleNodes++;
+		pointcloud.numVisiblePoints += node.getNumPoints();
+
+		if (node.isGeometryNode() && (!parent || parent.isTreeNode())) {
+			if (node.isLoaded() && loadedToGPUThisFrame < 2) {
+				node = pointcloud.toTreeNode(node, parent);
+				loadedToGPUThisFrame++;
+			} else {
+				unloadedGeometry.push({pointcloud,node});
+				visibleGeometry.push(node);
+			}
+		}
+
+		if (node.isTreeNode()) {
+			exports.lru.touch(node.geometryNode);
+			node.sceneNode.visible = true;
+			node.sceneNode.material = pointcloud.material;
+
+			visibleNodes.push(node);
+			pointcloud.visibleNodes.push(node);
+
+			if(node._transformVersion === undefined){
+				node._transformVersion = -1;
+			}
+			let transformVersion = pointcloudTransformVersion.get(pointcloud);
+			if(node._transformVersion !== transformVersion.number){
+				node.sceneNode.updateMatrix();
+				//node.sceneNode.matrixWorld.multiplyMatrices(pointcloud.matrixWorld, node.sceneNode.matrix);	
+				node.sceneNode.matrixWorld.multiplyMatrices(pointcloud.matrixWorld, node.sceneNode.matrix);	
+				
+                node._transformVersion = transformVersion.number;
+                               
+			}
+
+			if (pointcloud.showBoundingBox && !node.boundingBoxNode && node.getBoundingBox) {
+				let boxHelper = new Box3Helper(node.getBoundingBox());
+				boxHelper.matrixAutoUpdate = false;
+				pointcloud.boundingBoxNodes.push(boxHelper);
+				node.boundingBoxNode = boxHelper;
+				node.boundingBoxNode.matrix.copy(pointcloud.matrixWorld);
+			} else if (pointcloud.showBoundingBox) {
+				node.boundingBoxNode.visible = true;
+				node.boundingBoxNode.matrix.copy(pointcloud.matrixWorld);
+			} else if (!pointcloud.showBoundingBox && node.boundingBoxNode) {
+				node.boundingBoxNode.visible = false;
+			}
+
+			// if(node.boundingBoxNode !== undefined && exports.debug.allowedNodes !== undefined){
+			// 	if(!exports.debug.allowedNodes.includes(node.name)){
+			// 		node.boundingBoxNode.visible = false;
+			// 	}
+			// }
+		}
+
+		// add child nodes to priorityQueue
+		let children = node.getChildren();
+		for (let i = 0; i < children.length; i++) {
+			let child = children[i];
+
+			let weight = 0; 
+			if(camera.isPerspectiveCamera){
+				let sphere = child.getBoundingSphere();
+				let center = sphere.center;
+				//let distance = sphere.center.distanceTo(camObjPos);
+				
+				let dx = camObjPos.x - center.x;
+				let dy = camObjPos.y - center.y;
+				let dz = camObjPos.z - center.z;
+				
+				let dd = dx * dx + dy * dy + dz * dz;
+				let distance = Math.sqrt(dd);
+				
+				
+				let radius = sphere.radius;
+				
+				let fov = (camera.fov * Math.PI) / 180;
+				let slope = Math.tan(fov / 2);
+				let projFactor = (0.5 * domHeight) / (slope * distance);
+				let screenPixelRadius = radius * projFactor;
+				
+				if(screenPixelRadius < pointcloud.minimumNodePixelSize){
+					continue;
+				}
+			
+				weight = screenPixelRadius;
+
+				if(distance - radius < 0){
+					weight = Number.MAX_VALUE;
+				}
+			} else {
+				// TODO ortho visibility
+				let bb = child.getBoundingBox();				
+				let distance = child.getBoundingSphere().center.distanceTo(camObjPos);
+				let diagonal = bb.max.clone().sub(bb.min).length();
+				//weight = diagonal / distance;
+
+				weight = diagonal;
+			}
+
+			priorityQueue.push({pointcloud: element.pointcloud, node: child, parent: node, weight: weight});
+		}
+	}// end priority queue loop
+
+	{ // update DEM  这是什么
+		let maxDEMLevel = 4;
+		let candidates = pointclouds.filter(p => (p.generateDEM && p.dem instanceof Potree.DEM));
+		for (let pointcloud of candidates) {
+			let updatingNodes = pointcloud.visibleNodes.filter(n => n.getLevel() <= maxDEMLevel);
+			pointcloud.dem.update(updatingNodes);
+		}
+	}
+    //加载点云
+	for (let i = 0; i < Math.min(Potree.maxNodesLoading, unloadedGeometry.length); i++) {
+		unloadedGeometry[i].node.load(unloadedGeometry[i].pointcloud.pcoGeometry); 
+	}
+
+	return {
+		visibleNodes: visibleNodes,
+		numVisiblePoints: numVisiblePoints,
+		lowestSpacing: lowestSpacing
+	};
+};
+
+KeyCodes.BACKSPACE = 8

+ 537 - 0
src/custom/three.shim.js

@@ -0,0 +1,537 @@
+import * as THREE from "../../libs/three.js/build/three.module.js";
+
+!function() {
+    if ("performance"in window == 0 && (window.performance = {}),
+    "now"in window.performance == 0) {
+        var e = Date.now();
+        performance.timing && performance.timing.navigationStart && (e = performance.timing.navigationStart),
+        window.performance.now = function() {
+            return Date.now() - e
+        }
+    }
+}(),
+THREE.WebGLRenderer.prototype.paramThreeToGL = function(e) {
+    var t, i = this.extensions, r = this.getContext();//context;
+    if (e === THREE.RepeatWrapping)
+        return r.REPEAT;
+    if (e === THREE.ClampToEdgeWrapping)
+        return r.CLAMP_TO_EDGE;
+    if (e === THREE.MirroredRepeatWrapping)
+        return r.MIRRORED_REPEAT;
+    if (e === THREE.NearestFilter)
+        return r.NEAREST;
+    if (e === THREE.NearestMipMapNearestFilter)
+        return r.NEAREST_MIPMAP_NEAREST;
+    if (e === THREE.NearestMipMapLinearFilter)
+        return r.NEAREST_MIPMAP_LINEAR;
+    if (e === THREE.LinearFilter)
+        return r.LINEAR;
+    if (e === THREE.LinearMipMapNearestFilter)
+        return r.LINEAR_MIPMAP_NEAREST;
+    if (e === THREE.LinearMipMapLinearFilter)
+        return r.LINEAR_MIPMAP_LINEAR;
+    if (e === THREE.UnsignedByteType)
+        return r.UNSIGNED_BYTE;
+    if (e === THREE.UnsignedShort4444Type)
+        return r.UNSIGNED_SHORT_4_4_4_4;
+    if (e === THREE.UnsignedShort5551Type)
+        return r.UNSIGNED_SHORT_5_5_5_1;
+    if (e === THREE.UnsignedShort565Type)
+        return r.UNSIGNED_SHORT_5_6_5;
+    if (e === THREE.ByteType)
+        return r.BYTE;
+    if (e === THREE.ShortType)
+        return r.SHORT;
+    if (e === THREE.UnsignedShortType)
+        return r.UNSIGNED_SHORT;
+    if (e === THREE.IntType)
+        return r.INT;
+    if (e === THREE.UnsignedIntType)
+        return r.UNSIGNED_INT;
+    if (e === THREE.FloatType)
+        return r.FLOAT;
+    if (t = i.get("OES_texture_half_float"),
+    null !== t && e === THREE.HalfFloatType)
+        return t.HALF_FLOAT_OES;
+    if (e === THREE.AlphaFormat)
+        return r.ALPHA;
+    if (e === THREE.RGBFormat)
+        return r.RGB;
+    if (e === THREE.RGBAFormat)
+        return r.RGBA;
+    if (e === THREE.LuminanceFormat)
+        return r.LUMINANCE;
+    if (e === THREE.LuminanceAlphaFormat)
+        return r.LUMINANCE_ALPHA;
+    if (e === THREE.AddEquation)
+        return r.FUNC_ADD;
+    if (e === THREE.SubtractEquation)
+        return r.FUNC_SUBTRACT;
+    if (e === THREE.ReverseSubtractEquation)
+        return r.FUNC_REVERSE_SUBTRACT;
+    if (e === THREE.ZeroFactor)
+        return r.ZERO;
+    if (e === THREE.OneFactor)
+        return r.ONE;
+    if (e === THREE.SrcColorFactor)
+        return r.SRC_COLOR;
+    if (e === THREE.OneMinusSrcColorFactor)
+        return r.ONE_MINUS_SRC_COLOR;
+    if (e === THREE.SrcAlphaFactor)
+        return r.SRC_ALPHA;
+    if (e === THREE.OneMinusSrcAlphaFactor)
+        return r.ONE_MINUS_SRC_ALPHA;
+    if (e === THREE.DstAlphaFactor)
+        return r.DST_ALPHA;
+    if (e === THREE.OneMinusDstAlphaFactor)
+        return r.ONE_MINUS_DST_ALPHA;
+    if (e === THREE.DstColorFactor)
+        return r.DST_COLOR;
+    if (e === THREE.OneMinusDstColorFactor)
+        return r.ONE_MINUS_DST_COLOR;
+    if (e === THREE.SrcAlphaSaturateFactor)
+        return r.SRC_ALPHA_SATURATE;
+    if (t = i.get("WEBGL_compressed_texture_s3tc"),
+    null !== t) {
+        if (e === THREE.RGB_S3TC_DXT1_Format)
+            return t.COMPRESSED_RGB_S3TC_DXT1_EXT;
+        if (e === THREE.RGBA_S3TC_DXT1_Format)
+            return t.COMPRESSED_RGBA_S3TC_DXT1_EXT;
+        if (e === THREE.RGBA_S3TC_DXT3_Format)
+            return t.COMPRESSED_RGBA_S3TC_DXT3_EXT;
+        if (e === THREE.RGBA_S3TC_DXT5_Format)
+            return t.COMPRESSED_RGBA_S3TC_DXT5_EXT
+    }
+    if (t = i.get("WEBGL_compressed_texture_pvrtc"),
+    null !== t) {
+        if (e === THREE.RGB_PVRTC_4BPPV1_Format)
+            return t.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
+        if (e === THREE.RGB_PVRTC_2BPPV1_Format)
+            return t.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
+        if (e === THREE.RGBA_PVRTC_4BPPV1_Format)
+            return t.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
+        if (e === THREE.RGBA_PVRTC_2BPPV1_Format)
+            return t.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG
+    }
+    if (t = i.get("WEBGL_compressed_texture_etc1"),
+    null !== t && e === THREE.RGB_ETC1_Format)
+        return t.COMPRESSED_RGB_ETC1_WEBGL;
+    if (t = i.get("EXT_blend_minmax"),
+    null !== t) {
+        if (e === THREE.MinEquation)
+            return t.MIN_EXT;
+        if (e === THREE.MaxEquation)
+            return t.MAX_EXT
+    }
+    return 0
+}
+
+
+THREE.EventDispatcher.prototype.addEventListener = function(type, listener, importance=0){    //add importance
+	if ( this._listeners === undefined ) this._listeners = {};
+
+		const listeners = this._listeners;
+
+		if ( listeners[ type ] === undefined ) {
+
+			listeners[ type ] = [];
+
+		}
+
+		if ( !listeners[ type ].some(e=>e.listener == listener )  ) { 
+			//listeners[ type ].push( listener );
+            listeners[type].push({ listener,  importance});
+            listeners[type] = listeners[type].sort((e,a)=> a.importance - e.importance)//add
+		}
+}
+
+THREE.EventDispatcher.prototype.hasEventListener = function(type, listener){
+	if ( this._listeners === undefined ) return false;
+
+	const listeners = this._listeners;
+
+	return listeners[ type ] !== undefined &&  listeners[ type ].some(e=>e.listener == listener )  
+}
+
+THREE.EventDispatcher.prototype.removeEventListener = function(type, listener){
+	if ( this._listeners === undefined ) return;
+
+	const listeners = this._listeners;
+	const listenerArray = listeners[ type ];
+
+	if ( listenerArray !== undefined ) {
+
+		/* const index = listenerArray.indexOf( listener );
+
+		if ( index !== - 1 ) {
+
+			listenerArray.splice( index, 1 );
+
+		} */
+
+		let item = listenerArray.find(e=>e.listener == listener)
+		item && listenerArray.splice(listenerArray.indexOf(item), 1);
+
+	}
+}
+
+THREE.EventDispatcher.prototype.removeEventListeners = function(type){  //add
+	if(this._listeners && this._listeners[type] !== undefined){
+		delete this._listeners[type];
+	}
+}
+
+THREE.EventDispatcher.prototype.removeAllListeners = function(){  //add
+	this._listeners = {};
+}
+
+THREE.EventDispatcher.prototype.dispatchEvent = function(event){ 
+	if(typeof event == 'string'){//add
+            event = {type:event}
+        }
+		if ( this._listeners === undefined ) return;
+
+		const listeners = this._listeners;
+		const listenerArray = listeners[ event.type ];
+
+		if ( listenerArray !== undefined ) {
+
+			event.target = this;
+
+			// Make a copy, in case listeners are removed while iterating.
+			 
+            for(let {listener} of listenerArray.slice(0)){
+				let result = listener.call(this, event);   //add stopContinue
+                if(result && result.stopContinue){
+                    break
+                }
+			}
+
+		}
+}
+
+THREE.EventDispatcher.prototype.traverse = function(callback){ 
+		let result = callback( this );
+        if(result && result.stopContinue){//xzw add
+            return 
+        }
+             
+		const children = this.children;
+
+		for ( let i = 0, l = children.length; i < l; i ++ ) {
+
+			children[ i ].traverse( callback );
+
+		}
+}
+
+/* ,
+THREE.WebGLState = function(e, t, i) {
+    var r = this
+      , o = new THREE.Vector4
+      , a = e.getParameter(e.MAX_VERTEX_ATTRIBS)
+      , s = new Uint8Array(a)
+      , l = new Uint8Array(a)
+      , c = new Uint8Array(a)
+      , h = {}
+      , u = null
+      , d = null
+      , p = null
+      , f = null
+      , g = null
+      , m = null
+      , v = null
+      , A = null
+      , y = !1
+      , C = null
+      , I = null
+      , E = null
+      , b = null
+      , w = null
+      , _ = null
+      , T = null
+      , x = null
+      , S = null
+      , M = null
+      , R = null
+      , P = null
+      , O = null
+      , L = null
+      , D = null
+      , N = e.getParameter(e.MAX_TEXTURE_IMAGE_UNITS)
+      , B = void 0
+      , F = {}
+      , V = new THREE.Vector4
+      , U = null
+      , k = null
+      , H = new THREE.Vector4
+      , G = new THREE.Vector4;
+    this.init = function() {
+        this.clearColor(0, 0, 0, 1),
+        this.clearDepth(1),
+        this.clearStencil(0),
+        this.enable(e.DEPTH_TEST),
+        e.depthFunc(e.LEQUAL),
+        e.frontFace(e.CCW),
+        e.cullFace(e.BACK),
+        this.enable(e.CULL_FACE),
+        this.enable(e.BLEND),
+        e.blendEquation(e.FUNC_ADD),
+        e.blendFunc(e.SRC_ALPHA, e.ONE_MINUS_SRC_ALPHA)
+    }
+    ,
+    this.initAttributes = function() {
+        for (var e = 0, t = s.length; e < t; e++)
+            s[e] = 0
+    }
+    ,
+    this.enableAttribute = function(i) {
+        if (s[i] = 1,
+        0 === l[i] && (e.enableVertexAttribArray(i),
+        l[i] = 1),
+        0 !== c[i]) {
+            var n = t.get("ANGLE_instanced_arrays");
+            n.vertexAttribDivisorANGLE(i, 0),
+            c[i] = 0
+        }
+    }
+    ,
+    this.enableAttributeAndDivisor = function(t, i, n) {
+        s[t] = 1,
+        0 === l[t] && (e.enableVertexAttribArray(t),
+        l[t] = 1),
+        c[t] !== i && (n.vertexAttribDivisorANGLE(t, i),
+        c[t] = i)
+    }
+    ,
+    this.disableUnusedAttributes = function() {
+        for (var t = 0, i = l.length; t < i; t++)
+            l[t] !== s[t] && (e.disableVertexAttribArray(t),
+            l[t] = 0)
+    }
+    ,
+    this.enable = function(t) {
+        h[t] !== !0 && (e.enable(t),
+        h[t] = !0)
+    }
+    ,
+    this.disable = function(t) {
+        h[t] !== !1 && (e.disable(t),
+        h[t] = !1)
+    }
+    ,
+    this.getCompressedTextureFormats = function() {
+        if (null === u && (u = [],
+        t.get("WEBGL_compressed_texture_pvrtc") || t.get("WEBGL_compressed_texture_s3tc") || t.get("WEBGL_compressed_texture_etc1")))
+            for (var i = e.getParameter(e.COMPRESSED_TEXTURE_FORMATS), n = 0; n < i.length; n++)
+                u.push(i[n]);
+        return u
+    }
+    ,
+    this.setBlending = function(t, r, o, a, s, l, c, h) {
+        t === THREE.NoBlending ? this.disable(e.BLEND) : this.enable(e.BLEND),
+        t === d && h === y || (t === THREE.AdditiveBlending ? h ? (e.blendEquationSeparate(e.FUNC_ADD, e.FUNC_ADD),
+        e.blendFuncSeparate(e.ONE, e.ONE, e.ONE, e.ONE)) : (e.blendEquation(e.FUNC_ADD),
+        e.blendFunc(e.SRC_ALPHA, e.ONE)) : t === THREE.SubtractiveBlending ? h ? (e.blendEquationSeparate(e.FUNC_ADD, e.FUNC_ADD),
+        e.blendFuncSeparate(e.ZERO, e.ZERO, e.ONE_MINUS_SRC_COLOR, e.ONE_MINUS_SRC_ALPHA)) : (e.blendEquation(e.FUNC_ADD),
+        e.blendFunc(e.ZERO, e.ONE_MINUS_SRC_COLOR)) : t === THREE.MultiplyBlending ? h ? (e.blendEquationSeparate(e.FUNC_ADD, e.FUNC_ADD),
+        e.blendFuncSeparate(e.ZERO, e.ZERO, e.SRC_COLOR, e.SRC_ALPHA)) : (e.blendEquation(e.FUNC_ADD),
+        e.blendFunc(e.ZERO, e.SRC_COLOR)) : h ? (e.blendEquationSeparate(e.FUNC_ADD, e.FUNC_ADD),
+        e.blendFuncSeparate(e.ONE, e.ONE_MINUS_SRC_ALPHA, e.ONE, e.ONE_MINUS_SRC_ALPHA)) : (e.blendEquationSeparate(e.FUNC_ADD, e.FUNC_ADD),
+        e.blendFuncSeparate(e.SRC_ALPHA, e.ONE_MINUS_SRC_ALPHA, e.ONE, e.ONE_MINUS_SRC_ALPHA)),
+        d = t,
+        y = h),
+        t === THREE.CustomBlending ? (s = s || r,
+        l = l || o,
+        c = c || a,
+        r === p && s === m || (e.blendEquationSeparate(i(r), i(s)),
+        p = r,
+        m = s),
+        o === f && a === g && l === v && c === A || (e.blendFuncSeparate(i(o), i(a), i(l), i(c)),
+        f = o,
+        g = a,
+        v = l,
+        A = c)) : (p = null,
+        f = null,
+        g = null,
+        m = null,
+        v = null,
+        A = null)
+    }
+    ,
+    this.setDepthFunc = function(t) {
+        if (C !== t) {
+            if (t)
+                switch (t) {
+                case THREE.NeverDepth:
+                    e.depthFunc(e.NEVER);
+                    break;
+                case THREE.AlwaysDepth:
+                    e.depthFunc(e.ALWAYS);
+                    break;
+                case THREE.LessDepth:
+                    e.depthFunc(e.LESS);
+                    break;
+                case THREE.LessEqualDepth:
+                    e.depthFunc(e.LEQUAL);
+                    break;
+                case THREE.EqualDepth:
+                    e.depthFunc(e.EQUAL);
+                    break;
+                case THREE.GreaterEqualDepth:
+                    e.depthFunc(e.GEQUAL);
+                    break;
+                case THREE.GreaterDepth:
+                    e.depthFunc(e.GREATER);
+                    break;
+                case THREE.NotEqualDepth:
+                    e.depthFunc(e.NOTEQUAL);
+                    break;
+                default:
+                    e.depthFunc(e.LEQUAL)
+                }
+            else
+                e.depthFunc(e.LEQUAL);
+            C = t
+        }
+    }
+    ,
+    this.setDepthTest = function(t) {
+        t ? this.enable(e.DEPTH_TEST) : this.disable(e.DEPTH_TEST)
+    }
+    ,
+    this.setDepthWrite = function(t) {
+        I !== t && (e.depthMask(t),
+        I = t)
+    }
+    ,
+    this.setColorWrite = function(t) {
+        E !== t && (e.colorMask(t, t, t, t),
+        E = t)
+    }
+    ,
+    this.setStencilFunc = function(t, i, n) {
+        w === t && _ === i && T === n || (e.stencilFunc(t, i, n),
+        w = t,
+        _ = i,
+        T = n)
+    }
+    ,
+    this.setStencilOp = function(t, i, n) {
+        x === t && S === i && M === n || (e.stencilOp(t, i, n),
+        x = t,
+        S = i,
+        M = n)
+    }
+    ,
+    this.setStencilTest = function(t) {
+        t ? this.enable(e.STENCIL_TEST) : this.disable(e.STENCIL_TEST)
+    }
+    ,
+    this.setStencilWrite = function(t) {
+        b !== t && (e.stencilMask(t),
+        b = t)
+    }
+    ,
+    this.setFlipSided = function(t) {
+        R !== t && (t ? e.frontFace(e.CW) : e.frontFace(e.CCW),
+        R = t)
+    }
+    ,
+    this.setLineWidth = function(t) {
+        t !== P && (e.lineWidth(t),
+        P = t)
+    }
+    ,
+    this.setPolygonOffset = function(t, i, n) {
+        t ? this.enable(e.POLYGON_OFFSET_FILL) : this.disable(e.POLYGON_OFFSET_FILL),
+        !t || O === i && L === n || (e.polygonOffset(i, n),
+        O = i,
+        L = n)
+    }
+    ,
+    this.getScissorTest = function() {
+        return D
+    }
+    ,
+    this.setScissorTest = function(t) {
+        D = t,
+        t ? this.enable(e.SCISSOR_TEST) : this.disable(e.SCISSOR_TEST)
+    }
+    ,
+    this.activeTexture = function(t) {
+        void 0 === t && (t = e.TEXTURE0 + N - 1),
+        B !== t && (e.activeTexture(t),
+        B = t)
+    }
+    ,
+    this.bindTexture = function(t, i) {
+        void 0 === B && r.activeTexture();
+        var n = F[B];
+        void 0 === n && (n = {
+            type: void 0,
+            texture: void 0
+        },
+        F[B] = n),
+        n.type === t && n.texture === i || (e.bindTexture(t, i),
+        n.type = t,
+        n.texture = i)
+    }
+    ,
+    this.compressedTexImage2D = function() {
+        try {
+            e.compressedTexImage2D.apply(e, arguments)
+        } catch (e) {
+            console.error(e)
+        }
+    }
+    ,
+    this.texImage2D = function() {
+        try {
+            e.texImage2D.apply(e, arguments)
+        } catch (e) {
+            console.error(e)
+        }
+    }
+    ,
+    this.clearColor = function(t, i, n, r) {
+        o.set(t, i, n, r),
+        V.equals(o) === !1 && (e.clearColor(t, i, n, r),
+        V.copy(o))
+    }
+    ,
+    this.clearDepth = function(t) {
+        U !== t && (e.clearDepth(t),
+        U = t)
+    }
+    ,
+    this.clearStencil = function(t) {
+        k !== t && (e.clearStencil(t),
+        k = t)
+    }
+    ,
+    this.scissor = function(t) {
+        H.equals(t) === !1 && (e.scissor(t.x, t.y, t.z, t.w),
+        H.copy(t))
+    }
+    ,
+    this.viewport = function(t) {
+        G.equals(t) === !1 && (e.viewport(t.x, t.y, t.z, t.w),
+        G.copy(t))
+    }
+    ,
+    this.reset = function() {
+        for (var t = 0; t < l.length; t++)
+            1 === l[t] && (e.disableVertexAttribArray(t),
+            l[t] = 0);
+        h = {},
+        u = null,
+        B = void 0,
+        F = {},
+        d = null,
+        E = null,
+        I = null,
+        b = null,
+        R = null
+    }
+} */

+ 102 - 0
src/custom/utils/WorkerPool.js

@@ -0,0 +1,102 @@
+/**
+ * @author Deepkolos / https://github.com/deepkolos
+ */
+
+export class WorkerPool {
+
+	constructor( pool = 4 ) {
+
+		this.pool = pool;
+		this.queue = [];
+		this.workers = [];
+		this.workersResolve = [];
+		this.workerStatus = 0;
+
+	}
+
+	_initWorker( workerId ) {
+
+		if ( ! this.workers[ workerId ] ) {
+
+			const worker = this.workerCreator();
+			worker.addEventListener( 'message', this._onMessage.bind( this, workerId ) );
+			this.workers[ workerId ] = worker;
+
+		}
+
+	}
+
+	_getIdleWorker() {
+
+		for ( let i = 0; i < this.pool; i ++ )
+			if ( ! ( this.workerStatus & ( 1 << i ) ) ) return i;
+
+		return - 1;
+
+	}
+
+	_onMessage( workerId, msg ) {
+
+		const resolve = this.workersResolve[ workerId ];
+		resolve && resolve( msg );
+
+		if ( this.queue.length ) {
+
+			const { resolve, msg, transfer } = this.queue.shift();
+			this.workersResolve[ workerId ] = resolve;
+			this.workers[ workerId ].postMessage( msg, transfer );
+
+		} else {
+
+			this.workerStatus ^= 1 << workerId;
+
+		}
+
+	}
+
+	setWorkerCreator( workerCreator ) {
+
+		this.workerCreator = workerCreator;
+
+	}
+
+	setWorkerLimit( pool ) {
+
+		this.pool = pool;
+
+	}
+
+	postMessage( msg, transfer ) {
+
+		return new Promise( ( resolve ) => {
+
+			const workerId = this._getIdleWorker();
+
+			if ( workerId !== - 1 ) {
+
+				this._initWorker( workerId );
+				this.workerStatus |= 1 << workerId;
+				this.workersResolve[ workerId ] = resolve;
+				this.workers[ workerId ].postMessage( msg, transfer );
+
+			} else {
+
+				this.queue.push( { resolve, msg, transfer } );
+
+			}
+
+		} );
+
+	}
+
+	dispose() {
+
+		this.workers.forEach( ( worker ) => worker.terminate() );
+		this.workersResolve.length = 0;
+		this.workers.length = 0;
+		this.queue.length = 0;
+		this.workerStatus = 0;
+
+	}
+
+}

+ 102 - 0
src/utils/AnnotationTool.js

@@ -0,0 +1,102 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import {Annotation} from "../Annotation.js";
+import {Utils} from "../utils.js";
+import {CameraMode} from "../defines.js";
+import {EventDispatcher} from "../EventDispatcher.js";
+
+export class AnnotationTool extends EventDispatcher{
+	constructor (viewer) {
+		super();
+
+		this.viewer = viewer;
+		this.renderer = viewer.renderer;
+
+		this.sg = new THREE.SphereGeometry(0.1);
+		this.sm = new THREE.MeshNormalMaterial();
+		this.s = new THREE.Mesh(this.sg, this.sm);
+	}
+
+	startInsertion (args = {}) {
+		let domElement = this.viewer.renderer.domElement;
+
+		let annotation = new Annotation({
+			position: [589748.270, 231444.540, 753.675],
+			title: "Annotation Title",
+			description: `Annotation Description`
+		});
+		this.dispatchEvent({type: 'start_inserting_annotation', annotation: annotation});
+
+		const annotations = this.viewer.scene.annotations;
+		annotations.add(annotation);
+
+		let callbacks = {
+			cancel: null,
+			finish: null,
+		};
+
+		let insertionCallback = (e) => {
+			if (e.button === THREE.MOUSE.LEFT) {
+				callbacks.finish();
+			} else if (e.button === THREE.MOUSE.RIGHT) {
+				callbacks.cancel();
+			}
+		};
+
+		callbacks.cancel = e => {
+			annotations.remove(annotation);
+
+			domElement.removeEventListener('mouseup', insertionCallback, true);
+		};
+
+		callbacks.finish = e => {
+			domElement.removeEventListener('mouseup', insertionCallback, true);
+		};
+
+		domElement.addEventListener('mouseup', insertionCallback, true);
+
+		let drag = (e) => {
+			let I = Utils.getMousePointCloudIntersection(
+				e.drag.end, 
+				e.viewer.scene.getActiveCamera(), 
+				e.viewer, 
+				e.viewer.scene.pointclouds,
+				{pickClipped: true});
+
+			if (I) {
+				this.s.position.copy(I.location);
+
+				annotation.position.copy(I.location);
+			}
+		};
+
+		let drop = (e) => {
+			viewer.scene.scene.remove(this.s);
+			this.s.removeEventListener("drag", drag);
+			this.s.removeEventListener("drop", drop);
+		};
+
+		this.s.addEventListener('drag', drag);
+		this.s.addEventListener('drop', drop);
+
+		this.viewer.scene.scene.add(this.s);
+		this.viewer.inputHandler.startDragging(this.s);
+
+		return annotation;
+	}
+	
+	update(){
+		// let camera = this.viewer.scene.getActiveCamera();
+		// let domElement = this.renderer.domElement;
+		// let measurements = this.viewer.scene.measurements;
+
+		// const renderAreaSize = this.renderer.getSize(new THREE.Vector2());
+		// let clientWidth = renderAreaSize.width;
+		// let clientHeight = renderAreaSize.height;
+
+	}
+
+	render(){
+		//this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera());
+	}
+};

+ 37 - 0
src/utils/Box3Helper.js

@@ -0,0 +1,37 @@
+/**
+ *
+ * code adapted from three.js BoxHelper.js
+ * https://github.com/mrdoob/three.js/blob/dev/src/helpers/BoxHelper.js
+ *
+ * @author mrdoob / http://mrdoob.com/
+ * @author Mugen87 / http://github.com/Mugen87
+ * @author mschuetz / http://potree.org
+ */
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+
+export class Box3Helper extends THREE.LineSegments {
+	constructor (box, color) {
+		if (color === undefined) color = 0xffff00;
+
+		let indices = new Uint16Array([ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ]);
+		let positions = new Float32Array([
+			box.min.x, box.min.y, box.min.z,
+			box.max.x, box.min.y, box.min.z,
+			box.max.x, box.min.y, box.max.z,
+			box.min.x, box.min.y, box.max.z,
+			box.min.x, box.max.y, box.min.z,
+			box.max.x, box.max.y, box.min.z,
+			box.max.x, box.max.y, box.max.z,
+			box.min.x, box.max.y, box.max.z
+		]);
+
+		let geometry = new THREE.BufferGeometry();
+		geometry.setIndex(new THREE.BufferAttribute(indices, 1));
+		geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+
+		let material = new THREE.LineBasicMaterial({ color: color });
+
+		super(geometry, material);
+	}
+}

+ 290 - 0
src/utils/ClipVolume.js

@@ -0,0 +1,290 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+
+export class ClipVolume extends THREE.Object3D{
+	
+	constructor(args){
+		super();
+		
+		this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;
+		this.name = "clip_volume_" + this.constructor.counter;
+
+		let alpha = args.alpha || 0;
+		let beta = args.beta || 0;
+		let gamma = args.gamma || 0;
+
+		this.rotation.x = alpha;
+		this.rotation.y = beta;
+		this.rotation.z = gamma;
+
+		this.clipOffset = 0.001;
+		this.clipRotOffset = 1;
+				
+		let boxGeometry = new THREE.BoxGeometry(1, 1, 1);
+		boxGeometry.computeBoundingBox();
+		
+		let boxFrameGeometry = new THREE.Geometry();
+		{			
+			// bottom
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, 0.5));
+			// top
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, 0.5));
+			// sides
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, -0.5));
+
+			boxFrameGeometry.colors.push(new THREE.Vector3(1, 1, 1));
+		}
+
+		let planeFrameGeometry = new THREE.Geometry();
+		{						
+			// middle line
+			planeFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, 0.0));
+			planeFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, 0.0));
+			planeFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, 0.0));
+			planeFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, 0.0));
+			planeFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, 0.0));
+			planeFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, 0.0));
+			planeFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, 0.0));
+			planeFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, 0.0));
+		}
+
+		this.dimension = new THREE.Vector3(1, 1, 1);
+		this.material = new THREE.MeshBasicMaterial( {
+			color: 0x00ff00, 
+			transparent: true, 
+			opacity: 0.3,
+			depthTest: true, 
+			depthWrite: false} );
+		this.box = new THREE.Mesh(boxGeometry, this.material);
+		this.box.geometry.computeBoundingBox();
+		this.boundingBox = this.box.geometry.boundingBox;
+		this.add(this.box);
+		
+		this.frame = new THREE.LineSegments( boxFrameGeometry, new THREE.LineBasicMaterial({color: 0x000000}));
+		this.add(this.frame);
+		this.planeFrame = new THREE.LineSegments( planeFrameGeometry, new THREE.LineBasicMaterial({color: 0xff0000}));
+		this.add(this.planeFrame);
+
+		// set default thickness
+		this.setScaleZ(0.1);
+
+		// create local coordinate system
+		let createArrow = (name, direction, color) => {
+			let material = new THREE.MeshBasicMaterial({
+				color: color, 
+				depthTest: false, 
+				depthWrite: false});
+				
+			let shaftGeometry = new THREE.Geometry();
+			shaftGeometry.vertices.push(new THREE.Vector3(0, 0, 0));
+			shaftGeometry.vertices.push(new THREE.Vector3(0, 1, 0));
+			
+			let shaftMaterial = new THREE.LineBasicMaterial({
+				color: color, 
+				depthTest: false, 
+				depthWrite: false,
+				transparent: true
+				});
+			let shaft = new THREE.Line(shaftGeometry, shaftMaterial);
+			shaft.name = name + "_shaft";
+			
+			let headGeometry = new THREE.CylinderGeometry(0, 0.04, 0.1, 10, 1, false);
+			let headMaterial = material;
+			let head = new THREE.Mesh(headGeometry, headMaterial);
+			head.name = name + "_head";
+			head.position.y = 1;
+			
+			let arrow = new THREE.Object3D();
+			arrow.name = name;
+			arrow.add(shaft);
+			arrow.add(head);
+
+			return arrow;
+		};
+		
+		this.arrowX = createArrow("arrow_x", new THREE.Vector3(1, 0, 0), 0xFF0000);
+		this.arrowY = createArrow("arrow_y", new THREE.Vector3(0, 1, 0), 0x00FF00);
+		this.arrowZ = createArrow("arrow_z", new THREE.Vector3(0, 0, 1), 0x0000FF);
+		
+		this.arrowX.rotation.z = -Math.PI / 2;
+		this.arrowZ.rotation.x = Math.PI / 2;
+
+		this.arrowX.visible = false;
+		this.arrowY.visible = false;
+		this.arrowZ.visible = false;
+
+		this.add(this.arrowX);
+		this.add(this.arrowY);
+		this.add(this.arrowZ);
+		
+		{ // event listeners
+			this.addEventListener("ui_select", e => { 
+				this.arrowX.visible = true;
+				this.arrowY.visible = true;
+				this.arrowZ.visible = true; 
+			});
+			this.addEventListener("ui_deselect", e => {
+				this.arrowX.visible = false;
+				this.arrowY.visible = false;
+				this.arrowZ.visible = false; 				
+			});
+			this.addEventListener("select", e => { 
+				let scene_header = $("#" + this.name + " .scene_header");
+				if(!scene_header.next().is(":visible")) {
+					scene_header.click();
+				}
+			});
+			this.addEventListener("deselect", e => { 
+				let scene_header = $("#" + this.name + " .scene_header");
+				if(scene_header.next().is(":visible")) {
+					scene_header.click();
+				}
+			});
+		}
+		
+		this.update();
+	};
+
+	setClipOffset(offset) {		
+		this.clipOffset = offset;	
+	}
+
+	setClipRotOffset(offset) {		
+		this.clipRotOffset = offset;		
+	}
+
+	setScaleX(x) {
+		this.box.scale.x = x;
+		this.frame.scale.x = x;
+		this.planeFrame.scale.x = x;			
+	}
+
+	setScaleY(y) {
+		this.box.scale.y = y;
+		this.frame.scale.y = y;
+		this.planeFrame.scale.y = y;		
+	}
+
+	setScaleZ(z) {
+		this.box.scale.z = z;
+		this.frame.scale.z = z;
+		this.planeFrame.scale.z = z;		
+	}
+
+	offset(args) {
+		let cs = args.cs || null;
+		let axis = args.axis || null;
+		let dir = args.dir || null;
+
+		if(!cs || !axis || !dir) return;
+
+		if(axis === "x") {
+			if(cs === "local") {
+				this.position.add(this.localX.clone().multiplyScalar(dir * this.clipOffset));
+			} else if(cs === "global") {
+				this.position.x = this.position.x + dir * this.clipOffset;
+			}
+		}else if(axis === "y") {
+			if(cs === "local") {
+				this.position.add(this.localY.clone().multiplyScalar(dir * this.clipOffset));
+			} else if(cs === "global") {
+				this.position.y = this.position.y + dir * this.clipOffset;
+			}
+		}else if(axis === "z") {
+			if(cs === "local") {
+				this.position.add(this.localZ.clone().multiplyScalar(dir * this.clipOffset));
+			} else if(cs === "global") {
+				this.position.z = this.position.z + dir * this.clipOffset;
+			}
+		}
+
+		this.dispatchEvent({"type": "clip_volume_changed", "viewer": viewer, "volume": this});
+	}	
+
+	rotate(args) {
+		let cs = args.cs || null;
+		let axis = args.axis || null;
+		let dir = args.dir || null;
+
+		if(!cs || !axis || !dir) return;
+
+		if(cs === "local") {
+			if(axis === "x") {
+				this.rotateOnAxis(new THREE.Vector3(1, 0, 0), dir * this.clipRotOffset * Math.PI / 180);
+			} else if(axis === "y") {
+				this.rotateOnAxis(new THREE.Vector3(0, 1, 0), dir * this.clipRotOffset * Math.PI / 180);
+			} else if(axis === "z") {
+				this.rotateOnAxis(new THREE.Vector3(0, 0, 1), dir * this.clipRotOffset * Math.PI / 180);
+			}
+		} else if(cs === "global") {
+			let rotaxis = new THREE.Vector4(1, 0, 0, 0);	
+			if(axis === "y") {
+				rotaxis = new THREE.Vector4(0, 1, 0, 0);
+			} else if(axis === "z") {
+				rotaxis = new THREE.Vector4(0, 0, 1, 0);
+			}
+			this.updateMatrixWorld();
+			let invM = newthis.matrixWorld.clone().invert();
+			rotaxis = rotaxis.applyMatrix4(invM).normalize();
+			rotaxis = new THREE.Vector3(rotaxis.x, rotaxis.y, rotaxis.z);
+			this.rotateOnAxis(rotaxis, dir * this.clipRotOffset * Math.PI / 180);
+		}
+
+		this.updateLocalSystem();
+
+		this.dispatchEvent({"type": "clip_volume_changed", "viewer": viewer, "volume": this});
+	}	
+
+	update(){
+		this.boundingBox = this.box.geometry.boundingBox;
+		this.boundingSphere = this.boundingBox.getBoundingSphere(new THREE.Sphere());
+		
+		this.box.visible = false;
+
+		this.updateLocalSystem();
+	};
+
+	updateLocalSystem() {		
+		// extract local coordinate axes
+		let rotQuat = this.getWorldQuaternion();
+		this.localX = new THREE.Vector3(1, 0, 0).applyQuaternion(rotQuat).normalize();
+		this.localY = new THREE.Vector3(0, 1, 0).applyQuaternion(rotQuat).normalize();
+		this.localZ = new THREE.Vector3(0, 0, 1).applyQuaternion(rotQuat).normalize();
+	}
+	
+	raycast(raycaster, intersects){
+		
+		let is = [];
+		this.box.raycast(raycaster, is);
+	
+		if(is.length > 0){
+			let I = is[0];
+			intersects.push({
+				distance: I.distance,
+				object: this,
+				point: I.point.clone()
+			});
+		}
+	};
+};

+ 173 - 0
src/utils/ClippingTool.js

@@ -0,0 +1,173 @@
+
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import {ClipVolume} from "./ClipVolume.js";
+import {PolygonClipVolume} from "./PolygonClipVolume.js";
+import { EventDispatcher } from "../EventDispatcher.js";
+
+export class ClippingTool extends EventDispatcher{
+
+	constructor(viewer){
+		super(); 
+
+		this.viewer = viewer;
+
+		this.maxPolygonVertices = 8; 
+		
+		this.addEventListener("start_inserting_clipping_volume", e => {
+			this.viewer.dispatchEvent({
+				type: "cancel_insertions"
+			});
+		});
+
+		this.sceneMarker = new THREE.Scene();
+		this.sceneVolume = new THREE.Scene();
+		this.sceneVolume.name = "scene_clip_volume";
+		this.viewer.inputHandler.registerInteractiveScene(this.sceneVolume);
+
+		this.onRemove = e => {
+			this.sceneVolume.remove(e.volume);
+		};
+		
+		this.onAdd = e => {
+			this.sceneVolume.add(e.volume);
+		};
+		
+		this.viewer.inputHandler.addEventListener("delete", e => {
+			let volumes = e.selection.filter(e => (e instanceof ClipVolume));
+			volumes.forEach(e => this.viewer.scene.removeClipVolume(e));
+			let polyVolumes = e.selection.filter(e => (e instanceof PolygonClipVolume));
+			polyVolumes.forEach(e => this.viewer.scene.removePolygonClipVolume(e));
+		});
+	}
+
+	setScene(scene){
+		if(this.scene === scene){
+			return;
+		}
+		
+		if(this.scene){
+			this.scene.removeEventListeners("clip_volume_added", this.onAdd);
+			this.scene.removeEventListeners("clip_volume_removed", this.onRemove);
+			this.scene.removeEventListeners("polygon_clip_volume_added", this.onAdd);
+			this.scene.removeEventListeners("polygon_clip_volume_removed", this.onRemove);
+		}
+		
+		this.scene = scene;
+		
+		this.scene.addEventListener("clip_volume_added", this.onAdd);
+		this.scene.addEventListener("clip_volume_removed", this.onRemove);
+		this.scene.addEventListener("polygon_clip_volume_added", this.onAdd);
+		this.scene.addEventListener("polygon_clip_volume_removed", this.onRemove);
+	}
+
+	startInsertion(args = {}) {	
+		let type = args.type || null;
+
+		if(!type) return null;
+
+		let domElement = this.viewer.renderer.domElement;
+		let canvasSize = this.viewer.renderer.getSize(new THREE.Vector2());
+
+		let svg = $(`
+		<svg height="${canvasSize.height}" width="${canvasSize.width}" style="position:absolute; pointer-events: none">
+
+			<defs>
+				 <marker id="diamond" markerWidth="24" markerHeight="24" refX="12" refY="12"
+						markerUnits="userSpaceOnUse">
+					<circle cx="12" cy="12" r="6" fill="white" stroke="black" stroke-width="3"/>
+				</marker>
+			</defs>
+
+			<polyline fill="none" stroke="black" 
+				style="stroke:rgb(0, 0, 0);
+				stroke-width:6;"
+				stroke-dasharray="9, 6"
+				stroke-dashoffset="2"
+				/>
+
+			<polyline fill="none" stroke="black" 
+				style="stroke:rgb(255, 255, 255);
+				stroke-width:2;"
+				stroke-dasharray="5, 10"
+				marker-start="url(#diamond)" 
+				marker-mid="url(#diamond)" 
+				marker-end="url(#diamond)" 
+				/>
+		</svg>`);
+		$(domElement.parentElement).append(svg);
+
+		let polyClipVol = new PolygonClipVolume(this.viewer.scene.getActiveCamera().clone());
+
+		this.dispatchEvent({"type": "start_inserting_clipping_volume"});
+
+		this.viewer.scene.addPolygonClipVolume(polyClipVol);
+		this.sceneMarker.add(polyClipVol);
+
+		let cancel = {
+			callback: null
+		};
+
+		let insertionCallback = (e) => {
+			if(e.button === THREE.MOUSE.LEFT){
+				
+				polyClipVol.addMarker();
+
+				// SVC Screen Line
+				svg.find("polyline").each((index, target) => {
+					let newPoint = svg[0].createSVGPoint();
+					newPoint.x = e.offsetX;
+					newPoint.y = e.offsetY;
+					let polyline = target.points.appendItem(newPoint);
+				});
+				
+				
+				if(polyClipVol.markers.length > this.maxPolygonVertices){
+					cancel.callback();
+				}
+				
+				this.viewer.inputHandler.startDragging(
+					polyClipVol.markers[polyClipVol.markers.length - 1]);
+			}else if(e.button === THREE.MOUSE.RIGHT){
+				cancel.callback(e);
+			}
+		};
+		
+		cancel.callback = e => {
+
+			//let first = svg.find("polyline")[0].points[0];
+			//svg.find("polyline").each((index, target) => {
+			//	let newPoint = svg[0].createSVGPoint();
+			//	newPoint.x = first.x;
+			//	newPoint.y = first.y;
+			//	let polyline = target.points.appendItem(newPoint);
+			//});
+			svg.remove();
+
+			if(polyClipVol.markers.length > 3) {
+				polyClipVol.removeLastMarker();
+				polyClipVol.initialized = true;	
+			} else {
+				this.viewer.scene.removePolygonClipVolume(polyClipVol);
+			}
+
+			this.viewer.renderer.domElement.removeEventListener("mouseup", insertionCallback, true);
+			this.viewer.removeEventListener("cancel_insertions", cancel.callback);
+			this.viewer.inputHandler.enabled = true;
+		};
+		
+		this.viewer.addEventListener("cancel_insertions", cancel.callback);
+		this.viewer.renderer.domElement.addEventListener("mouseup", insertionCallback , true);
+		this.viewer.inputHandler.enabled = false;
+		
+		polyClipVol.addMarker();
+		this.viewer.inputHandler.startDragging(
+			polyClipVol.markers[polyClipVol.markers.length - 1]);
+
+		return polyClipVol;
+	}
+
+	update() {
+
+	}
+};

+ 58 - 0
src/utils/Compass.js

@@ -0,0 +1,58 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+
+import {Utils} from "../utils.js";
+
+export class Compass{
+
+	constructor(viewer){
+		this.viewer = viewer;
+
+		this.visible = false;
+		this.dom = this.createElement();
+
+		viewer.addEventListener("update", () => {
+			const direction = viewer.scene.view.direction.clone();
+			direction.z = 0;
+			direction.normalize();
+
+			const camera = viewer.scene.getActiveCamera();
+
+			const p1 = camera.getWorldPosition(new THREE.Vector3());
+			const p2 = p1.clone().add(direction);
+
+			const projection = viewer.getProjection();
+			const azimuth = Utils.computeAzimuth(p1, p2, projection);
+			
+			this.dom.css("transform", `rotateZ(${-azimuth}rad)`);
+		});
+
+		this.dom.click( () => {
+			viewer.setTopView();
+		});
+
+		const renderArea = $(viewer.renderArea);
+		renderArea.append(this.dom);
+
+		this.setVisible(this.visible);
+	}
+
+	setVisible(visible){
+		this.visible = visible;
+
+		const value = visible ? "" : "none";
+		this.dom.css("display", value);
+	}
+
+	isVisible(){
+		return this.visible;
+	}
+
+	createElement(){
+		const style = `style="position: absolute; top: 10px; right: 10px; z-index: 10000; width: 64px;"`;
+		const img = $(`<img src="${Potree.resourcePath}/images/compas.svg" ${style} />`);
+
+		return img;
+	}
+
+};

+ 936 - 0
src/utils/Measure.js

@@ -0,0 +1,936 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import {TextSprite} from "../TextSprite.js";
+import {Utils} from "../utils.js";
+import {Line2} from "../../libs/three.js/lines/Line2.js";
+import {LineGeometry} from "../../libs/three.js/lines/LineGeometry.js";
+import {LineMaterial} from "../../libs/three.js/lines/LineMaterial.js";
+
+function createHeightLine(){
+	let lineGeometry = new LineGeometry();
+
+	lineGeometry.setPositions([
+		0, 0, 0,
+		0, 0, 0,
+	]);
+
+	let lineMaterial = new LineMaterial({ 
+		color: 0x00ff00, 
+		dashSize: 5, 
+		gapSize: 2,
+		linewidth: 2, 
+		resolution:  new THREE.Vector2(1000, 1000),
+	});
+
+	lineMaterial.depthTest = false;
+	const heightEdge = new Line2(lineGeometry, lineMaterial);
+	heightEdge.visible = false;
+
+	//this.add(this.heightEdge);
+	
+	return heightEdge;
+}
+
+function createHeightLabel(){
+	const heightLabel = new TextSprite('');
+
+	heightLabel.setTextColor({r: 140, g: 250, b: 140, a: 1.0});
+	heightLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
+	heightLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
+	heightLabel.fontsize = 16;
+	heightLabel.material.depthTest = false;
+	heightLabel.material.opacity = 1;
+	heightLabel.visible = false;
+
+	return heightLabel;
+}
+
+function createAreaLabel(){
+	const areaLabel = new TextSprite('');
+
+	areaLabel.setTextColor({r: 140, g: 250, b: 140, a: 1.0});
+	areaLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
+	areaLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
+	areaLabel.fontsize = 16;
+	areaLabel.material.depthTest = false;
+	areaLabel.material.opacity = 1;
+	areaLabel.visible = false;
+	
+	return areaLabel;
+}
+
+function createCircleRadiusLabel(){
+	const circleRadiusLabel = new TextSprite("");
+
+	circleRadiusLabel.setTextColor({r: 140, g: 250, b: 140, a: 1.0});
+	circleRadiusLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
+	circleRadiusLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
+	circleRadiusLabel.fontsize = 16;
+	circleRadiusLabel.material.depthTest = false;
+	circleRadiusLabel.material.opacity = 1;
+	circleRadiusLabel.visible = false;
+	
+	return circleRadiusLabel;
+}
+
+function createCircleRadiusLine(){
+	const lineGeometry = new LineGeometry();
+
+	lineGeometry.setPositions([
+		0, 0, 0,
+		0, 0, 0,
+	]);
+
+	const lineMaterial = new LineMaterial({ 
+		color: 0xff0000, 
+		linewidth: 2, 
+		resolution:  new THREE.Vector2(1000, 1000),
+		gapSize: 1,
+		dashed: true,
+	});
+
+	lineMaterial.depthTest = false;
+
+	const circleRadiusLine = new Line2(lineGeometry, lineMaterial);
+	circleRadiusLine.visible = false;
+
+	return circleRadiusLine;
+}
+
+function createCircleLine(){
+	const coordinates = [];
+
+	let n = 128;
+	for(let i = 0; i <= n; i++){
+		let u0 = 2 * Math.PI * (i / n);
+		let u1 = 2 * Math.PI * (i + 1) / n;
+
+		let p0 = new THREE.Vector3(
+			Math.cos(u0), 
+			Math.sin(u0), 
+			0
+		);
+
+		let p1 = new THREE.Vector3(
+			Math.cos(u1), 
+			Math.sin(u1), 
+			0
+		);
+
+		coordinates.push(
+			...p0.toArray(),
+			...p1.toArray(),
+		);
+	}
+
+	const geometry = new LineGeometry();
+	geometry.setPositions(coordinates);
+
+	const material = new LineMaterial({ 
+		color: 0xff0000, 
+		dashSize: 5, 
+		gapSize: 2,
+		linewidth: 2, 
+		resolution:  new THREE.Vector2(1000, 1000),
+	});
+
+	material.depthTest = false;
+
+	const circleLine = new Line2(geometry, material);
+	circleLine.visible = false;
+	circleLine.computeLineDistances();
+
+	return circleLine;
+}
+
+function createCircleCenter(){
+	const sg = new THREE.SphereGeometry(1, 32, 32);
+	const sm = new THREE.MeshNormalMaterial();
+	
+	const circleCenter = new THREE.Mesh(sg, sm);
+	circleCenter.visible = false;
+
+	return circleCenter;
+}
+
+function createLine(){
+	const geometry = new LineGeometry();
+
+	geometry.setPositions([
+		0, 0, 0,
+		0, 0, 0,
+	]);
+
+	const material = new LineMaterial({ 
+		color: 0xff0000, 
+		linewidth: 2, 
+		resolution:  new THREE.Vector2(1000, 1000),
+		gapSize: 1,
+		dashed: true,
+	});
+
+	material.depthTest = false;
+
+	const line = new Line2(geometry, material);
+
+	return line;
+}
+
+function createCircle(){
+
+	const coordinates = [];
+
+	let n = 128;
+	for(let i = 0; i <= n; i++){
+		let u0 = 2 * Math.PI * (i / n);
+		let u1 = 2 * Math.PI * (i + 1) / n;
+
+		let p0 = new THREE.Vector3(
+			Math.cos(u0), 
+			Math.sin(u0), 
+			0
+		);
+
+		let p1 = new THREE.Vector3(
+			Math.cos(u1), 
+			Math.sin(u1), 
+			0
+		);
+
+		coordinates.push(
+			...p0.toArray(),
+			...p1.toArray(),
+		);
+	}
+
+	const geometry = new LineGeometry();
+	geometry.setPositions(coordinates);
+
+	const material = new LineMaterial({ 
+		color: 0xff0000, 
+		dashSize: 5, 
+		gapSize: 2,
+		linewidth: 2, 
+		resolution:  new THREE.Vector2(1000, 1000),
+	});
+
+	material.depthTest = false;
+
+	const line = new Line2(geometry, material);
+	line.computeLineDistances();
+
+	return line;
+
+}
+
+function createAzimuth(){
+
+	const azimuth = {
+		label: null,
+		center: null,
+		target: null,
+		north: null,
+		centerToNorth: null,
+		centerToTarget: null,
+		centerToTargetground: null,
+		targetgroundToTarget: null,
+		circle: null,
+
+		node: null,
+	};
+
+	const sg = new THREE.SphereGeometry(1, 32, 32);
+	const sm = new THREE.MeshNormalMaterial();
+
+	{
+		const label = new TextSprite("");
+
+		label.setTextColor({r: 140, g: 250, b: 140, a: 1.0});
+		label.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
+		label.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
+		label.fontsize = 16;
+		label.material.depthTest = false;
+		label.material.opacity = 1;
+
+		azimuth.label = label;
+	}
+
+	azimuth.center = new THREE.Mesh(sg, sm);
+	azimuth.target = new THREE.Mesh(sg, sm);
+	azimuth.north = new THREE.Mesh(sg, sm);
+	azimuth.centerToNorth = createLine();
+	azimuth.centerToTarget = createLine();
+	azimuth.centerToTargetground = createLine();
+	azimuth.targetgroundToTarget = createLine();
+	azimuth.circle = createCircle();
+
+	azimuth.node = new THREE.Object3D();
+	azimuth.node.add(
+		azimuth.centerToNorth,
+		azimuth.centerToTarget,
+		azimuth.centerToTargetground,
+		azimuth.targetgroundToTarget,
+		azimuth.circle,
+		azimuth.label,
+		azimuth.center,
+		azimuth.target,
+		azimuth.north,
+	);
+
+	return azimuth;
+}
+
+export class Measure extends THREE.Object3D {
+	constructor () {
+		super();
+
+		this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;
+
+		this.name = 'Measure_' + this.constructor.counter;
+		this.points = [];
+		this._showDistances = true;
+		this._showCoordinates = false;
+		this._showArea = false;
+		this._closed = true;
+		this._showAngles = false;
+		this._showCircle = false;
+		this._showHeight = false;
+		this._showEdges = true;
+		this._showAzimuth = false;
+		this.maxMarkers = Number.MAX_SAFE_INTEGER;
+
+		this.sphereGeometry = new THREE.SphereGeometry(0.4, 10, 10);
+		this.color = new THREE.Color(0xff0000);
+
+		this.spheres = [];
+		this.edges = [];
+		this.sphereLabels = [];
+		this.edgeLabels = [];
+		this.angleLabels = [];
+		this.coordinateLabels = [];
+
+		this.heightEdge = createHeightLine();
+		this.heightLabel = createHeightLabel();
+		this.areaLabel = createAreaLabel();
+		this.circleRadiusLabel = createCircleRadiusLabel();
+		this.circleRadiusLine = createCircleRadiusLine();
+		this.circleLine = createCircleLine();
+		this.circleCenter = createCircleCenter();
+
+		this.azimuth = createAzimuth();
+
+		this.add(this.heightEdge);
+		this.add(this.heightLabel);
+		this.add(this.areaLabel);
+		this.add(this.circleRadiusLabel);
+		this.add(this.circleRadiusLine);
+		this.add(this.circleLine);
+		this.add(this.circleCenter);
+
+		this.add(this.azimuth.node);
+
+	}
+
+	createSphereMaterial () {
+		let sphereMaterial = new THREE.MeshLambertMaterial({
+			//shading: THREE.SmoothShading,
+			color: this.color,
+			depthTest: false,
+			depthWrite: false}
+		);
+
+		return sphereMaterial;
+	};
+
+	addMarker (point) {
+		if (point.x != null) {
+			point = {position: point};
+		}else if(point instanceof Array){
+			point = {position: new THREE.Vector3(...point)};
+		}
+		this.points.push(point);
+
+		// sphere
+		let sphere = new THREE.Mesh(this.sphereGeometry, this.createSphereMaterial());
+
+		this.add(sphere);
+		this.spheres.push(sphere);
+
+		{ // edges
+			let lineGeometry = new LineGeometry();
+			lineGeometry.setPositions( [
+					0, 0, 0,
+					0, 0, 0,
+			]);
+
+			let lineMaterial = new LineMaterial({
+				color: 0xff0000, 
+				linewidth: 2, 
+				resolution:  new THREE.Vector2(1000, 1000),
+			});
+
+			lineMaterial.depthTest = false;
+
+			let edge = new Line2(lineGeometry, lineMaterial);
+			edge.visible = true;
+
+			this.add(edge);
+			this.edges.push(edge);
+		}
+
+		{ // edge labels
+			let edgeLabel = new TextSprite();
+			edgeLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
+			edgeLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
+			edgeLabel.material.depthTest = false;
+			edgeLabel.visible = false;
+			edgeLabel.fontsize = 16;
+			this.edgeLabels.push(edgeLabel);
+			this.add(edgeLabel);
+		}
+
+		{ // angle labels
+			let angleLabel = new TextSprite();
+			angleLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
+			angleLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
+			angleLabel.fontsize = 16;
+			angleLabel.material.depthTest = false;
+			angleLabel.material.opacity = 1;
+			angleLabel.visible = false;
+			this.angleLabels.push(angleLabel);
+			this.add(angleLabel);
+		}
+
+		{ // coordinate labels
+			let coordinateLabel = new TextSprite();
+			coordinateLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
+			coordinateLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
+			coordinateLabel.fontsize = 16;
+			coordinateLabel.material.depthTest = false;
+			coordinateLabel.material.opacity = 1;
+			coordinateLabel.visible = false;
+			this.coordinateLabels.push(coordinateLabel);
+			this.add(coordinateLabel);
+		}
+
+		{ // Event Listeners
+			let drag = (e) => {
+				let I = Utils.getMousePointCloudIntersection(
+					e.drag.end, 
+					e.viewer.scene.getActiveCamera(), 
+					e.viewer, 
+					e.viewer.scene.pointclouds,
+					{pickClipped: true});
+
+				if (I) {
+					let i = this.spheres.indexOf(e.drag.object);
+					if (i !== -1) {
+						let point = this.points[i];
+						
+						// loop through current keys and cleanup ones that will be orphaned
+						for (let key of Object.keys(point)) {
+							if (!I.point[key]) {
+								delete point[key];
+							}
+						}
+
+						for (let key of Object.keys(I.point).filter(e => e !== 'position')) {
+							point[key] = I.point[key];
+						}
+
+						this.setPosition(i, I.location);
+					}
+				}
+			};
+
+			let drop = e => {
+				let i = this.spheres.indexOf(e.drag.object);
+				if (i !== -1) {
+					this.dispatchEvent({
+						'type': 'marker_dropped',
+						'measurement': this,
+						'index': i
+					});
+				}
+			};
+
+			let mouseover = (e) => e.object.material.emissive.setHex(0x888888);
+			let mouseleave = (e) => e.object.material.emissive.setHex(0x000000);
+
+			sphere.addEventListener('drag', drag);
+			sphere.addEventListener('drop', drop);
+			sphere.addEventListener('mouseover', mouseover);
+			sphere.addEventListener('mouseleave', mouseleave);
+		}
+
+		let event = {
+			type: 'marker_added',
+			measurement: this,
+			sphere: sphere
+		};
+		this.dispatchEvent(event);
+
+		this.setMarker(this.points.length - 1, point);
+	};
+
+	removeMarker (index) {
+		this.points.splice(index, 1);
+
+		this.remove(this.spheres[index]);
+
+		let edgeIndex = (index === 0) ? 0 : (index - 1);
+		this.remove(this.edges[edgeIndex]);
+		this.edges.splice(edgeIndex, 1);
+
+		this.remove(this.edgeLabels[edgeIndex]);
+		this.edgeLabels.splice(edgeIndex, 1);
+		this.coordinateLabels.splice(index, 1);
+
+		this.remove(this.angleLabels[index]);
+		this.angleLabels.splice(index, 1);
+
+		this.spheres.splice(index, 1);
+
+		this.update();
+
+		this.dispatchEvent({type: 'marker_removed', measurement: this});
+	};
+
+	setMarker (index, point) {
+		this.points[index] = point;
+
+		let event = {
+			type: 'marker_moved',
+			measure:	this,
+			index:	index,
+			position: point.position.clone()
+		};
+		this.dispatchEvent(event);
+
+		this.update();
+	}
+
+	setPosition (index, position) {
+		let point = this.points[index];
+		point.position.copy(position);
+
+		let event = {
+			type: 'marker_moved',
+			measure:	this,
+			index:	index,
+			position: position.clone()
+		};
+		this.dispatchEvent(event);
+
+		this.update();
+	};
+
+	getArea () {
+		let area = 0;
+		let j = this.points.length - 1;
+
+		for (let i = 0; i < this.points.length; i++) {
+			let p1 = this.points[i].position;
+			let p2 = this.points[j].position;
+			area += (p2.x + p1.x) * (p1.y - p2.y);
+			j = i;
+		}
+
+		return Math.abs(area / 2);
+	};
+
+	getTotalDistance () {
+		if (this.points.length === 0) {
+			return 0;
+		}
+
+		let distance = 0;
+
+		for (let i = 1; i < this.points.length; i++) {
+			let prev = this.points[i - 1].position;
+			let curr = this.points[i].position;
+			let d = prev.distanceTo(curr);
+
+			distance += d;
+		}
+
+		if (this.closed && this.points.length > 1) {
+			let first = this.points[0].position;
+			let last = this.points[this.points.length - 1].position;
+			let d = last.distanceTo(first);
+
+			distance += d;
+		}
+
+		return distance;
+	}
+
+	getAngleBetweenLines (cornerPoint, point1, point2) {
+		let v1 = new THREE.Vector3().subVectors(point1.position, cornerPoint.position);
+		let v2 = new THREE.Vector3().subVectors(point2.position, cornerPoint.position);
+
+		// avoid the error printed by threejs if denominator is 0
+		const denominator = Math.sqrt( v1.lengthSq() * v2.lengthSq() );
+		if(denominator === 0){
+			return 0;
+		}else{
+			return v1.angleTo(v2);
+		}
+	};
+
+	getAngle (index) {
+		if (this.points.length < 3 || index >= this.points.length) {
+			return 0;
+		}
+
+		let previous = (index === 0) ? this.points[this.points.length - 1] : this.points[index - 1];
+		let point = this.points[index];
+		let next = this.points[(index + 1) % (this.points.length)];
+
+		return this.getAngleBetweenLines(point, previous, next);
+	}
+
+	// updateAzimuth(){
+	// 	// if(this.points.length !== 2){
+	// 	// 	return;
+	// 	// }
+
+	// 	// const azimuth = this.azimuth;
+
+	// 	// const [p0, p1] = this.points;
+
+	// 	// const r = p0.position.distanceTo(p1.position);
+		
+	// }
+
+	update () {
+		if (this.points.length === 0) {
+			return;
+		} else if (this.points.length === 1) {
+			let point = this.points[0];
+			let position = point.position;
+			this.spheres[0].position.copy(position);
+
+			{ // coordinate labels
+				let coordinateLabel = this.coordinateLabels[0];
+				
+				let msg = position.toArray().map(p => Utils.addCommas(p.toFixed(2))).join(" / ");
+				coordinateLabel.setText(msg);
+
+				coordinateLabel.visible = this.showCoordinates;
+			}
+
+			return;
+		}
+
+		let lastIndex = this.points.length - 1;
+
+		let centroid = new THREE.Vector3();
+		for (let i = 0; i <= lastIndex; i++) {
+			let point = this.points[i];
+			centroid.add(point.position);
+		}
+		centroid.divideScalar(this.points.length);
+
+		for (let i = 0; i <= lastIndex; i++) {
+			let index = i;
+			let nextIndex = (i + 1 > lastIndex) ? 0 : i + 1;
+			let previousIndex = (i === 0) ? lastIndex : i - 1;
+
+			let point = this.points[index];
+			let nextPoint = this.points[nextIndex];
+			let previousPoint = this.points[previousIndex];
+
+			let sphere = this.spheres[index];
+
+			// spheres
+			sphere.position.copy(point.position);
+			sphere.material.color = this.color;
+
+			{ // edges
+				let edge = this.edges[index];
+
+				edge.material.color = this.color;
+
+				edge.position.copy(point.position);
+
+				edge.geometry.setPositions([
+					0, 0, 0,
+					...nextPoint.position.clone().sub(point.position).toArray(),
+				]);
+
+				edge.geometry.verticesNeedUpdate = true;
+				edge.geometry.computeBoundingSphere();
+				edge.computeLineDistances();
+				edge.visible = index < lastIndex || this.closed;
+				
+				if(!this.showEdges){
+					edge.visible = false;
+				}
+			}
+
+			{ // edge labels
+				let edgeLabel = this.edgeLabels[i];
+
+				let center = new THREE.Vector3().add(point.position);
+				center.add(nextPoint.position);
+				center = center.multiplyScalar(0.5);
+				let distance = point.position.distanceTo(nextPoint.position);
+
+				edgeLabel.position.copy(center);
+
+				let suffix = "";
+				if(this.lengthUnit != null && this.lengthUnitDisplay != null){
+					distance = distance / this.lengthUnit.unitspermeter * this.lengthUnitDisplay.unitspermeter;  //convert to meters then to the display unit
+					suffix = this.lengthUnitDisplay.code;
+				}
+
+				let txtLength = Utils.addCommas(distance.toFixed(2));
+				edgeLabel.setText(`${txtLength} ${suffix}`);
+				edgeLabel.visible = this.showDistances && (index < lastIndex || this.closed) && this.points.length >= 2 && distance > 0;
+			}
+
+			{ // angle labels
+				let angleLabel = this.angleLabels[i];
+				let angle = this.getAngleBetweenLines(point, previousPoint, nextPoint);
+
+				let dir = nextPoint.position.clone().sub(previousPoint.position);
+				dir.multiplyScalar(0.5);
+				dir = previousPoint.position.clone().add(dir).sub(point.position).normalize();
+
+				let dist = Math.min(point.position.distanceTo(previousPoint.position), point.position.distanceTo(nextPoint.position));
+				dist = dist / 9;
+
+				let labelPos = point.position.clone().add(dir.multiplyScalar(dist));
+				angleLabel.position.copy(labelPos);
+
+				let msg = Utils.addCommas((angle * (180.0 / Math.PI)).toFixed(1)) + '\u00B0';
+				angleLabel.setText(msg);
+
+				angleLabel.visible = this.showAngles && (index < lastIndex || this.closed) && this.points.length >= 3 && angle > 0;
+			}
+		}
+
+		{ // update height stuff
+			let heightEdge = this.heightEdge;
+			heightEdge.visible = this.showHeight;
+			this.heightLabel.visible = this.showHeight;
+
+			if (this.showHeight) {
+				let sorted = this.points.slice().sort((a, b) => a.position.z - b.position.z);
+				let lowPoint = sorted[0].position.clone();
+				let highPoint = sorted[sorted.length - 1].position.clone();
+				let min = lowPoint.z;
+				let max = highPoint.z;
+				let height = max - min;
+
+				let start = new THREE.Vector3(highPoint.x, highPoint.y, min);
+				let end = new THREE.Vector3(highPoint.x, highPoint.y, max);
+
+				heightEdge.position.copy(lowPoint);
+
+				heightEdge.geometry.setPositions([
+					0, 0, 0,
+					...start.clone().sub(lowPoint).toArray(),
+					...start.clone().sub(lowPoint).toArray(),
+					...end.clone().sub(lowPoint).toArray(),
+				]);
+
+				heightEdge.geometry.verticesNeedUpdate = true;
+				// heightEdge.geometry.computeLineDistances();
+				// heightEdge.geometry.lineDistancesNeedUpdate = true;
+				heightEdge.geometry.computeBoundingSphere();
+				heightEdge.computeLineDistances();
+
+				// heightEdge.material.dashSize = height / 40;
+				// heightEdge.material.gapSize = height / 40;
+
+				let heightLabelPosition = start.clone().add(end).multiplyScalar(0.5);
+				this.heightLabel.position.copy(heightLabelPosition);
+
+				let suffix = "";
+				if(this.lengthUnit != null && this.lengthUnitDisplay != null){
+					height = height / this.lengthUnit.unitspermeter * this.lengthUnitDisplay.unitspermeter;  //convert to meters then to the display unit
+					suffix = this.lengthUnitDisplay.code;
+				}
+
+				let txtHeight = Utils.addCommas(height.toFixed(2));
+				let msg = `${txtHeight} ${suffix}`;
+				this.heightLabel.setText(msg);
+			}
+		}
+
+		{ // update circle stuff
+			const circleRadiusLabel = this.circleRadiusLabel;
+			const circleRadiusLine = this.circleRadiusLine;
+			const circleLine = this.circleLine;
+			const circleCenter = this.circleCenter;
+
+			const circleOkay = this.points.length === 3;
+
+			circleRadiusLabel.visible = this.showCircle && circleOkay;
+			circleRadiusLine.visible = this.showCircle && circleOkay;
+			circleLine.visible = this.showCircle && circleOkay;
+			circleCenter.visible = this.showCircle && circleOkay;
+
+			if(this.showCircle && circleOkay){
+
+				const A = this.points[0].position;
+				const B = this.points[1].position;
+				const C = this.points[2].position;
+				const AB = B.clone().sub(A);
+				const AC = C.clone().sub(A);
+				const N = AC.clone().cross(AB).normalize();
+
+				const center = Potree.Utils.computeCircleCenter(A, B, C);
+				const radius = center.distanceTo(A);
+
+
+				const scale = radius / 20;
+				circleCenter.position.copy(center);
+				circleCenter.scale.set(scale, scale, scale);
+
+				//circleRadiusLine.geometry.vertices[0].set(0, 0, 0);
+				//circleRadiusLine.geometry.vertices[1].copy(B.clone().sub(center));
+
+				circleRadiusLine.geometry.setPositions( [
+					0, 0, 0,
+					...B.clone().sub(center).toArray()
+				] );
+
+				circleRadiusLine.geometry.verticesNeedUpdate = true;
+				circleRadiusLine.geometry.computeBoundingSphere();
+				circleRadiusLine.position.copy(center);
+				circleRadiusLine.computeLineDistances();
+
+				const target = center.clone().add(N);
+				circleLine.position.copy(center);
+				circleLine.scale.set(radius, radius, radius);
+				circleLine.lookAt(target);
+				
+				circleRadiusLabel.visible = true;
+				circleRadiusLabel.position.copy(center.clone().add(B).multiplyScalar(0.5));
+				circleRadiusLabel.setText(`${radius.toFixed(3)}`);
+
+			}
+		}
+
+		{ // update area label
+			this.areaLabel.position.copy(centroid);
+			this.areaLabel.visible = this.showArea && this.points.length >= 3;
+			let area = this.getArea();
+
+			let suffix = "";
+			if(this.lengthUnit != null && this.lengthUnitDisplay != null){
+				area = area / Math.pow(this.lengthUnit.unitspermeter, 2) * Math.pow(this.lengthUnitDisplay.unitspermeter, 2);  //convert to square meters then to the square display unit
+				suffix = this.lengthUnitDisplay.code;
+			}
+
+			let txtArea = Utils.addCommas(area.toFixed(1));
+			let msg =  `${txtArea} ${suffix}\u00B2`;
+			this.areaLabel.setText(msg);
+		}
+
+		// this.updateAzimuth();
+	};
+
+	raycast (raycaster, intersects) {
+		for (let i = 0; i < this.points.length; i++) {
+			let sphere = this.spheres[i];
+
+			sphere.raycast(raycaster, intersects);
+		}
+
+		// recalculate distances because they are not necessarely correct
+		// for scaled objects.
+		// see https://github.com/mrdoob/three.js/issues/5827
+		// TODO: remove this once the bug has been fixed
+		for (let i = 0; i < intersects.length; i++) {
+			let I = intersects[i];
+			I.distance = raycaster.ray.origin.distanceTo(I.point);
+		}
+		intersects.sort(function (a, b) { return a.distance - b.distance; });
+	};
+
+	get showCoordinates () {
+		return this._showCoordinates;
+	}
+
+	set showCoordinates (value) {
+		this._showCoordinates = value;
+		this.update();
+	}
+
+	get showAngles () {
+		return this._showAngles;
+	}
+
+	set showAngles (value) {
+		this._showAngles = value;
+		this.update();
+	}
+
+	get showCircle () {
+		return this._showCircle;
+	}
+
+	set showCircle (value) {
+		this._showCircle = value;
+		this.update();
+	}
+
+	get showAzimuth(){
+		return this._showAzimuth;
+	}
+
+	set showAzimuth(value){
+		this._showAzimuth = value;
+		this.update();
+	}
+
+	get showEdges () {
+		return this._showEdges;
+	}
+
+	set showEdges (value) {
+		this._showEdges = value;
+		this.update();
+	}
+
+	get showHeight () {
+		return this._showHeight;
+	}
+
+	set showHeight (value) {
+		this._showHeight = value;
+		this.update();
+	}
+
+	get showArea () {
+		return this._showArea;
+	}
+
+	set showArea (value) {
+		this._showArea = value;
+		this.update();
+	}
+
+	get closed () {
+		return this._closed;
+	}
+
+	set closed (value) {
+		this._closed = value;
+		this.update();
+	}
+
+	get showDistances () {
+		return this._showDistances;
+	}
+
+	set showDistances (value) {
+		this._showDistances = value;
+		this.update();
+	}
+
+}

+ 420 - 0
src/utils/MeasuringTool.js

@@ -0,0 +1,420 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import {Measure} from "./Measure.js";
+import {Utils} from "../utils.js";
+import {CameraMode} from "../defines.js";
+import { EventDispatcher } from "../EventDispatcher.js";
+
+function updateAzimuth(viewer, measure){
+
+	const azimuth = measure.azimuth;
+
+	const isOkay = measure.points.length === 2;
+
+	azimuth.node.visible = isOkay && measure.showAzimuth;
+
+	if(!azimuth.node.visible){
+		return;
+	}
+
+	const camera = viewer.scene.getActiveCamera();
+	const renderAreaSize = viewer.renderer.getSize(new THREE.Vector2());
+	const width = renderAreaSize.width;
+	const height = renderAreaSize.height;
+	
+	const [p0, p1] = measure.points;
+	const r = p0.position.distanceTo(p1.position);
+	const northVec = Utils.getNorthVec(p0.position, r, viewer.getProjection());
+	const northPos = p0.position.clone().add(northVec);
+
+	azimuth.center.position.copy(p0.position);
+	azimuth.center.scale.set(2, 2, 2);
+	
+	azimuth.center.visible = false;
+	// azimuth.target.visible = false;
+
+
+	{ // north
+		azimuth.north.position.copy(northPos);
+		azimuth.north.scale.set(2, 2, 2);
+
+		let distance = azimuth.north.position.distanceTo(camera.position);
+		let pr = Utils.projectedRadius(1, camera, distance, width, height);
+
+		let scale = (5 / pr);
+		azimuth.north.scale.set(scale, scale, scale);
+	}
+
+	{ // target
+		azimuth.target.position.copy(p1.position);
+		azimuth.target.position.z = azimuth.north.position.z;
+
+		let distance = azimuth.target.position.distanceTo(camera.position);
+		let pr = Utils.projectedRadius(1, camera, distance, width, height);
+
+		let scale = (5 / pr);
+		azimuth.target.scale.set(scale, scale, scale);
+	}
+
+
+	azimuth.circle.position.copy(p0.position);
+	azimuth.circle.scale.set(r, r, r);
+	azimuth.circle.material.resolution.set(width, height);
+
+	// to target
+	azimuth.centerToTarget.geometry.setPositions([
+		0, 0, 0,
+		...p1.position.clone().sub(p0.position).toArray(),
+	]);
+	azimuth.centerToTarget.position.copy(p0.position);
+	azimuth.centerToTarget.geometry.verticesNeedUpdate = true;
+	azimuth.centerToTarget.geometry.computeBoundingSphere();
+	azimuth.centerToTarget.computeLineDistances();
+	azimuth.centerToTarget.material.resolution.set(width, height);
+
+	// to target ground
+	azimuth.centerToTargetground.geometry.setPositions([
+		0, 0, 0,
+		p1.position.x - p0.position.x,
+		p1.position.y - p0.position.y,
+		0,
+	]);
+	azimuth.centerToTargetground.position.copy(p0.position);
+	azimuth.centerToTargetground.geometry.verticesNeedUpdate = true;
+	azimuth.centerToTargetground.geometry.computeBoundingSphere();
+	azimuth.centerToTargetground.computeLineDistances();
+	azimuth.centerToTargetground.material.resolution.set(width, height);
+
+	// to north
+	azimuth.centerToNorth.geometry.setPositions([
+		0, 0, 0,
+		northPos.x - p0.position.x,
+		northPos.y - p0.position.y,
+		0,
+	]);
+	azimuth.centerToNorth.position.copy(p0.position);
+	azimuth.centerToNorth.geometry.verticesNeedUpdate = true;
+	azimuth.centerToNorth.geometry.computeBoundingSphere();
+	azimuth.centerToNorth.computeLineDistances();
+	azimuth.centerToNorth.material.resolution.set(width, height);
+
+	// label
+	const radians = Utils.computeAzimuth(p0.position, p1.position, viewer.getProjection());
+	let degrees = THREE.Math.radToDeg(radians);
+	if(degrees < 0){
+		degrees = 360 + degrees;
+	}
+	const txtDegrees = `${degrees.toFixed(2)}°`;
+	const labelDir = northPos.clone().add(p1.position).multiplyScalar(0.5).sub(p0.position);
+	if(labelDir.length() > 0){
+		labelDir.z = 0;
+		labelDir.normalize();
+		const labelVec = labelDir.clone().multiplyScalar(r);
+		const labelPos = p0.position.clone().add(labelVec);
+		azimuth.label.position.copy(labelPos);
+	}
+	azimuth.label.setText(txtDegrees);
+	let distance = azimuth.label.position.distanceTo(camera.position);
+	let pr = Utils.projectedRadius(1, camera, distance, width, height);
+	let scale = (70 / pr);
+	azimuth.label.scale.set(scale, scale, scale);
+}
+
+export class MeasuringTool extends EventDispatcher{
+	constructor (viewer) {
+		super();
+
+		this.viewer = viewer;
+		this.renderer = viewer.renderer;
+
+		this.addEventListener('start_inserting_measurement', e => {
+			this.viewer.dispatchEvent({
+				type: 'cancel_insertions'
+			});
+		});
+
+		this.showLabels = true;
+		this.scene = new THREE.Scene();
+		this.scene.name = 'scene_measurement';
+		this.light = new THREE.PointLight(0xffffff, 1.0);
+		this.scene.add(this.light);
+
+		this.viewer.inputHandler.registerInteractiveScene(this.scene);
+
+		this.onRemove = (e) => { this.scene.remove(e.measurement);};
+		this.onAdd = e => {this.scene.add(e.measurement);};
+
+		for(let measurement of viewer.scene.measurements){
+			this.onAdd({measurement: measurement});
+		}
+
+		viewer.addEventListener("update", this.update.bind(this));
+		viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this));
+		viewer.addEventListener("scene_changed", this.onSceneChange.bind(this));
+
+		viewer.scene.addEventListener('measurement_added', this.onAdd);
+		viewer.scene.addEventListener('measurement_removed', this.onRemove);
+	}
+
+	onSceneChange(e){
+		if(e.oldScene){
+			e.oldScene.removeEventListener('measurement_added', this.onAdd);
+			e.oldScene.removeEventListener('measurement_removed', this.onRemove);
+		}
+
+		e.scene.addEventListener('measurement_added', this.onAdd);
+		e.scene.addEventListener('measurement_removed', this.onRemove);
+	}
+
+	startInsertion (args = {}) {
+		let domElement = this.viewer.renderer.domElement;
+
+		let measure = new Measure();
+
+		this.dispatchEvent({
+			type: 'start_inserting_measurement',
+			measure: measure
+		});
+
+		const pick = (defaul, alternative) => {
+			if(defaul != null){
+				return defaul;
+			}else{
+				return alternative;
+			}
+		};
+
+		measure.showDistances = (args.showDistances === null) ? true : args.showDistances;
+
+		measure.showArea = pick(args.showArea, false);
+		measure.showAngles = pick(args.showAngles, false);
+		measure.showCoordinates = pick(args.showCoordinates, false);
+		measure.showHeight = pick(args.showHeight, false);
+		measure.showCircle = pick(args.showCircle, false);
+		measure.showAzimuth = pick(args.showAzimuth, false);
+		measure.showEdges = pick(args.showEdges, true);
+		measure.closed = pick(args.closed, false);
+		measure.maxMarkers = pick(args.maxMarkers, Infinity);
+
+		measure.name = args.name || 'Measurement';
+
+		this.scene.add(measure);
+
+		let cancel = {
+			removeLastMarker: measure.maxMarkers > 3,
+			callback: null
+		};
+
+		let insertionCallback = (e) => {
+			if (e.button === THREE.MOUSE.LEFT) {
+				measure.addMarker(measure.points[measure.points.length - 1].position.clone());
+
+				if (measure.points.length >= measure.maxMarkers) {
+					cancel.callback();
+				}
+
+				this.viewer.inputHandler.startDragging(
+					measure.spheres[measure.spheres.length - 1]);
+			} else if (e.button === THREE.MOUSE.RIGHT) {
+				cancel.callback();
+			}
+		};
+
+		cancel.callback = e => {
+			if (cancel.removeLastMarker) {
+				measure.removeMarker(measure.points.length - 1);
+			}
+			domElement.removeEventListener('mouseup', insertionCallback, false);
+			this.viewer.removeEventListener('cancel_insertions', cancel.callback);
+		};
+
+		if (measure.maxMarkers > 1) {
+			this.viewer.addEventListener('cancel_insertions', cancel.callback);
+			domElement.addEventListener('mouseup', insertionCallback, false);
+		}
+
+		measure.addMarker(new THREE.Vector3(0, 0, 0));
+		this.viewer.inputHandler.startDragging(
+			measure.spheres[measure.spheres.length - 1]);
+
+		this.viewer.scene.addMeasurement(measure);
+
+		return measure;
+	}
+	
+	update(){
+		let camera = this.viewer.scene.getActiveCamera();
+		let domElement = this.renderer.domElement;
+		let measurements = this.viewer.scene.measurements;
+
+		const renderAreaSize = this.renderer.getSize(new THREE.Vector2());
+		let clientWidth = renderAreaSize.width;
+		let clientHeight = renderAreaSize.height;
+
+		this.light.position.copy(camera.position);
+
+		// make size independant of distance
+		for (let measure of measurements) {
+			measure.lengthUnit = this.viewer.lengthUnit;
+			measure.lengthUnitDisplay = this.viewer.lengthUnitDisplay;
+			measure.update();
+
+			updateAzimuth(this.viewer, measure);
+
+			// spheres
+			for(let sphere of measure.spheres){
+				let distance = camera.position.distanceTo(sphere.getWorldPosition(new THREE.Vector3()));
+				let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
+				let scale = (15 / pr);
+				sphere.scale.set(scale, scale, scale);
+			}
+
+			// labels
+			let labels = measure.edgeLabels.concat(measure.angleLabels);
+			for(let label of labels){
+				let distance = camera.position.distanceTo(label.getWorldPosition(new THREE.Vector3()));
+				let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
+				let scale = (70 / pr);
+
+				if(Potree.debug.scale){
+					scale = (Potree.debug.scale / pr);
+				}
+
+				label.scale.set(scale, scale, scale);
+			}
+
+			// coordinate labels
+			for (let j = 0; j < measure.coordinateLabels.length; j++) {
+				let label = measure.coordinateLabels[j];
+				let sphere = measure.spheres[j];
+
+				let distance = camera.position.distanceTo(sphere.getWorldPosition(new THREE.Vector3()));
+
+				let screenPos = sphere.getWorldPosition(new THREE.Vector3()).clone().project(camera);
+				screenPos.x = Math.round((screenPos.x + 1) * clientWidth / 2);
+				screenPos.y = Math.round((-screenPos.y + 1) * clientHeight / 2);
+				screenPos.z = 0;
+				screenPos.y -= 30;
+
+				let labelPos = new THREE.Vector3( 
+					(screenPos.x / clientWidth) * 2 - 1, 
+					-(screenPos.y / clientHeight) * 2 + 1, 
+					0.5 );
+				labelPos.unproject(camera);
+				if(this.viewer.scene.cameraMode == CameraMode.PERSPECTIVE) {
+					let direction = labelPos.sub(camera.position).normalize();
+					labelPos = new THREE.Vector3().addVectors(
+						camera.position, direction.multiplyScalar(distance));
+
+				}
+				label.position.copy(labelPos);
+				let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
+				let scale = (70 / pr);
+				label.scale.set(scale, scale, scale);
+			}
+
+			// height label
+			if (measure.showHeight) {
+				let label = measure.heightLabel;
+
+				{
+					let distance = label.position.distanceTo(camera.position);
+					let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
+					let scale = (70 / pr);
+					label.scale.set(scale, scale, scale);
+				}
+
+				{ // height edge
+					let edge = measure.heightEdge;
+
+					let sorted = measure.points.slice().sort((a, b) => a.position.z - b.position.z);
+					let lowPoint = sorted[0].position.clone();
+					let highPoint = sorted[sorted.length - 1].position.clone();
+					let min = lowPoint.z;
+					let max = highPoint.z;
+
+					let start = new THREE.Vector3(highPoint.x, highPoint.y, min);
+					let end = new THREE.Vector3(highPoint.x, highPoint.y, max);
+
+					let lowScreen = lowPoint.clone().project(camera);
+					let startScreen = start.clone().project(camera);
+					let endScreen = end.clone().project(camera);
+
+					let toPixelCoordinates = v => {
+						let r = v.clone().addScalar(1).divideScalar(2);
+						r.x = r.x * clientWidth;
+						r.y = r.y * clientHeight;
+						r.z = 0;
+
+						return r;
+					};
+
+					let lowEL = toPixelCoordinates(lowScreen);
+					let startEL = toPixelCoordinates(startScreen);
+					let endEL = toPixelCoordinates(endScreen);
+
+					let lToS = lowEL.distanceTo(startEL);
+					let sToE = startEL.distanceTo(endEL);
+
+					edge.geometry.lineDistances = [0, lToS, lToS, lToS + sToE];
+					edge.geometry.lineDistancesNeedUpdate = true;
+
+					edge.material.dashSize = 10;
+					edge.material.gapSize = 10;
+				}
+			}
+
+			{ // area label
+				let label = measure.areaLabel;
+				let distance = label.position.distanceTo(camera.position);
+				let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
+
+				let scale = (70 / pr);
+				label.scale.set(scale, scale, scale);
+			}
+
+			{ // radius label
+				let label = measure.circleRadiusLabel;
+				let distance = label.position.distanceTo(camera.position);
+				let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
+
+				let scale = (70 / pr);
+				label.scale.set(scale, scale, scale);
+			}
+
+			{ // edges
+				const materials = [
+					measure.circleRadiusLine.material,
+					...measure.edges.map( (e) => e.material),
+					measure.heightEdge.material,
+					measure.circleLine.material,
+				];
+
+				for(const material of materials){
+					material.resolution.set(clientWidth, clientHeight);
+				}
+			}
+
+			if(!this.showLabels){
+
+				const labels = [
+					...measure.sphereLabels, 
+					...measure.edgeLabels, 
+					...measure.angleLabels, 
+					...measure.coordinateLabels,
+					measure.heightLabel,
+					measure.areaLabel,
+					measure.circleRadiusLabel,
+				];
+
+				for(const label of labels){
+					label.visible = false;
+				}
+			}
+		}
+	}
+
+	render(){
+		this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera());
+	}
+};

+ 66 - 0
src/utils/PolygonClipVolume.js

@@ -0,0 +1,66 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+
+export class PolygonClipVolume extends THREE.Object3D{
+	
+	constructor(camera){
+		super();
+
+		this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;
+		this.name = "polygon_clip_volume_" + this.constructor.counter;
+
+		this.camera = camera.clone();
+		this.camera.rotation.set(...camera.rotation.toArray()); // [r85] workaround because camera.clone() doesn't work on rotation
+		this.camera.rotation.order = camera.rotation.order;
+		this.camera.updateMatrixWorld();
+		this.camera.updateProjectionMatrix();
+		this.camera.matrixWorldInverse.copy(this.camera.matrixWorld).invert();
+
+		this.viewMatrix = this.camera.matrixWorldInverse.clone();
+		this.projMatrix = this.camera.projectionMatrix.clone();
+
+		// projected markers
+		this.markers = [];
+		this.initialized = false;
+	}
+
+	addMarker() {
+
+		let marker = new THREE.Mesh();
+
+		let cancel;
+
+		let drag = e => {
+			let size = e.viewer.renderer.getSize(new THREE.Vector2());
+			let projectedPos = new THREE.Vector3(
+				2.0 * (e.drag.end.x / size.width) - 1.0,
+				-2.0 * (e.drag.end.y / size.height) + 1.0,
+				0
+			);
+
+			marker.position.copy(projectedPos);
+		};
+		
+		let drop = e => {	
+			cancel();
+		};
+		
+		cancel = e => {
+			marker.removeEventListener("drag", drag);
+			marker.removeEventListener("drop", drop);
+		};
+		
+		marker.addEventListener("drag", drag);
+		marker.addEventListener("drop", drop);
+
+
+		this.markers.push(marker);
+	}
+
+	removeLastMarker() {
+		if(this.markers.length > 0) {
+			this.markers.splice(this.markers.length - 1, 1);
+		}
+	}
+
+};

+ 323 - 0
src/utils/Profile.js

@@ -0,0 +1,323 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import {Utils} from "../utils.js";
+
+export class Profile extends THREE.Object3D{
+
+	constructor () {
+		super();
+
+		this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;
+
+		this.name = 'Profile_' + this.constructor.counter;
+		this.points = [];
+		this.spheres = [];
+		this.edges = [];
+		this.boxes = [];
+		this.width = 1;
+		this.height = 20;
+		this._modifiable = true;
+
+		this.sphereGeometry = new THREE.SphereGeometry(0.4, 10, 10);
+		this.color = new THREE.Color(0xff0000);
+		this.lineColor = new THREE.Color(0xff0000);
+	}
+
+	createSphereMaterial () {
+		let sphereMaterial = new THREE.MeshLambertMaterial({
+			//shading: THREE.SmoothShading,
+			color: 0xff0000,
+			depthTest: false,
+			depthWrite: false}
+		);
+
+		return sphereMaterial;
+	};
+
+	getSegments () {
+		let segments = [];
+
+		for (let i = 0; i < this.points.length - 1; i++) {
+			let start = this.points[i].clone();
+			let end = this.points[i + 1].clone();
+			segments.push({start: start, end: end});
+		}
+
+		return segments;
+	}
+
+	getSegmentMatrices () {
+		let segments = this.getSegments();
+		let matrices = [];
+
+		for (let segment of segments) {
+			let {start, end} = segment;
+
+			let box = new THREE.Object3D();
+
+			let length = start.clone().setZ(0).distanceTo(end.clone().setZ(0));
+			box.scale.set(length, 10000, this.width);
+			box.up.set(0, 0, 1);
+
+			let center = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
+			let diff = new THREE.Vector3().subVectors(end, start);
+			let target = new THREE.Vector3(diff.y, -diff.x, 0);
+
+			box.position.set(0, 0, 0);
+			box.lookAt(target);
+			box.position.copy(center);
+
+			box.updateMatrixWorld();
+			matrices.push(box.matrixWorld);
+		}
+
+		return matrices;
+	}
+
+	addMarker (point) {
+		this.points.push(point);
+
+		let sphere = new THREE.Mesh(this.sphereGeometry, this.createSphereMaterial());
+
+		this.add(sphere);
+		this.spheres.push(sphere);
+
+		// edges & boxes
+		if (this.points.length > 1) {
+			let lineGeometry = new THREE.Geometry();
+			lineGeometry.vertices.push(new THREE.Vector3(), new THREE.Vector3());
+			lineGeometry.colors.push(this.lineColor, this.lineColor, this.lineColor);
+			let lineMaterial = new THREE.LineBasicMaterial({
+				vertexColors: THREE.VertexColors,
+				linewidth: 2,
+				transparent: true,
+				opacity: 0.4
+			});
+			lineMaterial.depthTest = false;
+			let edge = new THREE.Line(lineGeometry, lineMaterial);
+			edge.visible = false;
+
+			this.add(edge);
+			this.edges.push(edge);
+
+			let boxGeometry = new THREE.BoxGeometry(1, 1, 1);
+			let boxMaterial = new THREE.MeshBasicMaterial({color: 0xff0000, transparent: true, opacity: 0.2});
+			let box = new THREE.Mesh(boxGeometry, boxMaterial);
+			box.visible = false;
+
+			this.add(box);
+			this.boxes.push(box);
+		}
+
+		{ // event listeners
+			let drag = (e) => {
+				let I = Utils.getMousePointCloudIntersection(
+					e.drag.end, 
+					e.viewer.scene.getActiveCamera(), 
+					e.viewer, 
+					e.viewer.scene.pointclouds);
+
+				if (I) {
+					let i = this.spheres.indexOf(e.drag.object);
+					if (i !== -1) {
+						this.setPosition(i, I.location);
+						//this.dispatchEvent({
+						//	'type': 'marker_moved',
+						//	'profile': this,
+						//	'index': i
+						//});
+					}
+				}
+			};
+
+			let drop = e => {
+				let i = this.spheres.indexOf(e.drag.object);
+				if (i !== -1) {
+					this.dispatchEvent({
+						'type': 'marker_dropped',
+						'profile': this,
+						'index': i
+					});
+				}
+			};
+
+			let mouseover = (e) => e.object.material.emissive.setHex(0x888888);
+			let mouseleave = (e) => e.object.material.emissive.setHex(0x000000);
+
+			sphere.addEventListener('drag', drag);
+			sphere.addEventListener('drop', drop);
+			sphere.addEventListener('mouseover', mouseover);
+			sphere.addEventListener('mouseleave', mouseleave);
+		}
+
+		let event = {
+			type: 'marker_added',
+			profile: this,
+			sphere: sphere
+		};
+		this.dispatchEvent(event);
+
+		this.setPosition(this.points.length - 1, point);
+	}
+
+	removeMarker (index) {
+		this.points.splice(index, 1);
+
+		this.remove(this.spheres[index]);
+
+		let edgeIndex = (index === 0) ? 0 : (index - 1);
+		this.remove(this.edges[edgeIndex]);
+		this.edges.splice(edgeIndex, 1);
+		this.remove(this.boxes[edgeIndex]);
+		this.boxes.splice(edgeIndex, 1);
+
+		this.spheres.splice(index, 1);
+
+		this.update();
+
+		this.dispatchEvent({
+			'type': 'marker_removed',
+			'profile': this
+		});
+	}
+
+	setPosition (index, position) {
+		let point = this.points[index];
+		point.copy(position);
+
+		let event = {
+			type: 'marker_moved',
+			profile:	this,
+			index:	index,
+			position: point.clone()
+		};
+		this.dispatchEvent(event);
+
+		this.update();
+	}
+
+	setWidth (width) {
+		this.width = width;
+
+		let event = {
+			type: 'width_changed',
+			profile:	this,
+			width:	width
+		};
+		this.dispatchEvent(event);
+
+		this.update();
+	}
+
+	getWidth () {
+		return this.width;
+	}
+
+	update () {
+		if (this.points.length === 0) {
+			return;
+		} else if (this.points.length === 1) {
+			let point = this.points[0];
+			this.spheres[0].position.copy(point);
+
+			return;
+		}
+
+		let min = this.points[0].clone();
+		let max = this.points[0].clone();
+		let centroid = new THREE.Vector3();
+		let lastIndex = this.points.length - 1;
+		for (let i = 0; i <= lastIndex; i++) {
+			let point = this.points[i];
+			let sphere = this.spheres[i];
+			let leftIndex = (i === 0) ? lastIndex : i - 1;
+			// let rightIndex = (i === lastIndex) ? 0 : i + 1;
+			let leftVertex = this.points[leftIndex];
+			// let rightVertex = this.points[rightIndex];
+			let leftEdge = this.edges[leftIndex];
+			let rightEdge = this.edges[i];
+			let leftBox = this.boxes[leftIndex];
+			// rightBox = this.boxes[i];
+
+			// let leftEdgeLength = point.distanceTo(leftVertex);
+			// let rightEdgeLength = point.distanceTo(rightVertex);
+			// let leftEdgeCenter = new THREE.Vector3().addVectors(leftVertex, point).multiplyScalar(0.5);
+			// let rightEdgeCenter = new THREE.Vector3().addVectors(point, rightVertex).multiplyScalar(0.5);
+
+			sphere.position.copy(point);
+
+			if (this._modifiable) {
+				sphere.visible = true;
+			} else {
+				sphere.visible = false;
+			}
+
+			if (leftEdge) {
+				leftEdge.geometry.vertices[1].copy(point);
+				leftEdge.geometry.verticesNeedUpdate = true;
+				leftEdge.geometry.computeBoundingSphere();
+			}
+
+			if (rightEdge) {
+				rightEdge.geometry.vertices[0].copy(point);
+				rightEdge.geometry.verticesNeedUpdate = true;
+				rightEdge.geometry.computeBoundingSphere();
+			}
+
+			if (leftBox) {
+				let start = leftVertex;
+				let end = point;
+				let length = start.clone().setZ(0).distanceTo(end.clone().setZ(0));
+				leftBox.scale.set(length, 1000000, this.width);
+				leftBox.up.set(0, 0, 1);
+
+				let center = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
+				let diff = new THREE.Vector3().subVectors(end, start);
+				let target = new THREE.Vector3(diff.y, -diff.x, 0);
+
+				leftBox.position.set(0, 0, 0);
+				leftBox.lookAt(target);
+				leftBox.position.copy(center);
+			}
+
+			centroid.add(point);
+			min.min(point);
+			max.max(point);
+		}
+		centroid.multiplyScalar(1 / this.points.length);
+
+		for (let i = 0; i < this.boxes.length; i++) {
+			let box = this.boxes[i];
+
+			box.position.z = min.z + (max.z - min.z) / 2;
+		}
+	}
+
+	raycast (raycaster, intersects) {
+		for (let i = 0; i < this.points.length; i++) {
+			let sphere = this.spheres[i];
+
+			sphere.raycast(raycaster, intersects);
+		}
+
+		// recalculate distances because they are not necessarely correct
+		// for scaled objects.
+		// see https://github.com/mrdoob/three.js/issues/5827
+		// TODO: remove this once the bug has been fixed
+		for (let i = 0; i < intersects.length; i++) {
+			let I = intersects[i];
+			I.distance = raycaster.ray.origin.distanceTo(I.point);
+		}
+		intersects.sort(function (a, b) { return a.distance - b.distance; });
+	};
+
+	get modifiable () {
+		return this._modifiable;
+	}
+
+	set modifiable (value) {
+		this._modifiable = value;
+		this.update();
+	}
+
+}

+ 133 - 0
src/utils/ProfileTool.js

@@ -0,0 +1,133 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import {Profile} from "./Profile.js";
+import {Utils} from "../utils.js";
+import { EventDispatcher } from "../EventDispatcher.js";
+
+
+export class ProfileTool extends EventDispatcher {
+	constructor (viewer) {
+		super();
+
+		this.viewer = viewer;
+		this.renderer = viewer.renderer;
+
+		this.addEventListener('start_inserting_profile', e => {
+			this.viewer.dispatchEvent({
+				type: 'cancel_insertions'
+			});
+		});
+
+		this.scene = new THREE.Scene();
+		this.scene.name = 'scene_profile';
+		this.light = new THREE.PointLight(0xffffff, 1.0);
+		this.scene.add(this.light);
+
+		this.viewer.inputHandler.registerInteractiveScene(this.scene);
+
+		this.onRemove = e => this.scene.remove(e.profile);
+		this.onAdd = e => this.scene.add(e.profile);
+
+		for(let profile of viewer.scene.profiles){
+			this.onAdd({profile: profile});
+		}
+
+		viewer.addEventListener("update", this.update.bind(this));
+		viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this));
+		viewer.addEventListener("scene_changed", this.onSceneChange.bind(this));
+
+		viewer.scene.addEventListener('profile_added', this.onAdd);
+		viewer.scene.addEventListener('profile_removed', this.onRemove);
+	}
+
+	onSceneChange(e){
+		if(e.oldScene){
+			e.oldScene.removeEventListeners('profile_added', this.onAdd);
+			e.oldScene.removeEventListeners('profile_removed', this.onRemove);
+		}
+
+		e.scene.addEventListener('profile_added', this.onAdd);
+		e.scene.addEventListener('profile_removed', this.onRemove);
+	}
+
+	startInsertion (args = {}) {
+		let domElement = this.viewer.renderer.domElement;
+
+		let profile = new Profile();
+		profile.name = args.name || 'Profile';
+
+		this.dispatchEvent({
+			type: 'start_inserting_profile',
+			profile: profile
+		});
+
+		this.scene.add(profile);
+
+		let cancel = {
+			callback: null
+		};
+
+		let insertionCallback = (e) => {
+			if(e.button === THREE.MOUSE.LEFT){
+				if(profile.points.length <= 1){
+					let camera = this.viewer.scene.getActiveCamera();
+					let distance = camera.position.distanceTo(profile.points[0]);
+					let clientSize = this.viewer.renderer.getSize(new THREE.Vector2());
+					let pr = Utils.projectedRadius(1, camera, distance, clientSize.width, clientSize.height);
+					let width = (10 / pr);
+
+					profile.setWidth(width);
+				}
+
+				profile.addMarker(profile.points[profile.points.length - 1].clone());
+
+				this.viewer.inputHandler.startDragging(
+					profile.spheres[profile.spheres.length - 1]);
+			} else if (e.button === THREE.MOUSE.RIGHT) {
+				cancel.callback();
+			}
+		};
+
+		cancel.callback = e => {
+			profile.removeMarker(profile.points.length - 1);
+			domElement.removeEventListener('mouseup', insertionCallback, false);
+			this.viewer.removeEventListener('cancel_insertions', cancel.callback);
+		};
+
+		this.viewer.addEventListener('cancel_insertions', cancel.callback);
+		domElement.addEventListener('mouseup', insertionCallback, false);
+
+		profile.addMarker(new THREE.Vector3(0, 0, 0));
+		this.viewer.inputHandler.startDragging(
+			profile.spheres[profile.spheres.length - 1]);
+
+		this.viewer.scene.addProfile(profile);
+
+		return profile;
+	}
+	
+	update(){
+		let camera = this.viewer.scene.getActiveCamera();
+		let profiles = this.viewer.scene.profiles;
+		let renderAreaSize = this.viewer.renderer.getSize(new THREE.Vector2());
+		let clientWidth = renderAreaSize.width;
+		let clientHeight = renderAreaSize.height;
+
+		this.light.position.copy(camera.position);
+
+		// make size independant of distance
+		for(let profile of profiles){
+			for(let sphere of profile.spheres){				
+				let distance = camera.position.distanceTo(sphere.getWorldPosition(new THREE.Vector3()));
+				let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
+				let scale = (15 / pr);
+				sphere.scale.set(scale, scale, scale);
+			}
+		}
+	}
+
+	render(){
+		this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera());
+	}
+
+}

+ 182 - 0
src/utils/ScreenBoxSelectTool.js

@@ -0,0 +1,182 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import {BoxVolume} from "./Volume.js";
+import {Utils} from "../utils.js";
+import {PointSizeType} from "../defines.js";
+import { EventDispatcher } from "../EventDispatcher.js";
+
+
+export class ScreenBoxSelectTool extends EventDispatcher{
+
+	constructor(viewer){
+		super();
+
+		this.viewer = viewer;
+		this.scene = new THREE.Scene();
+
+		viewer.addEventListener("update", this.update.bind(this));
+		viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this));
+		viewer.addEventListener("scene_changed", this.onSceneChange.bind(this));
+	}
+
+	onSceneChange(scene){
+		console.log("scene changed");
+	}
+
+	startInsertion(){
+		let domElement = this.viewer.renderer.domElement;
+
+		let volume = new BoxVolume();
+		volume.position.set(12345, 12345, 12345);
+		volume.showVolumeLabel = false;
+		volume.visible = false;
+		volume.update();
+		this.viewer.scene.addVolume(volume);
+
+		this.importance = 10;
+
+		let selectionBox = $(`<div style="position: absolute; border: 2px solid white; pointer-events: none; border-style:dashed"></div>`);
+		$(domElement.parentElement).append(selectionBox);
+		selectionBox.css("right", "10px");
+		selectionBox.css("bottom", "10px");
+
+		let drag = e =>{
+
+			volume.visible = true;
+
+			let mStart = e.drag.start;
+			let mEnd = e.drag.end;
+
+			let box2D = new THREE.Box2();
+			box2D.expandByPoint(mStart);
+			box2D.expandByPoint(mEnd);
+
+			selectionBox.css("left", `${box2D.min.x}px`);
+			selectionBox.css("top", `${box2D.min.y}px`);
+			selectionBox.css("width", `${box2D.max.x - box2D.min.x}px`);
+			selectionBox.css("height", `${box2D.max.y - box2D.min.y}px`);
+
+			let camera = e.viewer.scene.getActiveCamera();
+			let size = e.viewer.renderer.getSize(new THREE.Vector2());
+			let frustumSize = new THREE.Vector2(
+				camera.right - camera.left, 
+				camera.top - camera.bottom);
+
+			let screenCentroid = new THREE.Vector2().addVectors(e.drag.end, e.drag.start).multiplyScalar(0.5);
+			let ray = Utils.mouseToRay(screenCentroid, camera, size.width, size.height);
+
+			let diff = new THREE.Vector2().subVectors(e.drag.end, e.drag.start);
+			diff.divide(size).multiply(frustumSize);
+			
+			volume.position.copy(ray.origin);
+			volume.up.copy(camera.up);
+			volume.rotation.copy(camera.rotation);
+			volume.scale.set(diff.x, diff.y, 1000 * 100);
+
+			e.consume();
+		};
+
+		let drop = e => {
+			this.importance = 0;
+
+			$(selectionBox).remove();
+
+			this.viewer.inputHandler.deselectAll();
+			this.viewer.inputHandler.toggleSelection(volume);
+
+			let camera = e.viewer.scene.getActiveCamera();
+			let size = e.viewer.renderer.getSize(new THREE.Vector2());
+			let screenCentroid = new THREE.Vector2().addVectors(e.drag.end, e.drag.start).multiplyScalar(0.5);
+			let ray = Utils.mouseToRay(screenCentroid, camera, size.width, size.height);
+
+			let line = new THREE.Line3(ray.origin, new THREE.Vector3().addVectors(ray.origin, ray.direction));
+
+			this.removeEventListener("drag", drag);
+			this.removeEventListener("drop", drop);
+
+			let allPointsNear = [];
+			let allPointsFar = [];
+
+			// TODO support more than one point cloud
+			for(let pointcloud of this.viewer.scene.pointclouds){
+
+				if(!pointcloud.visible){
+					continue;
+				}
+
+				let volCam = camera.clone();
+				volCam.left = -volume.scale.x / 2; 
+				volCam.right = +volume.scale.x / 2;
+				volCam.top = +volume.scale.y / 2;
+				volCam.bottom = -volume.scale.y / 2;
+				volCam.near = -volume.scale.z / 2;
+				volCam.far = +volume.scale.z / 2;
+				volCam.rotation.copy(volume.rotation);
+				volCam.position.copy(volume.position);
+
+				volCam.updateMatrix();
+				volCam.updateMatrixWorld();
+				volCam.updateProjectionMatrix();
+				volCam.matrixWorldInverse.copy(volCam.matrixWorld).invert();
+
+				let ray = new THREE.Ray(volCam.getWorldPosition(new THREE.Vector3()), volCam.getWorldDirection(new THREE.Vector3()));
+				let rayInverse = new THREE.Ray(
+					ray.origin.clone().add(ray.direction.clone().multiplyScalar(volume.scale.z)),
+					ray.direction.clone().multiplyScalar(-1));
+
+				let pickerSettings = {
+					width: 8, 
+					height: 8, 
+					pickWindowSize: 8, 
+					all: true,
+					pickClipped: true,
+					pointSizeType: PointSizeType.FIXED,
+					pointSize: 1};
+				let pointsNear = pointcloud.pick(viewer, volCam, ray, pickerSettings);
+
+				volCam.rotateX(Math.PI);
+				volCam.updateMatrix();
+				volCam.updateMatrixWorld();
+				volCam.updateProjectionMatrix();
+				volCam.matrixWorldInverse.copy(volCam.matrixWorld).invert();
+				let pointsFar = pointcloud.pick(viewer, volCam, rayInverse, pickerSettings);
+
+				allPointsNear.push(...pointsNear);
+				allPointsFar.push(...pointsFar);
+			}
+
+			if(allPointsNear.length > 0 && allPointsFar.length > 0){
+				let viewLine = new THREE.Line3(ray.origin, new THREE.Vector3().addVectors(ray.origin, ray.direction));
+
+				let closestOnLine = allPointsNear.map(p => viewLine.closestPointToPoint(p.position, false, new THREE.Vector3()));
+				let closest = closestOnLine.sort( (a, b) => ray.origin.distanceTo(a) - ray.origin.distanceTo(b))[0];
+
+				let farthestOnLine = allPointsFar.map(p => viewLine.closestPointToPoint(p.position, false, new THREE.Vector3()));
+				let farthest = farthestOnLine.sort( (a, b) => ray.origin.distanceTo(b) - ray.origin.distanceTo(a))[0];
+
+				let distance = closest.distanceTo(farthest);
+				let centroid = new THREE.Vector3().addVectors(closest, farthest).multiplyScalar(0.5);
+				volume.scale.z = distance * 1.1;
+				volume.position.copy(centroid);
+			}
+
+			volume.clip = true;
+		};
+
+		this.addEventListener("drag", drag);
+		this.addEventListener("drop", drop);
+
+		viewer.inputHandler.addInputListener(this);
+
+		return volume;
+	}
+
+	update(e){
+		//console.log(e.delta)
+	}
+
+	render(){
+		this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera());
+	}
+
+}

+ 78 - 0
src/utils/SpotLightHelper.js

@@ -0,0 +1,78 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+
+export class SpotLightHelper extends THREE.Object3D{
+
+	constructor(light, color){
+		super();
+
+		this.light = light;
+		this.color = color;
+
+		//this.up.set(0, 0, 1);
+		this.updateMatrix();
+		this.updateMatrixWorld();
+
+		{ // SPHERE
+			let sg = new THREE.SphereGeometry(1, 32, 32);
+			let sm = new THREE.MeshNormalMaterial();
+			this.sphere = new THREE.Mesh(sg, sm);
+			this.sphere.scale.set(0.5, 0.5, 0.5);
+			this.add(this.sphere);
+		}
+
+		{ // LINES
+			
+
+			let positions = new Float32Array([
+				+0, +0, +0,     +0, +0, -1,
+
+				+0, +0, +0,     -1, -1, -1,
+				+0, +0, +0,     +1, -1, -1,
+				+0, +0, +0,     +1, +1, -1,
+				+0, +0, +0,     -1, +1, -1,
+
+				-1, -1, -1,     +1, -1, -1,
+				+1, -1, -1,     +1, +1, -1,
+				+1, +1, -1,     -1, +1, -1,
+				-1, +1, -1,     -1, -1, -1,
+			]);
+
+			let geometry = new THREE.BufferGeometry();
+			geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
+
+			let material = new THREE.LineBasicMaterial();
+
+			this.frustum = new THREE.LineSegments(geometry, material);
+			this.add(this.frustum);
+
+		}
+
+		this.update();
+	}
+
+	update(){
+
+		this.light.updateMatrix();
+		this.light.updateMatrixWorld();
+
+		let position = this.light.position;
+		let target = new THREE.Vector3().addVectors(
+			this.light.position, this.light.getWorldDirection(new THREE.Vector3()).multiplyScalar(-1));
+		
+		let quat = new THREE.Quaternion().setFromRotationMatrix(
+			new THREE.Matrix4().lookAt( position, target, new THREE.Vector3( 0, 0, 1 ) )
+		);
+
+		this.setRotationFromQuaternion(quat);
+		this.position.copy(position);
+
+
+		let coneLength = (this.light.distance > 0) ? this.light.distance : 1000;
+		let coneWidth = coneLength * Math.tan( this.light.angle * 0.5 );
+
+		this.frustum.scale.set(coneWidth, coneWidth, coneLength);
+
+	}
+
+}

+ 871 - 0
src/utils/TransformationTool.js

@@ -0,0 +1,871 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import {Utils} from "../utils.js";
+
+export class TransformationTool {
+	constructor(viewer) {
+		this.viewer = viewer;
+
+		this.scene = new THREE.Scene();
+
+		this.selection = [];
+		this.pivot = new THREE.Vector3();
+		this.dragging = false;
+		this.showPickVolumes = false;
+
+		this.viewer.inputHandler.registerInteractiveScene(this.scene);
+		this.viewer.inputHandler.addEventListener('selection_changed', (e) => {
+			for(let selected of this.selection){
+				this.viewer.inputHandler.blacklist.delete(selected);
+			}
+
+			this.selection = e.selection;
+
+			for(let selected of this.selection){
+				this.viewer.inputHandler.blacklist.add(selected);
+			}
+
+		});
+
+		let red = 0xE73100;
+		let green = 0x44A24A;
+		let blue = 0x2669E7;
+		
+		this.activeHandle = null;
+		this.scaleHandles = {
+			"scale.x+": {name: "scale.x+", node: new THREE.Object3D(), color: red, alignment: [+1, +0, +0]},
+			"scale.x-": {name: "scale.x-", node: new THREE.Object3D(), color: red, alignment: [-1, +0, +0]},
+			"scale.y+": {name: "scale.y+", node: new THREE.Object3D(), color: green, alignment: [+0, +1, +0]},
+			"scale.y-": {name: "scale.y-", node: new THREE.Object3D(), color: green, alignment: [+0, -1, +0]},
+			"scale.z+": {name: "scale.z+", node: new THREE.Object3D(), color: blue, alignment: [+0, +0, +1]},
+			"scale.z-": {name: "scale.z-", node: new THREE.Object3D(), color: blue, alignment: [+0, +0, -1]},
+		};
+		this.focusHandles = {
+			"focus.x+": {name: "focus.x+", node:  new THREE.Object3D(), color: red, alignment: [+1, +0, +0]},
+			"focus.x-": {name: "focus.x-", node:  new THREE.Object3D(), color: red, alignment: [-1, +0, +0]},
+			"focus.y+": {name: "focus.y+", node:  new THREE.Object3D(), color: green, alignment: [+0, +1, +0]},
+			"focus.y-": {name: "focus.y-", node:  new THREE.Object3D(), color: green, alignment: [+0, -1, +0]},
+			"focus.z+": {name: "focus.z+", node:  new THREE.Object3D(), color: blue, alignment: [+0, +0, +1]},
+			"focus.z-": {name: "focus.z-", node:  new THREE.Object3D(), color: blue, alignment: [+0, +0, -1]},
+		};
+		this.translationHandles = {
+			"translation.x": {name: "translation.x", node:  new THREE.Object3D(), color: red, alignment: [1, 0, 0]},
+			"translation.y": {name: "translation.y", node:  new THREE.Object3D(), color: green, alignment: [0, 1, 0]},
+			"translation.z": {name: "translation.z", node:  new THREE.Object3D(), color: blue, alignment: [0, 0, 1]},
+		};
+		this.rotationHandles = {
+			"rotation.x": {name: "rotation.x", node:  new THREE.Object3D(), color: red, alignment: [1, 0, 0]},
+			"rotation.y": {name: "rotation.y", node:  new THREE.Object3D(), color: green, alignment: [0, 1, 0]},
+			"rotation.z": {name: "rotation.z", node:  new THREE.Object3D(), color: blue, alignment: [0, 0, 1]},
+		};
+		this.handles = Object.assign({}, this.scaleHandles, this.focusHandles, this.translationHandles, this.rotationHandles);
+		this.pickVolumes = [];
+
+		this.initializeScaleHandles();
+		this.initializeFocusHandles();
+		this.initializeTranslationHandles();
+		this.initializeRotationHandles();
+
+
+		let boxFrameGeometry = new THREE.Geometry();
+		{
+			// bottom
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, 0.5));
+			// top
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, 0.5));
+			// sides
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, -0.5));
+		}
+		this.frame = new THREE.LineSegments(boxFrameGeometry, new THREE.LineBasicMaterial({color: 0xffff00}));
+		this.scene.add(this.frame);
+
+		
+	}
+
+	initializeScaleHandles(){
+		let sgSphere = new THREE.SphereGeometry(1, 32, 32);
+		let sgLowPolySphere = new THREE.SphereGeometry(1, 16, 16);
+
+		for(let handleName of Object.keys(this.scaleHandles)){
+			let handle = this.scaleHandles[handleName];
+			let node = handle.node;
+			this.scene.add(node);
+			node.position.set(...handle.alignment).multiplyScalar(0.5);
+
+			let material = new THREE.MeshBasicMaterial({
+				color: handle.color,
+				opacity: 0.4,
+				transparent: true
+				});
+
+			let outlineMaterial = new THREE.MeshBasicMaterial({
+				color: 0x000000, 
+				side: THREE.BackSide,
+				opacity: 0.4,
+				transparent: true});
+
+			let pickMaterial = new THREE.MeshNormalMaterial({
+				opacity: 0.2,
+				transparent: true,
+				visible: this.showPickVolumes});
+
+			let sphere = new THREE.Mesh(sgSphere, material);
+			sphere.scale.set(1.3, 1.3, 1.3);
+			sphere.name = `${handleName}.handle`;
+			node.add(sphere);
+			
+			let outline = new THREE.Mesh(sgSphere, outlineMaterial);
+			outline.scale.set(1.4, 1.4, 1.4);
+			outline.name = `${handleName}.outline`;
+			sphere.add(outline);
+
+			let pickSphere = new THREE.Mesh(sgLowPolySphere, pickMaterial);
+			pickSphere.name = `${handleName}.pick_volume`;
+			pickSphere.scale.set(3, 3, 3);
+			sphere.add(pickSphere);
+			pickSphere.handle = handleName;
+			this.pickVolumes.push(pickSphere);
+
+			node.setOpacity = (target) => {
+				let opacity = {x: material.opacity};
+				let t = new TWEEN.Tween(opacity).to({x: target}, 100);
+				t.onUpdate(() => {
+					sphere.visible = opacity.x > 0;
+					pickSphere.visible = opacity.x > 0;
+					material.opacity = opacity.x;
+					outlineMaterial.opacity = opacity.x;
+					pickSphere.material.opacity = opacity.x * 0.5;
+				});
+				t.start();
+			};
+
+			pickSphere.addEventListener("drag", (e) => this.dragScaleHandle(e));
+			pickSphere.addEventListener("drop", (e) => this.dropScaleHandle(e));
+
+			pickSphere.addEventListener("mouseover", e => {
+				//node.setOpacity(1);
+			});
+
+			pickSphere.addEventListener("click", e => {
+				e.consume();
+			});
+
+			pickSphere.addEventListener("mouseleave", e => {
+				//node.setOpacity(0.4);
+			});
+		}
+	}
+
+	initializeFocusHandles(){
+		//let sgBox = new THREE.BoxGeometry(1, 1, 1);
+		let sgPlane = new THREE.PlaneGeometry(4, 4, 1, 1);
+		let sgLowPolySphere = new THREE.SphereGeometry(1, 16, 16);
+
+		let texture = new THREE.TextureLoader().load(`${exports.resourcePath}/icons/eye_2.png`);
+
+		for(let handleName of Object.keys(this.focusHandles)){
+			let handle = this.focusHandles[handleName];
+			let node = handle.node;
+			this.scene.add(node);
+			let align = handle.alignment;
+
+			//node.lookAt(new THREE.Vector3().addVectors(node.position, new THREE.Vector3(...align)));
+			node.lookAt(new THREE.Vector3(...align));
+
+			let off = 0.8;
+			if(align[0] === 1){
+				node.position.set(1, off, -off).multiplyScalar(0.5);
+				node.rotation.z = Math.PI / 2;
+			}else if(align[0] === -1){
+				node.position.set(-1, -off, -off).multiplyScalar(0.5);
+				node.rotation.z = Math.PI / 2;
+			}else if(align[1] === 1){
+				node.position.set(-off, 1, -off).multiplyScalar(0.5);
+				node.rotation.set(Math.PI / 2, Math.PI, 0.0);
+			}else if(align[1] === -1){
+				node.position.set(off, -1, -off).multiplyScalar(0.5);
+				node.rotation.set(Math.PI / 2, 0.0, 0.0);
+			}else if(align[2] === 1){
+				node.position.set(off, off, 1).multiplyScalar(0.5);
+			}else if(align[2] === -1){
+				node.position.set(-off, off, -1).multiplyScalar(0.5);
+			}
+
+			let material = new THREE.MeshBasicMaterial({
+				color: handle.color,
+				opacity: 0,
+				transparent: true,
+				map: texture
+			});
+
+			//let outlineMaterial = new THREE.MeshBasicMaterial({
+			//	color: 0x000000, 
+			//	side: THREE.BackSide,
+			//	opacity: 0,
+			//	transparent: true});
+
+			let pickMaterial = new THREE.MeshNormalMaterial({
+				//opacity: 0,
+				transparent: true,
+				visible: this.showPickVolumes});
+
+			let box = new THREE.Mesh(sgPlane, material);
+			box.name = `${handleName}.handle`;
+			box.scale.set(1.5, 1.5, 1.5);
+			box.position.set(0, 0, 0);
+			box.visible = false;
+			node.add(box);
+			//handle.focusNode = box;
+			
+			//let outline = new THREE.Mesh(sgPlane, outlineMaterial);
+			//outline.scale.set(1.4, 1.4, 1.4);
+			//outline.name = `${handleName}.outline`;
+			//box.add(outline);
+
+			let pickSphere = new THREE.Mesh(sgLowPolySphere, pickMaterial);
+			pickSphere.name = `${handleName}.pick_volume`;
+			pickSphere.scale.set(3, 3, 3);
+			box.add(pickSphere);
+			pickSphere.handle = handleName;
+			this.pickVolumes.push(pickSphere);
+
+			node.setOpacity = (target) => {
+				let opacity = {x: material.opacity};
+				let t = new TWEEN.Tween(opacity).to({x: target}, 100);
+				t.onUpdate(() => {
+					pickSphere.visible = opacity.x > 0;
+					box.visible = opacity.x > 0;
+					material.opacity = opacity.x;
+					//outlineMaterial.opacity = opacity.x;
+					pickSphere.material.opacity = opacity.x * 0.5;
+				});
+				t.start();
+			};
+
+			pickSphere.addEventListener("drag", e => {});
+
+			pickSphere.addEventListener("mouseup", e => {
+				e.consume();
+			});
+
+			pickSphere.addEventListener("mousedown", e => {
+				e.consume();
+			});
+
+			pickSphere.addEventListener("click", e => {
+				e.consume();
+
+				let selected = this.selection[0];
+				let maxScale = Math.max(...selected.scale.toArray());
+				let minScale = Math.min(...selected.scale.toArray());
+				let handleLength = Math.abs(selected.scale.dot(new THREE.Vector3(...handle.alignment)));
+				let alignment = new THREE.Vector3(...handle.alignment).multiplyScalar(2 * maxScale / handleLength);
+				alignment.applyMatrix4(selected.matrixWorld);
+				let newCamPos = alignment;
+				let newCamTarget = selected.getWorldPosition(new THREE.Vector3());
+
+				Utils.moveTo(this.viewer.scene, newCamPos, newCamTarget);
+			});
+
+			pickSphere.addEventListener("mouseover", e => {
+				//box.setOpacity(1);
+			});
+
+			pickSphere.addEventListener("mouseleave", e => {
+				//box.setOpacity(0.4);
+			});
+		}
+	}
+
+	initializeTranslationHandles(){
+		let boxGeometry = new THREE.BoxGeometry(1, 1, 1);
+
+		for(let handleName of Object.keys(this.translationHandles)){
+			let handle = this.handles[handleName];
+			let node = handle.node;
+			this.scene.add(node);
+
+			let material = new THREE.MeshBasicMaterial({
+				color: handle.color,
+				opacity: 0.4,
+				transparent: true});
+
+			let outlineMaterial = new THREE.MeshBasicMaterial({
+				color: 0x000000, 
+				side: THREE.BackSide,
+				opacity: 0.4,
+				transparent: true});
+
+			let pickMaterial = new THREE.MeshNormalMaterial({
+				opacity: 0.2,
+				transparent: true,
+				visible: this.showPickVolumes
+			});
+
+			let box = new THREE.Mesh(boxGeometry, material);
+			box.name = `${handleName}.handle`;
+			box.scale.set(0.2, 0.2, 40);
+			box.lookAt(new THREE.Vector3(...handle.alignment));
+			box.renderOrder = 10;
+			node.add(box);
+			handle.translateNode = box;
+
+			let outline = new THREE.Mesh(boxGeometry, outlineMaterial);
+			outline.name = `${handleName}.outline`;
+			outline.scale.set(3, 3, 1.03);
+			outline.renderOrder = 0;
+			box.add(outline);
+
+			let pickVolume = new THREE.Mesh(boxGeometry, pickMaterial);
+			pickVolume.name = `${handleName}.pick_volume`;
+			pickVolume.scale.set(12, 12, 1.1);
+			pickVolume.handle = handleName;
+			box.add(pickVolume);
+			this.pickVolumes.push(pickVolume);
+
+			node.setOpacity = (target) => {
+				let opacity = {x: material.opacity};
+				let t = new TWEEN.Tween(opacity).to({x: target}, 100);
+				t.onUpdate(() => {
+					box.visible = opacity.x > 0;
+					pickVolume.visible = opacity.x > 0;
+					material.opacity = opacity.x;
+					outlineMaterial.opacity = opacity.x;
+					pickMaterial.opacity = opacity.x * 0.5;
+				});
+				t.start();
+			};
+
+			pickVolume.addEventListener("drag", (e) => {this.dragTranslationHandle(e)});
+			pickVolume.addEventListener("drop", (e) => {this.dropTranslationHandle(e)});
+		}
+	}
+
+	initializeRotationHandles(){
+		let adjust = 0.5;
+		let torusGeometry = new THREE.TorusGeometry(1, adjust * 0.015, 8, 64, Math.PI / 2);
+		let outlineGeometry = new THREE.TorusGeometry(1, adjust * 0.04, 8, 64, Math.PI / 2);
+		let pickGeometry = new THREE.TorusGeometry(1, adjust * 0.1, 6, 4, Math.PI / 2);
+
+		for(let handleName of Object.keys(this.rotationHandles)){
+			let handle = this.handles[handleName];
+			let node = handle.node;
+			this.scene.add(node);
+
+			let material = new THREE.MeshBasicMaterial({
+				color: handle.color,
+				opacity: 0.4,
+				transparent: true});
+
+			let outlineMaterial = new THREE.MeshBasicMaterial({
+				color: 0x000000, 
+				side: THREE.BackSide,
+				opacity: 0.4,
+				transparent: true});
+
+			let pickMaterial = new THREE.MeshNormalMaterial({
+				opacity: 0.2,
+				transparent: true,
+				visible: this.showPickVolumes
+			});
+
+			let box = new THREE.Mesh(torusGeometry, material);
+			box.name = `${handleName}.handle`;
+			box.scale.set(20, 20, 20);
+			box.lookAt(new THREE.Vector3(...handle.alignment));
+			node.add(box);
+			handle.translateNode = box;
+
+			let outline = new THREE.Mesh(outlineGeometry, outlineMaterial);
+			outline.name = `${handleName}.outline`;
+			outline.scale.set(1, 1, 1);
+			outline.renderOrder = 0;
+			box.add(outline);
+
+			let pickVolume = new THREE.Mesh(pickGeometry, pickMaterial);
+			pickVolume.name = `${handleName}.pick_volume`;
+			pickVolume.scale.set(1, 1, 1);
+			pickVolume.handle = handleName;
+			box.add(pickVolume);
+			this.pickVolumes.push(pickVolume);
+
+			node.setOpacity = (target) => {
+				let opacity = {x: material.opacity};
+				let t = new TWEEN.Tween(opacity).to({x: target}, 100);
+				t.onUpdate(() => {
+					box.visible = opacity.x > 0;
+					pickVolume.visible = opacity.x > 0;
+					material.opacity = opacity.x;
+					outlineMaterial.opacity = opacity.x;
+					pickMaterial.opacity = opacity.x * 0.5;
+				});
+				t.start();
+			};
+
+
+			//pickVolume.addEventListener("mouseover", (e) => {
+			//	//let a = this.viewer.scene.getActiveCamera().getWorldDirection(new THREE.Vector3()).dot(pickVolume.getWorldDirection(new THREE.Vector3()));
+			//	console.log(pickVolume.getWorldDirection(new THREE.Vector3()));
+			//});
+			
+			pickVolume.addEventListener("drag", (e) => {this.dragRotationHandle(e)});
+			pickVolume.addEventListener("drop", (e) => {this.dropRotationHandle(e)});
+		}
+	}
+
+	dragRotationHandle(e){
+		let drag = e.drag;
+		let handle = this.activeHandle;
+		let camera = this.viewer.scene.getActiveCamera();
+
+		if(!handle){
+			return
+		};
+
+		let localNormal = new THREE.Vector3(...handle.alignment);
+		let n = new THREE.Vector3();
+		n.copy(new THREE.Vector4(...localNormal.toArray(), 0).applyMatrix4(handle.node.matrixWorld));
+		n.normalize();
+
+		if (!drag.intersectionStart){
+
+			//this.viewer.scene.scene.remove(this.debug);
+			//this.debug = new THREE.Object3D();
+			//this.viewer.scene.scene.add(this.debug);
+			//Utils.debugSphere(this.debug, drag.location, 3, 0xaaaaaa);
+			//let debugEnd = drag.location.clone().add(n.clone().multiplyScalar(20));
+			//Utils.debugLine(this.debug, drag.location, debugEnd, 0xff0000);
+
+			drag.intersectionStart = drag.location;
+			drag.objectStart = drag.object.getWorldPosition(new THREE.Vector3());
+			drag.handle = handle;
+
+			let plane = new THREE.Plane().setFromNormalAndCoplanarPoint(n, drag.intersectionStart);
+
+			drag.dragPlane = plane;
+			drag.pivot = drag.intersectionStart;
+		}else{
+			handle = drag.handle;
+		}
+
+		this.dragging = true;
+
+		let mouse = drag.end;
+		let domElement = this.viewer.renderer.domElement;
+		let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);
+		
+		let I = ray.intersectPlane(drag.dragPlane, new THREE.Vector3());
+
+		if (I) {
+			let center = this.scene.getWorldPosition(new THREE.Vector3());
+			let from = drag.pivot;
+			let to = I;
+
+			let v1 = from.clone().sub(center).normalize();
+			let v2 = to.clone().sub(center).normalize();
+
+			let angle = Math.acos(v1.dot(v2));
+			let sign = Math.sign(v1.cross(v2).dot(n));
+			angle = angle * sign;
+			if (Number.isNaN(angle)) {
+				return;
+			}
+
+			let normal = new THREE.Vector3(...handle.alignment);
+			for (let selection of this.selection) {
+				selection.rotateOnAxis(normal, angle);
+				selection.dispatchEvent({
+					type: "orientation_changed",
+					object: selection
+				});
+			}
+
+			drag.pivot = I;
+		}
+	}
+
+	dropRotationHandle(e){
+		this.dragging = false;
+		this.setActiveHandle(null);
+	}
+
+	dragTranslationHandle(e){
+		let drag = e.drag;
+		let handle = this.activeHandle;
+		let camera = this.viewer.scene.getActiveCamera();
+			
+		if(!drag.intersectionStart && handle){
+			drag.intersectionStart = drag.location;
+			drag.objectStart = drag.object.getWorldPosition(new THREE.Vector3());
+
+			let start = drag.intersectionStart;
+			let dir = new THREE.Vector4(...handle.alignment, 0).applyMatrix4(this.scene.matrixWorld);
+			let end = new THREE.Vector3().addVectors(start, dir);
+			let line = new THREE.Line3(start.clone(), end.clone());
+			drag.line = line;
+
+			let camOnLine = line.closestPointToPoint(camera.position, false, new THREE.Vector3());
+			let normal = new THREE.Vector3().subVectors(camera.position, camOnLine);
+			let plane = new THREE.Plane().setFromNormalAndCoplanarPoint(normal, drag.intersectionStart);
+			drag.dragPlane = plane;
+			drag.pivot = drag.intersectionStart;
+		}else{
+			handle = drag.handle;
+		}
+
+		this.dragging = true;
+
+		{
+			let mouse = drag.end;
+			let domElement = this.viewer.renderer.domElement;
+			let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);
+			let I = ray.intersectPlane(drag.dragPlane, new THREE.Vector3());
+
+			if (I) {
+				let iOnLine = drag.line.closestPointToPoint(I, false, new THREE.Vector3());
+
+				let diff = new THREE.Vector3().subVectors(iOnLine, drag.pivot);
+
+				for (let selection of this.selection) {
+					selection.position.add(diff);
+					selection.dispatchEvent({
+						type: "position_changed",
+						object: selection
+					});
+				}
+
+				drag.pivot = drag.pivot.add(diff);
+			}
+		}
+	}
+
+	dropTranslationHandle(e){
+		this.dragging = false;
+		this.setActiveHandle(null);
+	}
+
+	dropScaleHandle(e){
+		this.dragging = false;
+		this.setActiveHandle(null);
+	}
+
+	dragScaleHandle(e){
+		let drag = e.drag;
+		let handle = this.activeHandle;
+		let camera = this.viewer.scene.getActiveCamera();
+
+		if(!drag.intersectionStart){
+			drag.intersectionStart = drag.location;
+			drag.objectStart = drag.object.getWorldPosition(new THREE.Vector3());
+			drag.handle = handle;
+
+			let start = drag.intersectionStart;
+			let dir = new THREE.Vector4(...handle.alignment, 0).applyMatrix4(this.scene.matrixWorld);
+			let end = new THREE.Vector3().addVectors(start, dir);
+			let line = new THREE.Line3(start.clone(), end.clone());
+			drag.line = line;
+
+			let camOnLine = line.closestPointToPoint(camera.position, false, new THREE.Vector3());
+			let normal = new THREE.Vector3().subVectors(camera.position, camOnLine);
+			let plane = new THREE.Plane().setFromNormalAndCoplanarPoint(normal, drag.intersectionStart);
+			drag.dragPlane = plane;
+			drag.pivot = drag.intersectionStart;
+
+			//Utils.debugSphere(viewer.scene.scene, drag.pivot, 0.05);
+		}else{
+			handle = drag.handle;
+		}
+
+		this.dragging = true;
+
+		{
+			let mouse = drag.end;
+			let domElement = this.viewer.renderer.domElement;
+			let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);
+			let I = ray.intersectPlane(drag.dragPlane, new THREE.Vector3());
+
+			if (I) {
+				let iOnLine = drag.line.closestPointToPoint(I, false, new THREE.Vector3());
+				let direction = handle.alignment.reduce( (a, v) => a + v, 0);
+
+				let toObjectSpace = this.selection[0].matrixWorld.clone().invert();
+				let iOnLineOS = iOnLine.clone().applyMatrix4(toObjectSpace);
+				let pivotOS = drag.pivot.clone().applyMatrix4(toObjectSpace);
+				let diffOS = new THREE.Vector3().subVectors(iOnLineOS, pivotOS);
+				let dragDirectionOS = diffOS.clone().normalize();
+				if(iOnLine.distanceTo(drag.pivot) === 0){
+					dragDirectionOS.set(0, 0, 0);
+				}
+				let dragDirection = dragDirectionOS.dot(new THREE.Vector3(...handle.alignment));
+
+				let diff = new THREE.Vector3().subVectors(iOnLine, drag.pivot);
+				let diffScale = new THREE.Vector3(...handle.alignment).multiplyScalar(diff.length() * direction * dragDirection);
+				let diffPosition = diff.clone().multiplyScalar(0.5);
+
+				for (let selection of this.selection) {
+					selection.scale.add(diffScale);
+					selection.scale.x = Math.max(0.1, selection.scale.x);
+					selection.scale.y = Math.max(0.1, selection.scale.y);
+					selection.scale.z = Math.max(0.1, selection.scale.z);
+					selection.position.add(diffPosition);
+					selection.dispatchEvent({
+						type: "position_changed",
+						object: selection
+					});
+					selection.dispatchEvent({
+						type: "scale_changed",
+						object: selection
+					});
+				}
+
+				drag.pivot.copy(iOnLine);
+				//Utils.debugSphere(viewer.scene.scene, drag.pivot, 0.05);
+			}
+		}
+	}
+
+	setActiveHandle(handle){
+		if(this.dragging){
+			return;
+		}
+
+		if(this.activeHandle === handle){
+			return;
+		}
+
+		this.activeHandle = handle;
+
+		if(handle === null){
+			for(let handleName of Object.keys(this.handles)){
+				let handle = this.handles[handleName];
+				handle.node.setOpacity(0);
+			}
+		}
+
+		for(let handleName of Object.keys(this.focusHandles)){
+			let handle = this.focusHandles[handleName];
+
+			if(this.activeHandle === handle){
+				handle.node.setOpacity(1.0);
+			}else{
+				handle.node.setOpacity(0.4)
+			}
+		}
+
+		for(let handleName of Object.keys(this.translationHandles)){
+			let handle = this.translationHandles[handleName];
+
+			if(this.activeHandle === handle){
+				handle.node.setOpacity(1.0);
+			}else{
+				handle.node.setOpacity(0.4)
+			}
+		}
+
+		for(let handleName of Object.keys(this.rotationHandles)){
+			let handle = this.rotationHandles[handleName];
+
+			//if(this.activeHandle === handle){
+			//	handle.node.setOpacity(1.0);
+			//}else{
+			//	handle.node.setOpacity(0.4)
+			//}
+
+			handle.node.setOpacity(0.4);
+		}
+
+		for(let handleName of Object.keys(this.scaleHandles)){
+			let handle = this.scaleHandles[handleName];
+
+			if(this.activeHandle === handle){
+				handle.node.setOpacity(1.0);
+
+				let relatedFocusHandle = this.focusHandles[handle.name.replace("scale", "focus")];
+				let relatedFocusNode = relatedFocusHandle.node;
+				relatedFocusNode.setOpacity(0.4);
+
+				for(let translationHandleName of Object.keys(this.translationHandles)){
+					let translationHandle = this.translationHandles[translationHandleName];
+					translationHandle.node.setOpacity(0.4);
+				}
+
+				//let relatedTranslationHandle = this.translationHandles[
+				//	handle.name.replace("scale", "translation").replace(/[+-]/g, "")];
+				//let relatedTranslationNode = relatedTranslationHandle.node;
+				//relatedTranslationNode.setOpacity(0.4);
+
+
+			}else{
+				handle.node.setOpacity(0.4)
+			}
+		}
+
+		
+
+
+
+		if(handle){
+			handle.node.setOpacity(1.0);
+		}
+
+		
+	}
+
+	update () {
+
+		if(this.selection.length === 1){
+
+			this.scene.visible = true;
+
+			this.scene.updateMatrix();
+			this.scene.updateMatrixWorld();
+
+			let selected = this.selection[0];
+			let world = selected.matrixWorld;
+			let camera = this.viewer.scene.getActiveCamera();
+			let domElement = this.viewer.renderer.domElement;
+			let mouse = this.viewer.inputHandler.mouse;
+
+			let center = selected.boundingBox.getCenter(new THREE.Vector3()).clone().applyMatrix4(selected.matrixWorld);
+
+			this.scene.scale.copy(selected.boundingBox.getSize(new THREE.Vector3()).multiply(selected.scale));
+			this.scene.position.copy(center);
+			this.scene.rotation.copy(selected.rotation);
+
+			this.scene.updateMatrixWorld();
+
+			{
+				// adjust scale of components
+				for(let handleName of Object.keys(this.handles)){
+					let handle = this.handles[handleName];
+					let node = handle.node;
+
+					let handlePos = node.getWorldPosition(new THREE.Vector3());
+					let distance = handlePos.distanceTo(camera.position);
+					let pr = Utils.projectedRadius(1, camera, distance, domElement.clientWidth, domElement.clientHeight);
+
+					let ws = node.parent.getWorldScale(new THREE.Vector3());
+
+					let s = (7 / pr);
+					let scale = new THREE.Vector3(s, s, s).divide(ws);
+
+					let rot = new THREE.Matrix4().makeRotationFromEuler(node.rotation);
+					let rotInv = rot.clone().invert();
+
+					scale.applyMatrix4(rotInv);
+					scale.x = Math.abs(scale.x);
+					scale.y = Math.abs(scale.y);
+					scale.z = Math.abs(scale.z);
+
+					node.scale.copy(scale);
+				}
+
+				// adjust rotation handles
+				if(!this.dragging){
+					let tWorld = this.scene.matrixWorld;
+					let tObject = tWorld.clone().invert();
+					let camObjectPos = camera.getWorldPosition(new THREE.Vector3()).applyMatrix4(tObject);
+
+					let x = this.rotationHandles["rotation.x"].node.rotation;
+					let y = this.rotationHandles["rotation.y"].node.rotation;
+					let z = this.rotationHandles["rotation.z"].node.rotation;
+
+					x.order = "ZYX";
+					y.order = "ZYX";
+
+					let above = camObjectPos.z > 0;
+					let below = !above;
+					let PI_HALF = Math.PI / 2;
+
+					if(above){
+						if(camObjectPos.x > 0 && camObjectPos.y > 0){
+							x.x = 1 * PI_HALF;
+							y.y = 3 * PI_HALF;
+							z.z = 0 * PI_HALF;
+						}else if(camObjectPos.x < 0 && camObjectPos.y > 0){
+							x.x = 1 * PI_HALF;
+							y.y = 2 * PI_HALF;
+							z.z = 1 * PI_HALF;
+						}else if(camObjectPos.x < 0 && camObjectPos.y < 0){
+							x.x = 2 * PI_HALF;
+							y.y = 2 * PI_HALF;
+							z.z = 2 * PI_HALF;
+						}else if(camObjectPos.x > 0 && camObjectPos.y < 0){
+							x.x = 2 * PI_HALF;
+							y.y = 3 * PI_HALF;
+							z.z = 3 * PI_HALF;
+						}
+					}else if(below){
+						if(camObjectPos.x > 0 && camObjectPos.y > 0){
+							x.x = 0 * PI_HALF;
+							y.y = 0 * PI_HALF;
+							z.z = 0 * PI_HALF;
+						}else if(camObjectPos.x < 0 && camObjectPos.y > 0){
+							x.x = 0 * PI_HALF;
+							y.y = 1 * PI_HALF;
+							z.z = 1 * PI_HALF;
+						}else if(camObjectPos.x < 0 && camObjectPos.y < 0){
+							x.x = 3 * PI_HALF;
+							y.y = 1 * PI_HALF;
+							z.z = 2 * PI_HALF;
+						}else if(camObjectPos.x > 0 && camObjectPos.y < 0){
+							x.x = 3 * PI_HALF;
+							y.y = 0 * PI_HALF;
+							z.z = 3 * PI_HALF;
+						}
+					}
+				}
+
+				{
+					let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);
+					let raycaster = new THREE.Raycaster(ray.origin, ray.direction);
+					let intersects = raycaster.intersectObjects(this.pickVolumes.filter(v => v.visible), true);
+
+					if(intersects.length > 0){
+						let I = intersects[0];
+						let handleName = I.object.handle;
+						this.setActiveHandle(this.handles[handleName]);
+					}else{
+						this.setActiveHandle(null);
+					}
+				}
+
+				// 
+				for(let handleName of Object.keys(this.scaleHandles)){
+					let handle = this.handles[handleName];
+					let node = handle.node;
+					let alignment = handle.alignment;
+
+					
+
+				}
+			}
+
+		}else{
+			this.scene.visible = false;
+		}
+		
+	}
+
+};

+ 340 - 0
src/utils/Volume.js

@@ -0,0 +1,340 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import {TextSprite} from "../TextSprite.js";
+
+export class Volume extends THREE.Object3D {
+	constructor (args = {}) {
+		super();
+
+		if(this.constructor.name === "Volume"){
+			console.warn("Can't create object of class Volume directly. Use classes BoxVolume or SphereVolume instead.");
+		}
+
+		//console.log(this);
+		//console.log(this.constructor);
+		//console.log(this.constructor.name);
+
+		this._clip = args.clip || false;
+		this._visible = true;
+		this.showVolumeLabel = true;
+		this._modifiable = args.modifiable || true;
+
+		this.label = new TextSprite('0');
+		this.label.setBorderColor({r: 0, g: 255, b: 0, a: 0.0});
+		this.label.setBackgroundColor({r: 0, g: 255, b: 0, a: 0.0});
+		this.label.material.depthTest = false;
+		this.label.material.depthWrite = false;
+		this.label.material.transparent = true;
+		this.label.position.y -= 0.5;
+		this.add(this.label);
+
+		this.label.updateMatrixWorld = () => {
+			let volumeWorldPos = new THREE.Vector3();
+			volumeWorldPos.setFromMatrixPosition(this.matrixWorld);
+			this.label.position.copy(volumeWorldPos);
+			this.label.updateMatrix();
+			this.label.matrixWorld.copy(this.label.matrix);
+			this.label.matrixWorldNeedsUpdate = false;
+
+			for (let i = 0, l = this.label.children.length; i < l; i++) {
+				this.label.children[ i ].updateMatrixWorld(true);
+			}
+		};
+
+		{ // event listeners
+			this.addEventListener('select', e => {});
+			this.addEventListener('deselect', e => {});
+		}
+
+	}
+
+	get visible(){
+		return this._visible;
+	}
+
+	set visible(value){
+		if(this._visible !== value){
+			this._visible = value;
+
+			this.dispatchEvent({type: "visibility_changed", object: this});
+		}
+	}
+
+	getVolume () {
+		console.warn("override this in subclass");
+	}
+
+	update () {
+		
+	};
+
+	raycast (raycaster, intersects) {
+
+	}
+
+	get clip () {
+		return this._clip;
+	}
+
+	set clip (value) {
+
+		if(this._clip !== value){
+			this._clip = value;
+
+			this.update();
+
+			this.dispatchEvent({
+				type: "clip_changed",
+				object: this
+			});
+		}
+		
+	}
+
+	get modifieable () {
+		return this._modifiable;
+	}
+
+	set modifieable (value) {
+		this._modifiable = value;
+
+		this.update();
+	}
+};
+
+
+export class BoxVolume extends Volume{
+
+	constructor(args = {}){
+		super(args);
+
+		this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;
+		this.name = 'box_' + this.constructor.counter;
+
+		let boxGeometry = new THREE.BoxGeometry(1, 1, 1);
+		boxGeometry.computeBoundingBox();
+
+		let boxFrameGeometry = new THREE.Geometry();
+		{
+			let Vector3 = THREE.Vector3;
+
+			boxFrameGeometry.vertices.push(
+
+				// bottom
+				new Vector3(-0.5, -0.5, 0.5),
+				new Vector3(0.5, -0.5, 0.5),
+				new Vector3(0.5, -0.5, 0.5),
+				new Vector3(0.5, -0.5, -0.5),
+				new Vector3(0.5, -0.5, -0.5),
+				new Vector3(-0.5, -0.5, -0.5),
+				new Vector3(-0.5, -0.5, -0.5),
+				new Vector3(-0.5, -0.5, 0.5),
+				// top
+				new Vector3(-0.5, 0.5, 0.5),
+				new Vector3(0.5, 0.5, 0.5),
+				new Vector3(0.5, 0.5, 0.5),
+				new Vector3(0.5, 0.5, -0.5),
+				new Vector3(0.5, 0.5, -0.5),
+				new Vector3(-0.5, 0.5, -0.5),
+				new Vector3(-0.5, 0.5, -0.5),
+				new Vector3(-0.5, 0.5, 0.5),
+				// sides
+				new Vector3(-0.5, -0.5, 0.5),
+				new Vector3(-0.5, 0.5, 0.5),
+				new Vector3(0.5, -0.5, 0.5),
+				new Vector3(0.5, 0.5, 0.5),
+				new Vector3(0.5, -0.5, -0.5),
+				new Vector3(0.5, 0.5, -0.5),
+				new Vector3(-0.5, -0.5, -0.5),
+				new Vector3(-0.5, 0.5, -0.5),
+
+			);
+
+		}
+
+		this.material = new THREE.MeshBasicMaterial({
+			color: 0x00ff00,
+			transparent: true,
+			opacity: 0.3,
+			depthTest: true,
+			depthWrite: false});
+		this.box = new THREE.Mesh(boxGeometry, this.material);
+		this.box.geometry.computeBoundingBox();
+		this.boundingBox = this.box.geometry.boundingBox;
+		this.add(this.box);
+
+		this.frame = new THREE.LineSegments(boxFrameGeometry, new THREE.LineBasicMaterial({color: 0x000000}));
+		// this.frame.mode = THREE.Lines;
+		this.add(this.frame);
+
+		this.update();
+	}
+
+	update(){
+		this.boundingBox = this.box.geometry.boundingBox;
+		this.boundingSphere = this.boundingBox.getBoundingSphere(new THREE.Sphere());
+
+		if (this._clip) {
+			this.box.visible = false;
+			this.label.visible = false;
+		} else {
+			this.box.visible = true;
+			this.label.visible = this.showVolumeLabel;
+		}
+	}
+
+	raycast (raycaster, intersects) {
+		let is = [];
+		this.box.raycast(raycaster, is);
+
+		if (is.length > 0) {
+			let I = is[0];
+			intersects.push({
+				distance: I.distance,
+				object: this,
+				point: I.point.clone()
+			});
+		}
+	}
+
+	getVolume(){
+		return Math.abs(this.scale.x * this.scale.y * this.scale.z);
+	}
+
+};
+
+export class SphereVolume extends Volume{
+
+	constructor(args = {}){
+		super(args);
+
+		this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;
+		this.name = 'sphere_' + this.constructor.counter;
+
+		let sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
+		sphereGeometry.computeBoundingBox();
+
+		this.material = new THREE.MeshBasicMaterial({
+			color: 0x00ff00,
+			transparent: true,
+			opacity: 0.3,
+			depthTest: true,
+			depthWrite: false});
+		this.sphere = new THREE.Mesh(sphereGeometry, this.material);
+		this.sphere.visible = false;
+		this.sphere.geometry.computeBoundingBox();
+		this.boundingBox = this.sphere.geometry.boundingBox;
+		this.add(this.sphere);
+
+		this.label.visible = false;
+
+
+		let frameGeometry = new THREE.Geometry();
+		{
+			let steps = 64;
+			let uSegments = 8;
+			let vSegments = 5;
+			let r = 1;
+
+			for(let uSegment = 0; uSegment < uSegments; uSegment++){
+
+				let alpha = (uSegment / uSegments) * Math.PI * 2;
+				let dirx = Math.cos(alpha);
+				let diry = Math.sin(alpha);
+
+				for(let i = 0; i <= steps; i++){
+					let v = (i / steps) * Math.PI * 2;
+					let vNext = v + 2 * Math.PI / steps;
+
+					let height = Math.sin(v);
+					let xyAmount = Math.cos(v);
+
+					let heightNext = Math.sin(vNext);
+					let xyAmountNext = Math.cos(vNext);
+
+					let vertex = new THREE.Vector3(dirx * xyAmount, diry * xyAmount, height);
+					frameGeometry.vertices.push(vertex);
+
+					let vertexNext = new THREE.Vector3(dirx * xyAmountNext, diry * xyAmountNext, heightNext);
+					frameGeometry.vertices.push(vertexNext);
+				}
+			}
+
+			// creates rings at poles, just because it's easier to implement
+			for(let vSegment = 0; vSegment <= vSegments + 1; vSegment++){
+
+				//let height = (vSegment / (vSegments + 1)) * 2 - 1; // -1 to 1
+				let uh = (vSegment / (vSegments + 1)); // -1 to 1
+				uh = (1 - uh) * (-Math.PI / 2) + uh *(Math.PI / 2);
+				let height = Math.sin(uh);
+
+				console.log(uh, height);
+
+				for(let i = 0; i <= steps; i++){
+					let u = (i / steps) * Math.PI * 2;
+					let uNext = u + 2 * Math.PI / steps;
+
+					let dirx = Math.cos(u);
+					let diry = Math.sin(u);
+
+					let dirxNext = Math.cos(uNext);
+					let diryNext = Math.sin(uNext);
+
+					let xyAmount = Math.sqrt(1 - height * height);
+
+					let vertex = new THREE.Vector3(dirx * xyAmount, diry * xyAmount, height);
+					frameGeometry.vertices.push(vertex);
+
+					let vertexNext = new THREE.Vector3(dirxNext * xyAmount, diryNext * xyAmount, height);
+					frameGeometry.vertices.push(vertexNext);
+				}
+			}
+		}
+
+		this.frame = new THREE.LineSegments(frameGeometry, new THREE.LineBasicMaterial({color: 0x000000}));
+		this.add(this.frame);
+
+		let frameMaterial = new THREE.MeshBasicMaterial({wireframe: true, color: 0x000000});
+		this.frame = new THREE.Mesh(sphereGeometry, frameMaterial);
+		//this.add(this.frame);
+
+		//this.frame = new THREE.LineSegments(boxFrameGeometry, new THREE.LineBasicMaterial({color: 0x000000}));
+		// this.frame.mode = THREE.Lines;
+		//this.add(this.frame);
+
+		this.update();
+	}
+
+	update(){
+		this.boundingBox = this.sphere.geometry.boundingBox;
+		this.boundingSphere = this.boundingBox.getBoundingSphere(new THREE.Sphere());
+
+		//if (this._clip) {
+		//	this.sphere.visible = false;
+		//	this.label.visible = false;
+		//} else {
+		//	this.sphere.visible = true;
+		//	this.label.visible = this.showVolumeLabel;
+		//}
+	}
+
+	raycast (raycaster, intersects) {
+		let is = [];
+		this.sphere.raycast(raycaster, is);
+
+		if (is.length > 0) {
+			let I = is[0];
+			intersects.push({
+				distance: I.distance,
+				object: this,
+				point: I.point.clone()
+			});
+		}
+	}
+	
+	// see https://en.wikipedia.org/wiki/Ellipsoid#Volume
+	getVolume(){
+		return (4 / 3) * Math.PI * this.scale.x * this.scale.y * this.scale.z;
+	}
+
+};

+ 167 - 0
src/utils/VolumeTool.js

@@ -0,0 +1,167 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import {Volume, BoxVolume} from "./Volume.js";
+import {Utils} from "../utils.js";
+import { EventDispatcher } from "../EventDispatcher.js";
+
+export class VolumeTool extends EventDispatcher{
+	constructor (viewer) {
+		super();
+
+		this.viewer = viewer;
+		this.renderer = viewer.renderer;
+
+		this.addEventListener('start_inserting_volume', e => {
+			this.viewer.dispatchEvent({
+				type: 'cancel_insertions'
+			});
+		});
+
+		this.scene = new THREE.Scene();
+		this.scene.name = 'scene_volume';
+
+		this.viewer.inputHandler.registerInteractiveScene(this.scene);
+
+		this.onRemove = e => {
+			this.scene.remove(e.volume);
+		};
+
+		this.onAdd = e => {
+			this.scene.add(e.volume);
+		};
+
+		for(let volume of viewer.scene.volumes){
+			this.onAdd({volume: volume});
+		}
+
+		this.viewer.inputHandler.addEventListener('delete', e => {
+			let volumes = e.selection.filter(e => (e instanceof Volume));
+			volumes.forEach(e => this.viewer.scene.removeVolume(e));
+		});
+
+		viewer.addEventListener("update", this.update.bind(this));
+		viewer.addEventListener("render.pass.scene", e => this.render(e));
+		viewer.addEventListener("scene_changed", this.onSceneChange.bind(this));
+
+		viewer.scene.addEventListener('volume_added', this.onAdd);
+		viewer.scene.addEventListener('volume_removed', this.onRemove);
+	}
+
+	onSceneChange(e){
+		if(e.oldScene){
+			e.oldScene.removeEventListeners('volume_added', this.onAdd);
+			e.oldScene.removeEventListeners('volume_removed', this.onRemove);
+		}
+
+		e.scene.addEventListener('volume_added', this.onAdd);
+		e.scene.addEventListener('volume_removed', this.onRemove);
+	}
+
+	startInsertion (args = {}) {
+		let volume;
+		if(args.type){
+			volume = new args.type();
+		}else{
+			volume = new BoxVolume();
+		}
+		
+		volume.clip = args.clip || false;
+		volume.name = args.name || 'Volume';
+
+		this.dispatchEvent({
+			type: 'start_inserting_volume',
+			volume: volume
+		});
+
+		this.viewer.scene.addVolume(volume);
+		this.scene.add(volume);
+
+		let cancel = {
+			callback: null
+		};
+
+		let drag = e => {
+			let camera = this.viewer.scene.getActiveCamera();
+			
+			let I = Utils.getMousePointCloudIntersection(
+				e.drag.end, 
+				this.viewer.scene.getActiveCamera(), 
+				this.viewer, 
+				this.viewer.scene.pointclouds, 
+				{pickClipped: false});
+
+			if (I) {
+				volume.position.copy(I.location);
+
+				let wp = volume.getWorldPosition(new THREE.Vector3()).applyMatrix4(camera.matrixWorldInverse);
+				// let pp = new THREE.Vector4(wp.x, wp.y, wp.z).applyMatrix4(camera.projectionMatrix);
+				let w = Math.abs((wp.z / 5));
+				volume.scale.set(w, w, w);
+			}
+		};
+
+		let drop = e => {
+			volume.removeEventListener('drag', drag);
+			volume.removeEventListener('drop', drop);
+
+			cancel.callback();
+		};
+
+		cancel.callback = e => {
+			volume.removeEventListener('drag', drag);
+			volume.removeEventListener('drop', drop);
+			this.viewer.removeEventListener('cancel_insertions', cancel.callback);
+		};
+
+		volume.addEventListener('drag', drag);
+		volume.addEventListener('drop', drop);
+		this.viewer.addEventListener('cancel_insertions', cancel.callback);
+
+		this.viewer.inputHandler.startDragging(volume);
+
+		return volume;
+	}
+
+	update(){
+		if (!this.viewer.scene) {
+			return;
+		}
+		
+		let camera = this.viewer.scene.getActiveCamera();
+		let renderAreaSize = this.viewer.renderer.getSize(new THREE.Vector2());
+		let clientWidth = renderAreaSize.width;
+		let clientHeight = renderAreaSize.height;
+
+		let volumes = this.viewer.scene.volumes;
+		for (let volume of volumes) {
+			let label = volume.label;
+			
+			{
+
+				let distance = label.position.distanceTo(camera.position);
+				let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
+
+				let scale = (70 / pr);
+				label.scale.set(scale, scale, scale);
+			}
+
+			let calculatedVolume = volume.getVolume();
+			calculatedVolume = calculatedVolume / Math.pow(this.viewer.lengthUnit.unitspermeter, 3) * Math.pow(this.viewer.lengthUnitDisplay.unitspermeter, 3);  //convert to cubic meters then to the cubic display unit
+			let text = Utils.addCommas(calculatedVolume.toFixed(3)) + ' ' + this.viewer.lengthUnitDisplay.code + '\u00B3';
+			label.setText(text);
+		}
+	}
+
+	render(params){
+		const renderer = this.viewer.renderer;
+
+		const oldTarget = renderer.getRenderTarget();
+		
+		if(params.renderTarget){
+			renderer.setRenderTarget(params.renderTarget);
+		}
+		renderer.render(this.scene, this.viewer.scene.getActiveCamera());
+		renderer.setRenderTarget(oldTarget);
+	}
+
+}