(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
	typeof define === 'function' && define.amd ? define(['exports'], factory) :
	(factory((global.Shadow = {})));
}(this, (function (exports) { 'use strict';

	/**
	 * @author mr.doob / http://mrdoob.com/
	 */

	var System = {

		browser: (function () {

			var ua = navigator.userAgent;

			if (/Arora/i.test(ua)) {

				return 'Arora';

			} else if (/Opera|OPR/.test(ua)) {

				return 'Opera';

			} else if (/Maxthon/i.test(ua)) {

				return 'Maxthon';

			} else if (/Vivaldi/i.test(ua)) {

				return 'Vivaldi';

			} else if (/YaBrowser/i.test(ua)) {

				return 'Yandex';

			} else if (/Chrome/i.test(ua)) {

				return 'Chrome';

			} else if (/Epiphany/i.test(ua)) {

				return 'Epiphany';

			} else if (/Firefox/i.test(ua)) {

				return 'Firefox';

			} else if (/Mobile(\/.*)? Safari/i.test(ua)) {

				return 'Mobile Safari';

			} else if (/MSIE/i.test(ua)) {

				return 'Internet Explorer';

			} else if (/Midori/i.test(ua)) {

				return 'Midori';

			} else if (/Safari/i.test(ua)) {

				return 'Safari';

			}

			return false;

		})(),

		os: (function () {

			var ua = navigator.userAgent;

			if (/Android/i.test(ua)) {

				return 'Android';

			} else if (/CrOS/i.test(ua)) {

				return 'Chrome OS';

			} else if (/iP[ao]d|iPhone/i.test(ua)) {

				return 'iOS';

			} else if (/Linux/i.test(ua)) {

				return 'Linux';

			} else if (/Mac OS/i.test(ua)) {

				return 'Mac OS';

			} else if (/windows/i.test(ua)) {

				return 'Windows';

			}

			return false;

		})(),

		support: {

			canvas: !!window.CanvasRenderingContext2D,

			localStorage: (function () {

				try {

					return !!window.localStorage.getItem;

				} catch (error) {

					return false;

				}

			})(),

			file: !!window.File && !!window.FileReader && !!window.FileList && !!window.Blob,

			fileSystem: !!window.requestFileSystem || !!window.webkitRequestFileSystem,

			getUserMedia: !!window.navigator.getUserMedia || !!window.navigator.webkitGetUserMedia || !!window.navigator.mozGetUserMedia || !!window.navigator.msGetUserMedia,

			requestAnimationFrame: !!window.mozRequestAnimationFrame || !!window.webkitRequestAnimationFrame || !!window.oRequestAnimationFrame || !!window.msRequestAnimationFrame,

			sessionStorage: (function () {

				try {

					return !!window.sessionStorage.getItem;

				} catch (error) {

					return false;

				}

			})(),

			svg: (function () { try { return !!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect; } catch (e) { return false; } })(),

			webgl: (function () { try { return !!window.WebGLRenderingContext && !!document.createElement('canvas').getContext('experimental-webgl'); } catch (e) { return false; } })(),

			worker: !!window.Worker,

			notifications: !! 'Notification' in Window

		}

	};

	/**
	 * @author qiao / https://github.com/qiao
	 * @author mrdoob / http://mrdoob.com
	 * @author alteredq / http://alteredqualia.com/
	 * @author WestLangley / http://github.com/WestLangley
	 */

	THREE.EditorControls = function ( object, domElement ) {

		domElement = ( domElement !== undefined ) ? domElement : document;

		// API

		this.enabled = true;
		this.center = new THREE.Vector3();
		this.panSpeed = 0.001;
		this.zoomSpeed = 0.001;
		this.rotationSpeed = 0.005;

		// internals

		var scope = this;
		var vector = new THREE.Vector3();

		var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2 };
		var state = STATE.NONE;

		var center = this.center;
		var normalMatrix = new THREE.Matrix3();
		var pointer = new THREE.Vector2();
		var pointerOld = new THREE.Vector2();
		var spherical = new THREE.Spherical();

		// events

		var changeEvent = { type: 'change' };

		this.focus = function ( target ) {

			var box = new THREE.Box3().setFromObject( target );

			var distance;

			if ( box.isEmpty() === false ) {

				center.copy( box.getCenter() );
				distance = box.getBoundingSphere().radius;

			} else {

				// Focusing on an Group, AmbientLight, etc

				center.setFromMatrixPosition( target.matrixWorld );
				distance = 0.1;

			}

			var delta = new THREE.Vector3( 0, 0, 1 );
			delta.applyQuaternion( object.quaternion );
			delta.multiplyScalar( distance * 4 );

			object.position.copy( center ).add( delta );

			scope.dispatchEvent( changeEvent );

		};

		this.pan = function ( delta ) {

			var distance = object.position.distanceTo( center );

			delta.multiplyScalar( distance * scope.panSpeed );
			delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );

			object.position.add( delta );
			center.add( delta );

			scope.dispatchEvent( changeEvent );

		};

		this.zoom = function ( delta ) {

			var distance = object.position.distanceTo( center );

			delta.multiplyScalar( distance * scope.zoomSpeed );

			if ( delta.length() > distance ) return;

			delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );

			object.position.add( delta );

			scope.dispatchEvent( changeEvent );

		};

		this.rotate = function ( delta ) {

			vector.copy( object.position ).sub( center );

			spherical.setFromVector3( vector );

			spherical.theta += delta.x;
			spherical.phi += delta.y;

			spherical.makeSafe();

			vector.setFromSpherical( spherical );

			object.position.copy( center ).add( vector );

			object.lookAt( center );

			scope.dispatchEvent( changeEvent );

		};

		// mouse

		function onMouseDown( event ) {

			if ( scope.enabled === false ) return;

			if ( event.button === 0 ) {

				state = STATE.ROTATE;

			} else if ( event.button === 1 ) {

				state = STATE.ZOOM;

			} else if ( event.button === 2 ) {

				state = STATE.PAN;

			}

			pointerOld.set( event.clientX, event.clientY );

			domElement.addEventListener( 'mousemove', onMouseMove, false );
			domElement.addEventListener( 'mouseup', onMouseUp, false );
			domElement.addEventListener( 'mouseout', onMouseUp, false );
			domElement.addEventListener( 'dblclick', onMouseUp, false );

		}

		function onMouseMove( event ) {

			if ( scope.enabled === false ) return;

			pointer.set( event.clientX, event.clientY );

			var movementX = pointer.x - pointerOld.x;
			var movementY = pointer.y - pointerOld.y;

			if ( state === STATE.ROTATE ) {

				scope.rotate( new THREE.Vector3( - movementX * scope.rotationSpeed, - movementY * scope.rotationSpeed, 0 ) );

			} else if ( state === STATE.ZOOM ) {

				scope.zoom( new THREE.Vector3( 0, 0, movementY ) );

			} else if ( state === STATE.PAN ) {

				scope.pan( new THREE.Vector3( - movementX, movementY, 0 ) );

			}

			pointerOld.set( event.clientX, event.clientY );

		}

		function onMouseUp( event ) {

			domElement.removeEventListener( 'mousemove', onMouseMove, false );
			domElement.removeEventListener( 'mouseup', onMouseUp, false );
			domElement.removeEventListener( 'mouseout', onMouseUp, false );
			domElement.removeEventListener( 'dblclick', onMouseUp, false );

			state = STATE.NONE;

		}

		function onMouseWheel( event ) {

			event.preventDefault();

			// if ( scope.enabled === false ) return;

			scope.zoom( new THREE.Vector3( 0, 0, event.deltaY ) );

		}

		function contextmenu( event ) {

			event.preventDefault();

		}

		this.dispose = function () {

			domElement.removeEventListener( 'contextmenu', contextmenu, false );
			domElement.removeEventListener( 'mousedown', onMouseDown, false );
			domElement.removeEventListener( 'wheel', onMouseWheel, false );

			domElement.removeEventListener( 'mousemove', onMouseMove, false );
			domElement.removeEventListener( 'mouseup', onMouseUp, false );
			domElement.removeEventListener( 'mouseout', onMouseUp, false );
			domElement.removeEventListener( 'dblclick', onMouseUp, false );

			domElement.removeEventListener( 'touchstart', touchStart, false );
			domElement.removeEventListener( 'touchmove', touchMove, false );

		};

		domElement.addEventListener( 'contextmenu', contextmenu, false );
		domElement.addEventListener( 'mousedown', onMouseDown, false );
		domElement.addEventListener( 'wheel', onMouseWheel, false );

		// touch

		var touches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
		var prevTouches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];

		var prevDistance = null;

		function touchStart( event ) {

			if ( scope.enabled === false ) return;

			switch ( event.touches.length ) {

				case 1:
					touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
					touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
					break;

				case 2:
					touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
					touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
					prevDistance = touches[ 0 ].distanceTo( touches[ 1 ] );
					break;

			}

			prevTouches[ 0 ].copy( touches[ 0 ] );
			prevTouches[ 1 ].copy( touches[ 1 ] );

		}


		function touchMove( event ) {

			if ( scope.enabled === false ) return;

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

			function getClosest( touch, touches ) {

				var closest = touches[ 0 ];

				for ( var i in touches ) {

					if ( closest.distanceTo( touch ) > touches[ i ].distanceTo( touch ) ) closest = touches[ i ];

				}

				return closest;

			}

			switch ( event.touches.length ) {

				case 1:
					touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
					touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
					scope.rotate( touches[ 0 ].sub( getClosest( touches[ 0 ], prevTouches ) ).multiplyScalar( - scope.rotationSpeed ) );
					break;

				case 2:
					touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
					touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
					var distance = touches[ 0 ].distanceTo( touches[ 1 ] );
					scope.zoom( new THREE.Vector3( 0, 0, prevDistance - distance ) );
					prevDistance = distance;


					var offset0 = touches[ 0 ].clone().sub( getClosest( touches[ 0 ], prevTouches ) );
					var offset1 = touches[ 1 ].clone().sub( getClosest( touches[ 1 ], prevTouches ) );
					offset0.x = - offset0.x;
					offset1.x = - offset1.x;

					scope.pan( offset0.add( offset1 ).multiplyScalar( 0.5 ) );

					break;

			}

			prevTouches[ 0 ].copy( touches[ 0 ] );
			prevTouches[ 1 ].copy( touches[ 1 ] );

		}

		domElement.addEventListener( 'touchstart', touchStart, false );
		domElement.addEventListener( 'touchmove', touchMove, false );

	};

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

	/**
	 * @author arodic / https://github.com/arodic
	 */

	( function () {

		var GizmoMaterial = function ( parameters ) {

			THREE.MeshBasicMaterial.call( this );

			this.depthTest = false;
			this.depthWrite = false;
			this.fog = false;
			this.side = THREE.FrontSide;
			this.transparent = true;

			this.setValues( parameters );

			this.oldColor = this.color.clone();
			this.oldOpacity = this.opacity;

			this.highlight = function ( highlighted ) {

				if ( highlighted ) {

					this.color.setRGB( 1, 1, 0 );
					this.opacity = 1;

				} else {

					this.color.copy( this.oldColor );
					this.opacity = this.oldOpacity;

				}

			};

		};

		GizmoMaterial.prototype = Object.create( THREE.MeshBasicMaterial.prototype );
		GizmoMaterial.prototype.constructor = GizmoMaterial;


		var GizmoLineMaterial = function ( parameters ) {

			THREE.LineBasicMaterial.call( this );

			this.depthTest = false;
			this.depthWrite = false;
			this.fog = false;
			this.transparent = true;
			this.linewidth = 1;

			this.setValues( parameters );

			this.oldColor = this.color.clone();
			this.oldOpacity = this.opacity;

			this.highlight = function ( highlighted ) {

				if ( highlighted ) {

					this.color.setRGB( 1, 1, 0 );
					this.opacity = 1;

				} else {

					this.color.copy( this.oldColor );
					this.opacity = this.oldOpacity;

				}

			};

		};

		GizmoLineMaterial.prototype = Object.create( THREE.LineBasicMaterial.prototype );
		GizmoLineMaterial.prototype.constructor = GizmoLineMaterial;


		var pickerMaterial = new GizmoMaterial( { visible: false, transparent: false } );


		THREE.TransformGizmo = function () {

			this.init = function () {

				THREE.Object3D.call( this );

				this.handles = new THREE.Object3D();
				this.pickers = new THREE.Object3D();
				this.planes = new THREE.Object3D();

				this.add( this.handles );
				this.add( this.pickers );
				this.add( this.planes );

				//// PLANES

				var planeGeometry = new THREE.PlaneBufferGeometry( 50, 50, 2, 2 );
				var planeMaterial = new THREE.MeshBasicMaterial( { visible: false, side: THREE.DoubleSide } );

				var planes = {
					"XY": new THREE.Mesh( planeGeometry, planeMaterial ),
					"YZ": new THREE.Mesh( planeGeometry, planeMaterial ),
					"XZ": new THREE.Mesh( planeGeometry, planeMaterial ),
					"XYZE": new THREE.Mesh( planeGeometry, planeMaterial )
				};

				this.activePlane = planes[ "XYZE" ];

				planes[ "YZ" ].rotation.set( 0, Math.PI / 2, 0 );
				planes[ "XZ" ].rotation.set( - Math.PI / 2, 0, 0 );

				for ( var i in planes ) {

					planes[ i ].name = i;
					this.planes.add( planes[ i ] );
					this.planes[ i ] = planes[ i ];

				}

				//// HANDLES AND PICKERS

				var setupGizmos = function ( gizmoMap, parent ) {

					for ( var name in gizmoMap ) {

						for ( i = gizmoMap[ name ].length; i --; ) {

							var object = gizmoMap[ name ][ i ][ 0 ];
							var position = gizmoMap[ name ][ i ][ 1 ];
							var rotation = gizmoMap[ name ][ i ][ 2 ];

							object.name = name;

							object.renderOrder = Infinity; // avoid being hidden by other transparent objects

							if ( position ) object.position.set( position[ 0 ], position[ 1 ], position[ 2 ] );
							if ( rotation ) object.rotation.set( rotation[ 0 ], rotation[ 1 ], rotation[ 2 ] );

							parent.add( object );

						}

					}

				};

				setupGizmos( this.handleGizmos, this.handles );
				setupGizmos( this.pickerGizmos, this.pickers );

				// reset Transformations

				this.traverse( function ( child ) {

					if ( child instanceof THREE.Mesh ) {

						child.updateMatrix();

						var tempGeometry = child.geometry.clone();
						tempGeometry.applyMatrix( child.matrix );
						child.geometry = tempGeometry;

						child.position.set( 0, 0, 0 );
						child.rotation.set( 0, 0, 0 );
						child.scale.set( 1, 1, 1 );

					}

				} );

			};

			this.highlight = function ( axis ) {

				this.traverse( function ( child ) {

					if ( child.material && child.material.highlight ) {

						if ( child.name === axis ) {

							child.material.highlight( true );

						} else {

							child.material.highlight( false );

						}

					}

				} );

			};

		};

		THREE.TransformGizmo.prototype = Object.create( THREE.Object3D.prototype );
		THREE.TransformGizmo.prototype.constructor = THREE.TransformGizmo;

		THREE.TransformGizmo.prototype.update = function ( rotation, eye ) {

			var vec1 = new THREE.Vector3( 0, 0, 0 );
			var vec2 = new THREE.Vector3( 0, 1, 0 );
			var lookAtMatrix = new THREE.Matrix4();

			this.traverse( function ( child ) {

				if ( child.name.search( "E" ) !== - 1 ) {

					child.quaternion.setFromRotationMatrix( lookAtMatrix.lookAt( eye, vec1, vec2 ) );

				} else if ( child.name.search( "X" ) !== - 1 || child.name.search( "Y" ) !== - 1 || child.name.search( "Z" ) !== - 1 ) {

					child.quaternion.setFromEuler( rotation );

				}

			} );

		};

		THREE.TransformGizmoTranslate = function () {

			THREE.TransformGizmo.call( this );

			var arrowGeometry = new THREE.ConeBufferGeometry( 0.05, 0.2, 12, 1, false );
			arrowGeometry.translate( 0, 0.5, 0 );

			var lineXGeometry = new THREE.BufferGeometry();
			lineXGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 1, 0, 0 ], 3 ) );

			var lineYGeometry = new THREE.BufferGeometry();
			lineYGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 1, 0 ], 3 ) );

			var lineZGeometry = new THREE.BufferGeometry();
			lineZGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 0, 1 ], 3 ) );

			this.handleGizmos = {

				X: [
					[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0xff0000 } ) ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ],
					[ new THREE.Line( lineXGeometry, new GizmoLineMaterial( { color: 0xff0000 } ) ) ]
				],

				Y: [
					[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x00ff00 } ) ), [ 0, 0.5, 0 ] ],
					[	new THREE.Line( lineYGeometry, new GizmoLineMaterial( { color: 0x00ff00 } ) ) ]
				],

				Z: [
					[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x0000ff } ) ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ] ],
					[ new THREE.Line( lineZGeometry, new GizmoLineMaterial( { color: 0x0000ff } ) ) ]
				],

				XYZ: [
					[ new THREE.Mesh( new THREE.OctahedronGeometry( 0.1, 0 ), new GizmoMaterial( { color: 0xffffff, opacity: 0.25 } ) ), [ 0, 0, 0 ], [ 0, 0, 0 ] ]
				],

				XY: [
					[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.29, 0.29 ), new GizmoMaterial( { color: 0xffff00, opacity: 0.25 } ) ), [ 0.15, 0.15, 0 ] ]
				],

				YZ: [
					[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.29, 0.29 ), new GizmoMaterial( { color: 0x00ffff, opacity: 0.25 } ) ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ] ]
				],

				XZ: [
					[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.29, 0.29 ), new GizmoMaterial( { color: 0xff00ff, opacity: 0.25 } ) ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ] ]
				]

			};

			this.pickerGizmos = {

				X: [
					[ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0.6, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ]
				],

				Y: [
					[ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0.6, 0 ] ]
				],

				Z: [
					[ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0, 0.6 ], [ Math.PI / 2, 0, 0 ] ]
				],

				XYZ: [
					[ new THREE.Mesh( new THREE.OctahedronGeometry( 0.2, 0 ), pickerMaterial ) ]
				],

				XY: [
					[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.4, 0.4 ), pickerMaterial ), [ 0.2, 0.2, 0 ] ]
				],

				YZ: [
					[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.4, 0.4 ), pickerMaterial ), [ 0, 0.2, 0.2 ], [ 0, Math.PI / 2, 0 ] ]
				],

				XZ: [
					[ new THREE.Mesh( new THREE.PlaneBufferGeometry( 0.4, 0.4 ), pickerMaterial ), [ 0.2, 0, 0.2 ], [ - Math.PI / 2, 0, 0 ] ]
				]

			};

			this.setActivePlane = function ( axis, eye ) {

				var tempMatrix = new THREE.Matrix4();
				eye.applyMatrix4( tempMatrix.getInverse( tempMatrix.extractRotation( this.planes[ "XY" ].matrixWorld ) ) );

				if ( axis === "X" ) {

					this.activePlane = this.planes[ "XY" ];

					if ( Math.abs( eye.y ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "XZ" ];

				}

				if ( axis === "Y" ) {

					this.activePlane = this.planes[ "XY" ];

					if ( Math.abs( eye.x ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "YZ" ];

				}

				if ( axis === "Z" ) {

					this.activePlane = this.planes[ "XZ" ];

					if ( Math.abs( eye.x ) > Math.abs( eye.y ) ) this.activePlane = this.planes[ "YZ" ];

				}

				if ( axis === "XYZ" ) this.activePlane = this.planes[ "XYZE" ];

				if ( axis === "XY" ) this.activePlane = this.planes[ "XY" ];

				if ( axis === "YZ" ) this.activePlane = this.planes[ "YZ" ];

				if ( axis === "XZ" ) this.activePlane = this.planes[ "XZ" ];

			};

			this.init();

		};

		THREE.TransformGizmoTranslate.prototype = Object.create( THREE.TransformGizmo.prototype );
		THREE.TransformGizmoTranslate.prototype.constructor = THREE.TransformGizmoTranslate;

		THREE.TransformGizmoRotate = function () {

			THREE.TransformGizmo.call( this );

			var CircleGeometry = function ( radius, facing, arc ) {

				var geometry = new THREE.BufferGeometry();
				var vertices = [];
				arc = arc ? arc : 1;

				for ( var i = 0; i <= 64 * arc; ++ i ) {

					if ( facing === 'x' ) vertices.push( 0, Math.cos( i / 32 * Math.PI ) * radius, Math.sin( i / 32 * Math.PI ) * radius );
					if ( facing === 'y' ) vertices.push( Math.cos( i / 32 * Math.PI ) * radius, 0, Math.sin( i / 32 * Math.PI ) * radius );
					if ( facing === 'z' ) vertices.push( Math.sin( i / 32 * Math.PI ) * radius, Math.cos( i / 32 * Math.PI ) * radius, 0 );

				}

				geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
				return geometry;

			};

			this.handleGizmos = {

				X: [
					[ new THREE.Line( new CircleGeometry( 1, 'x', 0.5 ), new GizmoLineMaterial( { color: 0xff0000 } ) ) ]
				],

				Y: [
					[ new THREE.Line( new CircleGeometry( 1, 'y', 0.5 ), new GizmoLineMaterial( { color: 0x00ff00 } ) ) ]
				],

				Z: [
					[ new THREE.Line( new CircleGeometry( 1, 'z', 0.5 ), new GizmoLineMaterial( { color: 0x0000ff } ) ) ]
				],

				E: [
					[ new THREE.Line( new CircleGeometry( 1.25, 'z', 1 ), new GizmoLineMaterial( { color: 0xcccc00 } ) ) ]
				],

				XYZE: [
					[ new THREE.Line( new CircleGeometry( 1, 'z', 1 ), new GizmoLineMaterial( { color: 0x787878 } ) ) ]
				]

			};

			this.pickerGizmos = {

				X: [
					[ new THREE.Mesh( new THREE.TorusBufferGeometry( 1, 0.12, 4, 12, Math.PI ), pickerMaterial ), [ 0, 0, 0 ], [ 0, - Math.PI / 2, - Math.PI / 2 ] ]
				],

				Y: [
					[ new THREE.Mesh( new THREE.TorusBufferGeometry( 1, 0.12, 4, 12, Math.PI ), pickerMaterial ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ] ]
				],

				Z: [
					[ new THREE.Mesh( new THREE.TorusBufferGeometry( 1, 0.12, 4, 12, Math.PI ), pickerMaterial ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ]
				],

				E: [
					[ new THREE.Mesh( new THREE.TorusBufferGeometry( 1.25, 0.12, 2, 24 ), pickerMaterial ) ]
				],

				XYZE: [
					[ new THREE.Mesh( new THREE.TorusBufferGeometry( 1, 0.12, 2, 24 ), pickerMaterial ) ]
				]

			};

			this.pickerGizmos.XYZE[ 0 ][ 0 ].visible = false; // disable XYZE picker gizmo

			this.setActivePlane = function ( axis ) {

				if ( axis === "E" ) this.activePlane = this.planes[ "XYZE" ];

				if ( axis === "X" ) this.activePlane = this.planes[ "YZ" ];

				if ( axis === "Y" ) this.activePlane = this.planes[ "XZ" ];

				if ( axis === "Z" ) this.activePlane = this.planes[ "XY" ];

			};

			this.update = function ( rotation, eye2 ) {

				THREE.TransformGizmo.prototype.update.apply( this, arguments );

				var tempMatrix = new THREE.Matrix4();
				var worldRotation = new THREE.Euler( 0, 0, 1 );
				var tempQuaternion = new THREE.Quaternion();
				var unitX = new THREE.Vector3( 1, 0, 0 );
				var unitY = new THREE.Vector3( 0, 1, 0 );
				var unitZ = new THREE.Vector3( 0, 0, 1 );
				var quaternionX = new THREE.Quaternion();
				var quaternionY = new THREE.Quaternion();
				var quaternionZ = new THREE.Quaternion();
				var eye = eye2.clone();

				worldRotation.copy( this.planes[ "XY" ].rotation );
				tempQuaternion.setFromEuler( worldRotation );

				tempMatrix.makeRotationFromQuaternion( tempQuaternion ).getInverse( tempMatrix );
				eye.applyMatrix4( tempMatrix );

				this.traverse( function( child ) {

					tempQuaternion.setFromEuler( worldRotation );

					if ( child.name === "X" ) {

						quaternionX.setFromAxisAngle( unitX, Math.atan2( - eye.y, eye.z ) );
						tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionX );
						child.quaternion.copy( tempQuaternion );

					}

					if ( child.name === "Y" ) {

						quaternionY.setFromAxisAngle( unitY, Math.atan2( eye.x, eye.z ) );
						tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionY );
						child.quaternion.copy( tempQuaternion );

					}

					if ( child.name === "Z" ) {

						quaternionZ.setFromAxisAngle( unitZ, Math.atan2( eye.y, eye.x ) );
						tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionZ );
						child.quaternion.copy( tempQuaternion );

					}

				} );

			};

			this.init();

		};

		THREE.TransformGizmoRotate.prototype = Object.create( THREE.TransformGizmo.prototype );
		THREE.TransformGizmoRotate.prototype.constructor = THREE.TransformGizmoRotate;

		THREE.TransformGizmoScale = function () {

			THREE.TransformGizmo.call( this );

			var arrowGeometry = new THREE.BoxBufferGeometry( 0.125, 0.125, 0.125 );
			arrowGeometry.translate( 0, 0.5, 0 );

			var lineXGeometry = new THREE.BufferGeometry();
			lineXGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0,  1, 0, 0 ], 3 ) );

			var lineYGeometry = new THREE.BufferGeometry();
			lineYGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0,  0, 1, 0 ], 3 ) );

			var lineZGeometry = new THREE.BufferGeometry();
			lineZGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0,  0, 0, 1 ], 3 ) );

			this.handleGizmos = {

				X: [
					[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0xff0000 } ) ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ],
					[ new THREE.Line( lineXGeometry, new GizmoLineMaterial( { color: 0xff0000 } ) ) ]
				],

				Y: [
					[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x00ff00 } ) ), [ 0, 0.5, 0 ] ],
					[ new THREE.Line( lineYGeometry, new GizmoLineMaterial( { color: 0x00ff00 } ) ) ]
				],

				Z: [
					[ new THREE.Mesh( arrowGeometry, new GizmoMaterial( { color: 0x0000ff } ) ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ] ],
					[ new THREE.Line( lineZGeometry, new GizmoLineMaterial( { color: 0x0000ff } ) ) ]
				],

				XYZ: [
					[ new THREE.Mesh( new THREE.BoxBufferGeometry( 0.125, 0.125, 0.125 ), new GizmoMaterial( { color: 0xffffff, opacity: 0.25 } ) ) ]
				]

			};

			this.pickerGizmos = {

				X: [
					[ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0.6, 0, 0 ], [ 0, 0, - Math.PI / 2 ] ]
				],

				Y: [
					[ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0.6, 0 ] ]
				],

				Z: [
					[ new THREE.Mesh( new THREE.CylinderBufferGeometry( 0.2, 0, 1, 4, 1, false ), pickerMaterial ), [ 0, 0, 0.6 ], [ Math.PI / 2, 0, 0 ] ]
				],

				XYZ: [
					[ new THREE.Mesh( new THREE.BoxBufferGeometry( 0.4, 0.4, 0.4 ), pickerMaterial ) ]
				]

			};

			this.setActivePlane = function ( axis, eye ) {

				var tempMatrix = new THREE.Matrix4();
				eye.applyMatrix4( tempMatrix.getInverse( tempMatrix.extractRotation( this.planes[ "XY" ].matrixWorld ) ) );

				if ( axis === "X" ) {

					this.activePlane = this.planes[ "XY" ];
					if ( Math.abs( eye.y ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "XZ" ];

				}

				if ( axis === "Y" ) {

					this.activePlane = this.planes[ "XY" ];
					if ( Math.abs( eye.x ) > Math.abs( eye.z ) ) this.activePlane = this.planes[ "YZ" ];

				}

				if ( axis === "Z" ) {

					this.activePlane = this.planes[ "XZ" ];
					if ( Math.abs( eye.x ) > Math.abs( eye.y ) ) this.activePlane = this.planes[ "YZ" ];

				}

				if ( axis === "XYZ" ) this.activePlane = this.planes[ "XYZE" ];

			};

			this.init();

		};

		THREE.TransformGizmoScale.prototype = Object.create( THREE.TransformGizmo.prototype );
		THREE.TransformGizmoScale.prototype.constructor = THREE.TransformGizmoScale;

		THREE.TransformControls = function ( camera, domElement ) {

			// TODO: Make non-uniform scale and rotate play nice in hierarchies
			// TODO: ADD RXYZ contol

			THREE.Object3D.call( this );

			domElement = ( domElement !== undefined ) ? domElement : document;

			this.object = undefined;
			this.visible = false;
			this.translationSnap = null;
			this.rotationSnap = null;
			this.space = "world";
			this.size = 1;
			this.axis = null;

			var scope = this;

			var _mode = "translate";
			var _dragging = false;
			var _gizmo = {

				"translate": new THREE.TransformGizmoTranslate(),
				"rotate": new THREE.TransformGizmoRotate(),
				"scale": new THREE.TransformGizmoScale()
			};

			for ( var type in _gizmo ) {

				var gizmoObj = _gizmo[ type ];

				gizmoObj.visible = ( type === _mode );
				this.add( gizmoObj );

			}

			var changeEvent = { type: "change" };
			var mouseDownEvent = { type: "mouseDown" };
			var mouseUpEvent = { type: "mouseUp", mode: _mode };
			var objectChangeEvent = { type: "objectChange" };

			var ray = new THREE.Raycaster();
			var pointerVector = new THREE.Vector2();

			var point = new THREE.Vector3();
			var offset = new THREE.Vector3();

			var rotation = new THREE.Vector3();
			var offsetRotation = new THREE.Vector3();
			var scale = 1;

			var lookAtMatrix = new THREE.Matrix4();
			var eye = new THREE.Vector3();

			var tempMatrix = new THREE.Matrix4();
			var tempVector = new THREE.Vector3();
			var tempQuaternion = new THREE.Quaternion();
			var unitX = new THREE.Vector3( 1, 0, 0 );
			var unitY = new THREE.Vector3( 0, 1, 0 );
			var unitZ = new THREE.Vector3( 0, 0, 1 );

			var quaternionXYZ = new THREE.Quaternion();
			var quaternionX = new THREE.Quaternion();
			var quaternionY = new THREE.Quaternion();
			var quaternionZ = new THREE.Quaternion();
			var quaternionE = new THREE.Quaternion();

			var oldPosition = new THREE.Vector3();
			var oldScale = new THREE.Vector3();
			var oldRotationMatrix = new THREE.Matrix4();

			var parentRotationMatrix = new THREE.Matrix4();
			var parentScale = new THREE.Vector3();

			var worldPosition = new THREE.Vector3();
			var worldRotation = new THREE.Euler();
			var worldRotationMatrix = new THREE.Matrix4();
			var camPosition = new THREE.Vector3();
			var camRotation = new THREE.Euler();

			domElement.addEventListener( "mousedown", onPointerDown, false );
			domElement.addEventListener( "touchstart", onPointerDown, false );

			domElement.addEventListener( "mousemove", onPointerHover, false );
			domElement.addEventListener( "touchmove", onPointerHover, false );

			domElement.addEventListener( "mousemove", onPointerMove, false );
			domElement.addEventListener( "touchmove", onPointerMove, false );

			domElement.addEventListener( "mouseup", onPointerUp, false );
			domElement.addEventListener( "mouseout", onPointerUp, false );
			domElement.addEventListener( "touchend", onPointerUp, false );
			domElement.addEventListener( "touchcancel", onPointerUp, false );
			domElement.addEventListener( "touchleave", onPointerUp, false );

			this.dispose = function () {

				domElement.removeEventListener( "mousedown", onPointerDown );
				domElement.removeEventListener( "touchstart", onPointerDown );

				domElement.removeEventListener( "mousemove", onPointerHover );
				domElement.removeEventListener( "touchmove", onPointerHover );

				domElement.removeEventListener( "mousemove", onPointerMove );
				domElement.removeEventListener( "touchmove", onPointerMove );

				domElement.removeEventListener( "mouseup", onPointerUp );
				domElement.removeEventListener( "mouseout", onPointerUp );
				domElement.removeEventListener( "touchend", onPointerUp );
				domElement.removeEventListener( "touchcancel", onPointerUp );
				domElement.removeEventListener( "touchleave", onPointerUp );

			};

			this.attach = function ( object ) {

				this.object = object;
				this.visible = true;
				this.update();

			};

			this.detach = function () {

				this.object = undefined;
				this.visible = false;
				this.axis = null;

			};

			this.getMode = function () {

				return _mode;

			};

			this.setMode = function ( mode ) {

				_mode = mode ? mode : _mode;

				if ( _mode === "scale" ) scope.space = "local";

				for ( var type in _gizmo ) _gizmo[ type ].visible = ( type === _mode );

				this.update();
				scope.dispatchEvent( changeEvent );

			};

			this.setTranslationSnap = function ( translationSnap ) {

				scope.translationSnap = translationSnap;

			};

			this.setRotationSnap = function ( rotationSnap ) {

				scope.rotationSnap = rotationSnap;

			};

			this.setSize = function ( size ) {

				scope.size = size;
				this.update();
				scope.dispatchEvent( changeEvent );

			};

			this.setSpace = function ( space ) {

				scope.space = space;
				this.update();
				scope.dispatchEvent( changeEvent );

			};

			this.update = function () {

				if ( scope.object === undefined ) return;

				scope.object.updateMatrixWorld();
				worldPosition.setFromMatrixPosition( scope.object.matrixWorld );
				worldRotation.setFromRotationMatrix( tempMatrix.extractRotation( scope.object.matrixWorld ) );

				camera.updateMatrixWorld();
				camPosition.setFromMatrixPosition( camera.matrixWorld );
				camRotation.setFromRotationMatrix( tempMatrix.extractRotation( camera.matrixWorld ) );

				scale = worldPosition.distanceTo( camPosition ) / 6 * scope.size;
				this.position.copy( worldPosition );
				this.scale.set( scale, scale, scale );

				if ( camera instanceof THREE.PerspectiveCamera ) {

					eye.copy( camPosition ).sub( worldPosition ).normalize();

				} else if ( camera instanceof THREE.OrthographicCamera ) {

					eye.copy( camPosition ).normalize();

				}

				if ( scope.space === "local" ) {

					_gizmo[ _mode ].update( worldRotation, eye );

				} else if ( scope.space === "world" ) {

					_gizmo[ _mode ].update( new THREE.Euler(), eye );

				}

				_gizmo[ _mode ].highlight( scope.axis );

			};

			function onPointerHover( event ) {

				if ( scope.object === undefined || _dragging === true || ( event.button !== undefined && event.button !== 0 ) ) return;

				var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event;

				var intersect = intersectObjects( pointer, _gizmo[ _mode ].pickers.children );

				var axis = null;

				if ( intersect ) {

					axis = intersect.object.name;

					event.preventDefault();

				}

				if ( scope.axis !== axis ) {

					scope.axis = axis;
					scope.update();
					scope.dispatchEvent( changeEvent );

				}

			}

			function onPointerDown( event ) {

				if ( scope.object === undefined || _dragging === true || ( event.button !== undefined && event.button !== 0 ) ) return;

				var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event;

				if ( pointer.button === 0 || pointer.button === undefined ) {

					var intersect = intersectObjects( pointer, _gizmo[ _mode ].pickers.children );

					if ( intersect ) {

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

						scope.axis = intersect.object.name;

						scope.dispatchEvent( mouseDownEvent );

						scope.update();

						eye.copy( camPosition ).sub( worldPosition ).normalize();

						_gizmo[ _mode ].setActivePlane( scope.axis, eye );

						var planeIntersect = intersectObjects( pointer, [ _gizmo[ _mode ].activePlane ] );

						if ( planeIntersect ) {

							oldPosition.copy( scope.object.position );
							oldScale.copy( scope.object.scale );

							oldRotationMatrix.extractRotation( scope.object.matrix );
							worldRotationMatrix.extractRotation( scope.object.matrixWorld );

							parentRotationMatrix.extractRotation( scope.object.parent.matrixWorld );
							parentScale.setFromMatrixScale( tempMatrix.getInverse( scope.object.parent.matrixWorld ) );

							offset.copy( planeIntersect.point );

						}

					}

				}

				_dragging = true;

			}

			function onPointerMove( event ) {

				if ( scope.object === undefined || scope.axis === null || _dragging === false || ( event.button !== undefined && event.button !== 0 ) ) return;

				var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event;

				var planeIntersect = intersectObjects( pointer, [ _gizmo[ _mode ].activePlane ] );

				if ( planeIntersect === false ) return;

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

				point.copy( planeIntersect.point );

				if ( _mode === "translate" ) {

					point.sub( offset );
					point.multiply( parentScale );

					if ( scope.space === "local" ) {

						point.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );

						if ( scope.axis.search( "X" ) === - 1 ) point.x = 0;
						if ( scope.axis.search( "Y" ) === - 1 ) point.y = 0;
						if ( scope.axis.search( "Z" ) === - 1 ) point.z = 0;

						point.applyMatrix4( oldRotationMatrix );

						scope.object.position.copy( oldPosition );
						scope.object.position.add( point );

					}

					if ( scope.space === "world" || scope.axis.search( "XYZ" ) !== - 1 ) {

						if ( scope.axis.search( "X" ) === - 1 ) point.x = 0;
						if ( scope.axis.search( "Y" ) === - 1 ) point.y = 0;
						if ( scope.axis.search( "Z" ) === - 1 ) point.z = 0;

						point.applyMatrix4( tempMatrix.getInverse( parentRotationMatrix ) );

						scope.object.position.copy( oldPosition );
						scope.object.position.add( point );

					}

					if ( scope.translationSnap !== null ) {

						if ( scope.space === "local" ) {

							scope.object.position.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );

						}

						if ( scope.axis.search( "X" ) !== - 1 ) scope.object.position.x = Math.round( scope.object.position.x / scope.translationSnap ) * scope.translationSnap;
						if ( scope.axis.search( "Y" ) !== - 1 ) scope.object.position.y = Math.round( scope.object.position.y / scope.translationSnap ) * scope.translationSnap;
						if ( scope.axis.search( "Z" ) !== - 1 ) scope.object.position.z = Math.round( scope.object.position.z / scope.translationSnap ) * scope.translationSnap;

						if ( scope.space === "local" ) {

							scope.object.position.applyMatrix4( worldRotationMatrix );

						}

					}

				} else if ( _mode === "scale" ) {

					point.sub( offset );
					point.multiply( parentScale );

					if ( scope.space === "local" ) {

						if ( scope.axis === "XYZ" ) {

							scale = 1 + ( ( point.y ) / Math.max( oldScale.x, oldScale.y, oldScale.z ) );

							scope.object.scale.x = oldScale.x * scale;
							scope.object.scale.y = oldScale.y * scale;
							scope.object.scale.z = oldScale.z * scale;

						} else {

							point.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );

							if ( scope.axis === "X" ) scope.object.scale.x = oldScale.x * ( 1 + point.x / oldScale.x );
							if ( scope.axis === "Y" ) scope.object.scale.y = oldScale.y * ( 1 + point.y / oldScale.y );
							if ( scope.axis === "Z" ) scope.object.scale.z = oldScale.z * ( 1 + point.z / oldScale.z );

						}

					}

				} else if ( _mode === "rotate" ) {

					point.sub( worldPosition );
					point.multiply( parentScale );
					tempVector.copy( offset ).sub( worldPosition );
					tempVector.multiply( parentScale );

					if ( scope.axis === "E" ) {

						point.applyMatrix4( tempMatrix.getInverse( lookAtMatrix ) );
						tempVector.applyMatrix4( tempMatrix.getInverse( lookAtMatrix ) );

						rotation.set( Math.atan2( point.z, point.y ), Math.atan2( point.x, point.z ), Math.atan2( point.y, point.x ) );
						offsetRotation.set( Math.atan2( tempVector.z, tempVector.y ), Math.atan2( tempVector.x, tempVector.z ), Math.atan2( tempVector.y, tempVector.x ) );

						tempQuaternion.setFromRotationMatrix( tempMatrix.getInverse( parentRotationMatrix ) );

						quaternionE.setFromAxisAngle( eye, rotation.z - offsetRotation.z );
						quaternionXYZ.setFromRotationMatrix( worldRotationMatrix );

						tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionE );
						tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionXYZ );

						scope.object.quaternion.copy( tempQuaternion );

					} else if ( scope.axis === "XYZE" ) {

						quaternionE.setFromEuler( point.clone().cross( tempVector ).normalize() ); // rotation axis

						tempQuaternion.setFromRotationMatrix( tempMatrix.getInverse( parentRotationMatrix ) );
						quaternionX.setFromAxisAngle( quaternionE, - point.clone().angleTo( tempVector ) );
						quaternionXYZ.setFromRotationMatrix( worldRotationMatrix );

						tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionX );
						tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionXYZ );

						scope.object.quaternion.copy( tempQuaternion );

					} else if ( scope.space === "local" ) {

						point.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );

						tempVector.applyMatrix4( tempMatrix.getInverse( worldRotationMatrix ) );

						rotation.set( Math.atan2( point.z, point.y ), Math.atan2( point.x, point.z ), Math.atan2( point.y, point.x ) );
						offsetRotation.set( Math.atan2( tempVector.z, tempVector.y ), Math.atan2( tempVector.x, tempVector.z ), Math.atan2( tempVector.y, tempVector.x ) );

						quaternionXYZ.setFromRotationMatrix( oldRotationMatrix );

						if ( scope.rotationSnap !== null ) {

							quaternionX.setFromAxisAngle( unitX, Math.round( ( rotation.x - offsetRotation.x ) / scope.rotationSnap ) * scope.rotationSnap );
							quaternionY.setFromAxisAngle( unitY, Math.round( ( rotation.y - offsetRotation.y ) / scope.rotationSnap ) * scope.rotationSnap );
							quaternionZ.setFromAxisAngle( unitZ, Math.round( ( rotation.z - offsetRotation.z ) / scope.rotationSnap ) * scope.rotationSnap );

						} else {

							quaternionX.setFromAxisAngle( unitX, rotation.x - offsetRotation.x );
							quaternionY.setFromAxisAngle( unitY, rotation.y - offsetRotation.y );
							quaternionZ.setFromAxisAngle( unitZ, rotation.z - offsetRotation.z );

						}

						if ( scope.axis === "X" ) quaternionXYZ.multiplyQuaternions( quaternionXYZ, quaternionX );
						if ( scope.axis === "Y" ) quaternionXYZ.multiplyQuaternions( quaternionXYZ, quaternionY );
						if ( scope.axis === "Z" ) quaternionXYZ.multiplyQuaternions( quaternionXYZ, quaternionZ );

						scope.object.quaternion.copy( quaternionXYZ );

					} else if ( scope.space === "world" ) {

						rotation.set( Math.atan2( point.z, point.y ), Math.atan2( point.x, point.z ), Math.atan2( point.y, point.x ) );
						offsetRotation.set( Math.atan2( tempVector.z, tempVector.y ), Math.atan2( tempVector.x, tempVector.z ), Math.atan2( tempVector.y, tempVector.x ) );

						tempQuaternion.setFromRotationMatrix( tempMatrix.getInverse( parentRotationMatrix ) );

						if ( scope.rotationSnap !== null ) {

							quaternionX.setFromAxisAngle( unitX, Math.round( ( rotation.x - offsetRotation.x ) / scope.rotationSnap ) * scope.rotationSnap );
							quaternionY.setFromAxisAngle( unitY, Math.round( ( rotation.y - offsetRotation.y ) / scope.rotationSnap ) * scope.rotationSnap );
							quaternionZ.setFromAxisAngle( unitZ, Math.round( ( rotation.z - offsetRotation.z ) / scope.rotationSnap ) * scope.rotationSnap );

						} else {

							quaternionX.setFromAxisAngle( unitX, rotation.x - offsetRotation.x );
							quaternionY.setFromAxisAngle( unitY, rotation.y - offsetRotation.y );
							quaternionZ.setFromAxisAngle( unitZ, rotation.z - offsetRotation.z );

						}

						quaternionXYZ.setFromRotationMatrix( worldRotationMatrix );

						if ( scope.axis === "X" ) tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionX );
						if ( scope.axis === "Y" ) tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionY );
						if ( scope.axis === "Z" ) tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionZ );

						tempQuaternion.multiplyQuaternions( tempQuaternion, quaternionXYZ );

						scope.object.quaternion.copy( tempQuaternion );

					}

				}

				scope.update();
				scope.dispatchEvent( changeEvent );
				scope.dispatchEvent( objectChangeEvent );

			}

			function onPointerUp( event ) {

				event.preventDefault(); // Prevent MouseEvent on mobile

				if ( event.button !== undefined && event.button !== 0 ) return;

				if ( _dragging && ( scope.axis !== null ) ) {

					mouseUpEvent.mode = _mode;
					scope.dispatchEvent( mouseUpEvent );

				}

				_dragging = false;

				if ( 'TouchEvent' in window && event instanceof TouchEvent ) {

					// Force "rollover"

					scope.axis = null;
					scope.update();
					scope.dispatchEvent( changeEvent );

				} else {

					onPointerHover( event );

				}

			}

			function intersectObjects( pointer, objects ) {

				var rect = domElement.getBoundingClientRect();
				var x = ( pointer.clientX - rect.left ) / rect.width;
				var y = ( pointer.clientY - rect.top ) / rect.height;

				pointerVector.set( ( x * 2 ) - 1, - ( y * 2 ) + 1 );
				ray.setFromCamera( pointerVector, camera );

				var intersections = ray.intersectObjects( objects, true );
				return intersections[ 0 ] ? intersections[ 0 ] : false;

			}

		};

		THREE.TransformControls.prototype = Object.create( THREE.Object3D.prototype );
		THREE.TransformControls.prototype.constructor = THREE.TransformControls;

	}() );

	/**
	 * @author qiao / https://github.com/qiao
	 * @author mrdoob / http://mrdoob.com
	 * @author alteredq / http://alteredqualia.com/
	 * @author WestLangley / http://github.com/WestLangley
	 * @author erich666 / http://erichaines.com
	 */

	// This set of controls performs orbiting, dollying (zooming), and panning.
	// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
	//
	//    Orbit - left mouse / touch: one-finger move
	//    Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
	//    Pan - right mouse, or arrow keys / touch: two-finger move

	THREE.OrbitControls = function ( object, domElement ) {

		this.object = object;

		this.domElement = ( domElement !== undefined ) ? domElement : document;

		// Set to false to disable this control
		this.enabled = true;

		// "target" sets the location of focus, where the object orbits around
		this.target = new THREE.Vector3();

		// How far you can dolly in and out ( PerspectiveCamera only )
		this.minDistance = 0;
		this.maxDistance = Infinity;

		// How far you can zoom in and out ( OrthographicCamera only )
		this.minZoom = 0;
		this.maxZoom = Infinity;

		// How far you can orbit vertically, upper and lower limits.
		// Range is 0 to Math.PI radians.
		this.minPolarAngle = 0; // radians
		this.maxPolarAngle = Math.PI; // radians

		// How far you can orbit horizontally, upper and lower limits.
		// If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
		this.minAzimuthAngle = - Infinity; // radians
		this.maxAzimuthAngle = Infinity; // radians

		// Set to true to enable damping (inertia)
		// If damping is enabled, you must call controls.update() in your animation loop
		this.enableDamping = false;
		this.dampingFactor = 0.25;

		// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
		// Set to false to disable zooming
		this.enableZoom = true;
		this.zoomSpeed = 1.0;

		// Set to false to disable rotating
		this.enableRotate = true;
		this.rotateSpeed = 1.0;

		// Set to false to disable panning
		this.enablePan = true;
		this.panSpeed = 1.0;
		this.screenSpacePanning = false; // if true, pan in screen-space
		this.keyPanSpeed = 7.0;	// pixels moved per arrow key push

		// Set to true to automatically rotate around the target
		// If auto-rotate is enabled, you must call controls.update() in your animation loop
		this.autoRotate = false;
		this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60

		// Set to false to disable use of the keys
		this.enableKeys = true;

		// The four arrow keys
		this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };

		// Mouse buttons
		this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT };

		// for reset
		this.target0 = this.target.clone();
		this.position0 = this.object.position.clone();
		this.zoom0 = this.object.zoom;

		//
		// public methods
		//

		this.getPolarAngle = function () {

			return spherical.phi;

		};

		this.getAzimuthalAngle = function () {

			return spherical.theta;

		};

		this.saveState = function () {

			scope.target0.copy( scope.target );
			scope.position0.copy( scope.object.position );
			scope.zoom0 = scope.object.zoom;

		};

		this.reset = function () {

			scope.target.copy( scope.target0 );
			scope.object.position.copy( scope.position0 );
			scope.object.zoom = scope.zoom0;

			scope.object.updateProjectionMatrix();
			scope.dispatchEvent( changeEvent );

			scope.update();

			state = STATE.NONE;

		};

		// this method is exposed, but perhaps it would be better if we can make it private...
		this.update = function () {

			var offset = new THREE.Vector3();

			// so camera.up is the orbit axis
			var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
			var quatInverse = quat.clone().inverse();

			var lastPosition = new THREE.Vector3();
			var lastQuaternion = new THREE.Quaternion();

			return function update() {

				var position = scope.object.position;

				offset.copy( position ).sub( scope.target );

				// rotate offset to "y-axis-is-up" space
				offset.applyQuaternion( quat );

				// angle from z-axis around y-axis
				spherical.setFromVector3( offset );

				if ( scope.autoRotate && state === STATE.NONE ) {

					rotateLeft( getAutoRotationAngle() );

				}

				spherical.theta += sphericalDelta.theta;
				spherical.phi += sphericalDelta.phi;

				// restrict theta to be between desired limits
				spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) );

				// restrict phi to be between desired limits
				spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );

				spherical.makeSafe();


				spherical.radius *= scale;

				// restrict radius to be between desired limits
				spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );

				// move target to panned location
				scope.target.add( panOffset );

				offset.setFromSpherical( spherical );

				// rotate offset back to "camera-up-vector-is-up" space
				offset.applyQuaternion( quatInverse );

				position.copy( scope.target ).add( offset );

				scope.object.lookAt( scope.target );

				if ( scope.enableDamping === true ) {

					sphericalDelta.theta *= ( 1 - scope.dampingFactor );
					sphericalDelta.phi *= ( 1 - scope.dampingFactor );

					panOffset.multiplyScalar( 1 - scope.dampingFactor );

				} else {

					sphericalDelta.set( 0, 0, 0 );

					panOffset.set( 0, 0, 0 );

				}

				scale = 1;

				// update condition is:
				// min(camera displacement, camera rotation in radians)^2 > EPS
				// using small-angle approximation cos(x/2) = 1 - x^2 / 8

				if ( zoomChanged ||
					lastPosition.distanceToSquared( scope.object.position ) > EPS ||
					8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {

					scope.dispatchEvent( changeEvent );

					lastPosition.copy( scope.object.position );
					lastQuaternion.copy( scope.object.quaternion );
					zoomChanged = false;

					return true;

				}

				return false;

			};

		}();

		this.dispose = function () {

			scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false );
			scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
			scope.domElement.removeEventListener( 'wheel', onMouseWheel, false );

			scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
			scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
			scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );

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

			window.removeEventListener( 'keydown', onKeyDown, false );

			//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?

		};

		//
		// internals
		//

		var scope = this;

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

		var STATE = { NONE: - 1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY_PAN: 4 };

		var state = STATE.NONE;

		var EPS = 0.000001;

		// current position in spherical coordinates
		var spherical = new THREE.Spherical();
		var sphericalDelta = new THREE.Spherical();

		var scale = 1;
		var panOffset = new THREE.Vector3();
		var zoomChanged = false;

		var rotateStart = new THREE.Vector2();
		var rotateEnd = new THREE.Vector2();
		var rotateDelta = new THREE.Vector2();

		var panStart = new THREE.Vector2();
		var panEnd = new THREE.Vector2();
		var panDelta = new THREE.Vector2();

		var dollyStart = new THREE.Vector2();
		var dollyEnd = new THREE.Vector2();
		var dollyDelta = new THREE.Vector2();

		function getAutoRotationAngle() {

			return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;

		}

		function getZoomScale() {

			return Math.pow( 0.95, scope.zoomSpeed );

		}

		function rotateLeft( angle ) {

			sphericalDelta.theta -= angle;

		}

		function rotateUp( angle ) {

			sphericalDelta.phi -= angle;

		}

		var panLeft = function () {

			var v = new THREE.Vector3();

			return function panLeft( distance, objectMatrix ) {

				v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
				v.multiplyScalar( - distance );

				panOffset.add( v );

			};

		}();

		var panUp = function () {

			var v = new THREE.Vector3();

			return function panUp( distance, objectMatrix ) {

				if ( scope.screenSpacePanning === true ) {

					v.setFromMatrixColumn( objectMatrix, 1 );

				} else {

					v.setFromMatrixColumn( objectMatrix, 0 );
					v.crossVectors( scope.object.up, v );

				}

				v.multiplyScalar( distance );

				panOffset.add( v );

			};

		}();

		// deltaX and deltaY are in pixels; right and down are positive
		var pan = function () {

			var offset = new THREE.Vector3();

			return function pan( deltaX, deltaY ) {

				var element = scope.domElement === document ? scope.domElement.body : scope.domElement;

				if ( scope.object.isPerspectiveCamera ) {

					// perspective
					var position = scope.object.position;
					offset.copy( position ).sub( scope.target );
					var targetDistance = offset.length();

					// half of the fov is center to top of screen
					targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );

					// we use only clientHeight here so aspect ratio does not distort speed
					panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
					panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );

				} else if ( scope.object.isOrthographicCamera ) {

					// orthographic
					panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
					panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );

				} else {

					// camera neither orthographic nor perspective
					console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
					scope.enablePan = false;

				}

			};

		}();

		function dollyIn( dollyScale ) {

			if ( scope.object.isPerspectiveCamera ) {

				scale /= dollyScale;

			} else if ( scope.object.isOrthographicCamera ) {

				scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
				scope.object.updateProjectionMatrix();
				zoomChanged = true;

			} else {

				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
				scope.enableZoom = false;

			}

		}

		function dollyOut( dollyScale ) {

			if ( scope.object.isPerspectiveCamera ) {

				scale *= dollyScale;

			} else if ( scope.object.isOrthographicCamera ) {

				scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
				scope.object.updateProjectionMatrix();
				zoomChanged = true;

			} else {

				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
				scope.enableZoom = false;

			}

		}

		//
		// event callbacks - update the object state
		//

		function handleMouseDownRotate( event ) {

			//console.log( 'handleMouseDownRotate' );

			rotateStart.set( event.clientX, event.clientY );

		}

		function handleMouseDownDolly( event ) {

			//console.log( 'handleMouseDownDolly' );

			dollyStart.set( event.clientX, event.clientY );

		}

		function handleMouseDownPan( event ) {

			//console.log( 'handleMouseDownPan' );

			panStart.set( event.clientX, event.clientY );

		}

		function handleMouseMoveRotate( event ) {

			//console.log( 'handleMouseMoveRotate' );

			rotateEnd.set( event.clientX, event.clientY );

			rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );

			var element = scope.domElement === document ? scope.domElement.body : scope.domElement;

			rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height

			rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );

			rotateStart.copy( rotateEnd );

			scope.update();

		}

		function handleMouseMoveDolly( event ) {

			//console.log( 'handleMouseMoveDolly' );

			dollyEnd.set( event.clientX, event.clientY );

			dollyDelta.subVectors( dollyEnd, dollyStart );

			if ( dollyDelta.y > 0 ) {

				dollyIn( getZoomScale() );

			} else if ( dollyDelta.y < 0 ) {

				dollyOut( getZoomScale() );

			}

			dollyStart.copy( dollyEnd );

			scope.update();

		}

		function handleMouseMovePan( event ) {

			//console.log( 'handleMouseMovePan' );

			panEnd.set( event.clientX, event.clientY );

			panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );

			pan( panDelta.x, panDelta.y );

			panStart.copy( panEnd );

			scope.update();

		}

		function handleMouseWheel( event ) {

			// console.log( 'handleMouseWheel' );

			if ( event.deltaY < 0 ) {

				dollyOut( getZoomScale() );

			} else if ( event.deltaY > 0 ) {

				dollyIn( getZoomScale() );

			}

			scope.update();

		}

		function handleKeyDown( event ) {

			//console.log( 'handleKeyDown' );

			switch ( event.keyCode ) {

				case scope.keys.UP:
					pan( 0, scope.keyPanSpeed );
					scope.update();
					break;

				case scope.keys.BOTTOM:
					pan( 0, - scope.keyPanSpeed );
					scope.update();
					break;

				case scope.keys.LEFT:
					pan( scope.keyPanSpeed, 0 );
					scope.update();
					break;

				case scope.keys.RIGHT:
					pan( - scope.keyPanSpeed, 0 );
					scope.update();
					break;

			}

		}

		function handleTouchStartRotate( event ) {

			//console.log( 'handleTouchStartRotate' );

			rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );

		}

		function handleTouchStartDollyPan( event ) {

			//console.log( 'handleTouchStartDollyPan' );

			if ( scope.enableZoom ) {

				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;

				var distance = Math.sqrt( dx * dx + dy * dy );

				dollyStart.set( 0, distance );

			}

			if ( scope.enablePan ) {

				var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
				var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );

				panStart.set( x, y );

			}

		}

		function handleTouchMoveRotate( event ) {

			//console.log( 'handleTouchMoveRotate' );

			rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );

			rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );

			var element = scope.domElement === document ? scope.domElement.body : scope.domElement;

			rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height

			rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );

			rotateStart.copy( rotateEnd );

			scope.update();

		}

		function handleTouchMoveDollyPan( event ) {

			//console.log( 'handleTouchMoveDollyPan' );

			if ( scope.enableZoom ) {

				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;

				var distance = Math.sqrt( dx * dx + dy * dy );

				dollyEnd.set( 0, distance );

				dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );

				dollyIn( dollyDelta.y );

				dollyStart.copy( dollyEnd );

			}

			if ( scope.enablePan ) {

				var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
				var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );

				panEnd.set( x, y );

				panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );

				pan( panDelta.x, panDelta.y );

				panStart.copy( panEnd );

			}

			scope.update();

		}

		//
		// event handlers - FSM: listen for events and reset state
		//

		function onMouseDown( event ) {

			if ( scope.enabled === false ) return;

			event.preventDefault();

			switch ( event.button ) {

				case scope.mouseButtons.ORBIT:

					if ( scope.enableRotate === false ) return;

					handleMouseDownRotate( event );

					state = STATE.ROTATE;

					break;

				case scope.mouseButtons.ZOOM:

					if ( scope.enableZoom === false ) return;

					handleMouseDownDolly( event );

					state = STATE.DOLLY;

					break;

				case scope.mouseButtons.PAN:

					if ( scope.enablePan === false ) return;

					handleMouseDownPan( event );

					state = STATE.PAN;

					break;

			}

			if ( state !== STATE.NONE ) {

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

				scope.dispatchEvent( startEvent );

			}

		}

		function onMouseMove( event ) {

			if ( scope.enabled === false ) return;

			event.preventDefault();

			switch ( state ) {

				case STATE.ROTATE:

					if ( scope.enableRotate === false ) return;

					handleMouseMoveRotate( event );

					break;

				case STATE.DOLLY:

					if ( scope.enableZoom === false ) return;

					handleMouseMoveDolly( event );

					break;

				case STATE.PAN:

					if ( scope.enablePan === false ) return;

					handleMouseMovePan( event );

					break;

			}

		}

		function onMouseUp( event ) {

			if ( scope.enabled === false ) return;

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

			scope.dispatchEvent( endEvent );

			state = STATE.NONE;

		}

		function onMouseWheel( event ) {

			if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return;

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

			scope.dispatchEvent( startEvent );

			handleMouseWheel( event );

			scope.dispatchEvent( endEvent );

		}

		function onKeyDown( event ) {

			if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return;

			handleKeyDown( event );

		}

		function onTouchStart( event ) {

			if ( scope.enabled === false ) return;

			event.preventDefault();

			switch ( event.touches.length ) {

				case 1:	// one-fingered touch: rotate

					if ( scope.enableRotate === false ) return;

					handleTouchStartRotate( event );

					state = STATE.TOUCH_ROTATE;

					break;

				case 2:	// two-fingered touch: dolly-pan

					if ( scope.enableZoom === false && scope.enablePan === false ) return;

					handleTouchStartDollyPan( event );

					state = STATE.TOUCH_DOLLY_PAN;

					break;

				default:

					state = STATE.NONE;

			}

			if ( state !== STATE.NONE ) {

				scope.dispatchEvent( startEvent );

			}

		}

		function onTouchMove( event ) {

			if ( scope.enabled === false ) return;

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

			switch ( event.touches.length ) {

				case 1: // one-fingered touch: rotate

					if ( scope.enableRotate === false ) return;
					if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?

					handleTouchMoveRotate( event );

					break;

				case 2: // two-fingered touch: dolly-pan

					if ( scope.enableZoom === false && scope.enablePan === false ) return;
					if ( state !== STATE.TOUCH_DOLLY_PAN ) return; // is this needed?

					handleTouchMoveDollyPan( event );

					break;

				default:

					state = STATE.NONE;

			}

		}

		function onTouchEnd( event ) {

			if ( scope.enabled === false ) return;

			scope.dispatchEvent( endEvent );

			state = STATE.NONE;

		}

		function onContextMenu( event ) {

			if ( scope.enabled === false ) return;

			event.preventDefault();

		}

		//

		scope.domElement.addEventListener( 'contextmenu', onContextMenu, false );

		scope.domElement.addEventListener( 'mousedown', onMouseDown, false );
		scope.domElement.addEventListener( 'wheel', onMouseWheel, false );

		scope.domElement.addEventListener( 'touchstart', onTouchStart, false );
		scope.domElement.addEventListener( 'touchend', onTouchEnd, false );
		scope.domElement.addEventListener( 'touchmove', onTouchMove, false );

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

		// force an update at start

		this.update();

	};

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

	Object.defineProperties( THREE.OrbitControls.prototype, {

		center: {

			get: function () {

				console.warn( 'THREE.OrbitControls: .center has been renamed to .target' );
				return this.target;

			}

		},

		// backward compatibility

		noZoom: {

			get: function () {

				console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
				return ! this.enableZoom;

			},

			set: function ( value ) {

				console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' );
				this.enableZoom = ! value;

			}

		},

		noRotate: {

			get: function () {

				console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
				return ! this.enableRotate;

			},

			set: function ( value ) {

				console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' );
				this.enableRotate = ! value;

			}

		},

		noPan: {

			get: function () {

				console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
				return ! this.enablePan;

			},

			set: function ( value ) {

				console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' );
				this.enablePan = ! value;

			}

		},

		noKeys: {

			get: function () {

				console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
				return ! this.enableKeys;

			},

			set: function ( value ) {

				console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' );
				this.enableKeys = ! value;

			}

		},

		staticMoving: {

			get: function () {

				console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
				return ! this.enableDamping;

			},

			set: function ( value ) {

				console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' );
				this.enableDamping = ! value;

			}

		},

		dynamicDampingFactor: {

			get: function () {

				console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
				return this.dampingFactor;

			},

			set: function ( value ) {

				console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' );
				this.dampingFactor = value;

			}

		}

	} );

	/**
	 * @author Eric Haines / http://erichaines.com/
	 *
	 * Tessellates the famous Utah teapot database by Martin Newell into triangles.
	 *
	 * THREE.TeapotBufferGeometry = function ( size, segments, bottom, lid, body, fitLid, blinn )
	 *
	 * defaults: size = 50, segments = 10, bottom = true, lid = true, body = true,
	 *   fitLid = false, blinn = true
	 *
	 * size is a relative scale: I've scaled the teapot to fit vertically between -1 and 1.
	 * Think of it as a "radius".
	 * segments - number of line segments to subdivide each patch edge;
	 *   1 is possible but gives degenerates, so two is the real minimum.
	 * bottom - boolean, if true (default) then the bottom patches are added. Some consider
	 *   adding the bottom heresy, so set this to "false" to adhere to the One True Way.
	 * lid - to remove the lid and look inside, set to true.
	 * body - to remove the body and leave the lid, set this and "bottom" to false.
	 * fitLid - the lid is a tad small in the original. This stretches it a bit so you can't
	 *   see the teapot's insides through the gap.
	 * blinn - Jim Blinn scaled the original data vertically by dividing by about 1.3 to look
	 *   nicer. If you want to see the original teapot, similar to the real-world model, set
	 *   this to false. True by default.
	 *   See http://en.wikipedia.org/wiki/File:Original_Utah_Teapot.jpg for the original
	 *   real-world teapot (from http://en.wikipedia.org/wiki/Utah_teapot).
	 *
	 * Note that the bottom (the last four patches) is not flat - blame Frank Crow, not me.
	 *
	 * The teapot should normally be rendered as a double sided object, since for some
	 * patches both sides can be seen, e.g., the gap around the lid and inside the spout.
	 *
	 * Segments 'n' determines the number of triangles output.
	 *   Total triangles = 32*2*n*n - 8*n    [degenerates at the top and bottom cusps are deleted]
	 *
	 *   size_factor   # triangles
	 *       1          56
	 *       2         240
	 *       3         552
	 *       4         992
	 *
	 *      10        6320
	 *      20       25440
	 *      30       57360
	 *
	 * Code converted from my ancient SPD software, http://tog.acm.org/resources/SPD/
	 * Created for the Udacity course "Interactive Rendering", http://bit.ly/ericity
	 * Lesson: https://www.udacity.com/course/viewer#!/c-cs291/l-68866048/m-106482448
	 * YouTube video on teapot history: https://www.youtube.com/watch?v=DxMfblPzFNc
	 *
	 * See https://en.wikipedia.org/wiki/Utah_teapot for the history of the teapot
	 *
	 */
	/*global THREE */

	THREE.TeapotBufferGeometry = function ( size, segments, bottom, lid, body, fitLid, blinn ) {

		// 32 * 4 * 4 Bezier spline patches
		var teapotPatches = [
			/*rim*/
			0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
			3, 16, 17, 18, 7, 19, 20, 21, 11, 22, 23, 24, 15, 25, 26, 27,
			18, 28, 29, 30, 21, 31, 32, 33, 24, 34, 35, 36, 27, 37, 38, 39,
			30, 40, 41, 0, 33, 42, 43, 4, 36, 44, 45, 8, 39, 46, 47, 12,
			/*body*/
			12, 13, 14, 15, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
			15, 25, 26, 27, 51, 60, 61, 62, 55, 63, 64, 65, 59, 66, 67, 68,
			27, 37, 38, 39, 62, 69, 70, 71, 65, 72, 73, 74, 68, 75, 76, 77,
			39, 46, 47, 12, 71, 78, 79, 48, 74, 80, 81, 52, 77, 82, 83, 56,
			56, 57, 58, 59, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
			59, 66, 67, 68, 87, 96, 97, 98, 91, 99, 100, 101, 95, 102, 103, 104,
			68, 75, 76, 77, 98, 105, 106, 107, 101, 108, 109, 110, 104, 111, 112, 113,
			77, 82, 83, 56, 107, 114, 115, 84, 110, 116, 117, 88, 113, 118, 119, 92,
			/*handle*/
			120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135,
			123, 136, 137, 120, 127, 138, 139, 124, 131, 140, 141, 128, 135, 142, 143, 132,
			132, 133, 134, 135, 144, 145, 146, 147, 148, 149, 150, 151, 68, 152, 153, 154,
			135, 142, 143, 132, 147, 155, 156, 144, 151, 157, 158, 148, 154, 159, 160, 68,
			/*spout*/
			161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176,
			164, 177, 178, 161, 168, 179, 180, 165, 172, 181, 182, 169, 176, 183, 184, 173,
			173, 174, 175, 176, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196,
			176, 183, 184, 173, 188, 197, 198, 185, 192, 199, 200, 189, 196, 201, 202, 193,
			/*lid*/
			203, 203, 203, 203, 204, 205, 206, 207, 208, 208, 208, 208, 209, 210, 211, 212,
			203, 203, 203, 203, 207, 213, 214, 215, 208, 208, 208, 208, 212, 216, 217, 218,
			203, 203, 203, 203, 215, 219, 220, 221, 208, 208, 208, 208, 218, 222, 223, 224,
			203, 203, 203, 203, 221, 225, 226, 204, 208, 208, 208, 208, 224, 227, 228, 209,
			209, 210, 211, 212, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240,
			212, 216, 217, 218, 232, 241, 242, 243, 236, 244, 245, 246, 240, 247, 248, 249,
			218, 222, 223, 224, 243, 250, 251, 252, 246, 253, 254, 255, 249, 256, 257, 258,
			224, 227, 228, 209, 252, 259, 260, 229, 255, 261, 262, 233, 258, 263, 264, 237,
			/*bottom*/
			265, 265, 265, 265, 266, 267, 268, 269, 270, 271, 272, 273, 92, 119, 118, 113,
			265, 265, 265, 265, 269, 274, 275, 276, 273, 277, 278, 279, 113, 112, 111, 104,
			265, 265, 265, 265, 276, 280, 281, 282, 279, 283, 284, 285, 104, 103, 102, 95,
			265, 265, 265, 265, 282, 286, 287, 266, 285, 288, 289, 270, 95, 94, 93, 92
		];

		var teapotVertices = [
			1.4, 0, 2.4,
			1.4, - 0.784, 2.4,
			0.784, - 1.4, 2.4,
			0, - 1.4, 2.4,
			1.3375, 0, 2.53125,
			1.3375, - 0.749, 2.53125,
			0.749, - 1.3375, 2.53125,
			0, - 1.3375, 2.53125,
			1.4375, 0, 2.53125,
			1.4375, - 0.805, 2.53125,
			0.805, - 1.4375, 2.53125,
			0, - 1.4375, 2.53125,
			1.5, 0, 2.4,
			1.5, - 0.84, 2.4,
			0.84, - 1.5, 2.4,
			0, - 1.5, 2.4,
			- 0.784, - 1.4, 2.4,
			- 1.4, - 0.784, 2.4,
			- 1.4, 0, 2.4,
			- 0.749, - 1.3375, 2.53125,
			- 1.3375, - 0.749, 2.53125,
			- 1.3375, 0, 2.53125,
			- 0.805, - 1.4375, 2.53125,
			- 1.4375, - 0.805, 2.53125,
			- 1.4375, 0, 2.53125,
			- 0.84, - 1.5, 2.4,
			- 1.5, - 0.84, 2.4,
			- 1.5, 0, 2.4,
			- 1.4, 0.784, 2.4,
			- 0.784, 1.4, 2.4,
			0, 1.4, 2.4,
			- 1.3375, 0.749, 2.53125,
			- 0.749, 1.3375, 2.53125,
			0, 1.3375, 2.53125,
			- 1.4375, 0.805, 2.53125,
			- 0.805, 1.4375, 2.53125,
			0, 1.4375, 2.53125,
			- 1.5, 0.84, 2.4,
			- 0.84, 1.5, 2.4,
			0, 1.5, 2.4,
			0.784, 1.4, 2.4,
			1.4, 0.784, 2.4,
			0.749, 1.3375, 2.53125,
			1.3375, 0.749, 2.53125,
			0.805, 1.4375, 2.53125,
			1.4375, 0.805, 2.53125,
			0.84, 1.5, 2.4,
			1.5, 0.84, 2.4,
			1.75, 0, 1.875,
			1.75, - 0.98, 1.875,
			0.98, - 1.75, 1.875,
			0, - 1.75, 1.875,
			2, 0, 1.35,
			2, - 1.12, 1.35,
			1.12, - 2, 1.35,
			0, - 2, 1.35,
			2, 0, 0.9,
			2, - 1.12, 0.9,
			1.12, - 2, 0.9,
			0, - 2, 0.9,
			- 0.98, - 1.75, 1.875,
			- 1.75, - 0.98, 1.875,
			- 1.75, 0, 1.875,
			- 1.12, - 2, 1.35,
			- 2, - 1.12, 1.35,
			- 2, 0, 1.35,
			- 1.12, - 2, 0.9,
			- 2, - 1.12, 0.9,
			- 2, 0, 0.9,
			- 1.75, 0.98, 1.875,
			- 0.98, 1.75, 1.875,
			0, 1.75, 1.875,
			- 2, 1.12, 1.35,
			- 1.12, 2, 1.35,
			0, 2, 1.35,
			- 2, 1.12, 0.9,
			- 1.12, 2, 0.9,
			0, 2, 0.9,
			0.98, 1.75, 1.875,
			1.75, 0.98, 1.875,
			1.12, 2, 1.35,
			2, 1.12, 1.35,
			1.12, 2, 0.9,
			2, 1.12, 0.9,
			2, 0, 0.45,
			2, - 1.12, 0.45,
			1.12, - 2, 0.45,
			0, - 2, 0.45,
			1.5, 0, 0.225,
			1.5, - 0.84, 0.225,
			0.84, - 1.5, 0.225,
			0, - 1.5, 0.225,
			1.5, 0, 0.15,
			1.5, - 0.84, 0.15,
			0.84, - 1.5, 0.15,
			0, - 1.5, 0.15,
			- 1.12, - 2, 0.45,
			- 2, - 1.12, 0.45,
			- 2, 0, 0.45,
			- 0.84, - 1.5, 0.225,
			- 1.5, - 0.84, 0.225,
			- 1.5, 0, 0.225,
			- 0.84, - 1.5, 0.15,
			- 1.5, - 0.84, 0.15,
			- 1.5, 0, 0.15,
			- 2, 1.12, 0.45,
			- 1.12, 2, 0.45,
			0, 2, 0.45,
			- 1.5, 0.84, 0.225,
			- 0.84, 1.5, 0.225,
			0, 1.5, 0.225,
			- 1.5, 0.84, 0.15,
			- 0.84, 1.5, 0.15,
			0, 1.5, 0.15,
			1.12, 2, 0.45,
			2, 1.12, 0.45,
			0.84, 1.5, 0.225,
			1.5, 0.84, 0.225,
			0.84, 1.5, 0.15,
			1.5, 0.84, 0.15,
			- 1.6, 0, 2.025,
			- 1.6, - 0.3, 2.025,
			- 1.5, - 0.3, 2.25,
			- 1.5, 0, 2.25,
			- 2.3, 0, 2.025,
			- 2.3, - 0.3, 2.025,
			- 2.5, - 0.3, 2.25,
			- 2.5, 0, 2.25,
			- 2.7, 0, 2.025,
			- 2.7, - 0.3, 2.025,
			- 3, - 0.3, 2.25,
			- 3, 0, 2.25,
			- 2.7, 0, 1.8,
			- 2.7, - 0.3, 1.8,
			- 3, - 0.3, 1.8,
			- 3, 0, 1.8,
			- 1.5, 0.3, 2.25,
			- 1.6, 0.3, 2.025,
			- 2.5, 0.3, 2.25,
			- 2.3, 0.3, 2.025,
			- 3, 0.3, 2.25,
			- 2.7, 0.3, 2.025,
			- 3, 0.3, 1.8,
			- 2.7, 0.3, 1.8,
			- 2.7, 0, 1.575,
			- 2.7, - 0.3, 1.575,
			- 3, - 0.3, 1.35,
			- 3, 0, 1.35,
			- 2.5, 0, 1.125,
			- 2.5, - 0.3, 1.125,
			- 2.65, - 0.3, 0.9375,
			- 2.65, 0, 0.9375,
			- 2, - 0.3, 0.9,
			- 1.9, - 0.3, 0.6,
			- 1.9, 0, 0.6,
			- 3, 0.3, 1.35,
			- 2.7, 0.3, 1.575,
			- 2.65, 0.3, 0.9375,
			- 2.5, 0.3, 1.125,
			- 1.9, 0.3, 0.6,
			- 2, 0.3, 0.9,
			1.7, 0, 1.425,
			1.7, - 0.66, 1.425,
			1.7, - 0.66, 0.6,
			1.7, 0, 0.6,
			2.6, 0, 1.425,
			2.6, - 0.66, 1.425,
			3.1, - 0.66, 0.825,
			3.1, 0, 0.825,
			2.3, 0, 2.1,
			2.3, - 0.25, 2.1,
			2.4, - 0.25, 2.025,
			2.4, 0, 2.025,
			2.7, 0, 2.4,
			2.7, - 0.25, 2.4,
			3.3, - 0.25, 2.4,
			3.3, 0, 2.4,
			1.7, 0.66, 0.6,
			1.7, 0.66, 1.425,
			3.1, 0.66, 0.825,
			2.6, 0.66, 1.425,
			2.4, 0.25, 2.025,
			2.3, 0.25, 2.1,
			3.3, 0.25, 2.4,
			2.7, 0.25, 2.4,
			2.8, 0, 2.475,
			2.8, - 0.25, 2.475,
			3.525, - 0.25, 2.49375,
			3.525, 0, 2.49375,
			2.9, 0, 2.475,
			2.9, - 0.15, 2.475,
			3.45, - 0.15, 2.5125,
			3.45, 0, 2.5125,
			2.8, 0, 2.4,
			2.8, - 0.15, 2.4,
			3.2, - 0.15, 2.4,
			3.2, 0, 2.4,
			3.525, 0.25, 2.49375,
			2.8, 0.25, 2.475,
			3.45, 0.15, 2.5125,
			2.9, 0.15, 2.475,
			3.2, 0.15, 2.4,
			2.8, 0.15, 2.4,
			0, 0, 3.15,
			0.8, 0, 3.15,
			0.8, - 0.45, 3.15,
			0.45, - 0.8, 3.15,
			0, - 0.8, 3.15,
			0, 0, 2.85,
			0.2, 0, 2.7,
			0.2, - 0.112, 2.7,
			0.112, - 0.2, 2.7,
			0, - 0.2, 2.7,
			- 0.45, - 0.8, 3.15,
			- 0.8, - 0.45, 3.15,
			- 0.8, 0, 3.15,
			- 0.112, - 0.2, 2.7,
			- 0.2, - 0.112, 2.7,
			- 0.2, 0, 2.7,
			- 0.8, 0.45, 3.15,
			- 0.45, 0.8, 3.15,
			0, 0.8, 3.15,
			- 0.2, 0.112, 2.7,
			- 0.112, 0.2, 2.7,
			0, 0.2, 2.7,
			0.45, 0.8, 3.15,
			0.8, 0.45, 3.15,
			0.112, 0.2, 2.7,
			0.2, 0.112, 2.7,
			0.4, 0, 2.55,
			0.4, - 0.224, 2.55,
			0.224, - 0.4, 2.55,
			0, - 0.4, 2.55,
			1.3, 0, 2.55,
			1.3, - 0.728, 2.55,
			0.728, - 1.3, 2.55,
			0, - 1.3, 2.55,
			1.3, 0, 2.4,
			1.3, - 0.728, 2.4,
			0.728, - 1.3, 2.4,
			0, - 1.3, 2.4,
			- 0.224, - 0.4, 2.55,
			- 0.4, - 0.224, 2.55,
			- 0.4, 0, 2.55,
			- 0.728, - 1.3, 2.55,
			- 1.3, - 0.728, 2.55,
			- 1.3, 0, 2.55,
			- 0.728, - 1.3, 2.4,
			- 1.3, - 0.728, 2.4,
			- 1.3, 0, 2.4,
			- 0.4, 0.224, 2.55,
			- 0.224, 0.4, 2.55,
			0, 0.4, 2.55,
			- 1.3, 0.728, 2.55,
			- 0.728, 1.3, 2.55,
			0, 1.3, 2.55,
			- 1.3, 0.728, 2.4,
			- 0.728, 1.3, 2.4,
			0, 1.3, 2.4,
			0.224, 0.4, 2.55,
			0.4, 0.224, 2.55,
			0.728, 1.3, 2.55,
			1.3, 0.728, 2.55,
			0.728, 1.3, 2.4,
			1.3, 0.728, 2.4,
			0, 0, 0,
			1.425, 0, 0,
			1.425, 0.798, 0,
			0.798, 1.425, 0,
			0, 1.425, 0,
			1.5, 0, 0.075,
			1.5, 0.84, 0.075,
			0.84, 1.5, 0.075,
			0, 1.5, 0.075,
			- 0.798, 1.425, 0,
			- 1.425, 0.798, 0,
			- 1.425, 0, 0,
			- 0.84, 1.5, 0.075,
			- 1.5, 0.84, 0.075,
			- 1.5, 0, 0.075,
			- 1.425, - 0.798, 0,
			- 0.798, - 1.425, 0,
			0, - 1.425, 0,
			- 1.5, - 0.84, 0.075,
			- 0.84, - 1.5, 0.075,
			0, - 1.5, 0.075,
			0.798, - 1.425, 0,
			1.425, - 0.798, 0,
			0.84, - 1.5, 0.075,
			1.5, - 0.84, 0.075
		];

		THREE.BufferGeometry.call( this );

		size = size || 50;

		// number of segments per patch
		segments = segments !== undefined ? Math.max( 2, Math.floor( segments ) || 10 ) : 10;

		// which parts should be visible
		bottom = bottom === undefined ? true : bottom;
		lid = lid === undefined ? true : lid;
		body = body === undefined ? true : body;

		// Should the lid be snug? It's not traditional, but we make it snug by default
		fitLid = fitLid === undefined ? true : fitLid;

		// Jim Blinn scaled the teapot down in size by about 1.3 for
		// some rendering tests. He liked the new proportions that he kept
		// the data in this form. The model was distributed with these new
		// proportions and became the norm. Trivia: comparing images of the
		// real teapot and the computer model, the ratio for the bowl of the
		// real teapot is more like 1.25, but since 1.3 is the traditional
		// value given, we use it here.
		var blinnScale = 1.3;
		blinn = blinn === undefined ? true : blinn;

		// scale the size to be the real scaling factor
		var maxHeight = 3.15 * ( blinn ? 1 : blinnScale );

		var maxHeight2 = maxHeight / 2;
		var trueSize = size / maxHeight2;

		// Number of elements depends on what is needed. Subtract degenerate
		// triangles at tip of bottom and lid out in advance.
		var numTriangles = bottom ? ( 8 * segments - 4 ) * segments : 0;
		numTriangles += lid ? ( 16 * segments - 4 ) * segments : 0;
		numTriangles += body ? 40 * segments * segments : 0;

		var indices = new Uint32Array( numTriangles * 3 );

		var numVertices = bottom ? 4 : 0;
		numVertices += lid ? 8 : 0;
		numVertices += body ? 20 : 0;
		numVertices *= ( segments + 1 ) * ( segments + 1 );

		var vertices = new Float32Array( numVertices * 3 );
		var normals = new Float32Array( numVertices * 3 );
		var uvs = new Float32Array( numVertices * 2 );

		// Bezier form
		var ms = new THREE.Matrix4();
		ms.set(
			- 1.0, 3.0, - 3.0, 1.0,
			3.0, - 6.0, 3.0, 0.0,
			- 3.0, 3.0, 0.0, 0.0,
			1.0, 0.0, 0.0, 0.0 );

		var g = [];
		var i, r, c;

		var sp = [];
		var tp = [];
		var dsp = [];
		var dtp = [];

		// M * G * M matrix, sort of see
		// http://www.cs.helsinki.fi/group/goa/mallinnus/curves/surfaces.html
		var mgm = [];

		var vert = [];
		var sdir = [];
		var tdir = [];

		var norm = new THREE.Vector3();

		var tcoord;

		var sstep, tstep;
		var vertPerRow;

		var s, t, sval, tval, p;
		var dsval = 0;
		var dtval = 0;

		var normOut = new THREE.Vector3();
		var v1, v2, v3, v4;

		var gmx = new THREE.Matrix4();
		var tmtx = new THREE.Matrix4();

		var vsp = new THREE.Vector4();
		var vtp = new THREE.Vector4();
		var vdsp = new THREE.Vector4();
		var vdtp = new THREE.Vector4();

		var vsdir = new THREE.Vector3();
		var vtdir = new THREE.Vector3();

		var mst = ms.clone();
		mst.transpose();

		// internal function: test if triangle has any matching vertices;
		// if so, don't save triangle, since it won't display anything.
		var notDegenerate = function ( vtx1, vtx2, vtx3 ) {

			// if any vertex matches, return false
			return ! ( ( ( vertices[ vtx1 * 3 ] === vertices[ vtx2 * 3 ] ) &&
						 ( vertices[ vtx1 * 3 + 1 ] === vertices[ vtx2 * 3 + 1 ] ) &&
						 ( vertices[ vtx1 * 3 + 2 ] === vertices[ vtx2 * 3 + 2 ] ) ) ||
					   ( ( vertices[ vtx1 * 3 ] === vertices[ vtx3 * 3 ] ) &&
						 ( vertices[ vtx1 * 3 + 1 ] === vertices[ vtx3 * 3 + 1 ] ) &&
						 ( vertices[ vtx1 * 3 + 2 ] === vertices[ vtx3 * 3 + 2 ] ) ) ||
					   ( ( vertices[ vtx2 * 3 ] === vertices[ vtx3 * 3 ] ) &&
						 ( vertices[ vtx2 * 3 + 1 ] === vertices[ vtx3 * 3 + 1 ] ) &&
						 ( vertices[ vtx2 * 3 + 2 ] === vertices[ vtx3 * 3 + 2 ] ) ) );

		};


		for ( i = 0; i < 3; i ++ ) {

			mgm[ i ] = new THREE.Matrix4();

		}

		var minPatches = body ? 0 : 20;
		var maxPatches = bottom ? 32 : 28;

		vertPerRow = segments + 1;

		var surfCount = 0;

		var vertCount = 0;
		var normCount = 0;
		var uvCount = 0;

		var indexCount = 0;

		for ( var surf = minPatches; surf < maxPatches; surf ++ ) {

			// lid is in the middle of the data, patches 20-27,
			// so ignore it for this part of the loop if the lid is not desired
			if ( lid || ( surf < 20 || surf >= 28 ) ) {

				// get M * G * M matrix for x,y,z
				for ( i = 0; i < 3; i ++ ) {

					// get control patches
					for ( r = 0; r < 4; r ++ ) {

						for ( c = 0; c < 4; c ++ ) {

							// transposed
							g[ c * 4 + r ] = teapotVertices[ teapotPatches[ surf * 16 + r * 4 + c ] * 3 + i ];

							// is the lid to be made larger, and is this a point on the lid
							// that is X or Y?
							if ( fitLid && ( surf >= 20 && surf < 28 ) && ( i !== 2 ) ) {

								// increase XY size by 7.7%, found empirically. I don't
								// increase Z so that the teapot will continue to fit in the
								// space -1 to 1 for Y (Y is up for the final model).
								g[ c * 4 + r ] *= 1.077;

							}

							// Blinn "fixed" the teapot by dividing Z by blinnScale, and that's the
							// data we now use. The original teapot is taller. Fix it:
							if ( ! blinn && ( i === 2 ) ) {

								g[ c * 4 + r ] *= blinnScale;

							}

						}

					}

					gmx.set( g[ 0 ], g[ 1 ], g[ 2 ], g[ 3 ], g[ 4 ], g[ 5 ], g[ 6 ], g[ 7 ], g[ 8 ], g[ 9 ], g[ 10 ], g[ 11 ], g[ 12 ], g[ 13 ], g[ 14 ], g[ 15 ] );

					tmtx.multiplyMatrices( gmx, ms );
					mgm[ i ].multiplyMatrices( mst, tmtx );

				}

				// step along, get points, and output
				for ( sstep = 0; sstep <= segments; sstep ++ ) {

					s = sstep / segments;

					for ( tstep = 0; tstep <= segments; tstep ++ ) {

						t = tstep / segments;

						// point from basis
						// get power vectors and their derivatives
						for ( p = 4, sval = tval = 1.0; p --; ) {

							sp[ p ] = sval;
							tp[ p ] = tval;
							sval *= s;
							tval *= t;

							if ( p === 3 ) {

								dsp[ p ] = dtp[ p ] = 0.0;
								dsval = dtval = 1.0;

							} else {

								dsp[ p ] = dsval * ( 3 - p );
								dtp[ p ] = dtval * ( 3 - p );
								dsval *= s;
								dtval *= t;

							}

						}

						vsp.fromArray( sp );
						vtp.fromArray( tp );
						vdsp.fromArray( dsp );
						vdtp.fromArray( dtp );

						// do for x,y,z
						for ( i = 0; i < 3; i ++ ) {

							// multiply power vectors times matrix to get value
							tcoord = vsp.clone();
							tcoord.applyMatrix4( mgm[ i ] );
							vert[ i ] = tcoord.dot( vtp );

							// get s and t tangent vectors
							tcoord = vdsp.clone();
							tcoord.applyMatrix4( mgm[ i ] );
							sdir[ i ] = tcoord.dot( vtp );

							tcoord = vsp.clone();
							tcoord.applyMatrix4( mgm[ i ] );
							tdir[ i ] = tcoord.dot( vdtp );

						}

						// find normal
						vsdir.fromArray( sdir );
						vtdir.fromArray( tdir );
						norm.crossVectors( vtdir, vsdir );
						norm.normalize();

						// if X and Z length is 0, at the cusp, so point the normal up or down, depending on patch number
						if ( vert[ 0 ] === 0 && vert[ 1 ] === 0 ) {

							// if above the middle of the teapot, normal points up, else down
							normOut.set( 0, vert[ 2 ] > maxHeight2 ? 1 : - 1, 0 );

						} else {

							// standard output: rotate on X axis
							normOut.set( norm.x, norm.z, - norm.y );

						}

						// store it all
						vertices[ vertCount ++ ] = trueSize * vert[ 0 ];
						vertices[ vertCount ++ ] = trueSize * ( vert[ 2 ] - maxHeight2 );
						vertices[ vertCount ++ ] = - trueSize * vert[ 1 ];

						normals[ normCount ++ ] = normOut.x;
						normals[ normCount ++ ] = normOut.y;
						normals[ normCount ++ ] = normOut.z;

						uvs[ uvCount ++ ] = 1 - t;
						uvs[ uvCount ++ ] = 1 - s;

					}

				}

				// save the faces
				for ( sstep = 0; sstep < segments; sstep ++ ) {

					for ( tstep = 0; tstep < segments; tstep ++ ) {

						v1 = surfCount * vertPerRow * vertPerRow + sstep * vertPerRow + tstep;
						v2 = v1 + 1;
						v3 = v2 + vertPerRow;
						v4 = v1 + vertPerRow;

						// Normals and UVs cannot be shared. Without clone(), you can see the consequences
						// of sharing if you call geometry.applyMatrix( matrix ).
						if ( notDegenerate( v1, v2, v3 ) ) {

							indices[ indexCount ++ ] = v1;
							indices[ indexCount ++ ] = v2;
							indices[ indexCount ++ ] = v3;

						}
						if ( notDegenerate( v1, v3, v4 ) ) {

							indices[ indexCount ++ ] = v1;
							indices[ indexCount ++ ] = v3;
							indices[ indexCount ++ ] = v4;

						}

					}

				}

				// increment only if a surface was used
				surfCount ++;

			}

		}

		this.setIndex( new THREE.BufferAttribute( indices, 1 ) );
		this.addAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
		this.addAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );
		this.addAttribute( 'uv', new THREE.BufferAttribute( uvs, 2 ) );

		this.computeBoundingSphere();

	};


	THREE.TeapotBufferGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
	THREE.TeapotBufferGeometry.prototype.constructor = THREE.TeapotBufferGeometry;

	/**
	 * @author yomotsu / http://yomotsu.net
	 * ported from http://webgl-fire.appspot.com/html/fire.html
	 *
	 * https://www.youtube.com/watch?v=jKRHmQmduDI
	 * https://graphics.ethz.ch/teaching/former/imagesynthesis_06/miniprojects/p3/
	 * https://www.iusb.edu/math-compsci/_prior-thesis/YVanzine_thesis.pdf
	 */

	( function ( root, factory ) {

	  if ( typeof define === 'function' && define.amd ) {

	    // AMD. Register as an anonymous module.
	    define( [], factory );

	  } else if ( typeof module === 'object' && module.exports ) {

	    // Node. Does not work with strict CommonJS, but
	    // only CommonJS-like environments that support module.exports,
	    // like Node.
	    module.exports = factory();

	  } else {

	    // Browser globals (root is window)
	    root.VolumetricFire = factory( root );

	  }

	}( window, function () {

	  var vs = [

	    'attribute vec3 position;',
	    'attribute vec3 tex;',
	    'uniform mat4 projectionMatrix;',
	    'uniform mat4 modelViewMatrix;',

	    'varying vec3 texOut;',

	    'void main ( void ) {',

	      'gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0 );',
	      'texOut = tex;',

	    '}',

	  ].join( '\n' );

	  var fs = [

	    'precision highp float;',

	    // Modified Blum Blum Shub pseudo-random number generator.
	    'vec2 mBBS( vec2 val, float modulus ) {',

	      'val = mod( val, modulus ); // For numerical consistancy.',
	      'return mod(val * val, modulus);',

	    '}',

	    // Pregenerated noise texture.
	    'uniform sampler2D nzw;',
	    'const float modulus = 61.0;  // Value used in pregenerated noise texture.',

	    /**
	     * Modified noise function.
	     * @see http://www.csee.umbc.edu/~olano/papers/index.html#mNoise
	     **/
	    'float mnoise ( vec3 pos ) {',

	      'float intArg = floor( pos.z );',
	      'float fracArg = fract( pos.z );',
	      'vec2 hash = mBBS( intArg * 3.0 + vec2( 0, 3 ), modulus );',
	      'vec4 g = vec4 (',
	        'texture2D( nzw, vec2( pos.x, pos.y + hash.x ) / modulus ).xy,',
	        'texture2D( nzw, vec2( pos.x, pos.y + hash.y ) / modulus ).xy',
	      ') * 2.0 - 1.0;',

	      'return mix(',
	        'g.x + g.y * fracArg,',
	        'g.z + g.w * ( fracArg - 1.0 ),',
	        'smoothstep( 0.0, 1.0, fracArg )',
	      ');',

	    '}',

	    'const int octives = 4;',
	    'const float lacunarity = 2.0;',
	    'const float gain = 0.5;',

	    /**
	     * Adds multiple octives of noise together.
	     **/
	    'float turbulence( vec3 pos ) {',

	      'float sum  = 0.0;',
	      'float freq = 1.0;',
	      'float amp  = 1.0;',

	      'for ( int i = 0; i < 4; i++ ) {',

	        'sum += abs( mnoise( pos * freq ) ) * amp;',
	        'freq *= lacunarity;',
	        'amp *= gain;',

	      '}',

	      'return sum;',

	    '}',

	    'const float magnatude = 1.3;',
	    'uniform float time;',
	    'uniform sampler2D fireProfile;',

	    /**
	     * Samples the fire.
	     *
	     * @param loc the normalized location (0.0-1.0) to sample the fire
	     * @param scale the 'size' of the fire in world space and time
	     **/
	    'vec4 sampleFire( vec3 loc, vec4 scale ) {',

	      // Convert xz to [-1.0, 1.0] range.
	      'loc.xz = loc.xz * 2.0 - 1.0;',

	      // Convert to (radius, height) to sample fire profile texture.
	      'vec2 st = vec2( sqrt( dot( loc.xz, loc.xz ) ), loc.y );',

	      // Convert loc to 'noise' space
	      'loc.y -= time * scale.w; // Scrolling noise upwards over time.',
	      'loc *= scale.xyz; // Scaling noise space.',

	      // Offsetting vertial texture lookup.
	      // We scale this by the sqrt of the height so that things are
	      // relatively stable at the base of the fire and volital at the
	      // top.
	      'float offset = sqrt( st.y ) * magnatude * turbulence( loc );',
	      'st.y += offset;',

	      // TODO: Update fireProfile texture to have a black row of pixels.
	      'if ( st.y > 1.0 ) {',

	        'return vec4( 0, 0, 0, 1 );',

	      '}',

	      'vec4 result = texture2D( fireProfile, st );',

	      // Fading out bottom so slice clipping isnt obvious
	      'if ( st.y < 0.1 ) {',

	        'result *= st.y / 0.1;',

	      '}',

	      'return result;',

	    '}',

	    'varying vec3 texOut;',

	    'void main( void ) {',

	      // Mapping texture coordinate to -1 => 1 for xy, 0=> 1 for y
	      'vec3 color = sampleFire( texOut, vec4( 1.0, 2.0, 1.0, 0.5 ) ).xyz;',
	      'gl_FragColor = vec4( color * 1.5, 1 );',

	      'if(color.x < 0.01 && color.y < 0.01 && color.z < 0.01) {',

	      ' discard; ',

	      '}',

	    '}',

	  ].join( '\n' );

	  var initMaterial =  ( function () {

	    var material;
	    var textureLoader = new THREE.TextureLoader();

	    return function () {

	      if ( !!material ) { return material; }

	      // TODO
	      // Canvas2D で noise 画像を作る
	      var nzw = textureLoader.load( VolumetricFire.texturePath + 'nzw.png' );
	      nzw.wrapS = THREE.RepeatWrapping;
	      nzw.wrapT = THREE.RepeatWrapping;
	      nzw.magFilter = THREE.LinearFilter;
	      nzw.minFilter = THREE.LinearFilter;

	      var fireProfile = textureLoader.load( VolumetricFire.texturePath + 'firetex.png' );
	      fireProfile.wrapS = THREE.ClampToEdgeWrapping;
	      fireProfile.wrapT = THREE.ClampToEdgeWrapping;
	      fireProfile.magFilter = THREE.LinearFilter;
	      fireProfile.minFilter = THREE.LinearFilter;

	      var uniforms = {
	        nzw: {
	          type: 't',
	          value: nzw
	        },
	        fireProfile: {
	          type: 't',
	          value: fireProfile
	        },
	        time: {
	          type: 'f',
	          value: 1.0
	        }
	      };

	      material = new THREE.RawShaderMaterial( {
	        vertexShader   : vs,
	        fragmentShader : fs,
	        uniforms       : uniforms,
	        side           : THREE.DoubleSide,
	        blending       : THREE.AdditiveBlending,
	        transparent    : true
	      } );

	      return material;

	    };

	  } )();


	  var cornerNeighbors = [
	    [ 1, 2, 4 ],
	    [ 0, 5, 3 ],
	    [ 0, 3, 6 ],
	    [ 1, 7, 2 ],
	    [ 0, 6, 5 ],
	    [ 1, 4, 7 ],
	    [ 2, 7, 4 ],
	    [ 3, 5, 6 ],
	  ];

	  var incomingEdges = [
	    [ -1,  2,  4, -1,  1, -1, -1, -1 ],
	    [  5, -1, -1,  0, -1,  3, -1, -1 ],
	    [  3, -1, -1,  6, -1, -1,  0, -1 ],
	    [ -1,  7,  1, -1, -1, -1, -1,  2 ],
	    [  6, -1, -1, -1, -1,  0,  5, -1 ],
	    [ -1,  4, -1, -1,  7, -1, -1,  1 ],
	    [ -1, -1,  7, -1,  2, -1, -1,  4 ],
	    [ -1, -1, -1,  5, -1,  6,  3, -1 ],
	  ];

	  var VolumetricFire = function ( width, height, depth, sliceSpacing, camera ) {

	    this.camera = camera;

	    this._sliceSpacing = sliceSpacing;

	    var widthHalf  = width  * 0.5;
	    var heightHalf = height * 0.5;
	    var depthHalf  = depth  * 0.5;

	    this._posCorners = [
	      new THREE.Vector3( -widthHalf, -heightHalf, -depthHalf ),
	      new THREE.Vector3(  widthHalf, -heightHalf, -depthHalf ),
	      new THREE.Vector3( -widthHalf,  heightHalf, -depthHalf ),
	      new THREE.Vector3(  widthHalf,  heightHalf, -depthHalf ),
	      new THREE.Vector3( -widthHalf, -heightHalf,  depthHalf ),
	      new THREE.Vector3(  widthHalf, -heightHalf,  depthHalf ),
	      new THREE.Vector3( -widthHalf,  heightHalf,  depthHalf ),
	      new THREE.Vector3(  widthHalf,  heightHalf,  depthHalf )
	    ];
	    this._texCorners = [
	      new THREE.Vector3( 0, 0, 0 ),
	      new THREE.Vector3( 1, 0, 0 ),
	      new THREE.Vector3( 0, 1, 0 ),
	      new THREE.Vector3( 1, 1, 0 ),
	      new THREE.Vector3( 0, 0, 1 ),
	      new THREE.Vector3( 1, 0, 1 ),
	      new THREE.Vector3( 0, 1, 1 ),
	      new THREE.Vector3( 1, 1, 1 )
	    ];

	    this._viewVector = new THREE.Vector3();


	    // TODO
	    // still did not figure out, how many vertexes should be...
	    // three.jsでは可変にできない、ひとまず多めに用意

	    var index    = new Uint16Array ( ( width + height + depth ) * 30 );
	    var position = new Float32Array( ( width + height + depth ) * 30 * 3 );
	    var tex      = new Float32Array( ( width + height + depth ) * 30 * 3 );

	    var geometry = new THREE.BufferGeometry();
	    geometry.dynamic = true;
	    geometry.setIndex( new THREE.BufferAttribute( index, 1 ) );
	    geometry.addAttribute( 'position', new THREE.BufferAttribute( position, 3 ) );
	    geometry.addAttribute( 'tex',      new THREE.BufferAttribute( tex,      3 ) );

	    var material = initMaterial();

	    this.mesh = new THREE.Mesh(
	      geometry,
	      material
	    );

	    this.mesh.updateMatrixWorld();

	  };

	  VolumetricFire.prototype.update = function ( elapsed ) {

	    this.updateViewVector();
	    this.slice();
	    this.updateGeometry();
	    this.mesh.material.uniforms.time.value = elapsed;

	  };

	  VolumetricFire.prototype.updateGeometry = function () {

	    this.mesh.geometry.index.array.set( this._indexes );
	    this.mesh.geometry.attributes.position.array.set( this._points );
	    this.mesh.geometry.attributes.tex.array.set( this._texCoords );

	    this.mesh.geometry.index.needsUpdate               = true;
	    this.mesh.geometry.attributes.position.needsUpdate = true;
	    this.mesh.geometry.attributes.tex.needsUpdate      = true;

	  };

	  VolumetricFire.prototype.updateViewVector = function () {

	    // TODO
	    // MVMatrixが前回と同じなら、アップデートしないようにする
	    //
	    // つまり、カメラの位置とオブジェクトの位置に変化なければ
	    // アップデートしないようにする

	    var modelViewMatrix = new THREE.Matrix4();

	    modelViewMatrix.multiplyMatrices(
	      this.camera.matrixWorldInverse,
	      this.mesh.matrixWorld
	    );

	    this._viewVector.set(
	      - modelViewMatrix.elements[  2 ],
	      - modelViewMatrix.elements[  6 ],
	      - modelViewMatrix.elements[ 10 ]
	    ).normalize();

	  };

	  VolumetricFire.prototype.slice = function () {

	    this._points    = [];
	    this._texCoords = [];
	    this._indexes   = [];

	    var i;
	    var cornerDistance0 = this._posCorners[ 0 ].dot( this._viewVector );

	    var cornerDistance = [ cornerDistance0 ];
	    var maxCorner = 0;
	    var minDistance = cornerDistance0;
	    var maxDistance = cornerDistance0;

	    for ( i = 1; i < 8; i = ( i + 1 )|0 ) {

	      cornerDistance[ i ] = this._posCorners[ i ].dot( this._viewVector );

	      if ( cornerDistance[ i ] > maxDistance ) {

	        maxCorner = i;
	        maxDistance = cornerDistance[ i ];

	      }

	      if ( cornerDistance[ i ] < minDistance ) {

	        minDistance = cornerDistance[ i ];

	      }

	    }

	    // Aligning slices
	    var sliceDistance = Math.floor( maxDistance / this._sliceSpacing ) * this._sliceSpacing;

	    var activeEdges = [];
	    var firstEdge   = 0;
	    var nextEdge    = 0;
	    var expirations = new PriorityQueue();

	    var createEdge = function ( startIndex, endIndex ) {

	      if ( nextEdge >= 12 ) { return undefined; }

	      var activeEdge = {
	          expired    : false,
	          startIndex : startIndex,
	          endIndex   : endIndex,
	          deltaPos   : new THREE.Vector3(),
	          deltaTex   : new THREE.Vector3(),
	          pos        : new THREE.Vector3(),
	          tex        : new THREE.Vector3(),
	          cur        : nextEdge
	      };

	      var range = cornerDistance[ startIndex ] - cornerDistance[ endIndex ];

	      if ( range !== 0.0 ) {

	        var irange = 1.0 / range;

	        activeEdge.deltaPos.subVectors(
	          this._posCorners[ endIndex ],
	          this._posCorners[ startIndex ]
	        ).multiplyScalar( irange );

	        activeEdge.deltaTex.subVectors(
	          this._texCorners[ endIndex ],
	          this._texCorners[ startIndex ]
	        ).multiplyScalar( irange );

	        var step = cornerDistance[ startIndex ] - sliceDistance;

	        activeEdge.pos.addVectors(
	          activeEdge.deltaPos.clone().multiplyScalar( step ),
	          this._posCorners[ startIndex ]
	        );

	        activeEdge.tex.addVectors(
	          activeEdge.deltaTex.clone().multiplyScalar( step ),
	          this._texCorners[ startIndex ]
	        );

	        activeEdge.deltaPos.multiplyScalar( this._sliceSpacing );
	        activeEdge.deltaTex.multiplyScalar( this._sliceSpacing );

	      }

	      expirations.push( activeEdge, cornerDistance[ endIndex ] );
	      activeEdges[ nextEdge++ ] = activeEdge;
	      return activeEdge;

	    };

	    for ( i = 0; i < 3; i = ( i + 1 )|0 ) {

	      var activeEdge = createEdge.call( this, maxCorner, cornerNeighbors[ maxCorner ][ i ] );
	      activeEdge.prev = ( i + 2 ) % 3;
	      activeEdge.next = ( i + 1 ) % 3;

	    }

	    var nextIndex = 0;

	    while ( sliceDistance > minDistance ) {

	      while ( expirations.top().priority >= sliceDistance ) {

	        var edge = expirations.pop().object;

	        if ( edge.expired ) { continue; }

	        if (
	          edge.endIndex !== activeEdges[ edge.prev ].endIndex &&
	          edge.endIndex !== activeEdges[ edge.next ].endIndex
	        ) {

	          // split this edge.
	          edge.expired = true;

	          // create two new edges.
	          var activeEdge1 = createEdge.call(
	            this,
	            edge.endIndex,
	            incomingEdges[ edge.endIndex ][ edge.startIndex ]
	          );
	          activeEdge1.prev = edge.prev;
	          activeEdges[ edge.prev ].next = nextEdge - 1;
	          activeEdge1.next = nextEdge;

	          var activeEdge2 = createEdge.call(
	            this,
	            edge.endIndex,
	            incomingEdges[ edge.endIndex ][ activeEdge1.endIndex ]
	          );
	          activeEdge2.prev = nextEdge - 2;
	          activeEdge2.next = edge.next;
	          activeEdges[activeEdge2.next].prev = nextEdge - 1;
	          firstEdge = nextEdge - 1;

	        } else {

	          // merge edge.
	          var prev;
	          var next;

	          if ( edge.endIndex === activeEdges[ edge.prev ].endIndex ) {

	            prev = activeEdges[ edge.prev ];
	            next = edge;

	          } else {

	            prev = edge;
	            next = activeEdges[ edge.next ];

	          }

	          prev.expired = true;
	          next.expired = true;

	          // make new edge
	          var activeEdge = createEdge.call(
	            this,
	            edge.endIndex,
	            incomingEdges[ edge.endIndex ][ prev.startIndex ]
	          );
	          activeEdge.prev = prev.prev;
	          activeEdges[ activeEdge.prev ].next = nextEdge - 1;
	          activeEdge.next = next.next;
	          activeEdges[ activeEdge.next ].prev = nextEdge - 1;
	          firstEdge = nextEdge - 1;

	        }

	      }

	      var cur = firstEdge;
	      var count = 0;

	      do {

	        ++count;
	        var activeEdge = activeEdges[ cur ];
	        this._points.push(
	          activeEdge.pos.x,
	          activeEdge.pos.y,
	          activeEdge.pos.z
	        );
	        this._texCoords.push(
	          activeEdge.tex.x,
	          activeEdge.tex.y,
	          activeEdge.tex.z
	        );
	        activeEdge.pos.add( activeEdge.deltaPos );
	        activeEdge.tex.add( activeEdge.deltaTex );
	        cur = activeEdge.next;

	      } while ( cur !== firstEdge );

	      for ( i = 2; i < count; i = ( i + 1 )|0 ) {

	        this._indexes.push(
	          nextIndex,
	          nextIndex + i - 1,
	          nextIndex + i
	        );

	      }

	      nextIndex += count;
	      sliceDistance -= this._sliceSpacing;

	    }

	  };

	  VolumetricFire.texturePath = './textures/';

	  ///

	  var PriorityQueue = function () {

	    this.contents = [];
	    this.sorted = false;

	  };

	  PriorityQueue.prototype = {

	    sort: function () {

	      this.contents.sort();
	      this.sorted = true;

	    },

	    pop: function () {

	      if ( !this.sorted ) {

	        this.sort();

	      }

	      return this.contents.pop();

	    },

	    top : function() {

	      if ( !this.sorted ) {

	        this.sort();

	      }

	      return this.contents[ this.contents.length - 1 ];

	    },

	    push : function( object, priority ) {

	      this.contents.push( { object: object, priority: priority } );
	      this.sorted = false;

	    }

	  };

	  return VolumetricFire;

	} ) );

	/*
	 * @author tamarintech / https://tamarintech.com
	 *
	 * Description: Early release of an AMF Loader following the pattern of the
	 * example loaders in the three.js project.
	 *
	 * More information about the AMF format: http://amf.wikispaces.com
	 *
	 * Usage:
	 *	var loader = new AMFLoader();
	 *	loader.load('/path/to/project.amf', function(objecttree) {
	 *		scene.add(objecttree);
	 *	});
	 *
	 * Materials now supported, material colors supported
	 * Zip support, requires jszip
	 * No constellation support (yet)!
	 *
	 */

	THREE.AMFLoader = function ( manager ) {

		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

	};

	THREE.AMFLoader.prototype = {

		constructor: THREE.AMFLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			var scope = this;

			var loader = new THREE.FileLoader( scope.manager );
			loader.setResponseType( 'arraybuffer' );
			loader.load( url, function ( text ) {

				onLoad( scope.parse( text ) );

			}, onProgress, onError );

		},

		parse: function ( data ) {

			function loadDocument( data ) {

				var view = new DataView( data );
				var magic = String.fromCharCode( view.getUint8( 0 ), view.getUint8( 1 ) );

				if ( magic === 'PK' ) {

					var zip = null;
					var file = null;

					console.log( 'THREE.AMFLoader: Loading Zip' );

					try {

						zip = new JSZip( data ); // eslint-disable-line no-undef

					} catch ( e ) {

						if ( e instanceof ReferenceError ) {

							console.log( 'THREE.AMFLoader: jszip missing and file is compressed.' );
							return null;

						}

					}

					for ( file in zip.files ) {

						if ( file.toLowerCase().substr( - 4 ) === '.amf' ) {

							break;

						}

					}

					console.log( 'THREE.AMFLoader: Trying to load file asset: ' + file );
					view = new DataView( zip.file( file ).asArrayBuffer() );

				}

				var fileText = THREE.LoaderUtils.decodeText( view );
				var xmlData = new DOMParser().parseFromString( fileText, 'application/xml' );

				if ( xmlData.documentElement.nodeName.toLowerCase() !== 'amf' ) {

					console.log( 'THREE.AMFLoader: Error loading AMF - no AMF document found.' );
					return null;

				}

				return xmlData;

			}

			function loadDocumentScale( node ) {

				var scale = 1.0;
				var unit = 'millimeter';

				if ( node.documentElement.attributes.unit !== undefined ) {

					unit = node.documentElement.attributes.unit.value.toLowerCase();

				}

				var scaleUnits = {
					millimeter: 1.0,
					inch: 25.4,
					feet: 304.8,
					meter: 1000.0,
					micron: 0.001
				};

				if ( scaleUnits[ unit ] !== undefined ) {

					scale = scaleUnits[ unit ];

				}

				console.log( 'THREE.AMFLoader: Unit scale: ' + scale );
				return scale;

			}

			function loadMaterials( node ) {

				var matName = 'AMF Material';
				var matId = node.attributes.id.textContent;
				var color = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };

				var loadedMaterial = null;

				for ( var i = 0; i < node.childNodes.length; i ++ ) {

					var matChildEl = node.childNodes[ i ];

					if ( matChildEl.nodeName === 'metadata' && matChildEl.attributes.type !== undefined ) {

						if ( matChildEl.attributes.type.value === 'name' ) {

							matName = matChildEl.textContent;

						}

					} else if ( matChildEl.nodeName === 'color' ) {

						color = loadColor( matChildEl );

					}

				}

				loadedMaterial = new THREE.MeshPhongMaterial( {
					flatShading: true,
					color: new THREE.Color( color.r, color.g, color.b ),
					name: matName
				} );

				if ( color.a !== 1.0 ) {

					loadedMaterial.transparent = true;
					loadedMaterial.opacity = color.a;

				}

				return { id: matId, material: loadedMaterial };

			}

			function loadColor( node ) {

				var color = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };

				for ( var i = 0; i < node.childNodes.length; i ++ ) {

					var matColor = node.childNodes[ i ];

					if ( matColor.nodeName === 'r' ) {

						color.r = matColor.textContent;

					} else if ( matColor.nodeName === 'g' ) {

						color.g = matColor.textContent;

					} else if ( matColor.nodeName === 'b' ) {

						color.b = matColor.textContent;

					} else if ( matColor.nodeName === 'a' ) {

						color.a = matColor.textContent;

					}

				}

				return color;

			}

			function loadMeshVolume( node ) {

				var volume = { name: '', triangles: [], materialid: null };

				var currVolumeNode = node.firstElementChild;

				if ( node.attributes.materialid !== undefined ) {

					volume.materialId = node.attributes.materialid.nodeValue;

				}

				while ( currVolumeNode ) {

					if ( currVolumeNode.nodeName === 'metadata' ) {

						if ( currVolumeNode.attributes.type !== undefined ) {

							if ( currVolumeNode.attributes.type.value === 'name' ) {

								volume.name = currVolumeNode.textContent;

							}

						}

					} else if ( currVolumeNode.nodeName === 'triangle' ) {

						var v1 = currVolumeNode.getElementsByTagName( 'v1' )[ 0 ].textContent;
						var v2 = currVolumeNode.getElementsByTagName( 'v2' )[ 0 ].textContent;
						var v3 = currVolumeNode.getElementsByTagName( 'v3' )[ 0 ].textContent;

						volume.triangles.push( v1, v2, v3 );

					}

					currVolumeNode = currVolumeNode.nextElementSibling;

				}

				return volume;

			}

			function loadMeshVertices( node ) {

				var vertArray = [];
				var normalArray = [];
				var currVerticesNode = node.firstElementChild;

				while ( currVerticesNode ) {

					if ( currVerticesNode.nodeName === 'vertex' ) {

						var vNode = currVerticesNode.firstElementChild;

						while ( vNode ) {

							if ( vNode.nodeName === 'coordinates' ) {

								var x = vNode.getElementsByTagName( 'x' )[ 0 ].textContent;
								var y = vNode.getElementsByTagName( 'y' )[ 0 ].textContent;
								var z = vNode.getElementsByTagName( 'z' )[ 0 ].textContent;

								vertArray.push( x, y, z );

							} else if ( vNode.nodeName === 'normal' ) {

								var nx = vNode.getElementsByTagName( 'nx' )[ 0 ].textContent;
								var ny = vNode.getElementsByTagName( 'ny' )[ 0 ].textContent;
								var nz = vNode.getElementsByTagName( 'nz' )[ 0 ].textContent;

								normalArray.push( nx, ny, nz );

							}

							vNode = vNode.nextElementSibling;

						}

					}
					currVerticesNode = currVerticesNode.nextElementSibling;

				}

				return { 'vertices': vertArray, 'normals': normalArray };

			}

			function loadObject( node ) {

				var objId = node.attributes.id.textContent;
				var loadedObject = { name: 'amfobject', meshes: [] };
				var currColor = null;
				var currObjNode = node.firstElementChild;

				while ( currObjNode ) {

					if ( currObjNode.nodeName === 'metadata' ) {

						if ( currObjNode.attributes.type !== undefined ) {

							if ( currObjNode.attributes.type.value === 'name' ) {

								loadedObject.name = currObjNode.textContent;

							}

						}

					} else if ( currObjNode.nodeName === 'color' ) {

						currColor = loadColor( currObjNode );

					} else if ( currObjNode.nodeName === 'mesh' ) {

						var currMeshNode = currObjNode.firstElementChild;
						var mesh = { vertices: [], normals: [], volumes: [], color: currColor };

						while ( currMeshNode ) {

							if ( currMeshNode.nodeName === 'vertices' ) {

								var loadedVertices = loadMeshVertices( currMeshNode );

								mesh.normals = mesh.normals.concat( loadedVertices.normals );
								mesh.vertices = mesh.vertices.concat( loadedVertices.vertices );

							} else if ( currMeshNode.nodeName === 'volume' ) {

								mesh.volumes.push( loadMeshVolume( currMeshNode ) );

							}

							currMeshNode = currMeshNode.nextElementSibling;

						}

						loadedObject.meshes.push( mesh );

					}

					currObjNode = currObjNode.nextElementSibling;

				}

				return { 'id': objId, 'obj': loadedObject };

			}

			var xmlData = loadDocument( data );
			var amfName = '';
			var amfAuthor = '';
			var amfScale = loadDocumentScale( xmlData );
			var amfMaterials = {};
			var amfObjects = {};
			var childNodes = xmlData.documentElement.childNodes;

			var i, j;

			for ( i = 0; i < childNodes.length; i ++ ) {

				var child = childNodes[ i ];

				if ( child.nodeName === 'metadata' ) {

					if ( child.attributes.type !== undefined ) {

						if ( child.attributes.type.value === 'name' ) {

							amfName = child.textContent;

						} else if ( child.attributes.type.value === 'author' ) {

							amfAuthor = child.textContent;

						}

					}

				} else if ( child.nodeName === 'material' ) {

					var loadedMaterial = loadMaterials( child );

					amfMaterials[ loadedMaterial.id ] = loadedMaterial.material;

				} else if ( child.nodeName === 'object' ) {

					var loadedObject = loadObject( child );

					amfObjects[ loadedObject.id ] = loadedObject.obj;

				}

			}

			var sceneObject = new THREE.Group();
			var defaultMaterial = new THREE.MeshPhongMaterial( { color: 0xaaaaff, flatShading: true } );

			sceneObject.name = amfName;
			sceneObject.userData.author = amfAuthor;
			sceneObject.userData.loader = 'AMF';

			for ( var id in amfObjects ) {

				var part = amfObjects[ id ];
				var meshes = part.meshes;
				var newObject = new THREE.Group();
				newObject.name = part.name || '';

				for ( i = 0; i < meshes.length; i ++ ) {

					var objDefaultMaterial = defaultMaterial;
					var mesh = meshes[ i ];
					var vertices = new THREE.Float32BufferAttribute( mesh.vertices, 3 );
					var normals = null;

					if ( mesh.normals.length ) {

						normals = new THREE.Float32BufferAttribute( mesh.normals, 3 );

					}

					if ( mesh.color ) {

						var color = mesh.color;

						objDefaultMaterial = defaultMaterial.clone();
						objDefaultMaterial.color = new THREE.Color( color.r, color.g, color.b );

						if ( color.a !== 1.0 ) {

							objDefaultMaterial.transparent = true;
							objDefaultMaterial.opacity = color.a;

						}

					}

					var volumes = mesh.volumes;

					for ( j = 0; j < volumes.length; j ++ ) {

						var volume = volumes[ j ];
						var newGeometry = new THREE.BufferGeometry();
						var material = objDefaultMaterial;

						newGeometry.setIndex( volume.triangles );
						newGeometry.addAttribute( 'position', vertices.clone() );

						if ( normals ) {

							newGeometry.addAttribute( 'normal', normals.clone() );

						}

						if ( amfMaterials[ volume.materialId ] !== undefined ) {

							material = amfMaterials[ volume.materialId ];

						}

						newGeometry.scale( amfScale, amfScale, amfScale );
						newObject.add( new THREE.Mesh( newGeometry, material.clone() ) );

					}

				}

				sceneObject.add( newObject );

			}

			return sceneObject;

		}

	};

	/**
	 * Author: Pierre Lepers
	 * Date: 09/12/2013 17:21
	 */

	( function () {

		var AWD_FIELD_INT8 = 1,
			AWD_FIELD_INT16 = 2,
			AWD_FIELD_INT32 = 3,
			AWD_FIELD_UINT8 = 4,
			AWD_FIELD_UINT16 = 5,
			AWD_FIELD_UINT32 = 6,
			AWD_FIELD_FLOAT32 = 7,
			AWD_FIELD_FLOAT64 = 8,
			AWD_FIELD_BOOL = 21,
			AWD_FIELD_BADDR = 23,
			AWD_FIELD_VECTOR2x1 = 41,
			AWD_FIELD_VECTOR3x1 = 42,
			AWD_FIELD_VECTOR4x1 = 43,
			AWD_FIELD_MTX3x2 = 44,
			AWD_FIELD_MTX3x3 = 45,
			AWD_FIELD_MTX4x3 = 46,
			AWD_FIELD_MTX4x4 = 47,

			BOOL = 21,
			BADDR = 23,

			UINT8 = 4,
			UINT16 = 5,
			FLOAT32 = 7,
			FLOAT64 = 8;

		var littleEndian = true;

		function Block() {

			this.id = 0;
			this.data = null;

		}

		function AWDProperties() {}

		AWDProperties.prototype = {
			set: function ( key, value ) {

				this[ key ] = value;

			},

			get: function ( key, fallback ) {

				if ( this.hasOwnProperty( key ) ) {

					return this[ key ];

				} else {

					return fallback;

				}

			}
		};

		THREE.AWDLoader = function ( manager ) {

			this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

			this.trunk = new THREE.Object3D();

			this.materialFactory = undefined;

			this._url = '';
			this._baseDir = '';

			this._data = undefined;
			this._ptr = 0;

			this._version = [];
			this._streaming = false;
			this._optimized_for_accuracy = false;
			this._compression = 0;
			this._bodylen = 0xFFFFFFFF;

			this._blocks = [ new Block() ];

			this._accuracyMatrix = false;
			this._accuracyGeo = false;
			this._accuracyProps = false;

		};

		THREE.AWDLoader.prototype = {

			constructor: THREE.AWDLoader,

			load: function ( url, onLoad, onProgress, onError ) {

				var scope = this;

				this._url = url;
				this._baseDir = url.substr( 0, url.lastIndexOf( '/' ) + 1 );

				var loader = new THREE.FileLoader( this.manager );
				loader.setResponseType( 'arraybuffer' );
				loader.load( url, function ( text ) {

					onLoad( scope.parse( text ) );

				}, onProgress, onError );

			},

			parse: function ( data ) {

				var blen = data.byteLength;

				this._ptr = 0;
				this._data = new DataView( data );

				this._parseHeader( );

				if ( this._compression != 0 ) {

					console.error( 'compressed AWD not supported' );

				}

				if ( ! this._streaming && this._bodylen != data.byteLength - this._ptr ) {

					console.error( 'AWDLoader: body len does not match file length', this._bodylen, blen - this._ptr );

				}

				while ( this._ptr < blen ) {

					this.parseNextBlock();

				}

				return this.trunk;

			},

			parseNextBlock: function () {

				var assetData,
					ns, type, len, block,
					blockId = this.readU32(),
					ns = this.readU8(),
					type = this.readU8(),
					flags = this.readU8(),
					len = this.readU32();


				switch ( type ) {

					case 1:
						assetData = this.parseMeshData( len );
						break;

					case 22:
						assetData = this.parseContainer( len );
						break;

					case 23:
						assetData = this.parseMeshInstance( len );
						break;

					case 81:
						assetData = this.parseMaterial( len );
						break;

					case 82:
						assetData = this.parseTexture( len );
						break;

					case 101:
						assetData = this.parseSkeleton( len );
						break;

		    //  case 111:
		    //    assetData = this.parseMeshPoseAnimation(len, true);
		    //    break;

					case 112:
						assetData = this.parseMeshPoseAnimation( len, false );
						break;

					case 113:
						assetData = this.parseVertexAnimationSet( len );
						break;

					case 102:
						assetData = this.parseSkeletonPose( len );
						break;

					case 103:
						assetData = this.parseSkeletonAnimation( len );
						break;

					case 122:
						assetData = this.parseAnimatorSet( len );
						break;

					// case 121:
					//  assetData = parseUVAnimation(len);
					//  break;

					default:
						//debug('Ignoring block!',type, len);
						this._ptr += len;
						break;

				}


				// Store block reference for later use
				this._blocks[ blockId ] = block = new Block();
				block.data = assetData;
				block.id = blockId;


			},

			_parseHeader: function () {

				var version = this._version,
					awdmagic = ( this.readU8() << 16 ) | ( this.readU8() << 8 ) | this.readU8();

				if ( awdmagic != 4282180 )
					throw new Error( "AWDLoader - bad magic" );

				version[ 0 ] = this.readU8();
				version[ 1 ] = this.readU8();

				var flags = this.readU16();

				this._streaming = ( flags & 0x1 ) == 0x1;

				if ( ( version[ 0 ] === 2 ) && ( version[ 1 ] === 1 ) ) {

					this._accuracyMatrix = ( flags & 0x2 ) === 0x2;
					this._accuracyGeo = ( flags & 0x4 ) === 0x4;
					this._accuracyProps = ( flags & 0x8 ) === 0x8;

				}

				this._geoNrType = this._accuracyGeo ? FLOAT64 : FLOAT32;
				this._matrixNrType = this._accuracyMatrix ? FLOAT64 : FLOAT32;
				this._propsNrType = this._accuracyProps ? FLOAT64 : FLOAT32;

				this._optimized_for_accuracy = ( flags & 0x2 ) === 0x2;

				this._compression = this.readU8();
				this._bodylen = this.readU32();

			},

			parseContainer: function ( len ) {

				var parent,
					ctr = new THREE.Object3D(),
					par_id = this.readU32(),
					mtx = this.parseMatrix4();

				ctr.name = this.readUTF();
				ctr.applyMatrix( mtx );

				parent = this._blocks[ par_id ].data || this.trunk;
				parent.add( ctr );

				this.parseProperties( {
					1: this._matrixNrType,
					2: this._matrixNrType,
					3: this._matrixNrType,
					4: UINT8
				} );

				ctr.extra = this.parseUserAttributes();

				return ctr;

			},

			parseMeshInstance: function ( len ) {

				var name,
					mesh, geometries, meshLen, meshes,
					par_id, data_id,
					mtx,
					materials, mat, mat_id,
					num_materials,
					parent,
					i;

				par_id = this.readU32();
				mtx = this.parseMatrix4();
				name = this.readUTF();
				data_id = this.readU32();
				num_materials = this.readU16();

				geometries = this.getBlock( data_id );

				materials = [];

				for ( i = 0; i < num_materials; i ++ ) {

					mat_id = this.readU32();
					mat = this.getBlock( mat_id );
					materials.push( mat );

				}

				meshLen = geometries.length;
				meshes = [];

				// TODO : BufferGeometry don't support "geometryGroups" for now.
				// so we create sub meshes for each groups
				if ( meshLen > 1 ) {

					mesh = new THREE.Object3D();
					for ( i = 0; i < meshLen; i ++ ) {

						var sm = new THREE.Mesh( geometries[ i ] );
						meshes.push( sm );
						mesh.add( sm );

					}

				} else {

					mesh = new THREE.Mesh( geometries[ 0 ] );
					meshes.push( mesh );

				}

				mesh.applyMatrix( mtx );
				mesh.name = name;


				parent = this.getBlock( par_id ) || this.trunk;
				parent.add( mesh );


				var matLen = materials.length;
				var maxLen = Math.max( meshLen, matLen );
				for ( i = 0; i < maxLen; i ++ )
					meshes[ i % meshLen ].material = materials[ i % matLen ];


				// Ignore for now
				this.parseProperties( null );
				mesh.extra = this.parseUserAttributes();

				return mesh;

			},

			parseMaterial: function ( len ) {

				var name,
					type,
					props,
					mat,
					attributes,
					num_methods,
					methods_parsed;

				name = this.readUTF();
				type = this.readU8();
				num_methods = this.readU8();

				//log( "AWDLoader parseMaterial ",name )

				// Read material numerical properties
				// (1=color, 2=bitmap url, 11=alpha_blending, 12=alpha_threshold, 13=repeat)
				props = this.parseProperties( {
					1: AWD_FIELD_INT32,
					2: AWD_FIELD_BADDR,
					11: AWD_FIELD_BOOL,
					12: AWD_FIELD_FLOAT32,
					13: AWD_FIELD_BOOL
				} );

				methods_parsed = 0;

				while ( methods_parsed < num_methods ) {

					var method_type = this.readU16();
					this.parseProperties( null );
					this.parseUserAttributes();

				}

				attributes = this.parseUserAttributes();

				if ( this.materialFactory !== undefined ) {

					mat = this.materialFactory( name );
					if ( mat ) return mat;

				}

				mat = new THREE.MeshPhongMaterial();

				if ( type === 1 ) {

					// Color material
					mat.color.setHex( props.get( 1, 0xcccccc ) );

				} else if ( type === 2 ) {

					// Bitmap material
					var tex_addr = props.get( 2, 0 );
					mat.map = this.getBlock( tex_addr );

				}

				mat.extra = attributes;
				mat.alphaThreshold = props.get( 12, 0.0 );
				mat.repeat = props.get( 13, false );


				return mat;

			},

			parseTexture: function ( len ) {

				var name = this.readUTF(),
					type = this.readU8(),
					asset,
					data_len;

				// External
				if ( type === 0 ) {

					data_len = this.readU32();
					var url = this.readUTFBytes( data_len );
					console.log( url );

					asset = this.loadTexture( url );

				}
				// Ignore for now
				this.parseProperties( null );

				this.parseUserAttributes();
				return asset;

			},

			loadTexture: function ( url ) {

				var tex = new THREE.Texture();

				var loader = new THREE.ImageLoader( this.manager );

				loader.load( this._baseDir + url, function ( image ) {

					tex.image = image;
					tex.needsUpdate = true;

				} );

				return tex;

			},

			parseSkeleton: function ( len ) {

				// Array<Bone>
				var name = this.readUTF(),
					num_joints = this.readU16(),
					skeleton = [],
					joints_parsed = 0;

				this.parseProperties( null );

				while ( joints_parsed < num_joints ) {

					var joint, ibp;

					// Ignore joint id
					this.readU16();

					joint = new THREE.Bone();
					joint.parent = this.readU16() - 1; // 0=null in AWD
					joint.name = this.readUTF();

					ibp = this.parseMatrix4();
					joint.skinMatrix = ibp;

					// Ignore joint props/attributes for now
					this.parseProperties( null );
					this.parseUserAttributes();

					skeleton.push( joint );
					joints_parsed ++;

				}

				// Discard attributes for now
				this.parseUserAttributes();


				return skeleton;

			},

			parseSkeletonPose: function ( blockID ) {

				var name = this.readUTF();

				var num_joints = this.readU16();
				this.parseProperties( null );

				// debug( 'parse Skeleton Pose. joints : ' + num_joints);

				var pose = [];

				var joints_parsed = 0;

				while ( joints_parsed < num_joints ) {

					var has_transform; //:uint;
					var mtx_data;

					has_transform = this.readU8();

					if ( has_transform === 1 ) {

						mtx_data = this.parseMatrix4();

					} else {

						mtx_data = new THREE.Matrix4();

					}
					pose[ joints_parsed ] = mtx_data;
					joints_parsed ++;

				}

				// Skip attributes for now
				this.parseUserAttributes();

				return pose;

			},

			parseSkeletonAnimation: function ( blockID ) {

				var frame_dur;
				var pose_addr;
				var pose;

				var name = this.readUTF();

				var clip = [];

				var num_frames = this.readU16();
				this.parseProperties( null );

				var frames_parsed = 0;

				// debug( 'parse Skeleton Animation. frames : ' + num_frames);

				while ( frames_parsed < num_frames ) {

					pose_addr = this.readU32();
					frame_dur = this.readU16();

					pose = this._blocks[ pose_addr ].data;
					// debug( 'pose address ',pose[2].elements[12],pose[2].elements[13],pose[2].elements[14] );
					clip.push( {
						pose: pose,
						duration: frame_dur
					} );

					frames_parsed ++;

				}

				if ( clip.length === 0 ) {

					// debug("Could not this SkeletonClipNode, because no Frames where set.");
					return;

				}
				// Ignore attributes for now
				this.parseUserAttributes();
				return clip;

			},

			parseVertexAnimationSet: function ( len ) {

				var poseBlockAdress,
					name = this.readUTF(),
					num_frames = this.readU16(),
					props = this.parseProperties( { 1: UINT16 } ),
					frames_parsed = 0,
					skeletonFrames = [];

				while ( frames_parsed < num_frames ) {

					poseBlockAdress = this.readU32();
					skeletonFrames.push( this._blocks[ poseBlockAdress ].data );
					frames_parsed ++;

				}

				this.parseUserAttributes();


				return skeletonFrames;

			},

			parseAnimatorSet: function ( len ) {

				var animSetBlockAdress; //:int

				var targetAnimationSet; //:AnimationSetBase;
				var name = this.readUTF();
				var type = this.readU16();

				var props = this.parseProperties( { 1: BADDR } );

				animSetBlockAdress = this.readU32();
				var targetMeshLength = this.readU16();

				var meshAdresses = []; //:Vector.<uint> = new Vector.<uint>;

				for ( var i = 0; i < targetMeshLength; i ++ )
					meshAdresses.push( this.readU32() );

				var activeState = this.readU16();
				var autoplay = Boolean( this.readU8() );
				this.parseUserAttributes();
				this.parseUserAttributes();
				var targetMeshes = []; //:Vector.<Mesh> = new Vector.<Mesh>;

				for ( i = 0; i < meshAdresses.length; i ++ ) {

					//      returnedArray = getAssetByID(meshAdresses[i], [AssetType.MESH]);
					//      if (returnedArray[0])
					targetMeshes.push( this._blocks[ meshAdresses[ i ] ].data );

				}

				targetAnimationSet = this._blocks[ animSetBlockAdress ].data;
				var thisAnimator;

				if ( type == 1 ) {


					thisAnimator = {
						animationSet: targetAnimationSet,
						skeleton: this._blocks[ props.get( 1, 0 ) ].data
					};

				}


				for ( i = 0; i < targetMeshes.length; i ++ ) {

					targetMeshes[ i ].animator = thisAnimator;

				}
				// debug("Parsed a Animator: Name = " + name);

				return thisAnimator;

			},

			parseMeshData: function ( len ) {

				var name = this.readUTF(),
					num_subs = this.readU16(),
					geom,
					subs_parsed = 0,
					buffer,
					geometries = [];

				// Ignore for now
				this.parseProperties( { 1: this._geoNrType, 2: this._geoNrType } );

				// Loop through sub meshes
				while ( subs_parsed < num_subs ) {

					var sm_len, sm_end, attrib;

					geom = new THREE.BufferGeometry();
					geom.name = name;
					geometries.push( geom );


					sm_len = this.readU32();
					sm_end = this._ptr + sm_len;


					// Ignore for now
					this.parseProperties( { 1: this._geoNrType, 2: this._geoNrType } );

					// Loop through data streams
					while ( this._ptr < sm_end ) {

						var idx = 0,
							str_type = this.readU8(),
							str_ftype = this.readU8(),
							str_len = this.readU32(),
							str_end = str_len + this._ptr;

						if ( str_type === 1 ) {

							// VERTICES

							buffer = new Float32Array( ( str_len / 12 ) * 3 );
							attrib = new THREE.BufferAttribute( buffer, 3 );

							geom.addAttribute( 'position', attrib );
							idx = 0;

							while ( this._ptr < str_end ) {

								buffer[ idx ] = - this.readF32();
								buffer[ idx + 1 ] = this.readF32();
								buffer[ idx + 2 ] = this.readF32();
								idx += 3;

							}

						} else if ( str_type === 2 ) {

							// INDICES

							buffer = new Uint16Array( str_len / 2 );
							attrib = new THREE.BufferAttribute( buffer, 1 );
							geom.setIndex( attrib );

							idx = 0;

							while ( this._ptr < str_end ) {

								buffer[ idx + 1 ] = this.readU16();
								buffer[ idx ] = this.readU16();
								buffer[ idx + 2 ] = this.readU16();
								idx += 3;

							}

						} else if ( str_type === 3 ) {

							// UVS

							buffer = new Float32Array( ( str_len / 8 ) * 2 );
							attrib = new THREE.BufferAttribute( buffer, 2 );

							geom.addAttribute( 'uv', attrib );
							idx = 0;

							while ( this._ptr < str_end ) {

								buffer[ idx ] = this.readF32();
								buffer[ idx + 1 ] = 1.0 - this.readF32();
								idx += 2;

							}

						} else if ( str_type === 4 ) {

							// NORMALS

							buffer = new Float32Array( ( str_len / 12 ) * 3 );
							attrib = new THREE.BufferAttribute( buffer, 3 );
							geom.addAttribute( 'normal', attrib );
							idx = 0;

							while ( this._ptr < str_end ) {

								buffer[ idx ] = - this.readF32();
								buffer[ idx + 1 ] = this.readF32();
								buffer[ idx + 2 ] = this.readF32();
								idx += 3;

							}

						} else {

							this._ptr = str_end;

						}

					}

					this.parseUserAttributes();

					geom.computeBoundingSphere();
					subs_parsed ++;

				}

				//geom.computeFaceNormals();

				this.parseUserAttributes();
				//finalizeAsset(geom, name);

				return geometries;

			},

			parseMeshPoseAnimation: function ( len, poseOnly ) {

				var num_frames = 1,
					num_submeshes,
					frames_parsed,
					subMeshParsed,
					frame_dur,
					str_len,
					str_end,
					geom,
					idx = 0,
					clip = {},
					num_Streams,
					streamsParsed,
					streamtypes = [],

					props,
					name = this.readUTF(),
					geoAdress = this.readU32();

				var mesh = this.getBlock( geoAdress );

				if ( mesh === null ) {

					console.log( "parseMeshPoseAnimation target mesh not found at:", geoAdress );
					return;

				}

				geom = mesh.geometry;
				geom.morphTargets = [];

				if ( ! poseOnly )
					num_frames = this.readU16();

				num_submeshes = this.readU16();
				num_Streams = this.readU16();

				// debug("VA num_frames : ", num_frames );
				// debug("VA num_submeshes : ", num_submeshes );
				// debug("VA numstreams : ", num_Streams );

				streamsParsed = 0;
				while ( streamsParsed < num_Streams ) {

					streamtypes.push( this.readU16() );
					streamsParsed ++;

				}
				props = this.parseProperties( { 1: BOOL, 2: BOOL } );

				clip.looping = props.get( 1, true );
				clip.stitchFinalFrame = props.get( 2, false );

				frames_parsed = 0;

				while ( frames_parsed < num_frames ) {

					frame_dur = this.readU16();
					subMeshParsed = 0;

					while ( subMeshParsed < num_submeshes ) {

						streamsParsed = 0;
						str_len = this.readU32();
						str_end = this._ptr + str_len;

						while ( streamsParsed < num_Streams ) {

							if ( streamtypes[ streamsParsed ] === 1 ) {

								//geom.addAttribute( 'morphTarget'+frames_parsed, Float32Array, str_len/12, 3 );
								var buffer = new Float32Array( str_len / 4 );
								geom.morphTargets.push( {
									array: buffer
								} );

								//buffer = geom.attributes['morphTarget'+frames_parsed].array
								idx = 0;

								while ( this._ptr < str_end ) {

									buffer[ idx ] = this.readF32();
									buffer[ idx + 1 ] = this.readF32();
									buffer[ idx + 2 ] = this.readF32();
									idx += 3;

								}


								subMeshParsed ++;

							} else
								this._ptr = str_end;
							streamsParsed ++;

						}

					}


					frames_parsed ++;

				}

				this.parseUserAttributes();

				return null;

			},

			getBlock: function ( id ) {

				return this._blocks[ id ].data;

			},

			parseMatrix4: function () {

				var mtx = new THREE.Matrix4();
				var e = mtx.elements;

				e[ 0 ] = this.readF32();
				e[ 1 ] = this.readF32();
				e[ 2 ] = this.readF32();
				e[ 3 ] = 0.0;
				//e[3] = 0.0;

				e[ 4 ] = this.readF32();
				e[ 5 ] = this.readF32();
				e[ 6 ] = this.readF32();
				//e[7] = this.readF32();
				e[ 7 ] = 0.0;

				e[ 8 ] = this.readF32();
				e[ 9 ] = this.readF32();
				e[ 10 ] = this.readF32();
				//e[11] = this.readF32();
				e[ 11 ] = 0.0;

				e[ 12 ] = - this.readF32();
				e[ 13 ] = this.readF32();
				e[ 14 ] = this.readF32();
				//e[15] = this.readF32();
				e[ 15 ] = 1.0;
				return mtx;

			},

			parseProperties: function ( expected ) {

				var list_len = this.readU32();
				var list_end = this._ptr + list_len;

				var props = new AWDProperties();

				if ( expected ) {

					while ( this._ptr < list_end ) {

						var key = this.readU16();
						var len = this.readU32();
						var type;

						if ( expected.hasOwnProperty( key ) ) {

							type = expected[ key ];
							props.set( key, this.parseAttrValue( type, len ) );

						} else {

							this._ptr += len;

						}

					}

				}

				return props;

			},

			parseUserAttributes: function () {

				// skip for now
				this._ptr = this.readU32() + this._ptr;
				return null;

			},

			parseAttrValue: function ( type, len ) {

				var elem_len;
				var read_func;

				switch ( type ) {

					case AWD_FIELD_INT8:
						elem_len = 1;
						read_func = this.readI8;
						break;

					case AWD_FIELD_INT16:
						elem_len = 2;
						read_func = this.readI16;
						break;

					case AWD_FIELD_INT32:
						elem_len = 4;
						read_func = this.readI32;
						break;

					case AWD_FIELD_BOOL:
					case AWD_FIELD_UINT8:
						elem_len = 1;
						read_func = this.readU8;
						break;

					case AWD_FIELD_UINT16:
						elem_len = 2;
						read_func = this.readU16;
						break;

					case AWD_FIELD_UINT32:
					case AWD_FIELD_BADDR:
						elem_len = 4;
						read_func = this.readU32;
						break;

					case AWD_FIELD_FLOAT32:
						elem_len = 4;
						read_func = this.readF32;
						break;

					case AWD_FIELD_FLOAT64:
						elem_len = 8;
						read_func = this.readF64;
						break;

					case AWD_FIELD_VECTOR2x1:
					case AWD_FIELD_VECTOR3x1:
					case AWD_FIELD_VECTOR4x1:
					case AWD_FIELD_MTX3x2:
					case AWD_FIELD_MTX3x3:
					case AWD_FIELD_MTX4x3:
					case AWD_FIELD_MTX4x4:
						elem_len = 8;
						read_func = this.readF64;
						break;

				}

				if ( elem_len < len ) {

					var list;
					var num_read;
					var num_elems;

					list = [];
					num_read = 0;
					num_elems = len / elem_len;

					while ( num_read < num_elems ) {

						list.push( read_func.call( this ) );
						num_read ++;

					}

					return list;

				} else {

					return read_func.call( this );

				}

			},

			readU8: function () {

				return this._data.getUint8( this._ptr ++ );

			},
			readI8: function () {

				return this._data.getInt8( this._ptr ++ );

			},
			readU16: function () {

				var a = this._data.getUint16( this._ptr, littleEndian );
				this._ptr += 2;
				return a;

			},
			readI16: function () {

				var a = this._data.getInt16( this._ptr, littleEndian );
				this._ptr += 2;
				return a;

			},
			readU32: function () {

				var a = this._data.getUint32( this._ptr, littleEndian );
				this._ptr += 4;
				return a;

			},
			readI32: function () {

				var a = this._data.getInt32( this._ptr, littleEndian );
				this._ptr += 4;
				return a;

			},
			readF32: function () {

				var a = this._data.getFloat32( this._ptr, littleEndian );
				this._ptr += 4;
				return a;

			},
			readF64: function () {

				var a = this._data.getFloat64( this._ptr, littleEndian );
				this._ptr += 8;
				return a;

			},

			/**
		 * Converts a UTF-8 byte array to JavaScript's 16-bit Unicode.
		 * @param {Array.<number>} bytes UTF-8 byte array.
		 * @return {string} 16-bit Unicode string.
		 */
			readUTF: function () {

				var len = this.readU16();
				return this.readUTFBytes( len );

			},

			/**
			 * Converts a UTF-8 byte array to JavaScript's 16-bit Unicode.
			 * @param {Array.<number>} bytes UTF-8 byte array.
			 * @return {string} 16-bit Unicode string.
			 */
			readUTFBytes: function ( len ) {

				// TODO(user): Use native implementations if/when available
				var out = [], c = 0;

				while ( out.length < len ) {

					var c1 = this._data.getUint8( this._ptr ++, littleEndian );
					if ( c1 < 128 ) {

						out[ c ++ ] = String.fromCharCode( c1 );

					} else if ( c1 > 191 && c1 < 224 ) {

						var c2 = this._data.getUint8( this._ptr ++, littleEndian );
						out[ c ++ ] = String.fromCharCode( ( c1 & 31 ) << 6 | c2 & 63 );

					} else {

						var c2 = this._data.getUint8( this._ptr ++, littleEndian );
						var c3 = this._data.getUint8( this._ptr ++, littleEndian );
						out[ c ++ ] = String.fromCharCode( ( c1 & 15 ) << 12 | ( c2 & 63 ) << 6 | c3 & 63 );

					}

				}
				return out.join( '' );

			}

		};

	} )();

	/**
	 * @author mrdoob / http://mrdoob.com/
	 * @author Mugen87 / https://github.com/Mugen87
	 */

	THREE.BabylonLoader = function ( manager ) {

		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

	};

	THREE.BabylonLoader.prototype = {

		constructor: THREE.BabylonLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			var scope = this;

			var loader = new THREE.FileLoader( scope.manager );
			loader.load( url, function ( text ) {

				onLoad( scope.parse( JSON.parse( text ) ) );

			}, onProgress, onError );

		},

		parse: function ( json ) {

			function parseMaterials( json ) {

				var materials = {};

				for ( var i = 0, l = json.materials.length; i < l; i ++ ) {

					var data = json.materials[ i ];

					var material = new THREE.MeshPhongMaterial();
					material.name = data.name;
					material.color.fromArray( data.diffuse );
					material.emissive.fromArray( data.emissive );
					material.specular.fromArray( data.specular );
					material.shininess = data.specularPower;
					material.opacity = data.alpha;

					materials[ data.id ] = material;

				}

				if ( json.multiMaterials ) {

					for ( var i = 0, l = json.multiMaterials.length; i < l; i ++ ) {

						var data = json.multiMaterials[ i ];

						console.warn( 'THREE.BabylonLoader: Multi materials not yet supported.' );

						materials[ data.id ] = new THREE.MeshPhongMaterial();

					}

				}

				return materials;

			}

			function parseGeometry( json ) {

				var geometry = new THREE.BufferGeometry();

				var indices = json.indices;
				var positions = json.positions;
				var normals = json.normals;
				var uvs = json.uvs;

				// indices

				geometry.setIndex( indices );

				// positions

				for ( var j = 2, jl = positions.length; j < jl; j += 3 ) {

					positions[ j ] = - positions[ j ];

				}

				geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );

				// normals

				if ( normals ) {

					for ( var j = 2, jl = normals.length; j < jl; j += 3 ) {

						normals[ j ] = - normals[ j ];

					}

					geometry.addAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) );

				}

				// uvs

				if ( uvs ) {

					geometry.addAttribute( 'uv', new THREE.Float32BufferAttribute( uvs, 2 ) );

				}

				// offsets

				var subMeshes = json.subMeshes;

				if ( subMeshes ) {

					for ( var j = 0, jl = subMeshes.length; j < jl; j ++ ) {

						var subMesh = subMeshes[ j ];

						geometry.addGroup( subMesh.indexStart, subMesh.indexCount );

					}

				}

				return geometry;

			}

			function parseObjects( json, materials ) {

				var objects = {};
				var scene = new THREE.Scene();

				var cameras = json.cameras;

				for ( var i = 0, l = cameras.length; i < l; i ++ ) {

					var data = cameras[ i ];

					var camera = new THREE.PerspectiveCamera( ( data.fov / Math.PI ) * 180, 1.33, data.minZ, data.maxZ );

					camera.name = data.name;
					camera.position.fromArray( data.position );
					if ( data.rotation ) camera.rotation.fromArray( data.rotation );

					objects[ data.id ] = camera;

				}

				var lights = json.lights;

				for ( var i = 0, l = lights.length; i < l; i ++ ) {

					var data = lights[ i ];

					var light;

					switch ( data.type ) {

						case 0:
							light = new THREE.PointLight();
							break;

						case 1:
							light = new THREE.DirectionalLight();
							break;

						case 2:
							light = new THREE.SpotLight();
							break;

						case 3:
							light = new THREE.HemisphereLight();
							break;

					}

					light.name = data.name;
					if ( data.position ) light.position.set( data.position[ 0 ], data.position[ 1 ], - data.position[ 2 ] );
					light.color.fromArray( data.diffuse );
					if ( data.groundColor ) light.groundColor.fromArray( data.groundColor );
					if ( data.intensity ) light.intensity = data.intensity;

					objects[ data.id ] = light;

					scene.add( light );

				}

				var meshes = json.meshes;

				for ( var i = 0, l = meshes.length; i < l; i ++ ) {

					var data = meshes[ i ];

					var object;

					if ( data.indices ) {

						var geometry = parseGeometry( data );

						object = new THREE.Mesh( geometry, materials[ data.materialId ] );

					} else {

						object = new THREE.Group();

					}

					object.name = data.name;
					object.position.set( data.position[ 0 ], data.position[ 1 ], - data.position[ 2 ] );
					object.rotation.fromArray( data.rotation );
					if ( data.rotationQuaternion ) object.quaternion.fromArray( data.rotationQuaternion );
					object.scale.fromArray( data.scaling );
					// object.visible = data.isVisible;

					if ( data.parentId ) {

						objects[ data.parentId ].add( object );

					} else {

						scene.add( object );

					}

					objects[ data.id ] = object;

				}

				return scene;

			}

			var materials = parseMaterials( json );
			var scene = parseObjects( json, materials );

			return scene;

		}

	};

	/**
	 * @author alteredq / http://alteredqualia.com/
	 */

	THREE.BinaryLoader = function ( manager ) {

		if ( typeof manager === 'boolean' ) {

			console.warn( 'THREE.BinaryLoader: showStatus parameter has been removed from constructor.' );
			manager = undefined;

		}

		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

	};

	THREE.BinaryLoader.prototype = {

		constructor: THREE.BinaryLoader,

		crossOrigin: 'Anonymous',

		// Load models generated by slim OBJ converter with BINARY option (converter_obj_three_slim.py -t binary)
		//  - binary models consist of two files: JS and BIN
		//  - parameters
		//		- url (required)
		//		- callback (required)
		//		- texturePath (optional: if not specified, textures will be assumed to be in the same folder as JS model file)
		//		- binaryPath (optional: if not specified, binary file will be assumed to be in the same folder as JS model file)
		load: function ( url, onLoad, onProgress, onError ) {

			// todo: unify load API to for easier SceneLoader use

			var texturePath = this.texturePath || THREE.LoaderUtils.extractUrlBase( url );
			var binaryPath = this.binaryPath || THREE.LoaderUtils.extractUrlBase( url );

			// #1 load JS part via web worker

			var scope = this;

			var jsonloader = new THREE.FileLoader( this.manager );
			jsonloader.load( url, function ( data ) {

				var json = JSON.parse( data );

				var bufferUrl = binaryPath + json.buffers;

				var bufferLoader = new THREE.FileLoader( scope.manager );
				bufferLoader.setResponseType( 'arraybuffer' );
				bufferLoader.load( bufferUrl, function ( bufData ) {

					// IEWEBGL needs this ???
					//buffer = ( new Uint8Array( xhr.responseBody ) ).buffer;

					//// iOS and other XMLHttpRequest level 1 ???

					scope.parse( bufData, onLoad, texturePath, json.materials );

				}, onProgress, onError );

			}, onProgress, onError );

		},

		setBinaryPath: function ( value ) {

			this.binaryPath = value;

		},

		setCrossOrigin: function ( value ) {

			this.crossOrigin = value;

		},

		setTexturePath: function ( value ) {

			this.texturePath = value;

		},

		parse: function ( data, callback, texturePath, jsonMaterials ) {

			var Model = function () {

				var scope = this,
					currentOffset = 0,
					md,
					normals = [],
					uvs = [],
					start_tri_flat, start_tri_smooth, start_tri_flat_uv, start_tri_smooth_uv,
					start_quad_flat, start_quad_smooth, start_quad_flat_uv, start_quad_smooth_uv,
					tri_size, quad_size,
					len_tri_flat, len_tri_smooth, len_tri_flat_uv, len_tri_smooth_uv,
					len_quad_flat, len_quad_smooth, len_quad_flat_uv;


				THREE.Geometry.call( this );

				md = parseMetaData( data, currentOffset );

				currentOffset += md.header_bytes;
				/*
						md.vertex_index_bytes = Uint32Array.BYTES_PER_ELEMENT;
						md.material_index_bytes = Uint16Array.BYTES_PER_ELEMENT;
						md.normal_index_bytes = Uint32Array.BYTES_PER_ELEMENT;
						md.uv_index_bytes = Uint32Array.BYTES_PER_ELEMENT;
				*/
				// buffers sizes

				tri_size = md.vertex_index_bytes * 3 + md.material_index_bytes;
				quad_size = md.vertex_index_bytes * 4 + md.material_index_bytes;

				len_tri_flat = md.ntri_flat * ( tri_size );
				len_tri_smooth = md.ntri_smooth * ( tri_size + md.normal_index_bytes * 3 );
				len_tri_flat_uv = md.ntri_flat_uv * ( tri_size + md.uv_index_bytes * 3 );
				len_tri_smooth_uv = md.ntri_smooth_uv * ( tri_size + md.normal_index_bytes * 3 + md.uv_index_bytes * 3 );

				len_quad_flat = md.nquad_flat * ( quad_size );
				len_quad_smooth = md.nquad_smooth * ( quad_size + md.normal_index_bytes * 4 );
				len_quad_flat_uv = md.nquad_flat_uv * ( quad_size + md.uv_index_bytes * 4 );

				// read buffers

				currentOffset += init_vertices( currentOffset );

				currentOffset += init_normals( currentOffset );
				currentOffset += handlePadding( md.nnormals * 3 );

				currentOffset += init_uvs( currentOffset );

				start_tri_flat = currentOffset;
				start_tri_smooth = start_tri_flat + len_tri_flat + handlePadding( md.ntri_flat * 2 );
				start_tri_flat_uv = start_tri_smooth + len_tri_smooth + handlePadding( md.ntri_smooth * 2 );
				start_tri_smooth_uv = start_tri_flat_uv + len_tri_flat_uv + handlePadding( md.ntri_flat_uv * 2 );

				start_quad_flat = start_tri_smooth_uv + len_tri_smooth_uv + handlePadding( md.ntri_smooth_uv * 2 );
				start_quad_smooth = start_quad_flat + len_quad_flat	+ handlePadding( md.nquad_flat * 2 );
				start_quad_flat_uv = start_quad_smooth + len_quad_smooth + handlePadding( md.nquad_smooth * 2 );
				start_quad_smooth_uv = start_quad_flat_uv + len_quad_flat_uv + handlePadding( md.nquad_flat_uv * 2 );

				// have to first process faces with uvs
				// so that face and uv indices match

				init_triangles_flat_uv( start_tri_flat_uv );
				init_triangles_smooth_uv( start_tri_smooth_uv );

				init_quads_flat_uv( start_quad_flat_uv );
				init_quads_smooth_uv( start_quad_smooth_uv );

				// now we can process untextured faces

				init_triangles_flat( start_tri_flat );
				init_triangles_smooth( start_tri_smooth );

				init_quads_flat( start_quad_flat );
				init_quads_smooth( start_quad_smooth );

				this.computeFaceNormals();

				function handlePadding( n ) {

					return ( n % 4 ) ? ( 4 - n % 4 ) : 0;

				}

				function parseMetaData( data, offset ) {

					var metaData = {

						'signature': parseString( data, offset, 12 ),
						'header_bytes': parseUChar8( data, offset + 12 ),

						'vertex_coordinate_bytes': parseUChar8( data, offset + 13 ),
						'normal_coordinate_bytes': parseUChar8( data, offset + 14 ),
						'uv_coordinate_bytes': parseUChar8( data, offset + 15 ),

						'vertex_index_bytes': parseUChar8( data, offset + 16 ),
						'normal_index_bytes': parseUChar8( data, offset + 17 ),
						'uv_index_bytes': parseUChar8( data, offset + 18 ),
						'material_index_bytes': parseUChar8( data, offset + 19 ),

						'nvertices': parseUInt32( data, offset + 20 ),
						'nnormals': parseUInt32( data, offset + 20 + 4 * 1 ),
						'nuvs': parseUInt32( data, offset + 20 + 4 * 2 ),

						'ntri_flat': parseUInt32( data, offset + 20 + 4 * 3 ),
						'ntri_smooth': parseUInt32( data, offset + 20 + 4 * 4 ),
						'ntri_flat_uv': parseUInt32( data, offset + 20 + 4 * 5 ),
						'ntri_smooth_uv': parseUInt32( data, offset + 20 + 4 * 6 ),

						'nquad_flat': parseUInt32( data, offset + 20 + 4 * 7 ),
						'nquad_smooth': parseUInt32( data, offset + 20 + 4 * 8 ),
						'nquad_flat_uv': parseUInt32( data, offset + 20 + 4 * 9 ),
						'nquad_smooth_uv': parseUInt32( data, offset + 20 + 4 * 10 )

					};
					/*
								console.log( "signature: " + metaData.signature );

								console.log( "header_bytes: " + metaData.header_bytes );
								console.log( "vertex_coordinate_bytes: " + metaData.vertex_coordinate_bytes );
								console.log( "normal_coordinate_bytes: " + metaData.normal_coordinate_bytes );
								console.log( "uv_coordinate_bytes: " + metaData.uv_coordinate_bytes );

								console.log( "vertex_index_bytes: " + metaData.vertex_index_bytes );
								console.log( "normal_index_bytes: " + metaData.normal_index_bytes );
								console.log( "uv_index_bytes: " + metaData.uv_index_bytes );
								console.log( "material_index_bytes: " + metaData.material_index_bytes );

								console.log( "nvertices: " + metaData.nvertices );
								console.log( "nnormals: " + metaData.nnormals );
								console.log( "nuvs: " + metaData.nuvs );

								console.log( "ntri_flat: " + metaData.ntri_flat );
								console.log( "ntri_smooth: " + metaData.ntri_smooth );
								console.log( "ntri_flat_uv: " + metaData.ntri_flat_uv );
								console.log( "ntri_smooth_uv: " + metaData.ntri_smooth_uv );

								console.log( "nquad_flat: " + metaData.nquad_flat );
								console.log( "nquad_smooth: " + metaData.nquad_smooth );
								console.log( "nquad_flat_uv: " + metaData.nquad_flat_uv );
								console.log( "nquad_smooth_uv: " + metaData.nquad_smooth_uv );

								var total = metaData.header_bytes
										  + metaData.nvertices * metaData.vertex_coordinate_bytes * 3
										  + metaData.nnormals * metaData.normal_coordinate_bytes * 3
										  + metaData.nuvs * metaData.uv_coordinate_bytes * 2
										  + metaData.ntri_flat * ( metaData.vertex_index_bytes*3 + metaData.material_index_bytes )
										  + metaData.ntri_smooth * ( metaData.vertex_index_bytes*3 + metaData.material_index_bytes + metaData.normal_index_bytes*3 )
										  + metaData.ntri_flat_uv * ( metaData.vertex_index_bytes*3 + metaData.material_index_bytes + metaData.uv_index_bytes*3 )
										  + metaData.ntri_smooth_uv * ( metaData.vertex_index_bytes*3 + metaData.material_index_bytes + metaData.normal_index_bytes*3 + metaData.uv_index_bytes*3 )
										  + metaData.nquad_flat * ( metaData.vertex_index_bytes*4 + metaData.material_index_bytes )
										  + metaData.nquad_smooth * ( metaData.vertex_index_bytes*4 + metaData.material_index_bytes + metaData.normal_index_bytes*4 )
										  + metaData.nquad_flat_uv * ( metaData.vertex_index_bytes*4 + metaData.material_index_bytes + metaData.uv_index_bytes*4 )
										  + metaData.nquad_smooth_uv * ( metaData.vertex_index_bytes*4 + metaData.material_index_bytes + metaData.normal_index_bytes*4 + metaData.uv_index_bytes*4 );
								console.log( "total bytes: " + total );
					*/

					return metaData;

				}

				function parseString( data, offset, length ) {

					return THREE.LoaderUtils.decodeText( new Uint8Array( data, offset, length ) );

				}

				function parseUChar8( data, offset ) {

					var charArray = new Uint8Array( data, offset, 1 );

					return charArray[ 0 ];

				}

				function parseUInt32( data, offset ) {

					var intArray = new Uint32Array( data, offset, 1 );

					return intArray[ 0 ];

				}

				function init_vertices( start ) {

					var nElements = md.nvertices;

					var coordArray = new Float32Array( data, start, nElements * 3 );

					var i, x, y, z;

					for ( i = 0; i < nElements; i ++ ) {

						x = coordArray[ i * 3 ];
						y = coordArray[ i * 3 + 1 ];
						z = coordArray[ i * 3 + 2 ];

						scope.vertices.push( new THREE.Vector3( x, y, z ) );

					}

					return nElements * 3 * Float32Array.BYTES_PER_ELEMENT;

				}

				function init_normals( start ) {

					var nElements = md.nnormals;

					if ( nElements ) {

						var normalArray = new Int8Array( data, start, nElements * 3 );

						var i, x, y, z;

						for ( i = 0; i < nElements; i ++ ) {

							x = normalArray[ i * 3 ];
							y = normalArray[ i * 3 + 1 ];
							z = normalArray[ i * 3 + 2 ];

							normals.push( x / 127, y / 127, z / 127 );

						}

					}

					return nElements * 3 * Int8Array.BYTES_PER_ELEMENT;

				}

				function init_uvs( start ) {

					var nElements = md.nuvs;

					if ( nElements ) {

						var uvArray = new Float32Array( data, start, nElements * 2 );

						var i, u, v;

						for ( i = 0; i < nElements; i ++ ) {

							u = uvArray[ i * 2 ];
							v = uvArray[ i * 2 + 1 ];

							uvs.push( u, v );

						}

					}

					return nElements * 2 * Float32Array.BYTES_PER_ELEMENT;

				}

				function init_uvs3( nElements, offset ) {

					var i, uva, uvb, uvc, u1, u2, u3, v1, v2, v3;

					var uvIndexBuffer = new Uint32Array( data, offset, 3 * nElements );

					for ( i = 0; i < nElements; i ++ ) {

						uva = uvIndexBuffer[ i * 3 ];
						uvb = uvIndexBuffer[ i * 3 + 1 ];
						uvc = uvIndexBuffer[ i * 3 + 2 ];

						u1 = uvs[ uva * 2 ];
						v1 = uvs[ uva * 2 + 1 ];

						u2 = uvs[ uvb * 2 ];
						v2 = uvs[ uvb * 2 + 1 ];

						u3 = uvs[ uvc * 2 ];
						v3 = uvs[ uvc * 2 + 1 ];

						scope.faceVertexUvs[ 0 ].push( [
							new THREE.Vector2( u1, v1 ),
							new THREE.Vector2( u2, v2 ),
							new THREE.Vector2( u3, v3 )
						] );

					}

				}

				function init_uvs4( nElements, offset ) {

					var i, uva, uvb, uvc, uvd, u1, u2, u3, u4, v1, v2, v3, v4;

					var uvIndexBuffer = new Uint32Array( data, offset, 4 * nElements );

					for ( i = 0; i < nElements; i ++ ) {

						uva = uvIndexBuffer[ i * 4 ];
						uvb = uvIndexBuffer[ i * 4 + 1 ];
						uvc = uvIndexBuffer[ i * 4 + 2 ];
						uvd = uvIndexBuffer[ i * 4 + 3 ];

						u1 = uvs[ uva * 2 ];
						v1 = uvs[ uva * 2 + 1 ];

						u2 = uvs[ uvb * 2 ];
						v2 = uvs[ uvb * 2 + 1 ];

						u3 = uvs[ uvc * 2 ];
						v3 = uvs[ uvc * 2 + 1 ];

						u4 = uvs[ uvd * 2 ];
						v4 = uvs[ uvd * 2 + 1 ];

						scope.faceVertexUvs[ 0 ].push( [
							new THREE.Vector2( u1, v1 ),
							new THREE.Vector2( u2, v2 ),
							new THREE.Vector2( u4, v4 )
						] );

						scope.faceVertexUvs[ 0 ].push( [
							new THREE.Vector2( u2, v2 ),
							new THREE.Vector2( u3, v3 ),
							new THREE.Vector2( u4, v4 )
						] );

					}

				}

				function init_faces3_flat( nElements, offsetVertices, offsetMaterials ) {

					var i, a, b, c, m;

					var vertexIndexBuffer = new Uint32Array( data, offsetVertices, 3 * nElements );
					var materialIndexBuffer = new Uint16Array( data, offsetMaterials, nElements );

					for ( i = 0; i < nElements; i ++ ) {

						a = vertexIndexBuffer[ i * 3 ];
						b = vertexIndexBuffer[ i * 3 + 1 ];
						c = vertexIndexBuffer[ i * 3 + 2 ];

						m = materialIndexBuffer[ i ];

						scope.faces.push( new THREE.Face3( a, b, c, null, null, m ) );

					}

				}

				function init_faces4_flat( nElements, offsetVertices, offsetMaterials ) {

					var i, a, b, c, d, m;

					var vertexIndexBuffer = new Uint32Array( data, offsetVertices, 4 * nElements );
					var materialIndexBuffer = new Uint16Array( data, offsetMaterials, nElements );

					for ( i = 0; i < nElements; i ++ ) {

						a = vertexIndexBuffer[ i * 4 ];
						b = vertexIndexBuffer[ i * 4 + 1 ];
						c = vertexIndexBuffer[ i * 4 + 2 ];
						d = vertexIndexBuffer[ i * 4 + 3 ];

						m = materialIndexBuffer[ i ];

						scope.faces.push( new THREE.Face3( a, b, d, null, null, m ) );
						scope.faces.push( new THREE.Face3( b, c, d, null, null, m ) );

					}

				}

				function init_faces3_smooth( nElements, offsetVertices, offsetNormals, offsetMaterials ) {

					var i, a, b, c, m;
					var na, nb, nc;

					var vertexIndexBuffer = new Uint32Array( data, offsetVertices, 3 * nElements );
					var normalIndexBuffer = new Uint32Array( data, offsetNormals, 3 * nElements );
					var materialIndexBuffer = new Uint16Array( data, offsetMaterials, nElements );

					for ( i = 0; i < nElements; i ++ ) {

						a = vertexIndexBuffer[ i * 3 ];
						b = vertexIndexBuffer[ i * 3 + 1 ];
						c = vertexIndexBuffer[ i * 3 + 2 ];

						na = normalIndexBuffer[ i * 3 ];
						nb = normalIndexBuffer[ i * 3 + 1 ];
						nc = normalIndexBuffer[ i * 3 + 2 ];

						m = materialIndexBuffer[ i ];

						var nax = normals[ na * 3 ],
							nay = normals[ na * 3 + 1 ],
							naz = normals[ na * 3 + 2 ],

							nbx = normals[ nb * 3 ],
							nby = normals[ nb * 3 + 1 ],
							nbz = normals[ nb * 3 + 2 ],

							ncx = normals[ nc * 3 ],
							ncy = normals[ nc * 3 + 1 ],
							ncz = normals[ nc * 3 + 2 ];

						scope.faces.push( new THREE.Face3( a, b, c, [
							new THREE.Vector3( nax, nay, naz ),
							new THREE.Vector3( nbx, nby, nbz ),
							new THREE.Vector3( ncx, ncy, ncz )
						], null, m ) );

					}

				}

				function init_faces4_smooth( nElements, offsetVertices, offsetNormals, offsetMaterials ) {

					var i, a, b, c, d, m;
					var na, nb, nc, nd;

					var vertexIndexBuffer = new Uint32Array( data, offsetVertices, 4 * nElements );
					var normalIndexBuffer = new Uint32Array( data, offsetNormals, 4 * nElements );
					var materialIndexBuffer = new Uint16Array( data, offsetMaterials, nElements );

					for ( i = 0; i < nElements; i ++ ) {

						a = vertexIndexBuffer[ i * 4 ];
						b = vertexIndexBuffer[ i * 4 + 1 ];
						c = vertexIndexBuffer[ i * 4 + 2 ];
						d = vertexIndexBuffer[ i * 4 + 3 ];

						na = normalIndexBuffer[ i * 4 ];
						nb = normalIndexBuffer[ i * 4 + 1 ];
						nc = normalIndexBuffer[ i * 4 + 2 ];
						nd = normalIndexBuffer[ i * 4 + 3 ];

						m = materialIndexBuffer[ i ];

						var nax = normals[ na * 3 ],
							nay = normals[ na * 3 + 1 ],
							naz = normals[ na * 3 + 2 ],

							nbx = normals[ nb * 3 ],
							nby = normals[ nb * 3 + 1 ],
							nbz = normals[ nb * 3 + 2 ],

							ncx = normals[ nc * 3 ],
							ncy = normals[ nc * 3 + 1 ],
							ncz = normals[ nc * 3 + 2 ],

							ndx = normals[ nd * 3 ],
							ndy = normals[ nd * 3 + 1 ],
							ndz = normals[ nd * 3 + 2 ];

						scope.faces.push( new THREE.Face3( a, b, d, [
							new THREE.Vector3( nax, nay, naz ),
							new THREE.Vector3( nbx, nby, nbz ),
							new THREE.Vector3( ndx, ndy, ndz )
						], null, m ) );

						scope.faces.push( new THREE.Face3( b, c, d, [
							new THREE.Vector3( nbx, nby, nbz ),
							new THREE.Vector3( ncx, ncy, ncz ),
							new THREE.Vector3( ndx, ndy, ndz )
						], null, m ) );

					}

				}

				function init_triangles_flat( start ) {

					var nElements = md.ntri_flat;

					if ( nElements ) {

						var offsetMaterials = start + nElements * Uint32Array.BYTES_PER_ELEMENT * 3;
						init_faces3_flat( nElements, start, offsetMaterials );

					}

				}

				function init_triangles_flat_uv( start ) {

					var nElements = md.ntri_flat_uv;

					if ( nElements ) {

						var offsetUvs = start + nElements * Uint32Array.BYTES_PER_ELEMENT * 3;
						var offsetMaterials = offsetUvs + nElements * Uint32Array.BYTES_PER_ELEMENT * 3;

						init_faces3_flat( nElements, start, offsetMaterials );
						init_uvs3( nElements, offsetUvs );

					}

				}

				function init_triangles_smooth( start ) {

					var nElements = md.ntri_smooth;

					if ( nElements ) {

						var offsetNormals = start + nElements * Uint32Array.BYTES_PER_ELEMENT * 3;
						var offsetMaterials = offsetNormals + nElements * Uint32Array.BYTES_PER_ELEMENT * 3;

						init_faces3_smooth( nElements, start, offsetNormals, offsetMaterials );

					}

				}

				function init_triangles_smooth_uv( start ) {

					var nElements = md.ntri_smooth_uv;

					if ( nElements ) {

						var offsetNormals = start + nElements * Uint32Array.BYTES_PER_ELEMENT * 3;
						var offsetUvs = offsetNormals + nElements * Uint32Array.BYTES_PER_ELEMENT * 3;
						var offsetMaterials = offsetUvs + nElements * Uint32Array.BYTES_PER_ELEMENT * 3;

						init_faces3_smooth( nElements, start, offsetNormals, offsetMaterials );
						init_uvs3( nElements, offsetUvs );

					}

				}

				function init_quads_flat( start ) {

					var nElements = md.nquad_flat;

					if ( nElements ) {

						var offsetMaterials = start + nElements * Uint32Array.BYTES_PER_ELEMENT * 4;
						init_faces4_flat( nElements, start, offsetMaterials );

					}

				}

				function init_quads_flat_uv( start ) {

					var nElements = md.nquad_flat_uv;

					if ( nElements ) {

						var offsetUvs = start + nElements * Uint32Array.BYTES_PER_ELEMENT * 4;
						var offsetMaterials = offsetUvs + nElements * Uint32Array.BYTES_PER_ELEMENT * 4;

						init_faces4_flat( nElements, start, offsetMaterials );
						init_uvs4( nElements, offsetUvs );

					}

				}

				function init_quads_smooth( start ) {

					var nElements = md.nquad_smooth;

					if ( nElements ) {

						var offsetNormals = start + nElements * Uint32Array.BYTES_PER_ELEMENT * 4;
						var offsetMaterials = offsetNormals + nElements * Uint32Array.BYTES_PER_ELEMENT * 4;

						init_faces4_smooth( nElements, start, offsetNormals, offsetMaterials );

					}

				}

				function init_quads_smooth_uv( start ) {

					var nElements = md.nquad_smooth_uv;

					if ( nElements ) {

						var offsetNormals = start + nElements * Uint32Array.BYTES_PER_ELEMENT * 4;
						var offsetUvs = offsetNormals + nElements * Uint32Array.BYTES_PER_ELEMENT * 4;
						var offsetMaterials = offsetUvs + nElements * Uint32Array.BYTES_PER_ELEMENT * 4;

						init_faces4_smooth( nElements, start, offsetNormals, offsetMaterials );
						init_uvs4( nElements, offsetUvs );

					}

				}

			};

			Model.prototype = Object.create( THREE.Geometry.prototype );
			Model.prototype.constructor = Model;

			var geometry = new Model();
			var materials = THREE.Loader.prototype.initMaterials( jsonMaterials, texturePath, this.crossOrigin );

			callback( geometry, materials );

		}

	};

	/**
	 * @author mrdoob / http://mrdoob.com/
	 */

	THREE.ColladaLoader = function (manager) {

		this.manager = (manager !== undefined) ? manager : THREE.DefaultLoadingManager;

	};

	THREE.ColladaLoader.prototype = {

		constructor: THREE.ColladaLoader,

		load: function (url, onLoad, onProgress, onError) {

			var scope = this;

			var loader = new THREE.FileLoader(scope.manager);
			loader.load(url, function (text) {

				onLoad(scope.parse(text));

			}, onProgress, onError);

		},

		options: {

			set convertUpAxis(value) {

				console.log('ColladaLoder.options.convertUpAxis: TODO');

			}

		},

		setCrossOrigin: function (value) {

			this.crossOrigin = value;

		},

		parse: function (text) {

			function getElementsByTagName(xml, name) {

				// Non recursive xml.getElementsByTagName() ...

				var array = [];
				var childNodes = xml.childNodes;

				for (var i = 0, l = childNodes.length; i < l; i++) {

					var child = childNodes[i];

					if (child.nodeName === name) {

						array.push(child);

					}

				}

				return array;

			}

			function parseFloats(text) {

				if (text.length === 0) return [];

				var parts = text.trim().split(/\s+/);
				var array = new Array(parts.length);

				for (var i = 0, l = parts.length; i < l; i++) {

					array[i] = parseFloat(parts[i]);

				}

				return array;

			}

			function parseInts(text) {

				if (text.length === 0) return [];

				var parts = text.trim().split(/\s+/);
				var array = new Array(parts.length);

				for (var i = 0, l = parts.length; i < l; i++) {

					array[i] = parseInt(parts[i]);

				}

				return array;

			}

			function parseId(text) {

				return text.substring(1);

			}

			// asset

			function parseAsset(xml) {

				return {
					unit: parseAssetUnit(getElementsByTagName(xml, 'unit')[0]),
					upAxis: parseAssetUpAxis(getElementsByTagName(xml, 'up_axis')[0])
				};

			}

			function parseAssetUnit(xml) {

				return xml !== undefined ? parseFloat(xml.getAttribute('meter')) : 1;

			}

			function parseAssetUpAxis(xml) {

				return xml !== undefined ? xml.textContent : 'Y_UP';

			}

			// library

			function parseLibrary(xml, libraryName, nodeName, parser) {

				var library = getElementsByTagName(xml, libraryName)[0];

				if (library !== undefined) {

					var elements = getElementsByTagName(library, nodeName);

					for (var i = 0; i < elements.length; i++) {

						parser(elements[i]);

					}

				}

			}

			function buildLibrary(data, builder) {

				for (var name in data) {

					var object = data[name];
					object.build = builder(data[name]);

				}

			}

			// get

			function getBuild(data, builder) {

				if (data.build !== undefined) return data.build;

				data.build = builder(data);

				return data.build;

			}

			// image

			function parseImage(xml) {

				var data = {
					init_from: getElementsByTagName(xml, 'init_from')[0].textContent
				};

				library.images[xml.getAttribute('id')] = data;

			}

			function buildImage(data) {

				if (data.build !== undefined) return data.build;

				return new Image();

			}

			function getImage(id) {

				return getBuild(library.images[id], buildImage);

			}

			// effect

			function parseEffect(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'profile_COMMON':
							data.profile = parseEffectProfileCOMMON(child);
							break;

					}

				}

				library.effects[xml.getAttribute('id')] = data;

			}

			function parseEffectProfileCOMMON(xml) {

				var data = {
					surfaces: {},
					samplers: {}
				};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'newparam':
							parseEffectNewparam(child, data);
							break;

						case 'technique':
							data.technique = parseEffectTechnique(child);
							break;

					}

				}

				return data;

			}

			function parseEffectNewparam(xml, data) {

				var sid = xml.getAttribute('sid');

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'surface':
							data.surfaces[sid] = parseEffectSurface(child);
							break;

						case 'sampler2D':
							data.samplers[sid] = parseEffectSampler(child);
							break;

					}

				}

			}

			function parseEffectSurface(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'init_from':
							data.init_from = child.textContent;
							break;

					}

				}

				return data;

			}

			function parseEffectSampler(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'source':
							data.source = child.textContent;
							break;

					}

				}

				return data;

			}

			function parseEffectTechnique(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'constant':
						case 'lambert':
						case 'blinn':
						case 'phong':
							data.type = child.nodeName;
							data.parameters = parseEffectParameters(child);
							break;

					}

				}

				return data;

			}

			function parseEffectParameters(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'emission':
						case 'diffuse':
						case 'specular':
						case 'shininess':
						case 'transparent':
						case 'transparency':
							data[child.nodeName] = parseEffectParameter(child);
							break;

					}

				}

				return data;

			}

			function parseEffectParameter(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'color':
							data[child.nodeName] = parseFloats(child.textContent);
							break;

						case 'float':
							data[child.nodeName] = parseFloat(child.textContent);
							break;

						case 'texture':
							data[child.nodeName] = { id: child.getAttribute('texture'), extra: parseEffectParameterTexture(child) };
							break;

					}

				}

				return data;

			}

			function parseEffectParameterTexture(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'extra':
							data = parseEffectParameterTextureExtra(child);
							break;

					}

				}

				return data;

			}

			function parseEffectParameterTextureExtra(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'technique':
							data[child.nodeName] = parseEffectParameterTextureExtraTechnique(child);
							break;

					}

				}

				return data;

			}

			function parseEffectParameterTextureExtraTechnique(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'repeatU':
						case 'repeatV':
						case 'offsetU':
						case 'offsetV':
							data[child.nodeName] = parseFloat(child.textContent);
							break;

						case 'wrapU':
						case 'wrapV':
							data[child.nodeName] = parseInt(child.textContent);
							break;

					}

				}

				return data;

			}

			function buildEffect(data) {

				return data;

			}

			function getEffect(id) {

				return getBuild(library.effects[id], buildEffect);

			}

			// material

			function parseMaterial(xml) {

				var data = {
					name: xml.getAttribute('name')
				};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'instance_effect':
							data.url = parseId(child.getAttribute('url'));
							break;

					}

				}

				library.materials[xml.getAttribute('id')] = data;

			}

			function buildMaterial(data) {

				var effect = getEffect(data.url);
				var technique = effect.profile.technique;

				var material;

				switch (technique.type) {

					case 'phong':
					case 'blinn':
						material = new THREE.MeshPhongMaterial();
						break;

					case 'lambert':
						material = new THREE.MeshLambertMaterial();
						break;

					default:
						material = new THREE.MeshBasicMaterial();
						break;

				}

				material.name = data.name;

				function getTexture(textureObject) {

					var sampler = effect.profile.samplers[textureObject.id];

					if (sampler !== undefined) {

						var surface = effect.profile.surfaces[sampler.source];

						var texture = new THREE.Texture(getImage(surface.init_from));

						var extra = textureObject.extra;

						if (extra !== undefined && extra.technique !== undefined) {

							var technique = extra.technique;

							texture.wrapS = technique.wrapU ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
							texture.wrapT = technique.wrapV ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;

							texture.offset.set(technique.offsetU, technique.offsetV);
							texture.repeat.set(technique.repeatU, technique.repeatV);

						} else {

							texture.wrapS = THREE.RepeatWrapping;
							texture.wrapT = THREE.RepeatWrapping;

						}

						texture.needsUpdate = true;

						return texture;

					}

					console.error('ColladaLoder: Undefined sampler', textureObject.id);

					return null;

				}

				var parameters = technique.parameters;

				for (var key in parameters) {

					var parameter = parameters[key];

					switch (key) {

						case 'diffuse':
							if (parameter.color) material.color.fromArray(parameter.color);
							if (parameter.texture) material.map = getTexture(parameter.texture);
							break;
						case 'specular':
							if (parameter.color && material.specular)
								material.specular.fromArray(parameter.color);
							break;
						case 'shininess':
							if (parameter.float && material.shininess)
								material.shininess = parameter.float;
							break;
						case 'emission':
							if (parameter.color && material.emissive)
								material.emissive.fromArray(parameter.color);
							break;
						case 'transparent':
							// if ( parameter.texture ) material.alphaMap = getTexture( parameter.texture );
							material.transparent = true;
							break;
						case 'transparency':
							if (parameter.float !== undefined) material.opacity = parameter.float;
							material.transparent = true;
							break;

					}

				}

				return material;

			}

			function getMaterial(id) {

				return getBuild(library.materials[id], buildMaterial);

			}

			// camera

			function parseCamera(xml) {

				var data = {
					name: xml.getAttribute('name')
				};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'optics':
							data.optics = parseCameraOptics(child);
							break;

					}

				}

				library.cameras[xml.getAttribute('id')] = data;

			}

			function parseCameraOptics(xml) {

				for (var i = 0; i < xml.childNodes.length; i++) {

					var child = xml.childNodes[i];

					switch (child.nodeName) {

						case 'technique_common':
							return parseCameraTechnique(child);

					}

				}

				return {};

			}

			function parseCameraTechnique(xml) {

				var data = {};

				for (var i = 0; i < xml.childNodes.length; i++) {

					var child = xml.childNodes[i];

					switch (child.nodeName) {

						case 'perspective':
						case 'orthographic':

							data.technique = child.nodeName;
							data.parameters = parseCameraParameters(child);

							break;

					}

				}

				return data;

			}

			function parseCameraParameters(xml) {

				var data = {};

				for (var i = 0; i < xml.childNodes.length; i++) {

					var child = xml.childNodes[i];

					switch (child.nodeName) {

						case 'xfov':
						case 'yfov':
						case 'xmag':
						case 'ymag':
						case 'znear':
						case 'zfar':
						case 'aspect_ratio':
							data[child.nodeName] = parseFloat(child.textContent);
							break;

					}

				}

				return data;

			}

			function buildCamera(data) {

				var camera;

				switch (data.optics.technique) {

					case 'perspective':
						camera = new THREE.PerspectiveCamera(
							data.optics.parameters.yfov,
							data.optics.parameters.aspect_ratio,
							data.optics.parameters.znear,
							data.optics.parameters.zfar
						);
						break;

					case 'orthographic':
						camera = new THREE.OrthographicCamera( /* TODO */);
						break;

					default:
						camera = new THREE.PerspectiveCamera();
						break;

				}

				camera.name = data.name;

				return camera;

			}

			function getCamera(id) {

				return getBuild(library.cameras[id], buildCamera);

			}

			// light

			function parseLight(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'technique_common':
							data = parseLightTechnique(child);
							break;

					}

				}

				library.lights[xml.getAttribute('id')] = data;

			}

			function parseLightTechnique(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'directional':
						case 'point':
						case 'spot':
						case 'ambient':

							data.technique = child.nodeName;
							data.parameters = parseLightParameters(child);

					}

				}

				return data;

			}

			function parseLightParameters(xml) {

				var data = {};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'color':
							var array = parseFloats(child.textContent);
							data.color = new THREE.Color().fromArray(array);
							break;

						case 'falloff_angle':
							data.falloffAngle = parseFloat(child.textContent);
							break;

						case 'quadratic_attenuation':
							var f = parseFloat(child.textContent);
							data.distance = f ? Math.sqrt(1 / f) : 0;
							break;

					}

				}

				return data;

			}

			function buildLight(data) {

				var light;

				switch (data.technique) {

					case 'directional':
						light = new THREE.DirectionalLight();
						break;

					case 'point':
						light = new THREE.PointLight();
						break;

					case 'spot':
						light = new THREE.SpotLight();
						break;

					case 'ambient':
						light = new THREE.AmbientLight();
						break;

				}

				if (data.parameters.color) light.color.copy(data.parameters.color);
				if (data.parameters.distance) light.distance = data.parameters.distance;

				return light;

			}

			function getLight(id) {

				return getBuild(library.lights[id], buildLight);

			}

			// geometry

			function parseGeometry(xml) {

				var data = {
					name: xml.getAttribute('name'),
					sources: {},
					vertices: {},
					primitives: []
				};

				var mesh = getElementsByTagName(xml, 'mesh')[0];

				for (var i = 0; i < mesh.childNodes.length; i++) {

					var child = mesh.childNodes[i];

					if (child.nodeType !== 1) continue;

					var id = child.getAttribute('id');

					switch (child.nodeName) {

						case 'source':
							data.sources[id] = parseGeometrySource(child);
							break;

						case 'vertices':
							// data.sources[ id ] = data.sources[ parseId( getElementsByTagName( child, 'input' )[ 0 ].getAttribute( 'source' ) ) ];
							data.vertices = parseGeometryVertices(child);
							break;

						case 'polygons':
							console.warn('ColladaLoader: Unsupported primitive type: ', child.nodeName);
							break;

						case 'lines':
						case 'linestrips':
						case 'polylist':
						case 'triangles':
							data.primitives.push(parseGeometryPrimitive(child));
							break;

						default:
							console.log(child);

					}

				}

				library.geometries[xml.getAttribute('id')] = data;

			}

			function parseGeometrySource(xml) {

				var data = {
					array: [],
					stride: 3
				};

				for (var i = 0; i < xml.childNodes.length; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'float_array':
							data.array = parseFloats(child.textContent);
							break;

						case 'technique_common':
							var accessor = getElementsByTagName(child, 'accessor')[0];

							if (accessor !== undefined) {

								data.stride = parseInt(accessor.getAttribute('stride'));

							}
							break;

						default:
							console.log(child);

					}

				}

				return data;

			}

			function parseGeometryVertices(xml) {

				var data = {};

				for (var i = 0; i < xml.childNodes.length; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					data[child.getAttribute('semantic')] = parseId(child.getAttribute('source'));

				}

				return data;

			}

			function parseGeometryPrimitive(xml) {

				var primitive = {
					type: xml.nodeName,
					material: xml.getAttribute('material'),
					inputs: {},
					stride: 0
				};

				for (var i = 0, l = xml.childNodes.length; i < l; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'input':
							var id = parseId(child.getAttribute('source'));
							var semantic = child.getAttribute('semantic');
							var offset = parseInt(child.getAttribute('offset'));
							primitive.inputs[semantic] = { id: id, offset: offset };
							primitive.stride = Math.max(primitive.stride, offset + 1);
							break;

						case 'vcount':
							primitive.vcount = parseInts(child.textContent);
							break;

						case 'p':
							primitive.p = parseInts(child.textContent);
							break;

					}

				}

				return primitive;

			}

			var DEFAULT_LINEMATERIAL = new THREE.LineBasicMaterial();
			var DEFAULT_MESHMATERIAL = new THREE.MeshPhongMaterial();

			function buildGeometry(data) {

				var group = {};

				var sources = data.sources;
				var vertices = data.vertices;
				var primitives = data.primitives;

				if (primitives.length === 0) return group;

				for (var p = 0; p < primitives.length; p++) {

					var primitive = primitives[p];
					var inputs = primitive.inputs;

					var geometry = new THREE.BufferGeometry();

					if (data.name) geometry.name = data.name;

					for (var name in inputs) {

						var input = inputs[name];

						switch (name) {

							case 'VERTEX':
								for (var key in vertices) {

									geometry.addAttribute(key.toLowerCase(), buildGeometryAttribute(primitive, sources[vertices[key]], input.offset));

								}
								break;

							case 'NORMAL':
								geometry.addAttribute('normal', buildGeometryAttribute(primitive, sources[input.id], input.offset));
								break;

							case 'COLOR':
								geometry.addAttribute('color', buildGeometryAttribute(primitive, sources[input.id], input.offset));
								break;

							case 'TEXCOORD':
								geometry.addAttribute('uv', buildGeometryAttribute(primitive, sources[input.id], input.offset));
								break;

						}

					}

					var object;

					switch (primitive.type) {

						case 'lines':
							object = new THREE.LineSegments(geometry, DEFAULT_LINEMATERIAL);
							break;

						case 'linestrips':
							object = new THREE.Line(geometry, DEFAULT_LINEMATERIAL);
							break;

						case 'triangles':
						case 'polylist':
							object = new THREE.Mesh(geometry, DEFAULT_MESHMATERIAL);
							break;

					}

					group[primitive.material] = object;

				}

				return group;

			}

			function buildGeometryAttribute(primitive, source, offset) {

				var indices = primitive.p;
				var stride = primitive.stride;
				var vcount = primitive.vcount;

				function pushVector(i) {

					var index = indices[i + offset] * sourceStride;
					var length = index + sourceStride;

					for (; index < length; index++) {

						array.push(sourceArray[index]);

					}

				}

				var maxcount = 0;

				var sourceArray = source.array;
				var sourceStride = source.stride;

				var array = [];

				if (primitive.vcount !== undefined) {

					var index = 0;

					for (var i = 0, l = vcount.length; i < l; i++) {

						var count = vcount[i];

						if (count === 4) {

							var a = index + stride * 0;
							var b = index + stride * 1;
							var c = index + stride * 2;
							var d = index + stride * 3;

							pushVector(a); pushVector(b); pushVector(d);
							pushVector(b); pushVector(c); pushVector(d);

						} else if (count === 3) {

							var a = index + stride * 0;
							var b = index + stride * 1;
							var c = index + stride * 2;

							pushVector(a); pushVector(b); pushVector(c);

						} else {

							maxcount = Math.max(maxcount, count);

						}

						index += stride * count;

					}

					if (maxcount > 0) {

						console.log('ColladaLoader: Geometry has faces with more than 4 vertices.');

					}

				} else {

					for (var i = 0, l = indices.length; i < l; i += stride) {

						pushVector(i);

					}

				}

				return new THREE.Float32BufferAttribute(array, sourceStride);

			}

			function getGeometry(id) {

				return getBuild(library.geometries[id], buildGeometry);

			}

			// nodes

			var matrix = new THREE.Matrix4();
			var vector = new THREE.Vector3();

			function parseNode(xml) {

				var data = {
					name: xml.getAttribute('name'),
					matrix: new THREE.Matrix4(),
					nodes: [],
					instanceCameras: [],
					instanceLights: [],
					instanceGeometries: [],
					instanceNodes: []
				};

				for (var i = 0; i < xml.childNodes.length; i++) {

					var child = xml.childNodes[i];

					if (child.nodeType !== 1) continue;

					switch (child.nodeName) {

						case 'node':

							if (child.hasAttribute('id')) {

								data.nodes.push(child.getAttribute('id'));
								parseNode(child);

							}

							break;

						case 'instance_camera':
							data.instanceCameras.push(parseId(child.getAttribute('url')));
							break;

						case 'instance_light':
							data.instanceLights.push(parseId(child.getAttribute('url')));
							break;

						case 'instance_geometry':
							data.instanceGeometries.push(parseNodeInstanceGeometry(child));
							break;

						case 'instance_node':
							data.instanceNodes.push(parseId(child.getAttribute('url')));
							break;

						case 'matrix':
							var array = parseFloats(child.textContent);
							data.matrix.multiply(matrix.fromArray(array).transpose()); // .transpose() when Z_UP?
							break;

						case 'translate':
							var array = parseFloats(child.textContent);
							vector.fromArray(array);
							data.matrix.multiply(matrix.makeTranslation(vector.x, vector.y, vector.z));
							break;

						case 'rotate':
							var array = parseFloats(child.textContent);
							var angle = THREE.Math.degToRad(array[3]);
							data.matrix.multiply(matrix.makeRotationAxis(vector.fromArray(array), angle));
							break;

						case 'scale':
							var array = parseFloats(child.textContent);
							data.matrix.scale(vector.fromArray(array));
							break;

						case 'extra':
							break;

						default:
							console.log(child);

					}

				}

				if (xml.hasAttribute('id')) {

					library.nodes[xml.getAttribute('id')] = data;

				}

				return data;

			}

			function parseNodeInstanceGeometry(xml) {

				var data = {
					id: parseId(xml.getAttribute('url')),
					materials: {}
				};

				for (var i = 0; i < xml.childNodes.length; i++) {

					var child = xml.childNodes[i];

					if (child.nodeName === 'bind_material') {

						var instances = child.getElementsByTagName('instance_material');

						for (var j = 0; j < instances.length; j++) {

							var instance = instances[j];
							var symbol = instance.getAttribute('symbol');
							var target = instance.getAttribute('target');

							data.materials[symbol] = parseId(target);

						}

						break;

					}

				}

				return data;

			}

			function buildNode(data) {

				var objects = [];

				var matrix = data.matrix;
				var nodes = data.nodes;
				var instanceCameras = data.instanceCameras;
				var instanceLights = data.instanceLights;
				var instanceGeometries = data.instanceGeometries;
				var instanceNodes = data.instanceNodes;

				for (var i = 0, l = nodes.length; i < l; i++) {

					objects.push(getNode(nodes[i]).clone());

				}

				for (var i = 0, l = instanceCameras.length; i < l; i++) {

					objects.push(getCamera(instanceCameras[i]).clone());

				}

				for (var i = 0, l = instanceLights.length; i < l; i++) {

					objects.push(getLight(instanceLights[i]).clone());

				}

				for (var i = 0, l = instanceGeometries.length; i < l; i++) {

					var instance = instanceGeometries[i];
					var geometries = getGeometry(instance.id);

					for (var key in geometries) {

						var object = geometries[key].clone();

						if (instance.materials[key] !== undefined) {

							object.material = getMaterial(instance.materials[key]);

						}

						objects.push(object);

					}

				}

				for (var i = 0, l = instanceNodes.length; i < l; i++) {

					objects.push(getNode(instanceNodes[i]).clone());

				}

				var object;

				if (nodes.length === 0 && objects.length === 1) {

					object = objects[0];

				} else {

					object = new THREE.Group();

					for (var i = 0; i < objects.length; i++) {

						object.add(objects[i]);

					}

				}

				object.name = data.name;
				matrix.decompose(object.position, object.quaternion, object.scale);

				return object;

			}

			function getNode(id) {

				return getBuild(library.nodes[id], buildNode);

			}

			// visual scenes

			function parseVisualScene(xml) {

				var data = {
					name: xml.getAttribute('name'),
					children: []
				};

				var elements = getElementsByTagName(xml, 'node');

				for (var i = 0; i < elements.length; i++) {

					data.children.push(parseNode(elements[i]));

				}

				library.visualScenes[xml.getAttribute('id')] = data;

			}

			function buildVisualScene(data) {

				var group = new THREE.Group();
				group.name = data.name;

				var children = data.children;

				for (var i = 0; i < children.length; i++) {

					group.add(buildNode(children[i]));

				}

				return group;

			}

			function getVisualScene(id) {

				return getBuild(library.visualScenes[id], buildVisualScene);

			}

			// scenes

			function parseScene(xml) {

				var instance = getElementsByTagName(xml, 'instance_visual_scene')[0];
				return getVisualScene(parseId(instance.getAttribute('url')));

			}

			console.time('ColladaLoader');

			if (text.length === 0) {

				return { scene: new THREE.Scene() };

			}

			console.time('ColladaLoader: DOMParser');

			var xml = new DOMParser().parseFromString(text, 'application/xml');

			console.timeEnd('ColladaLoader: DOMParser');

			var collada = getElementsByTagName(xml, 'COLLADA')[0];

			// metadata

			var version = collada.getAttribute('version');
			console.log('ColladaLoader: File version', version);

			var asset = parseAsset(getElementsByTagName(collada, 'asset')[0]);

			//

			var library = {
				images: {},
				effects: {},
				materials: {},
				cameras: {},
				lights: {},
				geometries: {},
				nodes: {},
				visualScenes: {}
			};

			console.time('ColladaLoader: Parse');

			parseLibrary(collada, 'library_images', 'image', parseImage);
			parseLibrary(collada, 'library_effects', 'effect', parseEffect);
			parseLibrary(collada, 'library_materials', 'material', parseMaterial);
			parseLibrary(collada, 'library_cameras', 'camera', parseCamera);
			parseLibrary(collada, 'library_lights', 'light', parseLight);
			parseLibrary(collada, 'library_geometries', 'geometry', parseGeometry);
			parseLibrary(collada, 'library_nodes', 'node', parseNode);
			parseLibrary(collada, 'library_visual_scenes', 'visual_scene', parseVisualScene);

			console.timeEnd('ColladaLoader: Parse');

			console.time('ColladaLoader: Build');

			buildLibrary(library.images, buildImage);
			buildLibrary(library.effects, buildEffect);
			buildLibrary(library.materials, buildMaterial);
			buildLibrary(library.cameras, buildCamera);
			buildLibrary(library.lights, buildLight);
			buildLibrary(library.geometries, buildGeometry);
			// buildLibrary( library.nodes, buildNode );
			buildLibrary(library.visualScenes, buildVisualScene);

			console.timeEnd('ColladaLoader: Build');

			// console.log( library );

			var scene = parseScene(getElementsByTagName(collada, 'scene')[0]);

			if (asset.upAxis === 'Z_UP') {

				scene.rotation.x = - Math.PI / 2;

			}

			scene.scale.multiplyScalar(asset.unit);

			console.timeEnd('ColladaLoader');

			// console.log( scene );

			return {
				animations: [],
				kinematics: { joints: [] },
				library: library,
				scene: scene
			};

		}

	};

	/**
	 * @author Kyle-Larson https://github.com/Kyle-Larson
	 * @author Takahiro https://github.com/takahirox
	 * @author Lewy Blue https://github.com/looeee
	 *
	 * Loader loads FBX file and generates Group representing FBX scene.
	 * Requires FBX file to be >= 7.0 and in ASCII or >= 6400 in Binary format
	 * Versions lower than this may load but will probably have errors
	 *
	 * Needs Support:
	 *  Morph normals / blend shape normals
	 *  Animation tracks for morph targets
	 *
	 *	Euler rotation order
	 *
	 * FBX format references:
	 * 	https://wiki.blender.org/index.php/User:Mont29/Foundation/FBX_File_Structure
	 * 	http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_index_html (C++ SDK reference)
	 *
	 * 	Binary format specification:
	 *		https://code.blender.org/2013/08/fbx-binary-file-format-specification/
	 */


	( function () {

		THREE.FBXLoader = function ( manager ) {

			this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

		};

		Object.assign( THREE.FBXLoader.prototype, {

			load: function ( url, onLoad, onProgress, onError ) {

				var self = this;

				var resourceDirectory = THREE.LoaderUtils.extractUrlBase( url );

				var loader = new THREE.FileLoader( this.manager );
				loader.setResponseType( 'arraybuffer' );
				loader.load( url, function ( buffer ) {

					try {

						var scene = self.parse( buffer, resourceDirectory );
						onLoad( scene );

					} catch ( error ) {

						window.setTimeout( function () {

							if ( onError ) onError( error );

							self.manager.itemError( url );

						}, 0 );

					}

				}, onProgress, onError );

			},

			parse: function ( FBXBuffer, resourceDirectory ) {

				var FBXTree;

				if ( isFbxFormatBinary( FBXBuffer ) ) {

					FBXTree = new BinaryParser().parse( FBXBuffer );

				} else {

					var FBXText = convertArrayBufferToString( FBXBuffer );

					if ( ! isFbxFormatASCII( FBXText ) ) {

						throw new Error( 'THREE.FBXLoader: Unknown format.' );

					}

					if ( getFbxVersion( FBXText ) < 7000 ) {

						throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + getFbxVersion( FBXText ) );

					}

					FBXTree = new TextParser().parse( FBXText );

				}

				// console.log( FBXTree );

				var connections = parseConnections( FBXTree );
				var images = parseImages( FBXTree );
				var textures = parseTextures( FBXTree, new THREE.TextureLoader( this.manager ).setPath( resourceDirectory ), images, connections );
				var materials = parseMaterials( FBXTree, textures, connections );
				var deformers = parseDeformers( FBXTree, connections );
				var geometryMap = parseGeometries( FBXTree, connections, deformers );
				var sceneGraph = parseScene( FBXTree, connections, deformers.skeletons, geometryMap, materials );

				return sceneGraph;

			}

		} );

		// Parses FBXTree.Connections which holds parent-child connections between objects (e.g. material -> texture, model->geometry )
		// and details the connection type
		function parseConnections( FBXTree ) {

			var connectionMap = new Map();

			if ( 'Connections' in FBXTree ) {

				var rawConnections = FBXTree.Connections.connections;

				rawConnections.forEach( function ( rawConnection ) {

					var fromID = rawConnection[ 0 ];
					var toID = rawConnection[ 1 ];
					var relationship = rawConnection[ 2 ];

					if ( ! connectionMap.has( fromID ) ) {

						connectionMap.set( fromID, {
							parents: [],
							children: []
						} );

					}

					var parentRelationship = { ID: toID, relationship: relationship };
					connectionMap.get( fromID ).parents.push( parentRelationship );

					if ( ! connectionMap.has( toID ) ) {

						connectionMap.set( toID, {
							parents: [],
							children: []
						} );

					}

					var childRelationship = { ID: fromID, relationship: relationship };
					connectionMap.get( toID ).children.push( childRelationship );

				} );

			}

			return connectionMap;

		}

		// Parse FBXTree.Objects.Video for embedded image data
		// These images are connected to textures in FBXTree.Objects.Textures
		// via FBXTree.Connections.
		function parseImages( FBXTree ) {

			var images = {};
			var blobs = {};

			if ( 'Video' in FBXTree.Objects ) {

				var videoNodes = FBXTree.Objects.Video;

				for ( var nodeID in videoNodes ) {

					var videoNode = videoNodes[ nodeID ];

					var id = parseInt( nodeID );

					images[ id ] = videoNode.RelativeFilename || videoNode.Filename;

					// raw image data is in videoNode.Content
					if ( 'Content' in videoNode ) {

						var arrayBufferContent = ( videoNode.Content instanceof ArrayBuffer ) && ( videoNode.Content.byteLength > 0 );
						var base64Content = ( typeof videoNode.Content === 'string' ) && ( videoNode.Content !== '' );

						if ( arrayBufferContent || base64Content ) {

							var image = parseImage( videoNodes[ nodeID ] );

							blobs[ videoNode.RelativeFilename || videoNode.Filename ] = image;

						}

					}

				}

			}

			for ( var id in images ) {

				var filename = images[ id ];

				if ( blobs[ filename ] !== undefined ) images[ id ] = blobs[ filename ];
				else images[ id ] = images[ id ].split( '\\' ).pop();

			}

			return images;

		}

		// Parse embedded image data in FBXTree.Video.Content
		function parseImage( videoNode ) {

			var content = videoNode.Content;
			var fileName = videoNode.RelativeFilename || videoNode.Filename;
			var extension = fileName.slice( fileName.lastIndexOf( '.' ) + 1 ).toLowerCase();

			var type;

			switch ( extension ) {

				case 'bmp':

					type = 'image/bmp';
					break;

				case 'jpg':
				case 'jpeg':

					type = 'image/jpeg';
					break;

				case 'png':

					type = 'image/png';
					break;

				case 'tif':

					type = 'image/tiff';
					break;

	 			case 'tga':

					if ( typeof THREE.TGALoader !== 'function' ) {

						console.warn( 'FBXLoader: THREE.TGALoader is required to load TGA textures' );
						return;

					} else {

						if ( THREE.Loader.Handlers.get( '.tga' ) === null ) {

							THREE.Loader.Handlers.add( /\.tga$/i, new THREE.TGALoader() );

						}

						type = 'image/tga';
						break;

					}

				default:

					console.warn( 'FBXLoader: Image type "' + extension + '" is not supported.' );
					return;

			}

			if ( typeof content === 'string' ) { // ASCII format

				return 'data:' + type + ';base64,' + content;

			} else { // Binary Format

				var array = new Uint8Array( content );
				return window.URL.createObjectURL( new Blob( [ array ], { type: type } ) );

			}

		}

		// Parse nodes in FBXTree.Objects.Texture
		// These contain details such as UV scaling, cropping, rotation etc and are connected
		// to images in FBXTree.Objects.Video
		function parseTextures( FBXTree, loader, images, connections ) {

			var textureMap = new Map();

			if ( 'Texture' in FBXTree.Objects ) {

				var textureNodes = FBXTree.Objects.Texture;
				for ( var nodeID in textureNodes ) {

					var texture = parseTexture( textureNodes[ nodeID ], loader, images, connections );
					textureMap.set( parseInt( nodeID ), texture );

				}

			}

			return textureMap;

		}

		// Parse individual node in FBXTree.Objects.Texture
		function parseTexture( textureNode, loader, images, connections ) {

			var texture = loadTexture( textureNode, loader, images, connections );

			texture.ID = textureNode.id;

			texture.name = textureNode.attrName;

			var wrapModeU = textureNode.WrapModeU;
			var wrapModeV = textureNode.WrapModeV;

			var valueU = wrapModeU !== undefined ? wrapModeU.value : 0;
			var valueV = wrapModeV !== undefined ? wrapModeV.value : 0;

			// http://download.autodesk.com/us/fbx/SDKdocs/FBX_SDK_Help/files/fbxsdkref/class_k_fbx_texture.html#889640e63e2e681259ea81061b85143a
			// 0: repeat(default), 1: clamp

			texture.wrapS = valueU === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
			texture.wrapT = valueV === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;

			if ( 'Scaling' in textureNode ) {

				var values = textureNode.Scaling.value;

				texture.repeat.x = values[ 0 ];
				texture.repeat.y = values[ 1 ];

			}

			return texture;

		}

		// load a texture specified as a blob or data URI, or via an external URL using THREE.TextureLoader
		function loadTexture( textureNode, loader, images, connections ) {

			var fileName;

			var currentPath = loader.path;

			var children = connections.get( textureNode.id ).children;

			if ( children !== undefined && children.length > 0 && images[ children[ 0 ].ID ] !== undefined ) {

				fileName = images[ children[ 0 ].ID ];

				if ( fileName.indexOf( 'blob:' ) === 0 || fileName.indexOf( 'data:' ) === 0 ) {

					loader.setPath( undefined );

				}

			}

			var texture;

			if ( textureNode.FileName.slice( -3 ).toLowerCase() === 'tga' ) {

	 			texture = THREE.Loader.Handlers.get( '.tga' ).load( fileName );

	 		} else {

	 			texture = loader.load( fileName );

	 		}

			loader.setPath( currentPath );

			return texture;

		}

		// Parse nodes in FBXTree.Objects.Material
		function parseMaterials( FBXTree, textureMap, connections ) {

			var materialMap = new Map();

			if ( 'Material' in FBXTree.Objects ) {

				var materialNodes = FBXTree.Objects.Material;

				for ( var nodeID in materialNodes ) {

					var material = parseMaterial( FBXTree, materialNodes[ nodeID ], textureMap, connections );

					if ( material !== null ) materialMap.set( parseInt( nodeID ), material );

				}

			}

			return materialMap;

		}

		// Parse single node in FBXTree.Objects.Material
		// Materials are connected to texture maps in FBXTree.Objects.Textures
		// FBX format currently only supports Lambert and Phong shading models
		function parseMaterial( FBXTree, materialNode, textureMap, connections ) {

			var ID = materialNode.id;
			var name = materialNode.attrName;
			var type = materialNode.ShadingModel;

			//Case where FBX wraps shading model in property object.
			if ( typeof type === 'object' ) {

				type = type.value;

			}

			// Ignore unused materials which don't have any connections.
			if ( ! connections.has( ID ) ) return null;

			var parameters = parseParameters( FBXTree, materialNode, textureMap, ID, connections );

			var material;

			switch ( type.toLowerCase() ) {

				case 'phong':
					material = new THREE.MeshPhongMaterial();
					break;
				case 'lambert':
					material = new THREE.MeshLambertMaterial();
					break;
				default:
					console.warn( 'THREE.FBXLoader: unknown material type "%s". Defaulting to MeshPhongMaterial.', type );
					material = new THREE.MeshPhongMaterial( { color: 0x3300ff } );
					break;

			}

			material.setValues( parameters );
			material.name = name;

			return material;

		}

		// Parse FBX material and return parameters suitable for a three.js material
		// Also parse the texture map and return any textures associated with the material
		function parseParameters( FBXTree, properties, textureMap, ID, connections ) {

			var parameters = {};

			if ( properties.BumpFactor ) {

				parameters.bumpScale = properties.BumpFactor.value;

			}
			if ( properties.Diffuse ) {

				parameters.color = new THREE.Color().fromArray( properties.Diffuse.value );

			} else if ( properties.DiffuseColor && properties.DiffuseColor.type === 'Color' ) {

				// The blender exporter exports diffuse here instead of in properties.Diffuse
				parameters.color = new THREE.Color().fromArray( properties.DiffuseColor.value );

			}
			if ( properties.DisplacementFactor ) {

				parameters.displacementScale = properties.DisplacementFactor.value;

			}
			if ( properties.Emissive ) {

				parameters.emissive = new THREE.Color().fromArray( properties.Emissive.value );

			} else if ( properties.EmissiveColor && properties.EmissiveColor.type === 'Color' ) {

				// The blender exporter exports emissive color here instead of in properties.Emissive
				parameters.emissive = new THREE.Color().fromArray( properties.EmissiveColor.value );

			}
			if ( properties.EmissiveFactor ) {

				parameters.emissiveIntensity = parseFloat( properties.EmissiveFactor.value );

			}
			if ( properties.Opacity ) {

				parameters.opacity = parseFloat( properties.Opacity.value );

			}
			if ( parameters.opacity < 1.0 ) {

				parameters.transparent = true;

			}
			if ( properties.ReflectionFactor ) {

				parameters.reflectivity = properties.ReflectionFactor.value;

			}
			if ( properties.Shininess ) {

				parameters.shininess = properties.Shininess.value;

			}
			if ( properties.Specular ) {

				parameters.specular = new THREE.Color().fromArray( properties.Specular.value );

			} else if ( properties.SpecularColor && properties.SpecularColor.type === 'Color' ) {

				// The blender exporter exports specular color here instead of in properties.Specular
				parameters.specular = new THREE.Color().fromArray( properties.SpecularColor.value );

			}

			connections.get( ID ).children.forEach( function ( child ) {

				var type = child.relationship;

				switch ( type ) {

					case 'Bump':
						parameters.bumpMap = textureMap.get( child.ID );
						break;

					case 'DiffuseColor':
						parameters.map = getTexture( FBXTree, textureMap, child.ID, connections );
						break;

					case 'DisplacementColor':
						parameters.displacementMap = getTexture( FBXTree, textureMap, child.ID, connections );
						break;


					case 'EmissiveColor':
						parameters.emissiveMap = getTexture( FBXTree, textureMap, child.ID, connections );
						break;

					case 'NormalMap':
						parameters.normalMap = getTexture( FBXTree, textureMap, child.ID, connections );
						break;

					case 'ReflectionColor':
						parameters.envMap = getTexture( FBXTree, textureMap, child.ID, connections );
						parameters.envMap.mapping = THREE.EquirectangularReflectionMapping;
						break;

					case 'SpecularColor':
						parameters.specularMap = getTexture( FBXTree, textureMap, child.ID, connections );
						break;

					case 'TransparentColor':
						parameters.alphaMap = getTexture( FBXTree, textureMap, child.ID, connections );
						parameters.transparent = true;
						break;

					case 'AmbientColor':
					case 'ShininessExponent': // AKA glossiness map
					case 'SpecularFactor': // AKA specularLevel
					case 'VectorDisplacementColor': // NOTE: Seems to be a copy of DisplacementColor
					default:
						console.warn( 'THREE.FBXLoader: %s map is not supported in three.js, skipping texture.', type );
						break;

				}

			} );

			return parameters;

		}

		// get a texture from the textureMap for use by a material.
		function getTexture( FBXTree, textureMap, id, connections ) {

			// if the texture is a layered texture, just use the first layer and issue a warning
			if ( 'LayeredTexture' in FBXTree.Objects && id in FBXTree.Objects.LayeredTexture ) {

				console.warn( 'THREE.FBXLoader: layered textures are not supported in three.js. Discarding all but first layer.' );
				id = connections.get( id ).children[ 0 ].ID;

			}

			return textureMap.get( id );

		}

		// Parse nodes in FBXTree.Objects.Deformer
		// Deformer node can contain skinning or Vertex Cache animation data, however only skinning is supported here
		// Generates map of Skeleton-like objects for use later when generating and binding skeletons.
		function parseDeformers( FBXTree, connections ) {

			var skeletons = {};
			var morphTargets = {};

			if ( 'Deformer' in FBXTree.Objects ) {

				var DeformerNodes = FBXTree.Objects.Deformer;

				for ( var nodeID in DeformerNodes ) {

					var deformerNode = DeformerNodes[ nodeID ];

					var relationships = connections.get( parseInt( nodeID ) );

					if ( deformerNode.attrType === 'Skin' ) {

						var skeleton = parseSkeleton( relationships, DeformerNodes );
						skeleton.ID = nodeID;

						if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: skeleton attached to more than one geometry is not supported.' );
						skeleton.geometryID = relationships.parents[ 0 ].ID;

						skeletons[ nodeID ] = skeleton;

					} else if ( deformerNode.attrType === 'BlendShape' ) {

						var morphTarget = {
							id: nodeID,
						};

						morphTarget.rawTargets = parseMorphTargets( relationships, deformerNode, DeformerNodes, connections, FBXTree );
						morphTarget.id = nodeID;

						if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: morph target attached to more than one geometry is not supported.' );
						morphTarget.parentGeoID = relationships.parents[ 0 ].ID;

						morphTargets[ nodeID ] = morphTarget;

					}

				}

			}

			return {

				skeletons: skeletons,
				morphTargets: morphTargets,

			};

		}

		// Parse single nodes in FBXTree.Objects.Deformer
		// The top level skeleton node has type 'Skin' and sub nodes have type 'Cluster'
		// Each skin node represents a skeleton and each cluster node represents a bone
		function parseSkeleton( connections, deformerNodes ) {

			var rawBones = [];

			connections.children.forEach( function ( child ) {

				var boneNode = deformerNodes[ child.ID ];

				if ( boneNode.attrType !== 'Cluster' ) return;

				var rawBone = {

					ID: child.ID,
					indices: [],
					weights: [],
					transform: new THREE.Matrix4().fromArray( boneNode.Transform.a ),
					transformLink: new THREE.Matrix4().fromArray( boneNode.TransformLink.a ),
					linkMode: boneNode.Mode,

				};

				if ( 'Indexes' in boneNode ) {

					rawBone.indices = boneNode.Indexes.a;
					rawBone.weights = boneNode.Weights.a;

				}

				rawBones.push( rawBone );

			} );

			return {

				rawBones: rawBones,
				bones: []

			};

		}

		// The top level morph deformer node has type "BlendShape" and sub nodes have type "BlendShapeChannel"
		function parseMorphTargets( relationships, deformerNode, deformerNodes, connections ) {

			var rawMorphTargets = [];

			for ( var i = 0; i < relationships.children.length; i ++ ) {

				if ( i === 8 ) {

					console.warn( 'FBXLoader: maximum of 8 morph targets supported. Ignoring additional targets.' );

					break;

				}

				var child = relationships.children[ i ];

				var morphTargetNode = deformerNodes[ child.ID ];

				var rawMorphTarget = {

					name: morphTargetNode.attrName,
					initialWeight: morphTargetNode.DeformPercent,
					id: morphTargetNode.id,
					fullWeights: morphTargetNode.FullWeights.a

				};

				if ( morphTargetNode.attrType !== 'BlendShapeChannel' ) return;

				var targetRelationships = connections.get( parseInt( child.ID ) );

				targetRelationships.children.forEach( function ( child ) {

					if ( child.relationship === 'DeformPercent' ) {

						// TODO: animation of morph targets is currently unsupported
						rawMorphTarget.weightCurveID = child.ID;
						// weightCurve = FBXTree.Objects.AnimationCurveNode[ weightCurveID ];

					} else {

						rawMorphTarget.geoID = child.ID;
						// morphGeo = FBXTree.Objects.Geometry[ geoID ];

					}

				} );

				rawMorphTargets.push( rawMorphTarget );

			}

			return rawMorphTargets;

		}

		// Parse nodes in FBXTree.Objects.Geometry
		function parseGeometries( FBXTree, connections, deformers ) {

			var geometryMap = new Map();

			if ( 'Geometry' in FBXTree.Objects ) {

				var geoNodes = FBXTree.Objects.Geometry;

				for ( var nodeID in geoNodes ) {

					var relationships = connections.get( parseInt( nodeID ) );
					var geo = parseGeometry( FBXTree, relationships, geoNodes[ nodeID ], deformers );

					geometryMap.set( parseInt( nodeID ), geo );

				}

			}

			return geometryMap;

		}

		// Parse single node in FBXTree.Objects.Geometry
		function parseGeometry( FBXTree, relationships, geoNode, deformers ) {

			switch ( geoNode.attrType ) {

				case 'Mesh':
					return parseMeshGeometry( FBXTree, relationships, geoNode, deformers );
					break;

				case 'NurbsCurve':
					return parseNurbsGeometry( geoNode );
					break;

			}

		}

		// Parse single node mesh geometry in FBXTree.Objects.Geometry
		function parseMeshGeometry( FBXTree, relationships, geoNode, deformers ) {

			var skeletons = deformers.skeletons;
			var morphTargets = deformers.morphTargets;

			var modelNodes = relationships.parents.map( function ( parent ) {

				return FBXTree.Objects.Model[ parent.ID ];

			} );

			// don't create geometry if it is not associated with any models
			if ( modelNodes.length === 0 ) return;

			var skeleton = relationships.children.reduce( function ( skeleton, child ) {

				if ( skeletons[ child.ID ] !== undefined ) skeleton = skeletons[ child.ID ];

				return skeleton;

			}, null );

			var morphTarget = relationships.children.reduce( function ( morphTarget, child ) {

				if ( morphTargets[ child.ID ] !== undefined ) morphTarget = morphTargets[ child.ID ];

				return morphTarget;

			}, null );

			var preTransform = new THREE.Matrix4();

			// TODO: if there is more than one model associated with the geometry, AND the models have
			// different geometric transforms, then this will cause problems
			// if ( modelNodes.length > 1 ) { }

			// For now just assume one model and get the preRotations from that
			var modelNode = modelNodes[ 0 ];

			if ( 'GeometricRotation' in modelNode ) {

				var array = modelNode.GeometricRotation.value.map( THREE.Math.degToRad );
				array[ 3 ] = 'ZYX';

				preTransform.makeRotationFromEuler( new THREE.Euler().fromArray( array ) );

			}

			if ( 'GeometricTranslation' in modelNode ) {

				preTransform.setPosition( new THREE.Vector3().fromArray( modelNode.GeometricTranslation.value ) );

			}

			if ( 'GeometricScaling' in modelNode ) {

				preTransform.scale( new THREE.Vector3().fromArray( modelNode.GeometricScaling.value ) );

			}

			return genGeometry( FBXTree, geoNode, skeleton, morphTarget, preTransform );

		}

		// Generate a THREE.BufferGeometry from a node in FBXTree.Objects.Geometry
		function genGeometry( FBXTree, geoNode, skeleton, morphTarget, preTransform ) {

			var geo = new THREE.BufferGeometry();
			if ( geoNode.attrName ) geo.name = geoNode.attrName;

			var geoInfo = getGeoInfo( geoNode, skeleton );

			var buffers = genBuffers( geoInfo );

			var positionAttribute = new THREE.Float32BufferAttribute( buffers.vertex, 3 );

			preTransform.applyToBufferAttribute( positionAttribute );

			geo.addAttribute( 'position', positionAttribute );

			if ( buffers.colors.length > 0 ) {

				geo.addAttribute( 'color', new THREE.Float32BufferAttribute( buffers.colors, 3 ) );

			}

			if ( skeleton ) {

				geo.addAttribute( 'skinIndex', new THREE.Uint16BufferAttribute( buffers.weightsIndices, 4 ) );

				geo.addAttribute( 'skinWeight', new THREE.Float32BufferAttribute( buffers.vertexWeights, 4 ) );

				// used later to bind the skeleton to the model
				geo.FBX_Deformer = skeleton;

			}

			if ( buffers.normal.length > 0 ) {

				var normalAttribute = new THREE.Float32BufferAttribute( buffers.normal, 3 );

				var normalMatrix = new THREE.Matrix3().getNormalMatrix( preTransform );
				normalMatrix.applyToBufferAttribute( normalAttribute );

				geo.addAttribute( 'normal', normalAttribute );

			}

			buffers.uvs.forEach( function ( uvBuffer, i ) {

				// subsequent uv buffers are called 'uv1', 'uv2', ...
				var name = 'uv' + ( i + 1 ).toString();

				// the first uv buffer is just called 'uv'
				if ( i === 0 ) {

					name = 'uv';

				}

				geo.addAttribute( name, new THREE.Float32BufferAttribute( buffers.uvs[ i ], 2 ) );

			} );

			if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {

				// Convert the material indices of each vertex into rendering groups on the geometry.
				var prevMaterialIndex = buffers.materialIndex[ 0 ];
				var startIndex = 0;

				buffers.materialIndex.forEach( function ( currentIndex, i ) {

					if ( currentIndex !== prevMaterialIndex ) {

						geo.addGroup( startIndex, i - startIndex, prevMaterialIndex );

						prevMaterialIndex = currentIndex;
						startIndex = i;

					}

				} );

				// the loop above doesn't add the last group, do that here.
				if ( geo.groups.length > 0 ) {

					var lastGroup = geo.groups[ geo.groups.length - 1 ];
					var lastIndex = lastGroup.start + lastGroup.count;

					if ( lastIndex !== buffers.materialIndex.length ) {

						geo.addGroup( lastIndex, buffers.materialIndex.length - lastIndex, prevMaterialIndex );

					}

				}

				// case where there are multiple materials but the whole geometry is only
				// using one of them
				if ( geo.groups.length === 0 ) {

					geo.addGroup( 0, buffers.materialIndex.length, buffers.materialIndex[ 0 ] );

				}

			}

			addMorphTargets( FBXTree, geo, geoNode, morphTarget, preTransform );

			return geo;

		}

		function getGeoInfo( geoNode, skeleton ) {

			var geoInfo = {};

			geoInfo.vertexPositions = ( geoNode.Vertices !== undefined ) ? geoNode.Vertices.a : [];
			geoInfo.vertexIndices = ( geoNode.PolygonVertexIndex !== undefined ) ? geoNode.PolygonVertexIndex.a : [];

			if ( geoNode.LayerElementColor ) {

				geoInfo.color = getColors( geoNode.LayerElementColor[ 0 ] );

			}

			if ( geoNode.LayerElementMaterial ) {

				geoInfo.material = getMaterials( geoNode.LayerElementMaterial[ 0 ] );

			}

			if ( geoNode.LayerElementNormal ) {

				geoInfo.normal = getNormals( geoNode.LayerElementNormal[ 0 ] );

			}

			if ( geoNode.LayerElementUV ) {

				geoInfo.uv = [];

				var i = 0;
				while ( geoNode.LayerElementUV[ i ] ) {

					geoInfo.uv.push( getUVs( geoNode.LayerElementUV[ i ] ) );
					i ++;

				}

			}

			geoInfo.weightTable = {};

			if ( skeleton !== null ) {

				geoInfo.skeleton = skeleton;

				skeleton.rawBones.forEach( function ( rawBone, i ) {

					// loop over the bone's vertex indices and weights
					rawBone.indices.forEach( function ( index, j ) {

						if ( geoInfo.weightTable[ index ] === undefined ) geoInfo.weightTable[ index ] = [];

						geoInfo.weightTable[ index ].push( {

							id: i,
							weight: rawBone.weights[ j ],

						} );

					} );

				} );

			}

			return geoInfo;

		}

		function genBuffers( geoInfo ) {

			var buffers = {
				vertex: [],
				normal: [],
				colors: [],
				uvs: [],
				materialIndex: [],
				vertexWeights: [],
				weightsIndices: [],
			};

			var polygonIndex = 0;
			var faceLength = 0;
			var displayedWeightsWarning = false;

			// these will hold data for a single face
			var facePositionIndexes = [];
			var faceNormals = [];
			var faceColors = [];
			var faceUVs = [];
			var faceWeights = [];
			var faceWeightIndices = [];

			geoInfo.vertexIndices.forEach( function ( vertexIndex, polygonVertexIndex ) {

				var endOfFace = false;

				// Face index and vertex index arrays are combined in a single array
				// A cube with quad faces looks like this:
				// PolygonVertexIndex: *24 {
				//  a: 0, 1, 3, -3, 2, 3, 5, -5, 4, 5, 7, -7, 6, 7, 1, -1, 1, 7, 5, -4, 6, 0, 2, -5
				//  }
				// Negative numbers mark the end of a face - first face here is 0, 1, 3, -3
				// to find index of last vertex bit shift the index: ^ - 1
				if ( vertexIndex < 0 ) {

					vertexIndex = vertexIndex ^ - 1; // equivalent to ( x * -1 ) - 1
					endOfFace = true;

				}

				var weightIndices = [];
				var weights = [];

				facePositionIndexes.push( vertexIndex * 3, vertexIndex * 3 + 1, vertexIndex * 3 + 2 );

				if ( geoInfo.color ) {

					var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.color );

					faceColors.push( data[ 0 ], data[ 1 ], data[ 2 ] );

				}

				if ( geoInfo.skeleton ) {

					if ( geoInfo.weightTable[ vertexIndex ] !== undefined ) {

						geoInfo.weightTable[ vertexIndex ].forEach( function ( wt ) {

							weights.push( wt.weight );
							weightIndices.push( wt.id );

						} );


					}

					if ( weights.length > 4 ) {

						if ( ! displayedWeightsWarning ) {

							console.warn( 'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.' );
							displayedWeightsWarning = true;

						}

						var wIndex = [ 0, 0, 0, 0 ];
						var Weight = [ 0, 0, 0, 0 ];

						weights.forEach( function ( weight, weightIndex ) {

							var currentWeight = weight;
							var currentIndex = weightIndices[ weightIndex ];

							Weight.forEach( function ( comparedWeight, comparedWeightIndex, comparedWeightArray ) {

								if ( currentWeight > comparedWeight ) {

									comparedWeightArray[ comparedWeightIndex ] = currentWeight;
									currentWeight = comparedWeight;

									var tmp = wIndex[ comparedWeightIndex ];
									wIndex[ comparedWeightIndex ] = currentIndex;
									currentIndex = tmp;

								}

							} );

						} );

						weightIndices = wIndex;
						weights = Weight;

					}

					// if the weight array is shorter than 4 pad with 0s
					while ( weights.length < 4 ) {

						weights.push( 0 );
						weightIndices.push( 0 );

					}

					for ( var i = 0; i < 4; ++ i ) {

						faceWeights.push( weights[ i ] );
						faceWeightIndices.push( weightIndices[ i ] );

					}

				}

				if ( geoInfo.normal ) {

					var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.normal );

					faceNormals.push( data[ 0 ], data[ 1 ], data[ 2 ] );

				}

				if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {

					var materialIndex = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.material )[ 0 ];

				}

				if ( geoInfo.uv ) {

					geoInfo.uv.forEach( function ( uv, i ) {

						var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, uv );

						if ( faceUVs[ i ] === undefined ) {

							faceUVs[ i ] = [];

						}

						faceUVs[ i ].push( data[ 0 ] );
						faceUVs[ i ].push( data[ 1 ] );

					} );

				}

				faceLength ++;

				if ( endOfFace ) {

					genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength );

					polygonIndex ++;
					faceLength = 0;

					// reset arrays for the next face
					facePositionIndexes = [];
					faceNormals = [];
					faceColors = [];
					faceUVs = [];
					faceWeights = [];
					faceWeightIndices = [];

				}

			} );

			return buffers;

		}

		// Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris
		function genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ) {

			for ( var i = 2; i < faceLength; i ++ ) {

				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 0 ] ] );
				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 1 ] ] );
				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 2 ] ] );

				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 ] ] );
				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 1 ] ] );
				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 2 ] ] );

				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 ] ] );
				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 1 ] ] );
				buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 2 ] ] );

				if ( geoInfo.skeleton ) {

					buffers.vertexWeights.push( faceWeights[ 0 ] );
					buffers.vertexWeights.push( faceWeights[ 1 ] );
					buffers.vertexWeights.push( faceWeights[ 2 ] );
					buffers.vertexWeights.push( faceWeights[ 3 ] );

					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 ] );
					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 1 ] );
					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 2 ] );
					buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 3 ] );

					buffers.vertexWeights.push( faceWeights[ i * 4 ] );
					buffers.vertexWeights.push( faceWeights[ i * 4 + 1 ] );
					buffers.vertexWeights.push( faceWeights[ i * 4 + 2 ] );
					buffers.vertexWeights.push( faceWeights[ i * 4 + 3 ] );

					buffers.weightsIndices.push( faceWeightIndices[ 0 ] );
					buffers.weightsIndices.push( faceWeightIndices[ 1 ] );
					buffers.weightsIndices.push( faceWeightIndices[ 2 ] );
					buffers.weightsIndices.push( faceWeightIndices[ 3 ] );

					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 ] );
					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 1 ] );
					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 2 ] );
					buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 3 ] );

					buffers.weightsIndices.push( faceWeightIndices[ i * 4 ] );
					buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 1 ] );
					buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 2 ] );
					buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 3 ] );

				}

				if ( geoInfo.color ) {

					buffers.colors.push( faceColors[ 0 ] );
					buffers.colors.push( faceColors[ 1 ] );
					buffers.colors.push( faceColors[ 2 ] );

					buffers.colors.push( faceColors[ ( i - 1 ) * 3 ] );
					buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 1 ] );
					buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 2 ] );

					buffers.colors.push( faceColors[ i * 3 ] );
					buffers.colors.push( faceColors[ i * 3 + 1 ] );
					buffers.colors.push( faceColors[ i * 3 + 2 ] );

				}

				if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {

					buffers.materialIndex.push( materialIndex );
					buffers.materialIndex.push( materialIndex );
					buffers.materialIndex.push( materialIndex );

				}

				if ( geoInfo.normal ) {

					buffers.normal.push( faceNormals[ 0 ] );
					buffers.normal.push( faceNormals[ 1 ] );
					buffers.normal.push( faceNormals[ 2 ] );

					buffers.normal.push( faceNormals[ ( i - 1 ) * 3 ] );
					buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 1 ] );
					buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 2 ] );

					buffers.normal.push( faceNormals[ i * 3 ] );
					buffers.normal.push( faceNormals[ i * 3 + 1 ] );
					buffers.normal.push( faceNormals[ i * 3 + 2 ] );

				}

				if ( geoInfo.uv ) {

					geoInfo.uv.forEach( function ( uv, j ) {

						if ( buffers.uvs[ j ] === undefined ) buffers.uvs[ j ] = [];

						buffers.uvs[ j ].push( faceUVs[ j ][ 0 ] );
						buffers.uvs[ j ].push( faceUVs[ j ][ 1 ] );

						buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 ] );
						buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 + 1 ] );

						buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 ] );
						buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 + 1 ] );

					} );

				}

			}

		}

		function addMorphTargets( FBXTree, parentGeo, parentGeoNode, morphTarget, preTransform ) {

			if ( morphTarget === null ) return;

			parentGeo.morphAttributes.position = [];
			parentGeo.morphAttributes.normal = [];

			morphTarget.rawTargets.forEach( function ( rawTarget ) {

				var morphGeoNode = FBXTree.Objects.Geometry[ rawTarget.geoID ];

				if ( morphGeoNode !== undefined ) {

					genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform );

				}

			} );

		}

		// a morph geometry node is similar to a standard  node, and the node is also contained
		// in FBXTree.Objects.Geometry, however it can only have attributes for position, normal
		// and a special attribute Index defining which vertices of the original geometry are affected
		// Normal and position attributes only have data for the vertices that are affected by the morph
		function genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform ) {

			var morphGeo = new THREE.BufferGeometry();
			if ( morphGeoNode.attrName ) morphGeo.name = morphGeoNode.attrName;

			var vertexIndices = ( parentGeoNode.PolygonVertexIndex !== undefined ) ? parentGeoNode.PolygonVertexIndex.a : [];

			// make a copy of the parent's vertex positions
			var vertexPositions = ( parentGeoNode.Vertices !== undefined ) ? parentGeoNode.Vertices.a.slice() : [];

			var morphPositions = ( morphGeoNode.Vertices !== undefined ) ? morphGeoNode.Vertices.a : [];
			var indices = ( morphGeoNode.Indexes !== undefined ) ? morphGeoNode.Indexes.a : [];

			for ( var i = 0; i < indices.length; i ++ ) {

				var morphIndex = indices[ i ] * 3;

				// FBX format uses blend shapes rather than morph targets. This can be converted
				// by additively combining the blend shape positions with the original geometry's positions
				vertexPositions[ morphIndex ] += morphPositions[ i * 3 ];
				vertexPositions[ morphIndex + 1 ] += morphPositions[ i * 3 + 1 ];
				vertexPositions[ morphIndex + 2 ] += morphPositions[ i * 3 + 2 ];

			}

			// TODO: add morph normal support
			var morphGeoInfo = {
				vertexIndices: vertexIndices,
				vertexPositions: vertexPositions,
			};

			var morphBuffers = genBuffers( morphGeoInfo );

			var positionAttribute = new THREE.Float32BufferAttribute( morphBuffers.vertex, 3 );
			positionAttribute.name = morphGeoNode.attrName;

			preTransform.applyToBufferAttribute( positionAttribute );

			parentGeo.morphAttributes.position.push( positionAttribute );

		}

		// Parse normal from FBXTree.Objects.Geometry.LayerElementNormal if it exists
		function getNormals( NormalNode ) {

			var mappingType = NormalNode.MappingInformationType;
			var referenceType = NormalNode.ReferenceInformationType;
			var buffer = NormalNode.Normals.a;
			var indexBuffer = [];
			if ( referenceType === 'IndexToDirect' ) {

				if ( 'NormalIndex' in NormalNode ) {

					indexBuffer = NormalNode.NormalIndex.a;

				} else if ( 'NormalsIndex' in NormalNode ) {

					indexBuffer = NormalNode.NormalsIndex.a;

				}

			}

			return {
				dataSize: 3,
				buffer: buffer,
				indices: indexBuffer,
				mappingType: mappingType,
				referenceType: referenceType
			};

		}

		// Parse UVs from FBXTree.Objects.Geometry.LayerElementUV if it exists
		function getUVs( UVNode ) {

			var mappingType = UVNode.MappingInformationType;
			var referenceType = UVNode.ReferenceInformationType;
			var buffer = UVNode.UV.a;
			var indexBuffer = [];
			if ( referenceType === 'IndexToDirect' ) {

				indexBuffer = UVNode.UVIndex.a;

			}

			return {
				dataSize: 2,
				buffer: buffer,
				indices: indexBuffer,
				mappingType: mappingType,
				referenceType: referenceType
			};

		}

		// Parse Vertex Colors from FBXTree.Objects.Geometry.LayerElementColor if it exists
		function getColors( ColorNode ) {

			var mappingType = ColorNode.MappingInformationType;
			var referenceType = ColorNode.ReferenceInformationType;
			var buffer = ColorNode.Colors.a;
			var indexBuffer = [];
			if ( referenceType === 'IndexToDirect' ) {

				indexBuffer = ColorNode.ColorIndex.a;

			}

			return {
				dataSize: 4,
				buffer: buffer,
				indices: indexBuffer,
				mappingType: mappingType,
				referenceType: referenceType
			};

		}

		// Parse mapping and material data in FBXTree.Objects.Geometry.LayerElementMaterial if it exists
		function getMaterials( MaterialNode ) {

			var mappingType = MaterialNode.MappingInformationType;
			var referenceType = MaterialNode.ReferenceInformationType;

			if ( mappingType === 'NoMappingInformation' ) {

				return {
					dataSize: 1,
					buffer: [ 0 ],
					indices: [ 0 ],
					mappingType: 'AllSame',
					referenceType: referenceType
				};

			}

			var materialIndexBuffer = MaterialNode.Materials.a;

			// Since materials are stored as indices, there's a bit of a mismatch between FBX and what
			// we expect.So we create an intermediate buffer that points to the index in the buffer,
			// for conforming with the other functions we've written for other data.
			var materialIndices = [];

			for ( var i = 0; i < materialIndexBuffer.length; ++ i ) {

				materialIndices.push( i );

			}

			return {
				dataSize: 1,
				buffer: materialIndexBuffer,
				indices: materialIndices,
				mappingType: mappingType,
				referenceType: referenceType
			};

		}

		var dataArray = [];

		function getData( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) {

			var index;

			switch ( infoObject.mappingType ) {

				case 'ByPolygonVertex' :
					index = polygonVertexIndex;
					break;
				case 'ByPolygon' :
					index = polygonIndex;
					break;
				case 'ByVertice' :
					index = vertexIndex;
					break;
				case 'AllSame' :
					index = infoObject.indices[ 0 ];
					break;
				default :
					console.warn( 'THREE.FBXLoader: unknown attribute mapping type ' + infoObject.mappingType );

			}

			if ( infoObject.referenceType === 'IndexToDirect' ) index = infoObject.indices[ index ];

			var from = index * infoObject.dataSize;
			var to = from + infoObject.dataSize;

			return slice( dataArray, infoObject.buffer, from, to );

		}

		// Generate a NurbGeometry from a node in FBXTree.Objects.Geometry
		function parseNurbsGeometry( geoNode ) {

			if ( THREE.NURBSCurve === undefined ) {

				console.error( 'THREE.FBXLoader: The loader relies on THREE.NURBSCurve for any nurbs present in the model. Nurbs will show up as empty geometry.' );
				return new THREE.BufferGeometry();

			}

			var order = parseInt( geoNode.Order );

			if ( isNaN( order ) ) {

				console.error( 'THREE.FBXLoader: Invalid Order %s given for geometry ID: %s', geoNode.Order, geoNode.id );
				return new THREE.BufferGeometry();

			}

			var degree = order - 1;

			var knots = geoNode.KnotVector.a;
			var controlPoints = [];
			var pointsValues = geoNode.Points.a;

			for ( var i = 0, l = pointsValues.length; i < l; i += 4 ) {

				controlPoints.push( new THREE.Vector4().fromArray( pointsValues, i ) );

			}

			var startKnot, endKnot;

			if ( geoNode.Form === 'Closed' ) {

				controlPoints.push( controlPoints[ 0 ] );

			} else if ( geoNode.Form === 'Periodic' ) {

				startKnot = degree;
				endKnot = knots.length - 1 - startKnot;

				for ( var i = 0; i < degree; ++ i ) {

					controlPoints.push( controlPoints[ i ] );

				}

			}

			var curve = new THREE.NURBSCurve( degree, knots, controlPoints, startKnot, endKnot );
			var vertices = curve.getPoints( controlPoints.length * 7 );

			var positions = new Float32Array( vertices.length * 3 );

			vertices.forEach( function ( vertex, i ) {

				vertex.toArray( positions, i * 3 );

			} );

			var geometry = new THREE.BufferGeometry();
			geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );

			return geometry;

		}

		// create the main THREE.Group() to be returned by the loader
		function parseScene( FBXTree, connections, skeletons, geometryMap, materialMap ) {

			var sceneGraph = new THREE.Group();

			var modelMap = parseModels( FBXTree, skeletons, geometryMap, materialMap, connections );

			var modelNodes = FBXTree.Objects.Model;

			modelMap.forEach( function ( model ) {

				var modelNode = modelNodes[ model.ID ];
				setLookAtProperties( FBXTree, model, modelNode, connections, sceneGraph );

				var parentConnections = connections.get( model.ID ).parents;

				parentConnections.forEach( function ( connection ) {

					var parent = modelMap.get( connection.ID );
					if ( parent !== undefined ) parent.add( model );

				} );

				if ( model.parent === null ) {

					sceneGraph.add( model );

				}


			} );

			bindSkeleton( FBXTree, skeletons, geometryMap, modelMap, connections );

			addAnimations( FBXTree, connections, sceneGraph );

			createAmbientLight( FBXTree, sceneGraph );

			return sceneGraph;

		}

		// parse nodes in FBXTree.Objects.Model
		function parseModels( FBXTree, skeletons, geometryMap, materialMap, connections ) {

			var modelMap = new Map();
			var modelNodes = FBXTree.Objects.Model;

			for ( var nodeID in modelNodes ) {

				var id = parseInt( nodeID );
				var node = modelNodes[ nodeID ];
				var relationships = connections.get( id );

				var model = buildSkeleton( relationships, skeletons, id, node.attrName );

				if ( ! model ) {

					switch ( node.attrType ) {

						case 'Camera':
							model = createCamera( FBXTree, relationships );
							break;
						case 'Light':
							model = createLight( FBXTree, relationships );
							break;
						case 'Mesh':
							model = createMesh( FBXTree, relationships, geometryMap, materialMap );
							break;
						case 'NurbsCurve':
							model = createCurve( relationships, geometryMap );
							break;
						case 'LimbNode': // usually associated with a Bone, however if a Bone was not created we'll make a Group instead
						case 'Null':
						default:
							model = new THREE.Group();
							break;

					}

					model.name = THREE.PropertyBinding.sanitizeNodeName( node.attrName );
					model.ID = id;

				}

				setModelTransforms( FBXTree, model, node );
				modelMap.set( id, model );

			}

			return modelMap;

		}

		function buildSkeleton( relationships, skeletons, id, name ) {

			var bone = null;

			relationships.parents.forEach( function ( parent ) {

				for ( var ID in skeletons ) {

					var skeleton = skeletons[ ID ];

					skeleton.rawBones.forEach( function ( rawBone, i ) {

						if ( rawBone.ID === parent.ID ) {

							var subBone = bone;
							bone = new THREE.Bone();
							bone.matrixWorld.copy( rawBone.transformLink );

							// set name and id here - otherwise in cases where "subBone" is created it will not have a name / id
							bone.name = THREE.PropertyBinding.sanitizeNodeName( name );
							bone.ID = id;

							skeleton.bones[ i ] = bone;

							// In cases where a bone is shared between multiple meshes
							// duplicate the bone here and and it as a child of the first bone
							if ( subBone !== null ) {

								bone.add( subBone );

							}

						}

					} );

				}

			} );

			return bone;

		}

		// create a THREE.PerspectiveCamera or THREE.OrthographicCamera
		function createCamera( FBXTree, relationships ) {

			var model;
			var cameraAttribute;

			relationships.children.forEach( function ( child ) {

				var attr = FBXTree.Objects.NodeAttribute[ child.ID ];

				if ( attr !== undefined ) {

					cameraAttribute = attr;

				}

			} );

			if ( cameraAttribute === undefined ) {

				model = new THREE.Object3D();

			} else {

				var type = 0;
				if ( cameraAttribute.CameraProjectionType !== undefined && cameraAttribute.CameraProjectionType.value === 1 ) {

					type = 1;

				}

				var nearClippingPlane = 1;
				if ( cameraAttribute.NearPlane !== undefined ) {

					nearClippingPlane = cameraAttribute.NearPlane.value / 1000;

				}

				var farClippingPlane = 1000;
				if ( cameraAttribute.FarPlane !== undefined ) {

					farClippingPlane = cameraAttribute.FarPlane.value / 1000;

				}


				var width = window.innerWidth;
				var height = window.innerHeight;

				if ( cameraAttribute.AspectWidth !== undefined && cameraAttribute.AspectHeight !== undefined ) {

					width = cameraAttribute.AspectWidth.value;
					height = cameraAttribute.AspectHeight.value;

				}

				var aspect = width / height;

				var fov = 45;
				if ( cameraAttribute.FieldOfView !== undefined ) {

					fov = cameraAttribute.FieldOfView.value;

				}

				var focalLength = cameraAttribute.FocalLength ? cameraAttribute.FocalLength.value : null;

				switch ( type ) {

					case 0: // Perspective
						model = new THREE.PerspectiveCamera( fov, aspect, nearClippingPlane, farClippingPlane );
						if ( focalLength !== null ) model.setFocalLength( focalLength );
						break;

					case 1: // Orthographic
						model = new THREE.OrthographicCamera( - width / 2, width / 2, height / 2, - height / 2, nearClippingPlane, farClippingPlane );
						break;

					default:
						console.warn( 'THREE.FBXLoader: Unknown camera type ' + type + '.' );
						model = new THREE.Object3D();
						break;

				}

			}

			return model;

		}

		// Create a THREE.DirectionalLight, THREE.PointLight or THREE.SpotLight
		function createLight( FBXTree, relationships ) {

			var model;
			var lightAttribute;

			relationships.children.forEach( function ( child ) {

				var attr = FBXTree.Objects.NodeAttribute[ child.ID ];

				if ( attr !== undefined ) {

					lightAttribute = attr;

				}

			} );

			if ( lightAttribute === undefined ) {

				model = new THREE.Object3D();

			} else {

				var type;

				// LightType can be undefined for Point lights
				if ( lightAttribute.LightType === undefined ) {

					type = 0;

				} else {

					type = lightAttribute.LightType.value;

				}

				var color = 0xffffff;

				if ( lightAttribute.Color !== undefined ) {

					color = new THREE.Color().fromArray( lightAttribute.Color.value );

				}

				var intensity = ( lightAttribute.Intensity === undefined ) ? 1 : lightAttribute.Intensity.value / 100;

				// light disabled
				if ( lightAttribute.CastLightOnObject !== undefined && lightAttribute.CastLightOnObject.value === 0 ) {

					intensity = 0;

				}

				var distance = 0;
				if ( lightAttribute.FarAttenuationEnd !== undefined ) {

					if ( lightAttribute.EnableFarAttenuation !== undefined && lightAttribute.EnableFarAttenuation.value === 0 ) {

						distance = 0;

					} else {

						distance = lightAttribute.FarAttenuationEnd.value;

					}

				}

				// TODO: could this be calculated linearly from FarAttenuationStart to FarAttenuationEnd?
				var decay = 1;

				switch ( type ) {

					case 0: // Point
						model = new THREE.PointLight( color, intensity, distance, decay );
						break;

					case 1: // Directional
						model = new THREE.DirectionalLight( color, intensity );
						break;

					case 2: // Spot
						var angle = Math.PI / 3;

						if ( lightAttribute.InnerAngle !== undefined ) {

							angle = THREE.Math.degToRad( lightAttribute.InnerAngle.value );

						}

						var penumbra = 0;
						if ( lightAttribute.OuterAngle !== undefined ) {

							// TODO: this is not correct - FBX calculates outer and inner angle in degrees
							// with OuterAngle > InnerAngle && OuterAngle <= Math.PI
							// while three.js uses a penumbra between (0, 1) to attenuate the inner angle
							penumbra = THREE.Math.degToRad( lightAttribute.OuterAngle.value );
							penumbra = Math.max( penumbra, 1 );

						}

						model = new THREE.SpotLight( color, intensity, distance, angle, penumbra, decay );
						break;

					default:
						console.warn( 'THREE.FBXLoader: Unknown light type ' + lightAttribute.LightType.value + ', defaulting to a THREE.PointLight.' );
						model = new THREE.PointLight( color, intensity );
						break;

				}

				if ( lightAttribute.CastShadows !== undefined && lightAttribute.CastShadows.value === 1 ) {

					model.castShadow = true;

				}

			}

			return model;

		}

		function createMesh( FBXTree, relationships, geometryMap, materialMap ) {

			var model;
			var geometry = null;
			var material = null;
			var materials = [];

			// get geometry and materials(s) from connections
			relationships.children.forEach( function ( child ) {

				if ( geometryMap.has( child.ID ) ) {

					geometry = geometryMap.get( child.ID );

				}

				if ( materialMap.has( child.ID ) ) {

					materials.push( materialMap.get( child.ID ) );

				}

			} );

			if ( materials.length > 1 ) {

				material = materials;

			} else if ( materials.length > 0 ) {

				material = materials[ 0 ];

			} else {

				material = new THREE.MeshPhongMaterial( { color: 0xcccccc } );
				materials.push( material );

			}

			if ( 'color' in geometry.attributes ) {

				materials.forEach( function ( material ) {

					material.vertexColors = THREE.VertexColors;

				} );

			}

			if ( geometry.FBX_Deformer ) {

				materials.forEach( function ( material ) {

					material.skinning = true;

				} );

				model = new THREE.SkinnedMesh( geometry, material );

			} else {

				model = new THREE.Mesh( geometry, material );

			}

			return model;

		}

		function createCurve( relationships, geometryMap ) {

			var geometry = relationships.children.reduce( function ( geo, child ) {

				if ( geometryMap.has( child.ID ) ) geo = geometryMap.get( child.ID );

				return geo;

			}, null );

			// FBX does not list materials for Nurbs lines, so we'll just put our own in here.
			var material = new THREE.LineBasicMaterial( { color: 0x3300ff, linewidth: 1 } );
			return new THREE.Line( geometry, material );

		}

		// Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light
		function createAmbientLight( FBXTree, sceneGraph ) {

			if ( 'GlobalSettings' in FBXTree && 'AmbientColor' in FBXTree.GlobalSettings ) {

				var ambientColor = FBXTree.GlobalSettings.AmbientColor.value;
				var r = ambientColor[ 0 ];
				var g = ambientColor[ 1 ];
				var b = ambientColor[ 2 ];

				if ( r !== 0 || g !== 0 || b !== 0 ) {

					var color = new THREE.Color( r, g, b );
					sceneGraph.add( new THREE.AmbientLight( color, 1 ) );

				}

			}

		}

		function setLookAtProperties( FBXTree, model, modelNode, connections, sceneGraph ) {

			if ( 'LookAtProperty' in modelNode ) {

				var children = connections.get( model.ID ).children;

				children.forEach( function ( child ) {

					if ( child.relationship === 'LookAtProperty' ) {

						var lookAtTarget = FBXTree.Objects.Model[ child.ID ];

						if ( 'Lcl_Translation' in lookAtTarget ) {

							var pos = lookAtTarget.Lcl_Translation.value;

							// DirectionalLight, SpotLight
							if ( model.target !== undefined ) {

								model.target.position.fromArray( pos );
								sceneGraph.add( model.target );

							} else { // Cameras and other Object3Ds

								model.lookAt( new THREE.Vector3().fromArray( pos ) );

							}

						}

					}

				} );

			}

		}

		// parse the model node for transform details and apply them to the model
		function setModelTransforms( FBXTree, model, modelNode ) {

			// http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_class_fbx_euler_html
			if ( 'RotationOrder' in modelNode ) {

				var enums = [
					'XYZ', // default
					'XZY',
					'YZX',
					'ZXY',
					'YXZ',
					'ZYX',
					'SphericXYZ',
				];

				var value = parseInt( modelNode.RotationOrder.value, 10 );

				if ( value > 0 && value < 6 ) {

					// model.rotation.order = enums[ value ];

					// Note: Euler order other than XYZ is currently not supported, so just display a warning for now
					console.warn( 'THREE.FBXLoader: unsupported Euler Order: %s. Currently only XYZ order is supported. Animations and rotations may be incorrect.', enums[ value ] );

				} else if ( value === 6 ) {

					console.warn( 'THREE.FBXLoader: unsupported Euler Order: Spherical XYZ. Animations and rotations may be incorrect.' );

				}

			}

			if ( 'Lcl_Translation' in modelNode ) {

				model.position.fromArray( modelNode.Lcl_Translation.value );

			}

			if ( 'Lcl_Rotation' in modelNode ) {

				var rotation = modelNode.Lcl_Rotation.value.map( THREE.Math.degToRad );
				rotation.push( 'ZYX' );
				model.quaternion.setFromEuler( new THREE.Euler().fromArray( rotation ) );

			}

			if ( 'Lcl_Scaling' in modelNode ) {

				model.scale.fromArray( modelNode.Lcl_Scaling.value );

			}

			if ( 'PreRotation' in modelNode ) {

				var array = modelNode.PreRotation.value.map( THREE.Math.degToRad );
				array[ 3 ] = 'ZYX';

				var preRotations = new THREE.Euler().fromArray( array );

				preRotations = new THREE.Quaternion().setFromEuler( preRotations );
				model.quaternion.premultiply( preRotations );

			}

		}

		function bindSkeleton( FBXTree, skeletons, geometryMap, modelMap, connections ) {

			var bindMatrices = parsePoseNodes( FBXTree );

			for ( var ID in skeletons ) {

				var skeleton = skeletons[ ID ];

				var parents = connections.get( parseInt( skeleton.ID ) ).parents;

				parents.forEach( function ( parent ) {

					if ( geometryMap.has( parent.ID ) ) {

						var geoID = parent.ID;
						var geoRelationships = connections.get( geoID );

						geoRelationships.parents.forEach( function ( geoConnParent ) {

							if ( modelMap.has( geoConnParent.ID ) ) {

								var model = modelMap.get( geoConnParent.ID );

								model.bind( new THREE.Skeleton( skeleton.bones ), bindMatrices[ geoConnParent.ID ] );

							}

						} );

					}

				} );

			}

		}

		function parsePoseNodes( FBXTree ) {

			var bindMatrices = {};

			if ( 'Pose' in FBXTree.Objects ) {

				var BindPoseNode = FBXTree.Objects.Pose;

				for ( var nodeID in BindPoseNode ) {

					if ( BindPoseNode[ nodeID ].attrType === 'BindPose' ) {

						var poseNodes = BindPoseNode[ nodeID ].PoseNode;

						if ( Array.isArray( poseNodes ) ) {

							poseNodes.forEach( function ( poseNode ) {

								bindMatrices[ poseNode.Node ] = new THREE.Matrix4().fromArray( poseNode.Matrix.a );

							} );

						} else {

							bindMatrices[ poseNodes.Node ] = new THREE.Matrix4().fromArray( poseNodes.Matrix.a );

						}

					}

				}

			}

			return bindMatrices;

		}

		function parseAnimations( FBXTree, connections ) {

			// since the actual transformation data is stored in FBXTree.Objects.AnimationCurve,
			// if this is undefined we can safely assume there are no animations
			if ( FBXTree.Objects.AnimationCurve === undefined ) return undefined;

			var curveNodesMap = parseAnimationCurveNodes( FBXTree );

			parseAnimationCurves( FBXTree, connections, curveNodesMap );

			var layersMap = parseAnimationLayers( FBXTree, connections, curveNodesMap );
			var rawClips = parseAnimStacks( FBXTree, connections, layersMap );

			return rawClips;

		}

		// parse nodes in FBXTree.Objects.AnimationCurveNode
		// each AnimationCurveNode holds data for an animation transform for a model (e.g. left arm rotation )
		// and is referenced by an AnimationLayer
		function parseAnimationCurveNodes( FBXTree ) {

			var rawCurveNodes = FBXTree.Objects.AnimationCurveNode;

			var curveNodesMap = new Map();

			for ( var nodeID in rawCurveNodes ) {

				var rawCurveNode = rawCurveNodes[ nodeID ];

				if ( rawCurveNode.attrName.match( /S|R|T/ ) !== null ) {

					var curveNode = {

						id: rawCurveNode.id,
						attr: rawCurveNode.attrName,
						curves: {},

					};

					curveNodesMap.set( curveNode.id, curveNode );

				}

			}

			return curveNodesMap;

		}

		// parse nodes in FBXTree.Objects.AnimationCurve and connect them up to
		// previously parsed AnimationCurveNodes. Each AnimationCurve holds data for a single animated
		// axis ( e.g. times and values of x rotation)
		function parseAnimationCurves( FBXTree, connections, curveNodesMap ) {

			var rawCurves = FBXTree.Objects.AnimationCurve;

			for ( var nodeID in rawCurves ) {

				var animationCurve = {

					id: rawCurves[ nodeID ].id,
					times: rawCurves[ nodeID ].KeyTime.a.map( convertFBXTimeToSeconds ),
					values: rawCurves[ nodeID ].KeyValueFloat.a,

				};

				var relationships = connections.get( animationCurve.id );

				if ( relationships !== undefined ) {

					var animationCurveID = relationships.parents[ 0 ].ID;
					var animationCurveRelationship = relationships.parents[ 0 ].relationship;

					if ( animationCurveRelationship.match( /X/ ) ) {

						curveNodesMap.get( animationCurveID ).curves[ 'x' ] = animationCurve;

					} else if ( animationCurveRelationship.match( /Y/ ) ) {

						curveNodesMap.get( animationCurveID ).curves[ 'y' ] = animationCurve;

					} else if ( animationCurveRelationship.match( /Z/ ) ) {

						curveNodesMap.get( animationCurveID ).curves[ 'z' ] = animationCurve;

					}

				}

			}

		}

		// parse nodes in FBXTree.Objects.AnimationLayer. Each layers holds references
		// to various AnimationCurveNodes and is referenced by an AnimationStack node
		// note: theoretically a stack can have multiple layers, however in practice there always seems to be one per stack
		function parseAnimationLayers( FBXTree, connections, curveNodesMap ) {

			var rawLayers = FBXTree.Objects.AnimationLayer;

			var layersMap = new Map();

			for ( var nodeID in rawLayers ) {

				var layerCurveNodes = [];

				var connection = connections.get( parseInt( nodeID ) );

				if ( connection !== undefined ) {

					// all the animationCurveNodes used in the layer
					var children = connection.children;

					children.forEach( function ( child, i ) {

						if ( curveNodesMap.has( child.ID ) ) {

							var curveNode = curveNodesMap.get( child.ID );

							// check that the curves are defined for at least one axis, otherwise ignore the curveNode
							if ( curveNode.curves.x !== undefined || curveNode.curves.y !== undefined || curveNode.curves.z !== undefined ) {

								if ( layerCurveNodes[ i ] === undefined ) {

									var modelID;

									connections.get( child.ID ).parents.forEach( function ( parent ) {

										if ( parent.relationship !== undefined ) modelID = parent.ID;

									} );

									var rawModel = FBXTree.Objects.Model[ modelID.toString() ];

									var node = {

										modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ),
										initialPosition: [ 0, 0, 0 ],
										initialRotation: [ 0, 0, 0 ],
										initialScale: [ 1, 1, 1 ],

									};

									if ( 'Lcl_Translation' in rawModel ) node.initialPosition = rawModel.Lcl_Translation.value;

									if ( 'Lcl_Rotation' in rawModel ) node.initialRotation = rawModel.Lcl_Rotation.value;

									if ( 'Lcl_Scaling' in rawModel ) node.initialScale = rawModel.Lcl_Scaling.value;

									// if the animated model is pre rotated, we'll have to apply the pre rotations to every
									// animation value as well
									if ( 'PreRotation' in rawModel ) node.preRotations = rawModel.PreRotation.value;

									layerCurveNodes[ i ] = node;

								}

								layerCurveNodes[ i ][ curveNode.attr ] = curveNode;

							}



						}

					} );

					layersMap.set( parseInt( nodeID ), layerCurveNodes );

				}

			}

			return layersMap;

		}

		// parse nodes in FBXTree.Objects.AnimationStack. These are the top level node in the animation
		// hierarchy. Each Stack node will be used to create a THREE.AnimationClip
		function parseAnimStacks( FBXTree, connections, layersMap ) {

			var rawStacks = FBXTree.Objects.AnimationStack;

			// connect the stacks (clips) up to the layers
			var rawClips = {};

			for ( var nodeID in rawStacks ) {

				var children = connections.get( parseInt( nodeID ) ).children;

				if ( children.length > 1 ) {

					// it seems like stacks will always be associated with a single layer. But just in case there are files
					// where there are multiple layers per stack, we'll display a warning
					console.warn( 'THREE.FBXLoader: Encountered an animation stack with multiple layers, this is currently not supported. Ignoring subsequent layers.' );

				}

				var layer = layersMap.get( children[ 0 ].ID );

				rawClips[ nodeID ] = {

					name: rawStacks[ nodeID ].attrName,
					layer: layer,

				};

			}

			return rawClips;

		}

		// take raw animation data from parseAnimations and connect it up to the loaded models
		function addAnimations( FBXTree, connections, sceneGraph ) {

			sceneGraph.animations = [];

			var rawClips = parseAnimations( FBXTree, connections );

			if ( rawClips === undefined ) return;


			for ( var key in rawClips ) {

				var rawClip = rawClips[ key ];

				var clip = addClip( rawClip );

				sceneGraph.animations.push( clip );

			}

		}

		function addClip( rawClip ) {

			var tracks = [];

			rawClip.layer.forEach( function ( rawTracks ) {

				tracks = tracks.concat( generateTracks( rawTracks ) );

			} );

			return new THREE.AnimationClip( rawClip.name, - 1, tracks );

		}

		function generateTracks( rawTracks ) {

			var tracks = [];

			if ( rawTracks.T !== undefined && Object.keys( rawTracks.T.curves ).length > 0 ) {

				var positionTrack = generateVectorTrack( rawTracks.modelName, rawTracks.T.curves, rawTracks.initialPosition, 'position' );
				if ( positionTrack !== undefined ) tracks.push( positionTrack );

			}

			if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) {

				var rotationTrack = generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, rawTracks.initialRotation, rawTracks.preRotations );
				if ( rotationTrack !== undefined ) tracks.push( rotationTrack );

			}

			if ( rawTracks.S !== undefined && Object.keys( rawTracks.S.curves ).length > 0 ) {

				var scaleTrack = generateVectorTrack( rawTracks.modelName, rawTracks.S.curves, rawTracks.initialScale, 'scale' );
				if ( scaleTrack !== undefined ) tracks.push( scaleTrack );

			}

			return tracks;

		}

		function generateVectorTrack( modelName, curves, initialValue, type ) {

			var times = getTimesForAllAxes( curves );
			var values = getKeyframeTrackValues( times, curves, initialValue );

			return new THREE.VectorKeyframeTrack( modelName + '.' + type, times, values );

		}

		function generateRotationTrack( modelName, curves, initialValue, preRotations ) {

			if ( curves.x !== undefined ) {

				interpolateRotations( curves.x );
				curves.x.values = curves.x.values.map( THREE.Math.degToRad );

			}
			if ( curves.y !== undefined ) {

				interpolateRotations( curves.y );
				curves.y.values = curves.y.values.map( THREE.Math.degToRad );

			}
			if ( curves.z !== undefined ) {

				interpolateRotations( curves.z );
				curves.z.values = curves.z.values.map( THREE.Math.degToRad );

			}

			var times = getTimesForAllAxes( curves );
			var values = getKeyframeTrackValues( times, curves, initialValue );

			if ( preRotations !== undefined ) {

				preRotations = preRotations.map( THREE.Math.degToRad );
				preRotations.push( 'ZYX' );

				preRotations = new THREE.Euler().fromArray( preRotations );
				preRotations = new THREE.Quaternion().setFromEuler( preRotations );

			}

			var quaternion = new THREE.Quaternion();
			var euler = new THREE.Euler();

			var quaternionValues = [];

			for ( var i = 0; i < values.length; i += 3 ) {

				euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], 'ZYX' );

				quaternion.setFromEuler( euler );

				if ( preRotations !== undefined )quaternion.premultiply( preRotations );

				quaternion.toArray( quaternionValues, ( i / 3 ) * 4 );

			}

			return new THREE.QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues );

		}

		function getKeyframeTrackValues( times, curves, initialValue ) {

			var prevValue = initialValue;

			var values = [];

			var xIndex = - 1;
			var yIndex = - 1;
			var zIndex = - 1;

			times.forEach( function ( time ) {

				if ( curves.x ) xIndex = curves.x.times.indexOf( time );
				if ( curves.y ) yIndex = curves.y.times.indexOf( time );
				if ( curves.z ) zIndex = curves.z.times.indexOf( time );

				// if there is an x value defined for this frame, use that
				if ( xIndex !== - 1 ) {

					var xValue = curves.x.values[ xIndex ];
					values.push( xValue );
					prevValue[ 0 ] = xValue;

				} else {

					// otherwise use the x value from the previous frame
					values.push( prevValue[ 0 ] );

				}

				if ( yIndex !== - 1 ) {

					var yValue = curves.y.values[ yIndex ];
					values.push( yValue );
					prevValue[ 1 ] = yValue;

				} else {

					values.push( prevValue[ 1 ] );

				}

				if ( zIndex !== - 1 ) {

					var zValue = curves.z.values[ zIndex ];
					values.push( zValue );
					prevValue[ 2 ] = zValue;

				} else {

					values.push( prevValue[ 2 ] );

				}

			} );

			return values;

		}

		// For all animated objects, times are defined separately for each axis
		// Here we'll combine the times into one sorted array without duplicates
		function getTimesForAllAxes( curves ) {

			var times = [];

			// first join together the times for each axis, if defined
			if ( curves.x !== undefined ) times = times.concat( curves.x.times );
			if ( curves.y !== undefined ) times = times.concat( curves.y.times );
			if ( curves.z !== undefined ) times = times.concat( curves.z.times );

			// then sort them and remove duplicates
			times = times.sort( function ( a, b ) {

				return a - b;

			} ).filter( function ( elem, index, array ) {

				return array.indexOf( elem ) == index;

			} );

			return times;

		}

		// Rotations are defined as Euler angles which can have values  of any size
		// These will be converted to quaternions which don't support values greater than
		// PI, so we'll interpolate large rotations
		function interpolateRotations( curve ) {

			for ( var i = 1; i < curve.values.length; i ++ ) {

				var initialValue = curve.values[ i - 1 ];
				var valuesSpan = curve.values[ i ] - initialValue;

				var absoluteSpan = Math.abs( valuesSpan );

				if ( absoluteSpan >= 180 ) {

					var numSubIntervals = absoluteSpan / 180;

					var step = valuesSpan / numSubIntervals;
					var nextValue = initialValue + step;

					var initialTime = curve.times[ i - 1 ];
					var timeSpan = curve.times[ i ] - initialTime;
					var interval = timeSpan / numSubIntervals;
					var nextTime = initialTime + interval;

					var interpolatedTimes = [];
					var interpolatedValues = [];

					while ( nextTime < curve.times[ i ] ) {

						interpolatedTimes.push( nextTime );
						nextTime += interval;

						interpolatedValues.push( nextValue );
						nextValue += step;

					}

					curve.times = inject( curve.times, i, interpolatedTimes );
					curve.values = inject( curve.values, i, interpolatedValues );

				}

			}

		}

		// parse an FBX file in ASCII format
		function TextParser() {}

		Object.assign( TextParser.prototype, {

			getPrevNode: function () {

				return this.nodeStack[ this.currentIndent - 2 ];

			},

			getCurrentNode: function () {

				return this.nodeStack[ this.currentIndent - 1 ];

			},

			getCurrentProp: function () {

				return this.currentProp;

			},

			pushStack: function ( node ) {

				this.nodeStack.push( node );
				this.currentIndent += 1;

			},

			popStack: function () {

				this.nodeStack.pop();
				this.currentIndent -= 1;

			},

			setCurrentProp: function ( val, name ) {

				this.currentProp = val;
				this.currentPropName = name;

			},

			parse: function ( text ) {

				this.currentIndent = 0;
				this.allNodes = new FBXTree();
				this.nodeStack = [];
				this.currentProp = [];
				this.currentPropName = '';

				var self = this;

				var split = text.split( '\n' );

				split.forEach( function ( line, i ) {

					var matchComment = line.match( /^[\s\t]*;/ );
					var matchEmpty = line.match( /^[\s\t]*$/ );

					if ( matchComment || matchEmpty ) return;

					var matchBeginning = line.match( '^\\t{' + self.currentIndent + '}(\\w+):(.*){', '' );
					var matchProperty = line.match( '^\\t{' + ( self.currentIndent ) + '}(\\w+):[\\s\\t\\r\\n](.*)' );
					var matchEnd = line.match( '^\\t{' + ( self.currentIndent - 1 ) + '}}' );

					if ( matchBeginning ) {

						self.parseNodeBegin( line, matchBeginning );

					} else if ( matchProperty ) {

						self.parseNodeProperty( line, matchProperty, split[ ++ i ] );

					} else if ( matchEnd ) {

						self.popStack();

					} else if ( line.match( /^[^\s\t}]/ ) ) {

						// large arrays are split over multiple lines terminated with a ',' character
						// if this is encountered the line needs to be joined to the previous line
						self.parseNodePropertyContinued( line );

					}

				} );

				return this.allNodes;

			},

			parseNodeBegin: function ( line, property ) {

				var nodeName = property[ 1 ].trim().replace( /^"/, '' ).replace( /"$/, '' );

				var nodeAttrs = property[ 2 ].split( ',' ).map( function ( attr ) {

					return attr.trim().replace( /^"/, '' ).replace( /"$/, '' );

				} );

				var node = { name: nodeName };
				var attrs = this.parseNodeAttr( nodeAttrs );

				var currentNode = this.getCurrentNode();

				// a top node
				if ( this.currentIndent === 0 ) {

					this.allNodes.add( nodeName, node );

				} else { // a subnode

					// if the subnode already exists, append it
					if ( nodeName in currentNode ) {

					// special case Pose needs PoseNodes as an array
						if ( nodeName === 'PoseNode' ) {

							currentNode.PoseNode.push( node );

						} else if ( currentNode[ nodeName ].id !== undefined ) {

							currentNode[ nodeName ] = {};
							currentNode[ nodeName ][ currentNode[ nodeName ].id ] = currentNode[ nodeName ];

						}

						if ( attrs.id !== '' ) currentNode[ nodeName ][ attrs.id ] = node;

					} else if ( typeof attrs.id === 'number' ) {

						currentNode[ nodeName ] = {};
						currentNode[ nodeName ][ attrs.id ] = node;

					} else if ( nodeName !== 'Properties70' ) {

						if ( nodeName === 'PoseNode' )	currentNode[ nodeName ] = [ node ];
						else currentNode[ nodeName ] = node;

					}

				}

				if ( typeof attrs.id === 'number' ) node.id = attrs.id;
				if ( attrs.name !== '' ) node.attrName = attrs.name;
				if ( attrs.type !== '' ) node.attrType = attrs.type;

				this.pushStack( node );

			},

			parseNodeAttr: function ( attrs ) {

				var id = attrs[ 0 ];

				if ( attrs[ 0 ] !== '' ) {

					id = parseInt( attrs[ 0 ] );

					if ( isNaN( id ) ) {

						id = attrs[ 0 ];

					}

				}

				var name = '', type = '';

				if ( attrs.length > 1 ) {

					name = attrs[ 1 ].replace( /^(\w+)::/, '' );
					type = attrs[ 2 ];

				}

				return { id: id, name: name, type: type };

			},

			parseNodeProperty: function ( line, property, contentLine ) {

				var propName = property[ 1 ].replace( /^"/, '' ).replace( /"$/, '' ).trim();
				var propValue = property[ 2 ].replace( /^"/, '' ).replace( /"$/, '' ).trim();

				// for special case: base64 image data follows "Content: ," line
				//	Content: ,
				//	 "/9j/4RDaRXhpZgAATU0A..."
				if ( propName === 'Content' && propValue === ',' ) {

					propValue = contentLine.replace( /"/g, '' ).replace( /,$/, '' ).trim();

				}

				var currentNode = this.getCurrentNode();
				var parentName = currentNode.name;

				if ( parentName === 'Properties70' ) {

					this.parseNodeSpecialProperty( line, propName, propValue );
					return;

				}

				// Connections
				if ( propName === 'C' ) {

					var connProps = propValue.split( ',' ).slice( 1 );
					var from = parseInt( connProps[ 0 ] );
					var to = parseInt( connProps[ 1 ] );

					var rest = propValue.split( ',' ).slice( 3 );

					rest = rest.map( function ( elem ) {

						return elem.trim().replace( /^"/, '' );

					} );

					propName = 'connections';
					propValue = [ from, to ];
					append( propValue, rest );

					if ( currentNode[ propName ] === undefined ) {

						currentNode[ propName ] = [];

					}

				}

				// Node
				if ( propName === 'Node' ) currentNode.id = propValue;

				// connections
				if ( propName in currentNode && Array.isArray( currentNode[ propName ] ) ) {

					currentNode[ propName ].push( propValue );

				} else {

					if ( propName !== 'a' ) currentNode[ propName ] = propValue;
					else currentNode.a = propValue;

				}

				this.setCurrentProp( currentNode, propName );

				// convert string to array, unless it ends in ',' in which case more will be added to it
				if ( propName === 'a' && propValue.slice( - 1 ) !== ',' ) {

					currentNode.a = parseNumberArray( propValue );

				}

			},

			parseNodePropertyContinued: function ( line ) {

				var currentNode = this.getCurrentNode();

				currentNode.a += line;

				// if the line doesn't end in ',' we have reached the end of the property value
				// so convert the string to an array
				if ( line.slice( - 1 ) !== ',' ) {

					currentNode.a = parseNumberArray( currentNode.a );

				}

			},

			// parse "Property70"
			parseNodeSpecialProperty: function ( line, propName, propValue ) {

				// split this
				// P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1
				// into array like below
				// ["Lcl Scaling", "Lcl Scaling", "", "A", "1,1,1" ]
				var props = propValue.split( '",' ).map( function ( prop ) {

					return prop.trim().replace( /^\"/, '' ).replace( /\s/, '_' );

				} );

				var innerPropName = props[ 0 ];
				var innerPropType1 = props[ 1 ];
				var innerPropType2 = props[ 2 ];
				var innerPropFlag = props[ 3 ];
				var innerPropValue = props[ 4 ];

				// cast values where needed, otherwise leave as strings
				switch ( innerPropType1 ) {

					case 'int':
					case 'enum':
					case 'bool':
					case 'ULongLong':
					case 'double':
					case 'Number':
					case 'FieldOfView':
						innerPropValue = parseFloat( innerPropValue );
						break;

					case 'Color':
					case 'ColorRGB':
					case 'Vector3D':
					case 'Lcl_Translation':
					case 'Lcl_Rotation':
					case 'Lcl_Scaling':
						innerPropValue = parseNumberArray( innerPropValue );
						break;

				}

				// CAUTION: these props must append to parent's parent
				this.getPrevNode()[ innerPropName ] = {

					'type': innerPropType1,
					'type2': innerPropType2,
					'flag': innerPropFlag,
					'value': innerPropValue

				};

				this.setCurrentProp( this.getPrevNode(), innerPropName );

			},

		} );

		// Parse an FBX file in Binary format
		function BinaryParser() {}

		Object.assign( BinaryParser.prototype, {

			parse: function ( buffer ) {

				var reader = new BinaryReader( buffer );
				reader.skip( 23 ); // skip magic 23 bytes

				var version = reader.getUint32();

				console.log( 'THREE.FBXLoader: FBX binary version: ' + version );

				var allNodes = new FBXTree();

				while ( ! this.endOfContent( reader ) ) {

					var node = this.parseNode( reader, version );
					if ( node !== null ) allNodes.add( node.name, node );

				}

				return allNodes;

			},

			// Check if reader has reached the end of content.
			endOfContent: function ( reader ) {

				// footer size: 160bytes + 16-byte alignment padding
				// - 16bytes: magic
				// - padding til 16-byte alignment (at least 1byte?)
				//	(seems like some exporters embed fixed 15 or 16bytes?)
				// - 4bytes: magic
				// - 4bytes: version
				// - 120bytes: zero
				// - 16bytes: magic
				if ( reader.size() % 16 === 0 ) {

					return ( ( reader.getOffset() + 160 + 16 ) & ~ 0xf ) >= reader.size();

				} else {

					return reader.getOffset() + 160 + 16 >= reader.size();

				}

			},

			// recursively parse nodes until the end of the file is reached
			parseNode: function ( reader, version ) {

				var node = {};

				// The first three data sizes depends on version.
				var endOffset = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32();
				var numProperties = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32();

				// note: do not remove this even if you get a linter warning as it moves the buffer forward
				var propertyListLen = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32();

				var nameLen = reader.getUint8();
				var name = reader.getString( nameLen );

				// Regards this node as NULL-record if endOffset is zero
				if ( endOffset === 0 ) return null;

				var propertyList = [];

				for ( var i = 0; i < numProperties; i ++ ) {

					propertyList.push( this.parseProperty( reader ) );

				}

				// Regards the first three elements in propertyList as id, attrName, and attrType
				var id = propertyList.length > 0 ? propertyList[ 0 ] : '';
				var attrName = propertyList.length > 1 ? propertyList[ 1 ] : '';
				var attrType = propertyList.length > 2 ? propertyList[ 2 ] : '';

				// check if this node represents just a single property
				// like (name, 0) set or (name2, [0, 1, 2]) set of {name: 0, name2: [0, 1, 2]}
				node.singleProperty = ( numProperties === 1 && reader.getOffset() === endOffset ) ? true : false;

				while ( endOffset > reader.getOffset() ) {

					var subNode = this.parseNode( reader, version );

					if ( subNode !== null ) this.parseSubNode( name, node, subNode );

				}

				node.propertyList = propertyList; // raw property list used by parent

				if ( typeof id === 'number' ) node.id = id;
				if ( attrName !== '' ) node.attrName = attrName;
				if ( attrType !== '' ) node.attrType = attrType;
				if ( name !== '' ) node.name = name;

				return node;

			},

			parseSubNode: function ( name, node, subNode ) {

				// special case: child node is single property
				if ( subNode.singleProperty === true ) {

					var value = subNode.propertyList[ 0 ];

					if ( Array.isArray( value ) ) {

						node[ subNode.name ] = subNode;

						subNode.a = value;

					} else {

						node[ subNode.name ] = value;

					}

				} else if ( name === 'Connections' && subNode.name === 'C' ) {

					var array = [];

					subNode.propertyList.forEach( function ( property, i ) {

						// first Connection is FBX type (OO, OP, etc.). We'll discard these
						if ( i !== 0 ) array.push( property );

					} );

					if ( node.connections === undefined ) {

						node.connections = [];

					}

					node.connections.push( array );

				} else if ( subNode.name === 'Properties70' ) {

					var keys = Object.keys( subNode );

					keys.forEach( function ( key ) {

						node[ key ] = subNode[ key ];

					} );

				} else if ( name === 'Properties70' && subNode.name === 'P' ) {

					var innerPropName = subNode.propertyList[ 0 ];
					var innerPropType1 = subNode.propertyList[ 1 ];
					var innerPropType2 = subNode.propertyList[ 2 ];
					var innerPropFlag = subNode.propertyList[ 3 ];
					var innerPropValue;

					if ( innerPropName.indexOf( 'Lcl ' ) === 0 ) innerPropName = innerPropName.replace( 'Lcl ', 'Lcl_' );
					if ( innerPropType1.indexOf( 'Lcl ' ) === 0 ) innerPropType1 = innerPropType1.replace( 'Lcl ', 'Lcl_' );

					if ( innerPropType1 === 'Color' || innerPropType1 === 'ColorRGB' || innerPropType1 === 'Vector' || innerPropType1 === 'Vector3D' || innerPropType1.indexOf( 'Lcl_' ) === 0 ) {

						innerPropValue = [
							subNode.propertyList[ 4 ],
							subNode.propertyList[ 5 ],
							subNode.propertyList[ 6 ]
						];

					} else {

						innerPropValue = subNode.propertyList[ 4 ];

					}

					// this will be copied to parent, see above
					node[ innerPropName ] = {

						'type': innerPropType1,
						'type2': innerPropType2,
						'flag': innerPropFlag,
						'value': innerPropValue

					};

				} else if ( node[ subNode.name ] === undefined ) {

					if ( typeof subNode.id === 'number' ) {

						node[ subNode.name ] = {};
						node[ subNode.name ][ subNode.id ] = subNode;

					} else {

						node[ subNode.name ] = subNode;

					}

				} else {

					if ( subNode.name === 'PoseNode' ) {

						if ( ! Array.isArray( node[ subNode.name ] ) ) {

							node[ subNode.name ] = [ node[ subNode.name ] ];

						}

						node[ subNode.name ].push( subNode );

					} else if ( node[ subNode.name ][ subNode.id ] === undefined ) {

						node[ subNode.name ][ subNode.id ] = subNode;

					}

				}

			},

			parseProperty: function ( reader ) {

				var type = reader.getString( 1 );

				switch ( type ) {

					case 'C':
						return reader.getBoolean();

					case 'D':
						return reader.getFloat64();

					case 'F':
						return reader.getFloat32();

					case 'I':
						return reader.getInt32();

					case 'L':
						return reader.getInt64();

					case 'R':
						var length = reader.getUint32();
						return reader.getArrayBuffer( length );

					case 'S':
						var length = reader.getUint32();
						return reader.getString( length );

					case 'Y':
						return reader.getInt16();

					case 'b':
					case 'c':
					case 'd':
					case 'f':
					case 'i':
					case 'l':

						var arrayLength = reader.getUint32();
						var encoding = reader.getUint32(); // 0: non-compressed, 1: compressed
						var compressedLength = reader.getUint32();

						if ( encoding === 0 ) {

							switch ( type ) {

								case 'b':
								case 'c':
									return reader.getBooleanArray( arrayLength );

								case 'd':
									return reader.getFloat64Array( arrayLength );

								case 'f':
									return reader.getFloat32Array( arrayLength );

								case 'i':
									return reader.getInt32Array( arrayLength );

								case 'l':
									return reader.getInt64Array( arrayLength );

							}

						}

						if ( window.Zlib === undefined ) {

							console.error( 'THREE.FBXLoader: External library Inflate.min.js required, obtain or import from https://github.com/imaya/zlib.js' );

						}

						var inflate = new Zlib.Inflate( new Uint8Array( reader.getArrayBuffer( compressedLength ) ) ); // eslint-disable-line no-undef
						var reader2 = new BinaryReader( inflate.decompress().buffer );

						switch ( type ) {

							case 'b':
							case 'c':
								return reader2.getBooleanArray( arrayLength );

							case 'd':
								return reader2.getFloat64Array( arrayLength );

							case 'f':
								return reader2.getFloat32Array( arrayLength );

							case 'i':
								return reader2.getInt32Array( arrayLength );

							case 'l':
								return reader2.getInt64Array( arrayLength );

						}

					default:
						throw new Error( 'THREE.FBXLoader: Unknown property type ' + type );

				}

			}

		} );


		function BinaryReader( buffer, littleEndian ) {

			this.dv = new DataView( buffer );
			this.offset = 0;
			this.littleEndian = ( littleEndian !== undefined ) ? littleEndian : true;

		}

		Object.assign( BinaryReader.prototype, {

			getOffset: function () {

				return this.offset;

			},

			size: function () {

				return this.dv.buffer.byteLength;

			},

			skip: function ( length ) {

				this.offset += length;

			},

			// seems like true/false representation depends on exporter.
			// true: 1 or 'Y'(=0x59), false: 0 or 'T'(=0x54)
			// then sees LSB.
			getBoolean: function () {

				return ( this.getUint8() & 1 ) === 1;

			},

			getBooleanArray: function ( size ) {

				var a = [];

				for ( var i = 0; i < size; i ++ ) {

					a.push( this.getBoolean() );

				}

				return a;

			},

			getUint8: function () {

				var value = this.dv.getUint8( this.offset );
				this.offset += 1;
				return value;

			},

			getInt16: function () {

				var value = this.dv.getInt16( this.offset, this.littleEndian );
				this.offset += 2;
				return value;

			},

			getInt32: function () {

				var value = this.dv.getInt32( this.offset, this.littleEndian );
				this.offset += 4;
				return value;

			},

			getInt32Array: function ( size ) {

				var a = [];

				for ( var i = 0; i < size; i ++ ) {

					a.push( this.getInt32() );

				}

				return a;

			},

			getUint32: function () {

				var value = this.dv.getUint32( this.offset, this.littleEndian );
				this.offset += 4;
				return value;

			},

			// JavaScript doesn't support 64-bit integer so calculate this here
			// 1 << 32 will return 1 so using multiply operation instead here.
			// There's a possibility that this method returns wrong value if the value
			// is out of the range between Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER.
			// TODO: safely handle 64-bit integer
			getInt64: function () {

				var low, high;

				if ( this.littleEndian ) {

					low = this.getUint32();
					high = this.getUint32();

				} else {

					high = this.getUint32();
					low = this.getUint32();

				}

				// calculate negative value
				if ( high & 0x80000000 ) {

					high = ~ high & 0xFFFFFFFF;
					low = ~ low & 0xFFFFFFFF;

					if ( low === 0xFFFFFFFF ) high = ( high + 1 ) & 0xFFFFFFFF;

					low = ( low + 1 ) & 0xFFFFFFFF;

					return - ( high * 0x100000000 + low );

				}

				return high * 0x100000000 + low;

			},

			getInt64Array: function ( size ) {

				var a = [];

				for ( var i = 0; i < size; i ++ ) {

					a.push( this.getInt64() );

				}

				return a;

			},

			// Note: see getInt64() comment
			getUint64: function () {

				var low, high;

				if ( this.littleEndian ) {

					low = this.getUint32();
					high = this.getUint32();

				} else {

					high = this.getUint32();
					low = this.getUint32();

				}

				return high * 0x100000000 + low;

			},

			getFloat32: function () {

				var value = this.dv.getFloat32( this.offset, this.littleEndian );
				this.offset += 4;
				return value;

			},

			getFloat32Array: function ( size ) {

				var a = [];

				for ( var i = 0; i < size; i ++ ) {

					a.push( this.getFloat32() );

				}

				return a;

			},

			getFloat64: function () {

				var value = this.dv.getFloat64( this.offset, this.littleEndian );
				this.offset += 8;
				return value;

			},

			getFloat64Array: function ( size ) {

				var a = [];

				for ( var i = 0; i < size; i ++ ) {

					a.push( this.getFloat64() );

				}

				return a;

			},

			getArrayBuffer: function ( size ) {

				var value = this.dv.buffer.slice( this.offset, this.offset + size );
				this.offset += size;
				return value;

			},

			getString: function ( size ) {

				var a = new Uint8Array( size );

				for ( var i = 0; i < size; i ++ ) {

					a[ i ] = this.getUint8();

				}

				var nullByte = a.indexOf( 0 );
				if ( nullByte >= 0 ) a = a.slice( 0, nullByte );

				return THREE.LoaderUtils.decodeText( a );

			}

		} );

		// FBXTree holds a representation of the FBX data, returned by the TextParser ( FBX ASCII format)
		// and BinaryParser( FBX Binary format)
		function FBXTree() {}

		Object.assign( FBXTree.prototype, {

			add: function ( key, val ) {

				this[ key ] = val;

			},

		} );

		function isFbxFormatBinary( buffer ) {

			var CORRECT = 'Kaydara FBX Binary  \0';

			return buffer.byteLength >= CORRECT.length && CORRECT === convertArrayBufferToString( buffer, 0, CORRECT.length );

		}

		function isFbxFormatASCII( text ) {

			var CORRECT = [ 'K', 'a', 'y', 'd', 'a', 'r', 'a', '\\', 'F', 'B', 'X', '\\', 'B', 'i', 'n', 'a', 'r', 'y', '\\', '\\' ];

			var cursor = 0;

			function read( offset ) {

				var result = text[ offset - 1 ];
				text = text.slice( cursor + offset );
				cursor ++;
				return result;

			}

			for ( var i = 0; i < CORRECT.length; ++ i ) {

				var num = read( 1 );
				if ( num === CORRECT[ i ] ) {

					return false;

				}

			}

			return true;

		}

		function getFbxVersion( text ) {

			var versionRegExp = /FBXVersion: (\d+)/;
			var match = text.match( versionRegExp );
			if ( match ) {

				var version = parseInt( match[ 1 ] );
				return version;

			}
			throw new Error( 'THREE.FBXLoader: Cannot find the version number for the file given.' );

		}

		// Converts FBX ticks into real time seconds.
		function convertFBXTimeToSeconds( time ) {

			return time / 46186158000;

		}


		// Parses comma separated list of numbers and returns them an array.
		// Used internally by the TextParser
		function parseNumberArray( value ) {

			var array = value.split( ',' ).map( function ( val ) {

				return parseFloat( val );

			} );

			return array;

		}

		function convertArrayBufferToString( buffer, from, to ) {

			if ( from === undefined ) from = 0;
			if ( to === undefined ) to = buffer.byteLength;

			return THREE.LoaderUtils.decodeText( new Uint8Array( buffer, from, to ) );

		}

		function append( a, b ) {

			for ( var i = 0, j = a.length, l = b.length; i < l; i ++, j ++ ) {

				a[ j ] = b[ i ];

			}

		}

		function slice( a, b, from, to ) {

			for ( var i = from, j = 0; i < to; i ++, j ++ ) {

				a[ j ] = b[ i ];

			}

			return a;

		}

		// inject array a2 into array a1 at index
		function inject( a1, index, a2 ) {

			return a1.slice( 0, index ).concat( a2 ).concat( a1.slice( index ) );

		}

	} )();

	/**
	 * @author Rich Tibbett / https://github.com/richtr
	 * @author mrdoob / http://mrdoob.com/
	 * @author Tony Parisi / http://www.tonyparisi.com/
	 * @author Takahiro / https://github.com/takahirox
	 * @author Don McCurdy / https://www.donmccurdy.com
	 */

	THREE.GLTFLoader = ( function () {

		function GLTFLoader( manager ) {

			this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
			this.dracoLoader = null;

		}

		GLTFLoader.prototype = {

			constructor: GLTFLoader,

			crossOrigin: 'Anonymous',

			load: function ( url, onLoad, onProgress, onError ) {

				var scope = this;

				var path = this.path !== undefined ? this.path : THREE.LoaderUtils.extractUrlBase( url );

				var loader = new THREE.FileLoader( scope.manager );

				loader.setResponseType( 'arraybuffer' );

				loader.load( url, function ( data ) {

					try {

						scope.parse( data, path, onLoad, onError );

					} catch ( e ) {

						if ( onError !== undefined ) {

							onError( e );

						} else {

							throw e;

						}

					}

				}, onProgress, onError );

			},

			setCrossOrigin: function ( value ) {

				this.crossOrigin = value;
				return this;

			},

			setPath: function ( value ) {

				this.path = value;
				return this;

			},

			setDRACOLoader: function ( dracoLoader ) {

				this.dracoLoader = dracoLoader;
				return this;

			},

			parse: function ( data, path, onLoad, onError ) {

				var content;
				var extensions = {};

				if ( typeof data === 'string' ) {

					content = data;

				} else {

					var magic = THREE.LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) );

					if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) {

						try {

							extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data );

						} catch ( error ) {

							if ( onError ) onError( error );
							return;

						}

						content = extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content;

					} else {

						content = THREE.LoaderUtils.decodeText( new Uint8Array( data ) );

					}

				}

				var json = JSON.parse( content );

				if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) {

					if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported. Use LegacyGLTFLoader instead.' ) );
					return;

				}

				if ( json.extensionsUsed ) {

					for ( var i = 0; i < json.extensionsUsed.length; ++ i ) {

						var extensionName = json.extensionsUsed[ i ];
						var extensionsRequired = json.extensionsRequired || [];

						switch ( extensionName ) {

							case EXTENSIONS.KHR_LIGHTS:
								extensions[ extensionName ] = new GLTFLightsExtension( json );
								break;

							case EXTENSIONS.KHR_MATERIALS_UNLIT:
								extensions[ extensionName ] = new GLTFMaterialsUnlitExtension( json );
								break;

							case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS:
								extensions[ extensionName ] = new GLTFMaterialsPbrSpecularGlossinessExtension();
								break;

							case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION:
								extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader );
								break;

							case EXTENSIONS.MSFT_TEXTURE_DDS:
								extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] = new GLTFTextureDDSExtension();
								break;

							default:

								if ( extensionsRequired.indexOf( extensionName ) >= 0 ) {

									console.warn( 'THREE.GLTFLoader: Unknown extension "' + extensionName + '".' );

								}

						}

					}

				}

				var parser = new GLTFParser( json, extensions, {

					path: path || this.path || '',
					crossOrigin: this.crossOrigin,
					manager: this.manager

				} );

				parser.parse( function ( scene, scenes, cameras, animations, json ) {

					var glTF = {
						scene: scene,
						scenes: scenes,
						cameras: cameras,
						animations: animations,
						asset: json.asset,
						parser: parser,
						userData: {}
					};

					addUnknownExtensionsToUserData( extensions, glTF, json );

					onLoad( glTF );

				}, onError );

			}

		};

		/* GLTFREGISTRY */

		function GLTFRegistry() {

			var objects = {};

			return	{

				get: function ( key ) {

					return objects[ key ];

				},

				add: function ( key, object ) {

					objects[ key ] = object;

				},

				remove: function ( key ) {

					delete objects[ key ];

				},

				removeAll: function () {

					objects = {};

				}

			};

		}

		/*********************************/
		/********** EXTENSIONS ***********/
		/*********************************/

		var EXTENSIONS = {
			KHR_BINARY_GLTF: 'KHR_binary_glTF',
			KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression',
			KHR_LIGHTS: 'KHR_lights',
			KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness',
			KHR_MATERIALS_UNLIT: 'KHR_materials_unlit',
			MSFT_TEXTURE_DDS: 'MSFT_texture_dds'
		};

		/**
		 * DDS Texture Extension
		 *
		 * Specification: 
		 * https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds
		 * 
		 */
		function GLTFTextureDDSExtension() {

			if ( ! THREE.DDSLoader ) {

				throw new Error( 'THREE.GLTFLoader: Attempting to load .dds texture without importing THREE.DDSLoader' );

			}

			this.name = EXTENSIONS.MSFT_TEXTURE_DDS;
			this.ddsLoader = new THREE.DDSLoader();

		}

		/**
		 * Lights Extension
		 *
		 * Specification: PENDING
		 */
		function GLTFLightsExtension( json ) {

			this.name = EXTENSIONS.KHR_LIGHTS;

			this.lights = {};

			var extension = ( json.extensions && json.extensions[ EXTENSIONS.KHR_LIGHTS ] ) || {};
			var lights = extension.lights || {};

			for ( var lightId in lights ) {

				var light = lights[ lightId ];
				var lightNode;

				var color = new THREE.Color().fromArray( light.color );

				switch ( light.type ) {

					case 'directional':
						lightNode = new THREE.DirectionalLight( color );
						lightNode.target.position.set( 0, 0, 1 );
						lightNode.add( lightNode.target );
						break;

					case 'point':
						lightNode = new THREE.PointLight( color );
						break;

					case 'spot':
						lightNode = new THREE.SpotLight( color );
						// Handle spotlight properties.
						light.spot = light.spot || {};
						light.spot.innerConeAngle = light.spot.innerConeAngle !== undefined ? light.spot.innerConeAngle : 0;
						light.spot.outerConeAngle = light.spot.outerConeAngle !== undefined ? light.spot.outerConeAngle : Math.PI / 4.0;
						lightNode.angle = light.spot.outerConeAngle;
						lightNode.penumbra = 1.0 - light.spot.innerConeAngle / light.spot.outerConeAngle;
						lightNode.target.position.set( 0, 0, 1 );
						lightNode.add( lightNode.target );
						break;

					case 'ambient':
						lightNode = new THREE.AmbientLight( color );
						break;

				}

				if ( lightNode ) {

					lightNode.decay = 2;

					if ( light.intensity !== undefined ) {

						lightNode.intensity = light.intensity;

					}

					lightNode.name = light.name || ( 'light_' + lightId );
					this.lights[ lightId ] = lightNode;

				}

			}

		}

		/**
		 * Unlit Materials Extension (pending)
		 *
		 * PR: https://github.com/KhronosGroup/glTF/pull/1163
		 */
		function GLTFMaterialsUnlitExtension( json ) {

			this.name = EXTENSIONS.KHR_MATERIALS_UNLIT;

		}

		GLTFMaterialsUnlitExtension.prototype.getMaterialType = function ( material ) {

			return THREE.MeshBasicMaterial;

		};

		GLTFMaterialsUnlitExtension.prototype.extendParams = function ( materialParams, material, parser ) {

			var pending = [];

			materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 );
			materialParams.opacity = 1.0;

			var metallicRoughness = material.pbrMetallicRoughness;

			if ( metallicRoughness ) {

				if ( Array.isArray( metallicRoughness.baseColorFactor ) ) {

					var array = metallicRoughness.baseColorFactor;

					materialParams.color.fromArray( array );
					materialParams.opacity = array[ 3 ];

				}

				if ( metallicRoughness.baseColorTexture !== undefined ) {

					pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture.index ) );

				}

			}

			return Promise.all( pending );

		};
		var BINARY_EXTENSION_HEADER_MAGIC = 'glTF';
		var BINARY_EXTENSION_HEADER_LENGTH = 12;
		var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 };

		function GLTFBinaryExtension( data ) {

			this.name = EXTENSIONS.KHR_BINARY_GLTF;
			this.content = null;
			this.body = null;

			var headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH );

			this.header = {
				magic: THREE.LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ),
				version: headerView.getUint32( 4, true ),
				length: headerView.getUint32( 8, true )
			};

			if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) {

				throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' );

			} else if ( this.header.version < 2.0 ) {

				throw new Error( 'THREE.GLTFLoader: Legacy binary file detected. Use LegacyGLTFLoader instead.' );

			}

			var chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH );
			var chunkIndex = 0;

			while ( chunkIndex < chunkView.byteLength ) {

				var chunkLength = chunkView.getUint32( chunkIndex, true );
				chunkIndex += 4;

				var chunkType = chunkView.getUint32( chunkIndex, true );
				chunkIndex += 4;

				if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) {

					var contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength );
					this.content = THREE.LoaderUtils.decodeText( contentArray );

				} else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) {

					var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex;
					this.body = data.slice( byteOffset, byteOffset + chunkLength );

				}

				// Clients must ignore chunks with unknown types.

				chunkIndex += chunkLength;

			}

			if ( this.content === null ) {

				throw new Error( 'THREE.GLTFLoader: JSON content not found.' );

			}

		}

		/**
		 * DRACO Mesh Compression Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/pull/874
		 */
		function GLTFDracoMeshCompressionExtension ( json, dracoLoader ) {

			if ( ! dracoLoader ) {

				throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' );

			}

			this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION;
			this.json = json;
			this.dracoLoader = dracoLoader;

		}

		GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function ( primitive, parser ) {

			var json = this.json;
			var dracoLoader = this.dracoLoader;
			var bufferViewIndex = primitive.extensions[ this.name ].bufferView;
			var gltfAttributeMap = primitive.extensions[ this.name ].attributes;
			var threeAttributeMap = {};
			var attributeNormalizedMap = {};
			var attributeTypeMap = {};

			for ( var attributeName in gltfAttributeMap ) {

				if ( !( attributeName in ATTRIBUTES ) ) continue;

				threeAttributeMap[ ATTRIBUTES[ attributeName ] ] = gltfAttributeMap[ attributeName ];

			}

			for ( attributeName in primitive.attributes ) {

				if ( ATTRIBUTES[ attributeName ] !== undefined && gltfAttributeMap[ attributeName ] !== undefined ) {

					var accessorDef = json.accessors[ primitive.attributes[ attributeName ] ];
					var componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ];

					attributeTypeMap[ ATTRIBUTES[ attributeName ] ]  = componentType;
					attributeNormalizedMap[ ATTRIBUTES[ attributeName ] ] = accessorDef.normalized === true;

				}

			}

			return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) {

				return new Promise( function ( resolve ) {

					dracoLoader.decodeDracoFile( bufferView, function ( geometry ) {

						for ( var attributeName in geometry.attributes ) {

							var attribute = geometry.attributes[ attributeName ];
							var normalized = attributeNormalizedMap[ attributeName ];

							if ( normalized !== undefined ) attribute.normalized = normalized;

						}

						resolve( geometry );

					}, threeAttributeMap, attributeTypeMap );

				} );

			} );

		};

		/**
		 * Specular-Glossiness Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness
		 */
		function GLTFMaterialsPbrSpecularGlossinessExtension() {

			return {

				name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS,

				specularGlossinessParams: [
					'color',
					'map',
					'lightMap',
					'lightMapIntensity',
					'aoMap',
					'aoMapIntensity',
					'emissive',
					'emissiveIntensity',
					'emissiveMap',
					'bumpMap',
					'bumpScale',
					'normalMap',
					'displacementMap',
					'displacementScale',
					'displacementBias',
					'specularMap',
					'specular',
					'glossinessMap',
					'glossiness',
					'alphaMap',
					'envMap',
					'envMapIntensity',
					'refractionRatio',
				],

				getMaterialType: function () {

					return THREE.ShaderMaterial;

				},

				extendParams: function ( params, material, parser ) {

					var pbrSpecularGlossiness = material.extensions[ this.name ];

					var shader = THREE.ShaderLib[ 'standard' ];

					var uniforms = THREE.UniformsUtils.clone( shader.uniforms );

					var specularMapParsFragmentChunk = [
						'#ifdef USE_SPECULARMAP',
						'	uniform sampler2D specularMap;',
						'#endif'
					].join( '\n' );

					var glossinessMapParsFragmentChunk = [
						'#ifdef USE_GLOSSINESSMAP',
						'	uniform sampler2D glossinessMap;',
						'#endif'
					].join( '\n' );

					var specularMapFragmentChunk = [
						'vec3 specularFactor = specular;',
						'#ifdef USE_SPECULARMAP',
						'	vec4 texelSpecular = texture2D( specularMap, vUv );',
						'	texelSpecular = sRGBToLinear( texelSpecular );',
						'	// reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture',
						'	specularFactor *= texelSpecular.rgb;',
						'#endif'
					].join( '\n' );

					var glossinessMapFragmentChunk = [
						'float glossinessFactor = glossiness;',
						'#ifdef USE_GLOSSINESSMAP',
						'	vec4 texelGlossiness = texture2D( glossinessMap, vUv );',
						'	// reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture',
						'	glossinessFactor *= texelGlossiness.a;',
						'#endif'
					].join( '\n' );

					var lightPhysicalFragmentChunk = [
						'PhysicalMaterial material;',
						'material.diffuseColor = diffuseColor.rgb;',
						'material.specularRoughness = clamp( 1.0 - glossinessFactor, 0.04, 1.0 );',
						'material.specularColor = specularFactor.rgb;',
					].join( '\n' );

					var fragmentShader = shader.fragmentShader
						.replace( 'uniform float roughness;', 'uniform vec3 specular;' )
						.replace( 'uniform float metalness;', 'uniform float glossiness;' )
						.replace( '#include <roughnessmap_pars_fragment>', specularMapParsFragmentChunk )
						.replace( '#include <metalnessmap_pars_fragment>', glossinessMapParsFragmentChunk )
						.replace( '#include <roughnessmap_fragment>', specularMapFragmentChunk )
						.replace( '#include <metalnessmap_fragment>', glossinessMapFragmentChunk )
						.replace( '#include <lights_physical_fragment>', lightPhysicalFragmentChunk );

					delete uniforms.roughness;
					delete uniforms.metalness;
					delete uniforms.roughnessMap;
					delete uniforms.metalnessMap;

					uniforms.specular = { value: new THREE.Color().setHex( 0x111111 ) };
					uniforms.glossiness = { value: 0.5 };
					uniforms.specularMap = { value: null };
					uniforms.glossinessMap = { value: null };

					params.vertexShader = shader.vertexShader;
					params.fragmentShader = fragmentShader;
					params.uniforms = uniforms;
					params.defines = { 'STANDARD': '' };

					params.color = new THREE.Color( 1.0, 1.0, 1.0 );
					params.opacity = 1.0;

					var pending = [];

					if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) {

						var array = pbrSpecularGlossiness.diffuseFactor;

						params.color.fromArray( array );
						params.opacity = array[ 3 ];

					}

					if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) {

						pending.push( parser.assignTexture( params, 'map', pbrSpecularGlossiness.diffuseTexture.index ) );

					}

					params.emissive = new THREE.Color( 0.0, 0.0, 0.0 );
					params.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0;
					params.specular = new THREE.Color( 1.0, 1.0, 1.0 );

					if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) {

						params.specular.fromArray( pbrSpecularGlossiness.specularFactor );

					}

					if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) {

						var specGlossIndex = pbrSpecularGlossiness.specularGlossinessTexture.index;
						pending.push( parser.assignTexture( params, 'glossinessMap', specGlossIndex ) );
						pending.push( parser.assignTexture( params, 'specularMap', specGlossIndex ) );

					}

					return Promise.all( pending );

				},

				createMaterial: function ( params ) {

					// setup material properties based on MeshStandardMaterial for Specular-Glossiness

					var material = new THREE.ShaderMaterial( {
						defines: params.defines,
						vertexShader: params.vertexShader,
						fragmentShader: params.fragmentShader,
						uniforms: params.uniforms,
						fog: true,
						lights: true,
						opacity: params.opacity,
						transparent: params.transparent
					} );

					material.isGLTFSpecularGlossinessMaterial = true;

					material.color = params.color;

					material.map = params.map === undefined ? null : params.map;

					material.lightMap = null;
					material.lightMapIntensity = 1.0;

					material.aoMap = params.aoMap === undefined ? null : params.aoMap;
					material.aoMapIntensity = 1.0;

					material.emissive = params.emissive;
					material.emissiveIntensity = 1.0;
					material.emissiveMap = params.emissiveMap === undefined ? null : params.emissiveMap;

					material.bumpMap = params.bumpMap === undefined ? null : params.bumpMap;
					material.bumpScale = 1;

					material.normalMap = params.normalMap === undefined ? null : params.normalMap;
					if ( params.normalScale ) material.normalScale = params.normalScale;

					material.displacementMap = null;
					material.displacementScale = 1;
					material.displacementBias = 0;

					material.specularMap = params.specularMap === undefined ? null : params.specularMap;
					material.specular = params.specular;

					material.glossinessMap = params.glossinessMap === undefined ? null : params.glossinessMap;
					material.glossiness = params.glossiness;

					material.alphaMap = null;

					material.envMap = params.envMap === undefined ? null : params.envMap;
					material.envMapIntensity = 1.0;

					material.refractionRatio = 0.98;

					material.extensions.derivatives = true;

					return material;

				},

				/**
				 * Clones a GLTFSpecularGlossinessMaterial instance. The ShaderMaterial.copy() method can
				 * copy only properties it knows about or inherits, and misses many properties that would
				 * normally be defined by MeshStandardMaterial.
				 *
				 * This method allows GLTFSpecularGlossinessMaterials to be cloned in the process of
				 * loading a glTF model, but cloning later (e.g. by the user) would require these changes
				 * AND also updating `.onBeforeRender` on the parent mesh.
				 *
				 * @param  {THREE.ShaderMaterial} source
				 * @return {THREE.ShaderMaterial}
				 */
				cloneMaterial: function ( source ) {

					var target = source.clone();

					target.isGLTFSpecularGlossinessMaterial = true;

					var params = this.specularGlossinessParams;

					for ( var i = 0, il = params.length; i < il; i ++ ) {

						target[ params[ i ] ] = source[ params[ i ] ];

					}

					return target;

				},

				// Here's based on refreshUniformsCommon() and refreshUniformsStandard() in WebGLRenderer.
				refreshUniforms: function ( renderer, scene, camera, geometry, material, group ) {

					if ( material.isGLTFSpecularGlossinessMaterial !== true ) {

						return;

					}

					var uniforms = material.uniforms;
					var defines = material.defines;

					uniforms.opacity.value = material.opacity;

					uniforms.diffuse.value.copy( material.color );
					uniforms.emissive.value.copy( material.emissive ).multiplyScalar( material.emissiveIntensity );

					uniforms.map.value = material.map;
					uniforms.specularMap.value = material.specularMap;
					uniforms.alphaMap.value = material.alphaMap;

					uniforms.lightMap.value = material.lightMap;
					uniforms.lightMapIntensity.value = material.lightMapIntensity;

					uniforms.aoMap.value = material.aoMap;
					uniforms.aoMapIntensity.value = material.aoMapIntensity;

					// uv repeat and offset setting priorities
					// 1. color map
					// 2. specular map
					// 3. normal map
					// 4. bump map
					// 5. alpha map
					// 6. emissive map

					var uvScaleMap;

					if ( material.map ) {

						uvScaleMap = material.map;

					} else if ( material.specularMap ) {

						uvScaleMap = material.specularMap;

					} else if ( material.displacementMap ) {

						uvScaleMap = material.displacementMap;

					} else if ( material.normalMap ) {

						uvScaleMap = material.normalMap;

					} else if ( material.bumpMap ) {

						uvScaleMap = material.bumpMap;

					} else if ( material.glossinessMap ) {

						uvScaleMap = material.glossinessMap;

					} else if ( material.alphaMap ) {

						uvScaleMap = material.alphaMap;

					} else if ( material.emissiveMap ) {

						uvScaleMap = material.emissiveMap;

					}

					if ( uvScaleMap !== undefined ) {

						// backwards compatibility
						if ( uvScaleMap.isWebGLRenderTarget ) {

							uvScaleMap = uvScaleMap.texture;

						}

						var offset;
						var repeat;

						if ( uvScaleMap.matrix !== undefined ) {

							// > r88.

							if ( uvScaleMap.matrixAutoUpdate === true ) {

								offset = uvScaleMap.offset;
								repeat = uvScaleMap.repeat;
								var rotation = uvScaleMap.rotation;
								var center = uvScaleMap.center;

								uvScaleMap.matrix.setUvTransform( offset.x, offset.y, repeat.x, repeat.y, rotation, center.x, center.y );

							}

							uniforms.uvTransform.value.copy( uvScaleMap.matrix );

						} else {

							// <= r87. Remove when reasonable.

							offset = uvScaleMap.offset;
							repeat = uvScaleMap.repeat;

							uniforms.offsetRepeat.value.set( offset.x, offset.y, repeat.x, repeat.y );

						}

					}

					uniforms.envMap.value = material.envMap;
					uniforms.envMapIntensity.value = material.envMapIntensity;
					uniforms.flipEnvMap.value = ( material.envMap && material.envMap.isCubeTexture ) ? - 1 : 1;

					uniforms.refractionRatio.value = material.refractionRatio;

					uniforms.specular.value.copy( material.specular );
					uniforms.glossiness.value = material.glossiness;

					uniforms.glossinessMap.value = material.glossinessMap;

					uniforms.emissiveMap.value = material.emissiveMap;
					uniforms.bumpMap.value = material.bumpMap;
					uniforms.normalMap.value = material.normalMap;

					uniforms.displacementMap.value = material.displacementMap;
					uniforms.displacementScale.value = material.displacementScale;
					uniforms.displacementBias.value = material.displacementBias;

					if ( uniforms.glossinessMap.value !== null && defines.USE_GLOSSINESSMAP === undefined ) {

						defines.USE_GLOSSINESSMAP = '';
						// set USE_ROUGHNESSMAP to enable vUv
						defines.USE_ROUGHNESSMAP = '';

					}

					if ( uniforms.glossinessMap.value === null && defines.USE_GLOSSINESSMAP !== undefined ) {

						delete defines.USE_GLOSSINESSMAP;
						delete defines.USE_ROUGHNESSMAP;

					}

				}

			};

		}

		/*********************************/
		/********** INTERPOLATION ********/
		/*********************************/

		// Spline Interpolation
		// Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation
		function GLTFCubicSplineInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) {

			THREE.Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer );

		}
		GLTFCubicSplineInterpolant.prototype = Object.create( THREE.Interpolant.prototype );
		GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant;

		GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) {

			var result = this.resultBuffer;
			var values = this.sampleValues;
			var stride = this.valueSize;

			var stride2 = stride * 2;
			var stride3 = stride * 3;

			var td = t1 - t0;

			var p = ( t - t0 ) / td;
			var pp = p * p;
			var ppp = pp * p;

			var offset1 = i1 * stride3;
			var offset0 = offset1 - stride3;

			var s0 = 2 * ppp - 3 * pp + 1;
			var s1 = ppp - 2 * pp + p;
			var s2 = - 2 * ppp + 3 * pp;
			var s3 = ppp - pp;

			// Layout of keyframe output values for CUBICSPLINE animations:
			//   [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ]
			for ( var i = 0; i !== stride; i ++ ) {

				var p0 = values[ offset0 + i + stride ];        // splineVertex_k
				var m0 = values[ offset0 + i + stride2 ] * td;  // outTangent_k * (t_k+1 - t_k)
				var p1 = values[ offset1 + i + stride ];        // splineVertex_k+1
				var m1 = values[ offset1 + i ] * td;            // inTangent_k+1 * (t_k+1 - t_k)

				result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1;

			}

			return result;

		};

		/*********************************/
		/********** INTERNALS ************/
		/*********************************/

		/* CONSTANTS */

		var WEBGL_CONSTANTS = {
			FLOAT: 5126,
			//FLOAT_MAT2: 35674,
			FLOAT_MAT3: 35675,
			FLOAT_MAT4: 35676,
			FLOAT_VEC2: 35664,
			FLOAT_VEC3: 35665,
			FLOAT_VEC4: 35666,
			LINEAR: 9729,
			REPEAT: 10497,
			SAMPLER_2D: 35678,
			POINTS: 0,
			LINES: 1,
			LINE_LOOP: 2,
			LINE_STRIP: 3,
			TRIANGLES: 4,
			TRIANGLE_STRIP: 5,
			TRIANGLE_FAN: 6,
			UNSIGNED_BYTE: 5121,
			UNSIGNED_SHORT: 5123
		};

		var WEBGL_TYPE = {
			5126: Number,
			//35674: THREE.Matrix2,
			35675: THREE.Matrix3,
			35676: THREE.Matrix4,
			35664: THREE.Vector2,
			35665: THREE.Vector3,
			35666: THREE.Vector4,
			35678: THREE.Texture
		};

		var WEBGL_COMPONENT_TYPES = {
			5120: Int8Array,
			5121: Uint8Array,
			5122: Int16Array,
			5123: Uint16Array,
			5125: Uint32Array,
			5126: Float32Array
		};

		var WEBGL_FILTERS = {
			9728: THREE.NearestFilter,
			9729: THREE.LinearFilter,
			9984: THREE.NearestMipMapNearestFilter,
			9985: THREE.LinearMipMapNearestFilter,
			9986: THREE.NearestMipMapLinearFilter,
			9987: THREE.LinearMipMapLinearFilter
		};

		var WEBGL_WRAPPINGS = {
			33071: THREE.ClampToEdgeWrapping,
			33648: THREE.MirroredRepeatWrapping,
			10497: THREE.RepeatWrapping
		};

		var WEBGL_TEXTURE_FORMATS = {
			6406: THREE.AlphaFormat,
			6407: THREE.RGBFormat,
			6408: THREE.RGBAFormat,
			6409: THREE.LuminanceFormat,
			6410: THREE.LuminanceAlphaFormat
		};

		var WEBGL_TEXTURE_DATATYPES = {
			5121: THREE.UnsignedByteType,
			32819: THREE.UnsignedShort4444Type,
			32820: THREE.UnsignedShort5551Type,
			33635: THREE.UnsignedShort565Type
		};

		var WEBGL_SIDES = {
			1028: THREE.BackSide, // Culling front
			1029: THREE.FrontSide // Culling back
			//1032: THREE.NoSide   // Culling front and back, what to do?
		};

		var WEBGL_DEPTH_FUNCS = {
			512: THREE.NeverDepth,
			513: THREE.LessDepth,
			514: THREE.EqualDepth,
			515: THREE.LessEqualDepth,
			516: THREE.GreaterEqualDepth,
			517: THREE.NotEqualDepth,
			518: THREE.GreaterEqualDepth,
			519: THREE.AlwaysDepth
		};

		var WEBGL_BLEND_EQUATIONS = {
			32774: THREE.AddEquation,
			32778: THREE.SubtractEquation,
			32779: THREE.ReverseSubtractEquation
		};

		var WEBGL_BLEND_FUNCS = {
			0: THREE.ZeroFactor,
			1: THREE.OneFactor,
			768: THREE.SrcColorFactor,
			769: THREE.OneMinusSrcColorFactor,
			770: THREE.SrcAlphaFactor,
			771: THREE.OneMinusSrcAlphaFactor,
			772: THREE.DstAlphaFactor,
			773: THREE.OneMinusDstAlphaFactor,
			774: THREE.DstColorFactor,
			775: THREE.OneMinusDstColorFactor,
			776: THREE.SrcAlphaSaturateFactor
			// The followings are not supported by Three.js yet
			//32769: CONSTANT_COLOR,
			//32770: ONE_MINUS_CONSTANT_COLOR,
			//32771: CONSTANT_ALPHA,
			//32772: ONE_MINUS_CONSTANT_COLOR
		};

		var WEBGL_TYPE_SIZES = {
			'SCALAR': 1,
			'VEC2': 2,
			'VEC3': 3,
			'VEC4': 4,
			'MAT2': 4,
			'MAT3': 9,
			'MAT4': 16
		};

		var ATTRIBUTES = {
			POSITION: 'position',
			NORMAL: 'normal',
			TEXCOORD_0: 'uv',
			TEXCOORD0: 'uv', // deprecated
			TEXCOORD: 'uv', // deprecated
			TEXCOORD_1: 'uv2',
			COLOR_0: 'color',
			COLOR0: 'color', // deprecated
			COLOR: 'color', // deprecated
			WEIGHTS_0: 'skinWeight',
			WEIGHT: 'skinWeight', // deprecated
			JOINTS_0: 'skinIndex',
			JOINT: 'skinIndex' // deprecated
		};

		var PATH_PROPERTIES = {
			scale: 'scale',
			translation: 'position',
			rotation: 'quaternion',
			weights: 'morphTargetInfluences'
		};

		var INTERPOLATION = {
			CUBICSPLINE: THREE.InterpolateSmooth, // We use custom interpolation GLTFCubicSplineInterpolation for CUBICSPLINE.
			                                      // KeyframeTrack.optimize() can't handle glTF Cubic Spline output values layout,
			                                      // using THREE.InterpolateSmooth for KeyframeTrack instantiation to prevent optimization.
			                                      // See KeyframeTrack.optimize() for the detail.
			LINEAR: THREE.InterpolateLinear,
			STEP: THREE.InterpolateDiscrete
		};

		var ALPHA_MODES = {
			OPAQUE: 'OPAQUE',
			MASK: 'MASK',
			BLEND: 'BLEND'
		};

		/* UTILITY FUNCTIONS */

		function resolveURL( url, path ) {

			// Invalid URL
			if ( typeof url !== 'string' || url === '' ) return '';

			// Absolute URL http://,https://,//
			if ( /^(https?:)?\/\//i.test( url ) ) return url;

			// Data URI
			if ( /^data:.*,.*$/i.test( url ) ) return url;

			// Blob URL
			if ( /^blob:.*$/i.test( url ) ) return url;

			// Relative URL
			return path + url;

		}

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material
		 */
		function createDefaultMaterial() {

			return new THREE.MeshStandardMaterial( {
				color: 0xFFFFFF,
				emissive: 0x000000,
				metalness: 1,
				roughness: 1,
				transparent: false,
				depthTest: true,
				side: THREE.FrontSide
			} );

		}

		function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) {

			// Add unknown glTF extensions to an object's userData.

			for ( var name in objectDef.extensions ) {

				if ( knownExtensions[ name ] === undefined ) {

					object.userData.gltfExtensions = object.userData.gltfExtensions || {};
					object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ];

				}

			}

		}

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets
		 *
		 * @param {THREE.Geometry} geometry
		 * @param {Array<GLTF.Target>} targets
		 * @param {Array<THREE.BufferAttribute>} accessors
		 */
		function addMorphTargets( geometry, targets, accessors ) {

			var hasMorphPosition = false;
			var hasMorphNormal = false;

			for ( var i = 0, il = targets.length; i < il; i ++ ) {

				var target = targets[ i ];

				if ( target.POSITION !== undefined ) hasMorphPosition = true;
				if ( target.NORMAL !== undefined ) hasMorphNormal = true;

				if ( hasMorphPosition && hasMorphNormal ) break;

			}

			if ( ! hasMorphPosition && ! hasMorphNormal ) return;

			var morphPositions = [];
			var morphNormals = [];

			for ( var i = 0, il = targets.length; i < il; i ++ ) {

				var target = targets[ i ];
				var attributeName = 'morphTarget' + i;

				if ( hasMorphPosition ) {

					// Three.js morph position is absolute value. The formula is
					//   basePosition
					//     + weight0 * ( morphPosition0 - basePosition )
					//     + weight1 * ( morphPosition1 - basePosition )
					//     ...
					// while the glTF one is relative
					//   basePosition
					//     + weight0 * glTFmorphPosition0
					//     + weight1 * glTFmorphPosition1
					//     ...
					// then we need to convert from relative to absolute here.

					if ( target.POSITION !== undefined ) {

						// Cloning not to pollute original accessor
						var positionAttribute = cloneBufferAttribute( accessors[ target.POSITION ] );
						positionAttribute.name = attributeName;

						var position = geometry.attributes.position;

						for ( var j = 0, jl = positionAttribute.count; j < jl; j ++ ) {

							positionAttribute.setXYZ(
								j,
								positionAttribute.getX( j ) + position.getX( j ),
								positionAttribute.getY( j ) + position.getY( j ),
								positionAttribute.getZ( j ) + position.getZ( j )
							);

						}

					} else {

						positionAttribute = geometry.attributes.position;

					}

					morphPositions.push( positionAttribute );

				}

				if ( hasMorphNormal ) {

					// see target.POSITION's comment

					var normalAttribute;

					if ( target.NORMAL !== undefined ) {

						var normalAttribute = cloneBufferAttribute( accessors[ target.NORMAL ] );
						normalAttribute.name = attributeName;

						var normal = geometry.attributes.normal;

						for ( var j = 0, jl = normalAttribute.count; j < jl; j ++ ) {

							normalAttribute.setXYZ(
								j,
								normalAttribute.getX( j ) + normal.getX( j ),
								normalAttribute.getY( j ) + normal.getY( j ),
								normalAttribute.getZ( j ) + normal.getZ( j )
							);

						}

					} else {

						normalAttribute = geometry.attributes.normal;

					}

					morphNormals.push( normalAttribute );

				}

			}

			if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions;
			if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals;

		}

		/**
		 * @param {THREE.Mesh} mesh
		 * @param {GLTF.Mesh} meshDef
		 */
		function updateMorphTargets( mesh, meshDef ) {

			mesh.updateMorphTargets();

			if ( meshDef.weights !== undefined ) {

				for ( var i = 0, il = meshDef.weights.length; i < il; i ++ ) {

					mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ];

				}

			}

			// .extras has user-defined data, so check that .extras.targetNames is an array.
			if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) {

				var targetNames = meshDef.extras.targetNames;

				if ( mesh.morphTargetInfluences.length === targetNames.length ) {

					mesh.morphTargetDictionary = {};

					for ( var i = 0, il = targetNames.length; i < il; i ++ ) {

						mesh.morphTargetDictionary[ targetNames[ i ] ] = i;

					}

				} else {

					console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' );

				}

			}

		}

		function isPrimitiveEqual( a, b ) {

			if ( a.indices !== b.indices ) {

				return false;

			}

			return isObjectEqual( a.attributes, b.attributes );

		}

		function isObjectEqual( a, b ) {

			if ( Object.keys( a ).length !== Object.keys( b ).length ) return false;

			for ( var key in a ) {

				if ( a[ key ] !== b[ key ] ) return false;

			}

			return true;

		}

		function isArrayEqual( a, b ) {

			if ( a.length !== b.length ) return false;

			for ( var i = 0, il = a.length; i < il; i ++ ) {

				if ( a[ i ] !== b[ i ] ) return false;

			}

			return true;

		}

		function getCachedGeometry( cache, newPrimitive ) {

			for ( var i = 0, il = cache.length; i < il; i ++ ) {

				var cached = cache[ i ];

				if ( isPrimitiveEqual( cached.primitive, newPrimitive ) ) return cached.promise;

			}

			return null;

		}

		function getCachedCombinedGeometry( cache, geometries ) {

			for ( var i = 0, il = cache.length; i < il; i ++ ) {

				var cached = cache[ i ];

				if ( isArrayEqual( geometries, cached.baseGeometries ) ) return cached.geometry;

			}

			return null;

		}

		function getCachedMultiPassGeometry( cache, geometry, primitives ) {

			for ( var i = 0, il = cache.length; i < il; i ++ ) {

				var cached = cache[ i ];

				if ( geometry === cached.baseGeometry && isArrayEqual( primitives, cached.primitives ) ) return cached.geometry;

			}

			return null;

		}

		function cloneBufferAttribute( attribute ) {

			if ( attribute.isInterleavedBufferAttribute ) {

				var count = attribute.count;
				var itemSize = attribute.itemSize;
				var array = attribute.array.slice( 0, count * itemSize );

				for ( var i = 0; i < count; ++ i ) {

					array[ i ] = attribute.getX( i );
					if ( itemSize >= 2 ) array[ i + 1 ] = attribute.getY( i );
					if ( itemSize >= 3 ) array[ i + 2 ] = attribute.getZ( i );
					if ( itemSize >= 4 ) array[ i + 3 ] = attribute.getW( i );

				}

				return new THREE.BufferAttribute( array, itemSize, attribute.normalized );

			}

			return attribute.clone();

		}

		/**
		 * Checks if we can build a single Mesh with MultiMaterial from multiple primitives.
		 * Returns true if all primitives use the same attributes/morphAttributes/mode
		 * and also have index. Otherwise returns false.
		 *
		 * @param {Array<GLTF.Primitive>} primitives
		 * @return {Boolean}
		 */
		function isMultiPassGeometry( primitives ) {

			if ( primitives.length < 2 ) return false;

			var primitive0 = primitives[ 0 ];
			var targets0 = primitive0.targets || [];

			if ( primitive0.indices === undefined ) return false;

			for ( var i = 1, il = primitives.length; i < il; i ++ ) {

				var primitive = primitives[ i ];

				if ( primitive0.mode !== primitive.mode ) return false;
				if ( primitive.indices === undefined ) return false;
				if ( ! isObjectEqual( primitive0.attributes, primitive.attributes ) ) return false;

				var targets = primitive.targets || [];

				if ( targets0.length !== targets.length ) return false;

				for ( var j = 0, jl = targets0.length; j < jl; j ++ ) {

					if ( ! isObjectEqual( targets0[ j ], targets[ j ] ) ) return false;

				}

			}

			return true;

		}

		/* GLTF PARSER */

		function GLTFParser( json, extensions, options ) {

			this.json = json || {};
			this.extensions = extensions || {};
			this.options = options || {};

			// loader object cache
			this.cache = new GLTFRegistry();

			// BufferGeometry caching
			this.primitiveCache = [];
			this.multiplePrimitivesCache = [];
			this.multiPassGeometryCache = [];

			this.textureLoader = new THREE.TextureLoader( this.options.manager );
			this.textureLoader.setCrossOrigin( this.options.crossOrigin );

			this.fileLoader = new THREE.FileLoader( this.options.manager );
			this.fileLoader.setResponseType( 'arraybuffer' );

		}

		GLTFParser.prototype.parse = function ( onLoad, onError ) {

			var json = this.json;

			// Clear the loader cache
			this.cache.removeAll();

			// Mark the special nodes/meshes in json for efficient parse
			this.markDefs();

			// Fire the callback on complete
			this.getMultiDependencies( [

				'scene',
				'animation',
				'camera'

			] ).then( function ( dependencies ) {

				var scenes = dependencies.scenes || [];
				var scene = scenes[ json.scene || 0 ];
				var animations = dependencies.animations || [];
				var cameras = dependencies.cameras || [];

				onLoad( scene, scenes, cameras, animations, json );

			} ).catch( onError );

		};

		/**
		 * Marks the special nodes/meshes in json for efficient parse.
		 */
		GLTFParser.prototype.markDefs = function () {

			var nodeDefs = this.json.nodes || [];
			var skinDefs = this.json.skins || [];
			var meshDefs = this.json.meshes || [];

			var meshReferences = {};
			var meshUses = {};

			// Nothing in the node definition indicates whether it is a Bone or an
			// Object3D. Use the skins' joint references to mark bones.
			for ( var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) {

				var joints = skinDefs[ skinIndex ].joints;

				for ( var i = 0, il = joints.length; i < il; i ++ ) {

					nodeDefs[ joints[ i ] ].isBone = true;

				}

			}

			// Meshes can (and should) be reused by multiple nodes in a glTF asset. To
			// avoid having more than one THREE.Mesh with the same name, count
			// references and rename instances below.
			//
			// Example: CesiumMilkTruck sample model reuses "Wheel" meshes.
			for ( var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) {

				var nodeDef = nodeDefs[ nodeIndex ];

				if ( nodeDef.mesh !== undefined ) {

					if ( meshReferences[ nodeDef.mesh ] === undefined ) {

						meshReferences[ nodeDef.mesh ] = meshUses[ nodeDef.mesh ] = 0;

					}

					meshReferences[ nodeDef.mesh ] ++;

					// Nothing in the mesh definition indicates whether it is
					// a SkinnedMesh or Mesh. Use the node's mesh reference
					// to mark SkinnedMesh if node has skin.
					if ( nodeDef.skin !== undefined ) {

						meshDefs[ nodeDef.mesh ].isSkinnedMesh = true;

					}

				}

			}

			this.json.meshReferences = meshReferences;
			this.json.meshUses = meshUses;

		};

		/**
		 * Requests the specified dependency asynchronously, with caching.
		 * @param {string} type
		 * @param {number} index
		 * @return {Promise<Object>}
		 */
		GLTFParser.prototype.getDependency = function ( type, index ) {

			var cacheKey = type + ':' + index;
			var dependency = this.cache.get( cacheKey );

			if ( ! dependency ) {

				switch ( type ) {

					case 'scene':
						dependency = this.loadScene( index );
						break;

					case 'node':
						dependency = this.loadNode( index );
						break;

					case 'mesh':
						dependency = this.loadMesh( index );
						break;

					case 'accessor':
						dependency = this.loadAccessor( index );
						break;

					case 'bufferView':
						dependency = this.loadBufferView( index );
						break;

					case 'buffer':
						dependency = this.loadBuffer( index );
						break;

					case 'material':
						dependency = this.loadMaterial( index );
						break;

					case 'texture':
						dependency = this.loadTexture( index );
						break;

					case 'skin':
						dependency = this.loadSkin( index );
						break;

					case 'animation':
						dependency = this.loadAnimation( index );
						break;

					case 'camera':
						dependency = this.loadCamera( index );
						break;

					default:
						throw new Error( 'Unknown type: ' + type );

				}

				this.cache.add( cacheKey, dependency );

			}

			return dependency;

		};

		/**
		 * Requests all dependencies of the specified type asynchronously, with caching.
		 * @param {string} type
		 * @return {Promise<Array<Object>>}
		 */
		GLTFParser.prototype.getDependencies = function ( type ) {

			var dependencies = this.cache.get( type );

			if ( ! dependencies ) {

				var parser = this;
				var defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || [];

				dependencies = Promise.all( defs.map( function ( def, index ) {

					return parser.getDependency( type, index );

				} ) );

				this.cache.add( type, dependencies );

			}

			return dependencies;

		};

		/**
		 * Requests all multiple dependencies of the specified types asynchronously, with caching.
		 * @param {Array<string>} types
		 * @return {Promise<Object<Array<Object>>>}
		 */
		GLTFParser.prototype.getMultiDependencies = function ( types ) {

			var results = {};
			var pendings = [];

			for ( var i = 0, il = types.length; i < il; i ++ ) {

				var type = types[ i ];
				var value = this.getDependencies( type );

				value = value.then( function ( key, value ) {

					results[ key ] = value;

				}.bind( this, type + ( type === 'mesh' ? 'es' : 's' ) ) );

				pendings.push( value );

			}

			return Promise.all( pendings ).then( function () {

				return results;

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
		 * @param {number} bufferIndex
		 * @return {Promise<ArrayBuffer>}
		 */
		GLTFParser.prototype.loadBuffer = function ( bufferIndex ) {

			var bufferDef = this.json.buffers[ bufferIndex ];
			var loader = this.fileLoader;

			if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) {

				throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' );

			}

			// If present, GLB container is required to be the first buffer.
			if ( bufferDef.uri === undefined && bufferIndex === 0 ) {

				return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body );

			}

			var options = this.options;

			return new Promise( function ( resolve, reject ) {

				loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () {

					reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) );

				} );

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
		 * @param {number} bufferViewIndex
		 * @return {Promise<ArrayBuffer>}
		 */
		GLTFParser.prototype.loadBufferView = function ( bufferViewIndex ) {

			var bufferViewDef = this.json.bufferViews[ bufferViewIndex ];

			return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) {

				var byteLength = bufferViewDef.byteLength || 0;
				var byteOffset = bufferViewDef.byteOffset || 0;
				return buffer.slice( byteOffset, byteOffset + byteLength );

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors
		 * @param {number} accessorIndex
		 * @return {Promise<THREE.BufferAttribute|THREE.InterleavedBufferAttribute>}
		 */
		GLTFParser.prototype.loadAccessor = function ( accessorIndex ) {

			var parser = this;
			var json = this.json;

			var accessorDef = this.json.accessors[ accessorIndex ];

			if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) {

				// Ignore empty accessors, which may be used to declare runtime
				// information about attributes coming from another source (e.g. Draco
				// compression extension).
				return null;

			}

			var pendingBufferViews = [];

			if ( accessorDef.bufferView !== undefined ) {

				pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) );

			} else {

				pendingBufferViews.push( null );

			}

			if ( accessorDef.sparse !== undefined ) {

				pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) );
				pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) );

			}

			return Promise.all( pendingBufferViews ).then( function ( bufferViews ) {

				var bufferView = bufferViews[ 0 ];

				var itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ];
				var TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ];

				// For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12.
				var elementBytes = TypedArray.BYTES_PER_ELEMENT;
				var itemBytes = elementBytes * itemSize;
				var byteOffset = accessorDef.byteOffset || 0;
				var byteStride = json.bufferViews[ accessorDef.bufferView ].byteStride;
				var normalized = accessorDef.normalized === true;
				var array, bufferAttribute;

				// The buffer is not interleaved if the stride is the item size in bytes.
				if ( byteStride && byteStride !== itemBytes ) {

					var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType;
					var ib = parser.cache.get( ibCacheKey );

					if ( ! ib ) {

						// Use the full buffer if it's interleaved.
						array = new TypedArray( bufferView );

						// Integer parameters to IB/IBA are in array elements, not bytes.
						ib = new THREE.InterleavedBuffer( array, byteStride / elementBytes );

						parser.cache.add( ibCacheKey, ib );

					}

					bufferAttribute = new THREE.InterleavedBufferAttribute( ib, itemSize, byteOffset / elementBytes, normalized );

				} else {

					if ( bufferView === null ) {

						array = new TypedArray( accessorDef.count * itemSize );

					} else {

						array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize );

					}

					bufferAttribute = new THREE.BufferAttribute( array, itemSize, normalized );

				}

				// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors
				if ( accessorDef.sparse !== undefined ) {

					var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR;
					var TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ];

					var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0;
					var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0;

					var sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices );
					var sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize );

					if ( bufferView !== null ) {

						// Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes.
						bufferAttribute.setArray( bufferAttribute.array.slice() );

					}

					for ( var i = 0, il = sparseIndices.length; i < il; i ++ ) {

						var index = sparseIndices[ i ];

						bufferAttribute.setX( index, sparseValues[ i * itemSize ] );
						if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] );
						if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] );
						if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] );
						if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' );

					}

				}

				return bufferAttribute;

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures
		 * @param {number} textureIndex
		 * @return {Promise<THREE.Texture>}
		 */
		GLTFParser.prototype.loadTexture = function ( textureIndex ) {

			var parser = this;
			var json = this.json;
			var options = this.options;
			var textureLoader = this.textureLoader;

			var URL = window.URL || window.webkitURL;

			var textureDef = json.textures[ textureIndex ];

			var textureExtensions = textureDef.extensions || {};

			var source;

			if ( textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] ) {

				source = json.images[ textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].source ];

			} else {

				source = json.images[ textureDef.source ];

			}

			var sourceURI = source.uri;
			var isObjectURL = false;

			if ( source.bufferView !== undefined ) {

				// Load binary image data from bufferView, if provided.

				sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) {

					isObjectURL = true;
					var blob = new Blob( [ bufferView ], { type: source.mimeType } );
					sourceURI = URL.createObjectURL( blob );
					return sourceURI;

				} );

			}

			return Promise.resolve( sourceURI ).then( function ( sourceURI ) {

				// Load Texture resource.

				var loader = THREE.Loader.Handlers.get( sourceURI );

				if ( ! loader ) {

					loader = textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ]
						? parser.extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].ddsLoader
						: textureLoader;

				}

				return new Promise( function ( resolve, reject ) {

					loader.load( resolveURL( sourceURI, options.path ), resolve, undefined, reject );

				} );

			} ).then( function ( texture ) {

				// Clean up resources and configure Texture.

				if ( isObjectURL === true ) {

					URL.revokeObjectURL( sourceURI );

				}

				texture.flipY = false;

				if ( textureDef.name !== undefined ) texture.name = textureDef.name;

				// .format of dds texture is set in DDSLoader
				if ( ! textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] ) {

					texture.format = textureDef.format !== undefined ? WEBGL_TEXTURE_FORMATS[ textureDef.format ] : THREE.RGBAFormat;

				}

				if ( textureDef.internalFormat !== undefined && texture.format !== WEBGL_TEXTURE_FORMATS[ textureDef.internalFormat ] ) {

					console.warn( 'THREE.GLTFLoader: Three.js does not support texture internalFormat which is different from texture format. ' +
												'internalFormat will be forced to be the same value as format.' );

				}

				texture.type = textureDef.type !== undefined ? WEBGL_TEXTURE_DATATYPES[ textureDef.type ] : THREE.UnsignedByteType;

				var samplers = json.samplers || {};
				var sampler = samplers[ textureDef.sampler ] || {};

				texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || THREE.LinearFilter;
				texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || THREE.LinearMipMapLinearFilter;
				texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || THREE.RepeatWrapping;
				texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || THREE.RepeatWrapping;

				return texture;

			} );

		};

		/**
		 * Asynchronously assigns a texture to the given material parameters.
		 * @param {Object} materialParams
		 * @param {string} textureName
		 * @param {number} textureIndex
		 * @return {Promise}
		 */
		GLTFParser.prototype.assignTexture = function ( materialParams, textureName, textureIndex ) {

			return this.getDependency( 'texture', textureIndex ).then( function ( texture ) {

				materialParams[ textureName ] = texture;

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials
		 * @param {number} materialIndex
		 * @return {Promise<THREE.Material>}
		 */
		GLTFParser.prototype.loadMaterial = function ( materialIndex ) {

			var parser = this;
			var json = this.json;
			var extensions = this.extensions;
			var materialDef = this.json.materials[ materialIndex ];

			var materialType;
			var materialParams = {};
			var materialExtensions = materialDef.extensions || {};

			var pending = [];

			if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) {

				var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ];
				materialType = sgExtension.getMaterialType( materialDef );
				pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) );

			} else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) {

				var kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ];
				materialType = kmuExtension.getMaterialType( materialDef );
				pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) );

			} else {

				// Specification:
				// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material

				materialType = THREE.MeshStandardMaterial;

				var metallicRoughness = materialDef.pbrMetallicRoughness || {};

				materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 );
				materialParams.opacity = 1.0;

				if ( Array.isArray( metallicRoughness.baseColorFactor ) ) {

					var array = metallicRoughness.baseColorFactor;

					materialParams.color.fromArray( array );
					materialParams.opacity = array[ 3 ];

				}

				if ( metallicRoughness.baseColorTexture !== undefined ) {

					pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture.index ) );

				}

				materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0;
				materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0;

				if ( metallicRoughness.metallicRoughnessTexture !== undefined ) {

					var textureIndex = metallicRoughness.metallicRoughnessTexture.index;
					pending.push( parser.assignTexture( materialParams, 'metalnessMap', textureIndex ) );
					pending.push( parser.assignTexture( materialParams, 'roughnessMap', textureIndex ) );

				}

			}

			if ( materialDef.doubleSided === true ) {

				materialParams.side = THREE.DoubleSide;

			}

			var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE;

			if ( alphaMode === ALPHA_MODES.BLEND ) {

				materialParams.transparent = true;

			} else {

				materialParams.transparent = false;

				if ( alphaMode === ALPHA_MODES.MASK ) {

					materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5;

				}

			}

			if ( materialDef.normalTexture !== undefined && materialType !== THREE.MeshBasicMaterial) {

				pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture.index ) );

				materialParams.normalScale = new THREE.Vector2( 1, 1 );

				if ( materialDef.normalTexture.scale !== undefined ) {

					materialParams.normalScale.set( materialDef.normalTexture.scale, materialDef.normalTexture.scale );

				}

			}

			if ( materialDef.occlusionTexture !== undefined && materialType !== THREE.MeshBasicMaterial) {

				pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture.index ) );

				if ( materialDef.occlusionTexture.strength !== undefined ) {

					materialParams.aoMapIntensity = materialDef.occlusionTexture.strength;

				}

			}

			if ( materialDef.emissiveFactor !== undefined && materialType !== THREE.MeshBasicMaterial) {

				materialParams.emissive = new THREE.Color().fromArray( materialDef.emissiveFactor );

			}

			if ( materialDef.emissiveTexture !== undefined && materialType !== THREE.MeshBasicMaterial) {

				pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture.index ) );

			}

			return Promise.all( pending ).then( function () {

				var material;

				if ( materialType === THREE.ShaderMaterial ) {

					material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams );

				} else {

					material = new materialType( materialParams );

				}

				if ( materialDef.name !== undefined ) material.name = materialDef.name;

				// Normal map textures use OpenGL conventions:
				// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materialnormaltexture
				if ( material.normalScale ) {

					material.normalScale.y = - material.normalScale.y;

				}

				// baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding.
				if ( material.map ) material.map.encoding = THREE.sRGBEncoding;
				if ( material.emissiveMap ) material.emissiveMap.encoding = THREE.sRGBEncoding;
				if ( material.specularMap ) material.specularMap.encoding = THREE.sRGBEncoding;

				if ( materialDef.extras ) material.userData = materialDef.extras;

				if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef );

				return material;

			} );

		};

		/**
		 * @param  {THREE.BufferGeometry} geometry
		 * @param  {GLTF.Primitive} primitiveDef
		 * @param  {Array<THREE.BufferAttribute>} accessors
		 */
		function addPrimitiveAttributes( geometry, primitiveDef, accessors ) {

			var attributes = primitiveDef.attributes;

			for ( var gltfAttributeName in attributes ) {

				var threeAttributeName = ATTRIBUTES[ gltfAttributeName ];
				var bufferAttribute = accessors[ attributes[ gltfAttributeName ] ];

				// Skip attributes already provided by e.g. Draco extension.
				if ( !threeAttributeName ) continue;
				if ( threeAttributeName in geometry.attributes ) continue;

				geometry.addAttribute( threeAttributeName, bufferAttribute );

			}

			if ( primitiveDef.indices !== undefined && ! geometry.index ) {

				geometry.setIndex( accessors[ primitiveDef.indices ] );

			}

			if ( primitiveDef.targets !== undefined ) {

				addMorphTargets( geometry, primitiveDef.targets, accessors );

			}

			if ( primitiveDef.extras !== undefined ) {

				geometry.userData = primitiveDef.extras;

			}

		}

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry
		 *
		 * Creates BufferGeometries from primitives.
		 * If we can build a single BufferGeometry with .groups from multiple primitives, returns one BufferGeometry.
		 * Otherwise, returns BufferGeometries without .groups as many as primitives.
		 *
		 * @param {Array<Object>} primitives
		 * @return {Promise<Array<THREE.BufferGeometry>>}
		 */
		GLTFParser.prototype.loadGeometries = function ( primitives ) {

			var parser = this;
			var extensions = this.extensions;
			var cache = this.primitiveCache;

			var isMultiPass = isMultiPassGeometry( primitives );
			var originalPrimitives;

			if ( isMultiPass ) {

				originalPrimitives = primitives; // save original primitives and use later

				// We build a single BufferGeometry with .groups from multiple primitives
				// because all primitives share the same attributes/morph/mode and have indices.

				primitives = [ primitives[ 0 ] ];

				// Sets .groups and combined indices to a geometry later in this method.

			}

			return this.getDependencies( 'accessor' ).then( function ( accessors ) {

				var pending = [];

				for ( var i = 0, il = primitives.length; i < il; i ++ ) {

					var primitive = primitives[ i ];

					// See if we've already created this geometry
					var cached = getCachedGeometry( cache, primitive );

					if ( cached ) {

						// Use the cached geometry if it exists
						pending.push( cached );

					} else if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) {

						// Use DRACO geometry if available
						var geometryPromise = extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]
							.decodePrimitive( primitive, parser )
							.then( function ( geometry ) {

								addPrimitiveAttributes( geometry, primitive, accessors );

								return geometry;

							} );

						cache.push( { primitive: primitive, promise: geometryPromise } );

						pending.push( geometryPromise );

					} else {

						// Otherwise create a new geometry
						var geometry = new THREE.BufferGeometry();

						addPrimitiveAttributes( geometry, primitive, accessors );

						var geometryPromise = Promise.resolve( geometry );

						// Cache this geometry
						cache.push( { primitive: primitive, promise: geometryPromise } );

						pending.push( geometryPromise );

					}

				}

				return Promise.all( pending ).then( function ( geometries ) {

					if ( isMultiPass ) {

						var baseGeometry = geometries[ 0 ];

						// See if we've already created this combined geometry
						var cache = parser.multiPassGeometryCache;
						var cached = getCachedMultiPassGeometry( cache, baseGeometry, originalPrimitives );

						if ( cached !== null ) return [ cached.geometry ];

						// Cloning geometry because of index override.
						// Attributes can be reused so cloning by myself here.
						var geometry = new THREE.BufferGeometry();

						geometry.name = baseGeometry.name;
						geometry.userData = baseGeometry.userData;

						for ( var key in baseGeometry.attributes ) geometry.addAttribute( key, baseGeometry.attributes[ key ] );
						for ( var key in baseGeometry.morphAttributes ) geometry.morphAttributes[ key ] = baseGeometry.morphAttributes[ key ];

						var indices = [];
						var offset = 0;

						for ( var i = 0, il = originalPrimitives.length; i < il; i ++ ) {

							var accessor = accessors[ originalPrimitives[ i ].indices ];

							for ( var j = 0, jl = accessor.count; j < jl; j ++ ) indices.push( accessor.array[ j ] );

							geometry.addGroup( offset, accessor.count, i );

							offset += accessor.count;

						}

						geometry.setIndex( indices );

						cache.push( { geometry: geometry, baseGeometry: baseGeometry, primitives: originalPrimitives } );

						return [ geometry ];

					} else if ( geometries.length > 1 && THREE.BufferGeometryUtils !== undefined ) {

						// Tries to merge geometries with BufferGeometryUtils if possible

						for ( var i = 1, il = primitives.length; i < il; i ++ ) {

							// can't merge if draw mode is different
							if ( primitives[ 0 ].mode !== primitives[ i ].mode ) return geometries;

						}

						// See if we've already created this combined geometry
						var cache = parser.multiplePrimitivesCache;
						var cached = getCachedCombinedGeometry( cache, geometries );

						if ( cached ) {

							if ( cached.geometry !== null ) return [ cached.geometry ];

						} else {

							var geometry = THREE.BufferGeometryUtils.mergeBufferGeometries( geometries, true );

							cache.push( { geometry: geometry, baseGeometries: geometries } );

							if ( geometry !== null ) return [ geometry ];

						}

					}

					return geometries;

				} );

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes
		 * @param {number} meshIndex
		 * @return {Promise<THREE.Group|THREE.Mesh|THREE.SkinnedMesh>}
		 */
		GLTFParser.prototype.loadMesh = function ( meshIndex ) {

			var scope = this;
			var json = this.json;
			var extensions = this.extensions;

			var meshDef = this.json.meshes[ meshIndex ];

			return this.getMultiDependencies( [

				'accessor',
				'material'

			] ).then( function ( dependencies ) {

				var primitives = meshDef.primitives;
				var originalMaterials = [];

				for ( var i = 0, il = primitives.length; i < il; i ++ ) {

					originalMaterials[ i ] = primitives[ i ].material === undefined
						? createDefaultMaterial()
						: dependencies.materials[ primitives[ i ].material ];

				}

				return scope.loadGeometries( primitives ).then( function ( geometries ) {

					var isMultiMaterial = geometries.length === 1 && geometries[ 0 ].groups.length > 0;

					var meshes = [];

					for ( var i = 0, il = geometries.length; i < il; i ++ ) {

						var geometry = geometries[ i ];
						var primitive = primitives[ i ];

						// 1. create Mesh

						var mesh;

						var material = isMultiMaterial ? originalMaterials : originalMaterials[ i ];

						if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES ||
							primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ||
							primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ||
							primitive.mode === undefined ) {

							// .isSkinnedMesh isn't in glTF spec. See .markDefs()
							mesh = meshDef.isSkinnedMesh === true
								? new THREE.SkinnedMesh( geometry, material )
								: new THREE.Mesh( geometry, material );

							if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) {

								mesh.drawMode = THREE.TriangleStripDrawMode;

							} else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) {

								mesh.drawMode = THREE.TriangleFanDrawMode;

							}

						} else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) {

							mesh = new THREE.LineSegments( geometry, material );

						} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) {

							mesh = new THREE.Line( geometry, material );

						} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) {

							mesh = new THREE.LineLoop( geometry, material );

						} else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) {

							mesh = new THREE.Points( geometry, material );

						} else {

							throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode );

						}

						if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) {

							updateMorphTargets( mesh, meshDef );

						}

						mesh.name = meshDef.name || ( 'mesh_' + meshIndex );

						if ( geometries.length > 1 ) mesh.name += '_' + i;

						if ( meshDef.extras !== undefined ) mesh.userData = meshDef.extras;

						meshes.push( mesh );

						// 2. update Material depending on Mesh and BufferGeometry

						var materials = isMultiMaterial ? mesh.material : [ mesh.material ];

						var useVertexColors = geometry.attributes.color !== undefined;
						var useFlatShading = geometry.attributes.normal === undefined;
						var useSkinning = mesh.isSkinnedMesh === true;
						var useMorphTargets = Object.keys( geometry.morphAttributes ).length > 0;
						var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined;

						for ( var j = 0, jl = materials.length; j < jl; j ++ ) {

							var material = materials[ j ];

							if ( mesh.isPoints ) {

								var cacheKey = 'PointsMaterial:' + material.uuid;

								var pointsMaterial = scope.cache.get( cacheKey );

								if ( ! pointsMaterial ) {

									pointsMaterial = new THREE.PointsMaterial();
									THREE.Material.prototype.copy.call( pointsMaterial, material );
									pointsMaterial.color.copy( material.color );
									pointsMaterial.map = material.map;
									pointsMaterial.lights = false;  // PointsMaterial doesn't support lights yet

									scope.cache.add( cacheKey, pointsMaterial );

								}

								material = pointsMaterial;

							} else if ( mesh.isLine ) {

								var cacheKey = 'LineBasicMaterial:' + material.uuid;

								var lineMaterial = scope.cache.get( cacheKey );

								if ( ! lineMaterial ) {

									lineMaterial = new THREE.LineBasicMaterial();
									THREE.Material.prototype.copy.call( lineMaterial, material );
									lineMaterial.color.copy( material.color );
									lineMaterial.lights = false;  // LineBasicMaterial doesn't support lights yet

									scope.cache.add( cacheKey, lineMaterial );

								}

								material = lineMaterial;

							}

							// Clone the material if it will be modified
							if ( useVertexColors || useFlatShading || useSkinning || useMorphTargets ) {

								var cacheKey = 'ClonedMaterial:' + material.uuid + ':';

								if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:';
								if ( useSkinning ) cacheKey += 'skinning:';
								if ( useVertexColors ) cacheKey += 'vertex-colors:';
								if ( useFlatShading ) cacheKey += 'flat-shading:';
								if ( useMorphTargets ) cacheKey += 'morph-targets:';
								if ( useMorphNormals ) cacheKey += 'morph-normals:';

								var cachedMaterial = scope.cache.get( cacheKey );

								if ( ! cachedMaterial ) {

									cachedMaterial = material.isGLTFSpecularGlossinessMaterial
											? extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].cloneMaterial( material )
											: material.clone();

									if ( useSkinning ) cachedMaterial.skinning = true;
									if ( useVertexColors ) cachedMaterial.vertexColors = THREE.VertexColors;
									if ( useFlatShading ) cachedMaterial.flatShading = true;
									if ( useMorphTargets ) cachedMaterial.morphTargets = true;
									if ( useMorphNormals ) cachedMaterial.morphNormals = true;

									scope.cache.add( cacheKey, cachedMaterial );

								}

								material = cachedMaterial;

							}

							materials[ j ] = material;

							// workarounds for mesh and geometry

							if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) {

								console.log( 'THREE.GLTFLoader: Duplicating UVs to support aoMap.' );
								geometry.addAttribute( 'uv2', new THREE.BufferAttribute( geometry.attributes.uv.array, 2 ) );

							}

							if ( material.isGLTFSpecularGlossinessMaterial ) {

								// for GLTFSpecularGlossinessMaterial(ShaderMaterial) uniforms runtime update
								mesh.onBeforeRender = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].refreshUniforms;

							}

						}

						mesh.material = isMultiMaterial ? materials : materials[ 0 ];

					}

					if ( meshes.length === 1 ) {

						return meshes[ 0 ];

					}

					var group = new THREE.Group();

					for ( var i = 0, il = meshes.length; i < il; i ++ ) {

						group.add( meshes[ i ] );

					}

					return group;

				} );

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras
		 * @param {number} cameraIndex
		 * @return {Promise<THREE.Camera>}
		 */
		GLTFParser.prototype.loadCamera = function ( cameraIndex ) {

			var camera;
			var cameraDef = this.json.cameras[ cameraIndex ];
			var params = cameraDef[ cameraDef.type ];

			if ( ! params ) {

				console.warn( 'THREE.GLTFLoader: Missing camera parameters.' );
				return;

			}

			if ( cameraDef.type === 'perspective' ) {

				camera = new THREE.PerspectiveCamera( THREE.Math.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 );

			} else if ( cameraDef.type === 'orthographic' ) {

				camera = new THREE.OrthographicCamera( params.xmag / - 2, params.xmag / 2, params.ymag / 2, params.ymag / - 2, params.znear, params.zfar );

			}

			if ( cameraDef.name !== undefined ) camera.name = cameraDef.name;
			if ( cameraDef.extras ) camera.userData = cameraDef.extras;

			return Promise.resolve( camera );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins
		 * @param {number} skinIndex
		 * @return {Promise<Object>}
		 */
		GLTFParser.prototype.loadSkin = function ( skinIndex ) {

			var skinDef = this.json.skins[ skinIndex ];

			var skinEntry = { joints: skinDef.joints };

			if ( skinDef.inverseBindMatrices === undefined ) {

				return Promise.resolve( skinEntry );

			}

			return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) {

				skinEntry.inverseBindMatrices = accessor;

				return skinEntry;

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations
		 * @param {number} animationIndex
		 * @return {Promise<THREE.AnimationClip>}
		 */
		GLTFParser.prototype.loadAnimation = function ( animationIndex ) {

			var json = this.json;

			var animationDef = this.json.animations[ animationIndex ];

			return this.getMultiDependencies( [

				'accessor',
				'node'

			] ).then( function ( dependencies ) {

				var tracks = [];

				for ( var i = 0, il = animationDef.channels.length; i < il; i ++ ) {

					var channel = animationDef.channels[ i ];
					var sampler = animationDef.samplers[ channel.sampler ];

					if ( sampler ) {

						var target = channel.target;
						var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated.
						var input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input;
						var output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output;

						var inputAccessor = dependencies.accessors[ input ];
						var outputAccessor = dependencies.accessors[ output ];

						var node = dependencies.nodes[ name ];

						if ( node ) {

							node.updateMatrix();
							node.matrixAutoUpdate = true;

							var TypedKeyframeTrack;

							switch ( PATH_PROPERTIES[ target.path ] ) {

								case PATH_PROPERTIES.weights:

									TypedKeyframeTrack = THREE.NumberKeyframeTrack;
									break;

								case PATH_PROPERTIES.rotation:

									TypedKeyframeTrack = THREE.QuaternionKeyframeTrack;
									break;

								case PATH_PROPERTIES.position:
								case PATH_PROPERTIES.scale:
								default:

									TypedKeyframeTrack = THREE.VectorKeyframeTrack;
									break;

							}

							var targetName = node.name ? node.name : node.uuid;

							var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : THREE.InterpolateLinear;

							var targetNames = [];

							if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) {

								// node can be THREE.Group here but
								// PATH_PROPERTIES.weights(morphTargetInfluences) should be
								// the property of a mesh object under group.

								node.traverse( function ( object ) {

									if ( object.isMesh === true && object.morphTargetInfluences ) {

										targetNames.push( object.name ? object.name : object.uuid );

									}

								} );

							} else {

								targetNames.push( targetName );

							}

							// KeyframeTrack.optimize() will modify given 'times' and 'values'
							// buffers before creating a truncated copy to keep. Because buffers may
							// be reused by other tracks, make copies here.
							for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) {

								var track = new TypedKeyframeTrack(
									targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ],
									THREE.AnimationUtils.arraySlice( inputAccessor.array, 0 ),
									THREE.AnimationUtils.arraySlice( outputAccessor.array, 0 ),
									interpolation
								);

								// Here is the trick to enable custom interpolation.
								// Overrides .createInterpolant in a factory method which creates custom interpolation.
								if ( sampler.interpolation === 'CUBICSPLINE' ) {

									track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) {

										// A CUBICSPLINE keyframe in glTF has three output values for each input value,
										// representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize()
										// must be divided by three to get the interpolant's sampleSize argument.

										return new GLTFCubicSplineInterpolant( this.times, this.values, this.getValueSize() / 3, result );

									};

									// Workaround, provide an alternate way to know if the interpolant type is cubis spline to track.
									// track.getInterpolation() doesn't return valid value for custom interpolant.
									track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true;

								}

								tracks.push( track );

							}

						}

					}

				}

				var name = animationDef.name !== undefined ? animationDef.name : 'animation_' + animationIndex;

				return new THREE.AnimationClip( name, undefined, tracks );

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy
		 * @param {number} nodeIndex
		 * @return {Promise<THREE.Object3D>}
		 */
		GLTFParser.prototype.loadNode = function ( nodeIndex ) {

			var json = this.json;
			var extensions = this.extensions;

			var meshReferences = this.json.meshReferences;
			var meshUses = this.json.meshUses;

			var nodeDef = this.json.nodes[ nodeIndex ];

			return this.getMultiDependencies( [

				'mesh',
				'skin',
				'camera',
				'light'

			] ).then( function ( dependencies ) {

				var node;

				// .isBone isn't in glTF spec. See .markDefs
				if ( nodeDef.isBone === true ) {

					node = new THREE.Bone();

				} else if ( nodeDef.mesh !== undefined ) {

					var mesh = dependencies.meshes[ nodeDef.mesh ];

					node = mesh.clone();

					// for Specular-Glossiness
					if ( mesh.isGroup === true ) {

						for ( var i = 0, il = mesh.children.length; i < il; i ++ ) {

							var child = mesh.children[ i ];

							if ( child.material && child.material.isGLTFSpecularGlossinessMaterial === true ) {

								node.children[ i ].onBeforeRender = child.onBeforeRender;

							}

						}

					} else {

						if ( mesh.material && mesh.material.isGLTFSpecularGlossinessMaterial === true ) {

							node.onBeforeRender = mesh.onBeforeRender;

						}

					}

					if ( meshReferences[ nodeDef.mesh ] > 1 ) {

						node.name += '_instance_' + meshUses[ nodeDef.mesh ] ++;

					}

				} else if ( nodeDef.camera !== undefined ) {

					node = dependencies.cameras[ nodeDef.camera ];

				} else if ( nodeDef.extensions
						 && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ]
						 && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light !== undefined ) {

					var lights = extensions[ EXTENSIONS.KHR_LIGHTS ].lights;
					node = lights[ nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light ];

				} else {

					node = new THREE.Object3D();

				}

				if ( nodeDef.name !== undefined ) {

					node.name = THREE.PropertyBinding.sanitizeNodeName( nodeDef.name );

				}

				if ( nodeDef.extras ) node.userData = nodeDef.extras;

				if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef );

				if ( nodeDef.matrix !== undefined ) {

					var matrix = new THREE.Matrix4();
					matrix.fromArray( nodeDef.matrix );
					node.applyMatrix( matrix );

				} else {

					if ( nodeDef.translation !== undefined ) {

						node.position.fromArray( nodeDef.translation );

					}

					if ( nodeDef.rotation !== undefined ) {

						node.quaternion.fromArray( nodeDef.rotation );

					}

					if ( nodeDef.scale !== undefined ) {

						node.scale.fromArray( nodeDef.scale );

					}

				}

				return node;

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes
		 * @param {number} sceneIndex
		 * @return {Promise<THREE.Scene>}
		 */
		GLTFParser.prototype.loadScene = function () {

			// scene node hierachy builder

			function buildNodeHierachy( nodeId, parentObject, json, allNodes, skins ) {

				var node = allNodes[ nodeId ];
				var nodeDef = json.nodes[ nodeId ];

				// build skeleton here as well

				if ( nodeDef.skin !== undefined ) {

					var meshes = node.isGroup === true ? node.children : [ node ];

					for ( var i = 0, il = meshes.length; i < il; i ++ ) {

						var mesh = meshes[ i ];
						var skinEntry = skins[ nodeDef.skin ];

						var bones = [];
						var boneInverses = [];

						for ( var j = 0, jl = skinEntry.joints.length; j < jl; j ++ ) {

							var jointId = skinEntry.joints[ j ];
							var jointNode = allNodes[ jointId ];

							if ( jointNode ) {

								bones.push( jointNode );

								var mat = new THREE.Matrix4();

								if ( skinEntry.inverseBindMatrices !== undefined ) {

									mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 );

								}

								boneInverses.push( mat );

							} else {

								console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', jointId );

							}

						}

						mesh.bind( new THREE.Skeleton( bones, boneInverses ), mesh.matrixWorld );

					}

				}

				// build node hierachy

				parentObject.add( node );

				if ( nodeDef.children ) {

					var children = nodeDef.children;

					for ( var i = 0, il = children.length; i < il; i ++ ) {

						var child = children[ i ];
						buildNodeHierachy( child, node, json, allNodes, skins );

					}

				}

			}

			return function loadScene( sceneIndex ) {

				var json = this.json;
				var extensions = this.extensions;
				var sceneDef = this.json.scenes[ sceneIndex ];

				return this.getMultiDependencies( [

					'node',
					'skin'

				] ).then( function ( dependencies ) {

					var scene = new THREE.Scene();
					if ( sceneDef.name !== undefined ) scene.name = sceneDef.name;

					if ( sceneDef.extras ) scene.userData = sceneDef.extras;

					if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef );

					var nodeIds = sceneDef.nodes || [];

					for ( var i = 0, il = nodeIds.length; i < il; i ++ ) {

						buildNodeHierachy( nodeIds[ i ], scene, json, dependencies.nodes, dependencies.skins );

					}

					// Ambient lighting, if present, is always attached to the scene root.
					if ( sceneDef.extensions
							 && sceneDef.extensions[ EXTENSIONS.KHR_LIGHTS ]
							 && sceneDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light !== undefined ) {

						var lights = extensions[ EXTENSIONS.KHR_LIGHTS ].lights;
						scene.add( lights[ sceneDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light ] );

					}

					return scene;

				} );

			};

		}();

		return GLTFLoader;

	} )();

	/**
	 * @author mrdoob / http://mrdoob.com/
	 */

	THREE.KMZLoader = function ( manager ) {

		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

	};

	THREE.KMZLoader.prototype = {

		constructor: THREE.KMZLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			var scope = this;

			var loader = new THREE.FileLoader( scope.manager );
			loader.setResponseType( 'arraybuffer' );
			loader.load( url, function ( text ) {

				onLoad( scope.parse( text ) );

			}, onProgress, onError );

		},

		parse: function ( data ) {

			var zip = new JSZip( data ); // eslint-disable-line no-undef

			// console.log( zip );

			// var xml = new DOMParser().parseFromString( zip.file( 'doc.kml' ).asText(), 'application/xml' );

			function loadImage( image ) {

				var path = decodeURI( image.init_from );

				// Hack to support relative paths
				path = path.replace( '../', '' );

				var regex = new RegExp( path + '$' );
				var files = zip.file( regex );

				// console.log( image, files );

				if ( files.length ) {

					var file = files[ 0 ];
					var blob = new Blob( [ file.asArrayBuffer() ], { type: 'application/octet-binary' } );
					image.build.src = URL.createObjectURL( blob );

				}

			}

			// load collada

			var files = zip.file( /dae$/i );

			if ( files.length ) {

				var file = files[ 0 ];

				var collada = new THREE.ColladaLoader().parse( file.asText() );

				// fix images

				var images = collada.library.images;

				for ( var name in images ) {

					loadImage( images[ name ] );

				}

				return collada;

			}

			console.error( 'KMZLoader: Couldn\'t find .dae file.' );

			return {
				scene: new THREE.Group()
			};

		}

	};

	/**
	 * @author mrdoob / http://mrdoob.com/
	 */

	THREE.MD2Loader = function ( manager ) {

		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

	};

	THREE.MD2Loader.prototype = {

		constructor: THREE.MD2Loader,

		load: function ( url, onLoad, onProgress, onError ) {

			var scope = this;

			var loader = new THREE.FileLoader( scope.manager );
			loader.setResponseType( 'arraybuffer' );
			loader.load( url, function ( buffer ) {

				onLoad( scope.parse( buffer ) );

			}, onProgress, onError );

		},

		parse: ( function () {

			var normals = [
				[ -0.525731,  0.000000,  0.850651 ], [ -0.442863,  0.238856,  0.864188 ],
				[ -0.295242,  0.000000,  0.955423 ], [ -0.309017,  0.500000,  0.809017 ],
				[ -0.162460,  0.262866,  0.951056 ], [  0.000000,  0.000000,  1.000000 ],
				[  0.000000,  0.850651,  0.525731 ], [ -0.147621,  0.716567,  0.681718 ],
				[  0.147621,  0.716567,  0.681718 ], [  0.000000,  0.525731,  0.850651 ],
				[  0.309017,  0.500000,  0.809017 ], [  0.525731,  0.000000,  0.850651 ],
				[  0.295242,  0.000000,  0.955423 ], [  0.442863,  0.238856,  0.864188 ],
				[  0.162460,  0.262866,  0.951056 ], [ -0.681718,  0.147621,  0.716567 ],
				[ -0.809017,  0.309017,  0.500000 ], [ -0.587785,  0.425325,  0.688191 ],
				[ -0.850651,  0.525731,  0.000000 ], [ -0.864188,  0.442863,  0.238856 ],
				[ -0.716567,  0.681718,  0.147621 ], [ -0.688191,  0.587785,  0.425325 ],
				[ -0.500000,  0.809017,  0.309017 ], [ -0.238856,  0.864188,  0.442863 ],
				[ -0.425325,  0.688191,  0.587785 ], [ -0.716567,  0.681718, -0.147621 ],
				[ -0.500000,  0.809017, -0.309017 ], [ -0.525731,  0.850651,  0.000000 ],
				[  0.000000,  0.850651, -0.525731 ], [ -0.238856,  0.864188, -0.442863 ],
				[  0.000000,  0.955423, -0.295242 ], [ -0.262866,  0.951056, -0.162460 ],
				[  0.000000,  1.000000,  0.000000 ], [  0.000000,  0.955423,  0.295242 ],
				[ -0.262866,  0.951056,  0.162460 ], [  0.238856,  0.864188,  0.442863 ],
				[  0.262866,  0.951056,  0.162460 ], [  0.500000,  0.809017,  0.309017 ],
				[  0.238856,  0.864188, -0.442863 ], [  0.262866,  0.951056, -0.162460 ],
				[  0.500000,  0.809017, -0.309017 ], [  0.850651,  0.525731,  0.000000 ],
				[  0.716567,  0.681718,  0.147621 ], [  0.716567,  0.681718, -0.147621 ],
				[  0.525731,  0.850651,  0.000000 ], [  0.425325,  0.688191,  0.587785 ],
				[  0.864188,  0.442863,  0.238856 ], [  0.688191,  0.587785,  0.425325 ],
				[  0.809017,  0.309017,  0.500000 ], [  0.681718,  0.147621,  0.716567 ],
				[  0.587785,  0.425325,  0.688191 ], [  0.955423,  0.295242,  0.000000 ],
				[  1.000000,  0.000000,  0.000000 ], [  0.951056,  0.162460,  0.262866 ],
				[  0.850651, -0.525731,  0.000000 ], [  0.955423, -0.295242,  0.000000 ],
				[  0.864188, -0.442863,  0.238856 ], [  0.951056, -0.162460,  0.262866 ],
				[  0.809017, -0.309017,  0.500000 ], [  0.681718, -0.147621,  0.716567 ],
				[  0.850651,  0.000000,  0.525731 ], [  0.864188,  0.442863, -0.238856 ],
				[  0.809017,  0.309017, -0.500000 ], [  0.951056,  0.162460, -0.262866 ],
				[  0.525731,  0.000000, -0.850651 ], [  0.681718,  0.147621, -0.716567 ],
				[  0.681718, -0.147621, -0.716567 ], [  0.850651,  0.000000, -0.525731 ],
				[  0.809017, -0.309017, -0.500000 ], [  0.864188, -0.442863, -0.238856 ],
				[  0.951056, -0.162460, -0.262866 ], [  0.147621,  0.716567, -0.681718 ],
				[  0.309017,  0.500000, -0.809017 ], [  0.425325,  0.688191, -0.587785 ],
				[  0.442863,  0.238856, -0.864188 ], [  0.587785,  0.425325, -0.688191 ],
				[  0.688191,  0.587785, -0.425325 ], [ -0.147621,  0.716567, -0.681718 ],
				[ -0.309017,  0.500000, -0.809017 ], [  0.000000,  0.525731, -0.850651 ],
				[ -0.525731,  0.000000, -0.850651 ], [ -0.442863,  0.238856, -0.864188 ],
				[ -0.295242,  0.000000, -0.955423 ], [ -0.162460,  0.262866, -0.951056 ],
				[  0.000000,  0.000000, -1.000000 ], [  0.295242,  0.000000, -0.955423 ],
				[  0.162460,  0.262866, -0.951056 ], [ -0.442863, -0.238856, -0.864188 ],
				[ -0.309017, -0.500000, -0.809017 ], [ -0.162460, -0.262866, -0.951056 ],
				[  0.000000, -0.850651, -0.525731 ], [ -0.147621, -0.716567, -0.681718 ],
				[  0.147621, -0.716567, -0.681718 ], [  0.000000, -0.525731, -0.850651 ],
				[  0.309017, -0.500000, -0.809017 ], [  0.442863, -0.238856, -0.864188 ],
				[  0.162460, -0.262866, -0.951056 ], [  0.238856, -0.864188, -0.442863 ],
				[  0.500000, -0.809017, -0.309017 ], [  0.425325, -0.688191, -0.587785 ],
				[  0.716567, -0.681718, -0.147621 ], [  0.688191, -0.587785, -0.425325 ],
				[  0.587785, -0.425325, -0.688191 ], [  0.000000, -0.955423, -0.295242 ],
				[  0.000000, -1.000000,  0.000000 ], [  0.262866, -0.951056, -0.162460 ],
				[  0.000000, -0.850651,  0.525731 ], [  0.000000, -0.955423,  0.295242 ],
				[  0.238856, -0.864188,  0.442863 ], [  0.262866, -0.951056,  0.162460 ],
				[  0.500000, -0.809017,  0.309017 ], [  0.716567, -0.681718,  0.147621 ],
				[  0.525731, -0.850651,  0.000000 ], [ -0.238856, -0.864188, -0.442863 ],
				[ -0.500000, -0.809017, -0.309017 ], [ -0.262866, -0.951056, -0.162460 ],
				[ -0.850651, -0.525731,  0.000000 ], [ -0.716567, -0.681718, -0.147621 ],
				[ -0.716567, -0.681718,  0.147621 ], [ -0.525731, -0.850651,  0.000000 ],
				[ -0.500000, -0.809017,  0.309017 ], [ -0.238856, -0.864188,  0.442863 ],
				[ -0.262866, -0.951056,  0.162460 ], [ -0.864188, -0.442863,  0.238856 ],
				[ -0.809017, -0.309017,  0.500000 ], [ -0.688191, -0.587785,  0.425325 ],
				[ -0.681718, -0.147621,  0.716567 ], [ -0.442863, -0.238856,  0.864188 ],
				[ -0.587785, -0.425325,  0.688191 ], [ -0.309017, -0.500000,  0.809017 ],
				[ -0.147621, -0.716567,  0.681718 ], [ -0.425325, -0.688191,  0.587785 ],
				[ -0.162460, -0.262866,  0.951056 ], [  0.442863, -0.238856,  0.864188 ],
				[  0.162460, -0.262866,  0.951056 ], [  0.309017, -0.500000,  0.809017 ],
				[  0.147621, -0.716567,  0.681718 ], [  0.000000, -0.525731,  0.850651 ],
				[  0.425325, -0.688191,  0.587785 ], [  0.587785, -0.425325,  0.688191 ],
				[  0.688191, -0.587785,  0.425325 ], [ -0.955423,  0.295242,  0.000000 ],
				[ -0.951056,  0.162460,  0.262866 ], [ -1.000000,  0.000000,  0.000000 ],
				[ -0.850651,  0.000000,  0.525731 ], [ -0.955423, -0.295242,  0.000000 ],
				[ -0.951056, -0.162460,  0.262866 ], [ -0.864188,  0.442863, -0.238856 ],
				[ -0.951056,  0.162460, -0.262866 ], [ -0.809017,  0.309017, -0.500000 ],
				[ -0.864188, -0.442863, -0.238856 ], [ -0.951056, -0.162460, -0.262866 ],
				[ -0.809017, -0.309017, -0.500000 ], [ -0.681718,  0.147621, -0.716567 ],
				[ -0.681718, -0.147621, -0.716567 ], [ -0.850651,  0.000000, -0.525731 ],
				[ -0.688191,  0.587785, -0.425325 ], [ -0.587785,  0.425325, -0.688191 ],
				[ -0.425325,  0.688191, -0.587785 ], [ -0.425325, -0.688191, -0.587785 ],
				[ -0.587785, -0.425325, -0.688191 ], [ -0.688191, -0.587785, -0.425325 ]
			];

			return function ( buffer ) {

				console.time( 'MD2Loader' );

				var data = new DataView( buffer );

				// http://tfc.duke.free.fr/coding/md2-specs-en.html

				var header = {};
				var headerNames = [
					'ident', 'version',
					'skinwidth', 'skinheight',
					'framesize',
					'num_skins', 'num_vertices', 'num_st', 'num_tris', 'num_glcmds', 'num_frames',
					'offset_skins', 'offset_st', 'offset_tris', 'offset_frames', 'offset_glcmds', 'offset_end'
				];

				for ( var i = 0; i < headerNames.length; i ++ ) {

					header[ headerNames[ i ] ] = data.getInt32( i * 4, true );

				}

				if ( header.ident !== 844121161 || header.version !== 8 ) {

					console.error( 'Not a valid MD2 file' );
					return;

				}

				if ( header.offset_end !== data.byteLength ) {

					console.error( 'Corrupted MD2 file' );
					return;

				}

				//

				var geometry = new THREE.Geometry();

				// uvs

				var uvs = [];
				var offset = header.offset_st;

				for ( var i = 0, l = header.num_st; i < l; i ++ ) {

					var u = data.getInt16( offset + 0, true );
					var v = data.getInt16( offset + 2, true );

					uvs.push( new THREE.Vector2( u / header.skinwidth, 1 - ( v / header.skinheight ) ) );

					offset += 4;

				}

				// triangles

				var offset = header.offset_tris;

				for ( var i = 0, l = header.num_tris; i < l; i ++ ) {

					var a = data.getUint16( offset + 0, true );
					var b = data.getUint16( offset + 2, true );
					var c = data.getUint16( offset + 4, true );

					geometry.faces.push( new THREE.Face3( a, b, c ) );

					geometry.faceVertexUvs[ 0 ].push( [
						uvs[ data.getUint16( offset + 6, true ) ],
						uvs[ data.getUint16( offset + 8, true ) ],
						uvs[ data.getUint16( offset + 10, true ) ]
					] );

					offset += 12;

				}

				// frames

				var translation = new THREE.Vector3();
				var scale = new THREE.Vector3();
				var string = [];

				var offset = header.offset_frames;

				for ( var i = 0, l = header.num_frames; i < l; i ++ ) {

					scale.set(
						data.getFloat32( offset + 0, true ),
						data.getFloat32( offset + 4, true ),
						data.getFloat32( offset + 8, true )
					);

					translation.set(
						data.getFloat32( offset + 12, true ),
						data.getFloat32( offset + 16, true ),
						data.getFloat32( offset + 20, true )
					);

					offset += 24;

					for ( var j = 0; j < 16; j ++ ) {

						var character = data.getUint8( offset + j, true );
						if ( character === 0 ) break;

						string[ j ] = character;

					}

					var frame = {
						name: String.fromCharCode.apply( null, string ),
						vertices: [],
						normals: []
					};

					offset += 16;

					for ( var j = 0; j < header.num_vertices; j ++ ) {

						var x = data.getUint8( offset ++, true );
						var y = data.getUint8( offset ++, true );
						var z = data.getUint8( offset ++, true );
						var n = normals[ data.getUint8( offset ++, true ) ];

						var vertex = new THREE.Vector3(
							x * scale.x + translation.x,
							z * scale.z + translation.z,
							y * scale.y + translation.y
						);

						var normal = new THREE.Vector3( n[ 0 ], n[ 2 ], n[ 1 ] );

						frame.vertices.push( vertex );
						frame.normals.push( normal );

					}

					geometry.morphTargets.push( frame );

				}

				// Static

				geometry.vertices = geometry.morphTargets[ 0 ].vertices;

				var morphTarget = geometry.morphTargets[ 0 ];

				for ( var j = 0, jl = geometry.faces.length; j < jl; j ++ ) {

					var face = geometry.faces[ j ];

					face.vertexNormals = [
						morphTarget.normals[ face.a ],
						morphTarget.normals[ face.b ],
						morphTarget.normals[ face.c ]
					];

				}


				// Convert to geometry.morphNormals

				for ( var i = 0, l = geometry.morphTargets.length; i < l; i ++ ) {

					var morphTarget = geometry.morphTargets[ i ];
					var vertexNormals = [];

					for ( var j = 0, jl = geometry.faces.length; j < jl; j ++ ) {

						var face = geometry.faces[ j ];

						vertexNormals.push( {
							a: morphTarget.normals[ face.a ],
							b: morphTarget.normals[ face.b ],
							c: morphTarget.normals[ face.c ]
						} );

					}

					geometry.morphNormals.push( { vertexNormals: vertexNormals } );

				}

				geometry.animations = THREE.AnimationClip.CreateClipsFromMorphTargetSequences( geometry.morphTargets, 10 );

				console.timeEnd( 'MD2Loader' );

				return geometry;

			};

		} )()

	};

	/**
	 * @author mrdoob / http://mrdoob.com/
	 */

	THREE.OBJLoader = ( function () {

		// o object_name | g group_name
		var object_pattern = /^[og]\s*(.+)?/;
		// mtllib file_reference
		var material_library_pattern = /^mtllib /;
		// usemtl material_name
		var material_use_pattern = /^usemtl /;

		function ParserState() {

			var state = {
				objects: [],
				object: {},

				vertices: [],
				normals: [],
				colors: [],
				uvs: [],

				materialLibraries: [],

				startObject: function ( name, fromDeclaration ) {

					// If the current object (initial from reset) is not from a g/o declaration in the parsed
					// file. We need to use it for the first parsed g/o to keep things in sync.
					if ( this.object && this.object.fromDeclaration === false ) {

						this.object.name = name;
						this.object.fromDeclaration = ( fromDeclaration !== false );
						return;

					}

					var previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined );

					if ( this.object && typeof this.object._finalize === 'function' ) {

						this.object._finalize( true );

					}

					this.object = {
						name: name || '',
						fromDeclaration: ( fromDeclaration !== false ),

						geometry: {
							vertices: [],
							normals: [],
							colors: [],
							uvs: []
						},
						materials: [],
						smooth: true,

						startMaterial: function ( name, libraries ) {

							var previous = this._finalize( false );

							// New usemtl declaration overwrites an inherited material, except if faces were declared
							// after the material, then it must be preserved for proper MultiMaterial continuation.
							if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) {

								this.materials.splice( previous.index, 1 );

							}

							var material = {
								index: this.materials.length,
								name: name || '',
								mtllib: ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ),
								smooth: ( previous !== undefined ? previous.smooth : this.smooth ),
								groupStart: ( previous !== undefined ? previous.groupEnd : 0 ),
								groupEnd: - 1,
								groupCount: - 1,
								inherited: false,

								clone: function ( index ) {

									var cloned = {
										index: ( typeof index === 'number' ? index : this.index ),
										name: this.name,
										mtllib: this.mtllib,
										smooth: this.smooth,
										groupStart: 0,
										groupEnd: - 1,
										groupCount: - 1,
										inherited: false
									};
									cloned.clone = this.clone.bind( cloned );
									return cloned;

								}
							};

							this.materials.push( material );

							return material;

						},

						currentMaterial: function () {

							if ( this.materials.length > 0 ) {

								return this.materials[ this.materials.length - 1 ];

							}

							return undefined;

						},

						_finalize: function ( end ) {

							var lastMultiMaterial = this.currentMaterial();
							if ( lastMultiMaterial && lastMultiMaterial.groupEnd === - 1 ) {

								lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3;
								lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart;
								lastMultiMaterial.inherited = false;

							}

							// Ignore objects tail materials if no face declarations followed them before a new o/g started.
							if ( end && this.materials.length > 1 ) {

								for ( var mi = this.materials.length - 1; mi >= 0; mi -- ) {

									if ( this.materials[ mi ].groupCount <= 0 ) {

										this.materials.splice( mi, 1 );

									}

								}

							}

							// Guarantee at least one empty material, this makes the creation later more straight forward.
							if ( end && this.materials.length === 0 ) {

								this.materials.push( {
									name: '',
									smooth: this.smooth
								} );

							}

							return lastMultiMaterial;

						}
					};

					// Inherit previous objects material.
					// Spec tells us that a declared material must be set to all objects until a new material is declared.
					// If a usemtl declaration is encountered while this new object is being parsed, it will
					// overwrite the inherited material. Exception being that there was already face declarations
					// to the inherited material, then it will be preserved for proper MultiMaterial continuation.

					if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function' ) {

						var declared = previousMaterial.clone( 0 );
						declared.inherited = true;
						this.object.materials.push( declared );

					}

					this.objects.push( this.object );

				},

				finalize: function () {

					if ( this.object && typeof this.object._finalize === 'function' ) {

						this.object._finalize( true );

					}

				},

				parseVertexIndex: function ( value, len ) {

					var index = parseInt( value, 10 );
					return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;

				},

				parseNormalIndex: function ( value, len ) {

					var index = parseInt( value, 10 );
					return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;

				},

				parseUVIndex: function ( value, len ) {

					var index = parseInt( value, 10 );
					return ( index >= 0 ? index - 1 : index + len / 2 ) * 2;

				},

				addVertex: function ( a, b, c ) {

					var src = this.vertices;
					var dst = this.object.geometry.vertices;

					dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
					dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
					dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );

				},

				addVertexPoint: function ( a ) {

					var src = this.vertices;
					var dst = this.object.geometry.vertices;

					dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );

				},

				addVertexLine: function ( a ) {

					var src = this.vertices;
					var dst = this.object.geometry.vertices;

					dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );

				},

				addNormal: function ( a, b, c ) {

					var src = this.normals;
					var dst = this.object.geometry.normals;

					dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
					dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
					dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );

				},

				addColor: function ( a, b, c ) {

					var src = this.colors;
					var dst = this.object.geometry.colors;

					dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
					dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
					dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );

				},

				addUV: function ( a, b, c ) {

					var src = this.uvs;
					var dst = this.object.geometry.uvs;

					dst.push( src[ a + 0 ], src[ a + 1 ] );
					dst.push( src[ b + 0 ], src[ b + 1 ] );
					dst.push( src[ c + 0 ], src[ c + 1 ] );

				},

				addUVLine: function ( a ) {

					var src = this.uvs;
					var dst = this.object.geometry.uvs;

					dst.push( src[ a + 0 ], src[ a + 1 ] );

				},

				addFace: function ( a, b, c, ua, ub, uc, na, nb, nc ) {

					var vLen = this.vertices.length;

					var ia = this.parseVertexIndex( a, vLen );
					var ib = this.parseVertexIndex( b, vLen );
					var ic = this.parseVertexIndex( c, vLen );

					this.addVertex( ia, ib, ic );

					if ( ua !== undefined && ua !== '' ) {

						var uvLen = this.uvs.length;
						ia = this.parseUVIndex( ua, uvLen );
						ib = this.parseUVIndex( ub, uvLen );
						ic = this.parseUVIndex( uc, uvLen );
						this.addUV( ia, ib, ic );

					}

					if ( na !== undefined && na !== '' ) {

						// Normals are many times the same. If so, skip function call and parseInt.
						var nLen = this.normals.length;
						ia = this.parseNormalIndex( na, nLen );

						ib = na === nb ? ia : this.parseNormalIndex( nb, nLen );
						ic = na === nc ? ia : this.parseNormalIndex( nc, nLen );

						this.addNormal( ia, ib, ic );

					}

					if ( this.colors.length > 0 ) {

						this.addColor( ia, ib, ic );

					}

				},

				addPointGeometry: function ( vertices ) {

					this.object.geometry.type = 'Points';

					var vLen = this.vertices.length;

					for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) {

						this.addVertexPoint( this.parseVertexIndex( vertices[ vi ], vLen ) );

					}

				},

				addLineGeometry: function ( vertices, uvs ) {

					this.object.geometry.type = 'Line';

					var vLen = this.vertices.length;
					var uvLen = this.uvs.length;

					for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) {

						this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) );

					}

					for ( var uvi = 0, l = uvs.length; uvi < l; uvi ++ ) {

						this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) );

					}

				}

			};

			state.startObject( '', false );

			return state;

		}

		//

		function OBJLoader( manager ) {

			this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

			this.materials = null;

		}

		OBJLoader.prototype = {

			constructor: OBJLoader,

			load: function ( url, onLoad, onProgress, onError ) {

				var scope = this;

				var loader = new THREE.FileLoader( scope.manager );
				loader.setPath( this.path );
				loader.load( url, function ( text ) {

					onLoad( scope.parse( text ) );

				}, onProgress, onError );

			},

			setPath: function ( value ) {

				this.path = value;

				return this;

			},

			setMaterials: function ( materials ) {

				this.materials = materials;

				return this;

			},

			parse: function ( text ) {

				console.time( 'OBJLoader' );

				var state = new ParserState();

				if ( text.indexOf( '\r\n' ) !== - 1 ) {

					// This is faster than String.split with regex that splits on both
					text = text.replace( /\r\n/g, '\n' );

				}

				if ( text.indexOf( '\\\n' ) !== - 1 ) {

					// join lines separated by a line continuation character (\)
					text = text.replace( /\\\n/g, '' );

				}

				var lines = text.split( '\n' );
				var line = '', lineFirstChar = '';
				var lineLength = 0;
				var result = [];

				// Faster to just trim left side of the line. Use if available.
				var trimLeft = ( typeof ''.trimLeft === 'function' );

				for ( var i = 0, l = lines.length; i < l; i ++ ) {

					line = lines[ i ];

					line = trimLeft ? line.trimLeft() : line.trim();

					lineLength = line.length;

					if ( lineLength === 0 ) continue;

					lineFirstChar = line.charAt( 0 );

					// @todo invoke passed in handler if any
					if ( lineFirstChar === '#' ) continue;

					if ( lineFirstChar === 'v' ) {

						var data = line.split( /\s+/ );

						switch ( data[ 0 ] ) {

							case 'v':
								state.vertices.push(
									parseFloat( data[ 1 ] ),
									parseFloat( data[ 2 ] ),
									parseFloat( data[ 3 ] )
								);
								if ( data.length === 8 ) {

									state.colors.push(
										parseFloat( data[ 4 ] ),
										parseFloat( data[ 5 ] ),
										parseFloat( data[ 6 ] )

									);

								}
								break;
							case 'vn':
								state.normals.push(
									parseFloat( data[ 1 ] ),
									parseFloat( data[ 2 ] ),
									parseFloat( data[ 3 ] )
								);
								break;
							case 'vt':
								state.uvs.push(
									parseFloat( data[ 1 ] ),
									parseFloat( data[ 2 ] )
								);
								break;

						}

					} else if ( lineFirstChar === 'f' ) {

						var lineData = line.substr( 1 ).trim();
						var vertexData = lineData.split( /\s+/ );
						var faceVertices = [];

						// Parse the face vertex data into an easy to work with format

						for ( var j = 0, jl = vertexData.length; j < jl; j ++ ) {

							var vertex = vertexData[ j ];

							if ( vertex.length > 0 ) {

								var vertexParts = vertex.split( '/' );
								faceVertices.push( vertexParts );

							}

						}

						// Draw an edge between the first vertex and all subsequent vertices to form an n-gon

						var v1 = faceVertices[ 0 ];

						for ( var j = 1, jl = faceVertices.length - 1; j < jl; j ++ ) {

							var v2 = faceVertices[ j ];
							var v3 = faceVertices[ j + 1 ];

							state.addFace(
								v1[ 0 ], v2[ 0 ], v3[ 0 ],
								v1[ 1 ], v2[ 1 ], v3[ 1 ],
								v1[ 2 ], v2[ 2 ], v3[ 2 ]
							);

						}

					} else if ( lineFirstChar === 'l' ) {

						var lineParts = line.substring( 1 ).trim().split( " " );
						var lineVertices = [], lineUVs = [];

						if ( line.indexOf( "/" ) === - 1 ) {

							lineVertices = lineParts;

						} else {

							for ( var li = 0, llen = lineParts.length; li < llen; li ++ ) {

								var parts = lineParts[ li ].split( "/" );

								if ( parts[ 0 ] !== "" ) lineVertices.push( parts[ 0 ] );
								if ( parts[ 1 ] !== "" ) lineUVs.push( parts[ 1 ] );

							}

						}
						state.addLineGeometry( lineVertices, lineUVs );

					} else if ( lineFirstChar === 'p' ) {

						var lineData = line.substr( 1 ).trim();
						var pointData = lineData.split( " " );

						state.addPointGeometry( pointData );

					} else if ( ( result = object_pattern.exec( line ) ) !== null ) {

						// o object_name
						// or
						// g group_name

						// WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869
						// var name = result[ 0 ].substr( 1 ).trim();
						var name = ( " " + result[ 0 ].substr( 1 ).trim() ).substr( 1 );

						state.startObject( name );

					} else if ( material_use_pattern.test( line ) ) {

						// material

						state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries );

					} else if ( material_library_pattern.test( line ) ) {

						// mtl file

						state.materialLibraries.push( line.substring( 7 ).trim() );

					} else if ( lineFirstChar === 's' ) {

						result = line.split( ' ' );

						// smooth shading

						// @todo Handle files that have varying smooth values for a set of faces inside one geometry,
						// but does not define a usemtl for each face set.
						// This should be detected and a dummy material created (later MultiMaterial and geometry groups).
						// This requires some care to not create extra material on each smooth value for "normal" obj files.
						// where explicit usemtl defines geometry groups.
						// Example asset: examples/models/obj/cerberus/Cerberus.obj

						/*
						 * http://paulbourke.net/dataformats/obj/
						 * or
						 * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf
						 *
						 * From chapter "Grouping" Syntax explanation "s group_number":
						 * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off.
						 * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form
						 * surfaces, smoothing groups are either turned on or off; there is no difference between values greater
						 * than 0."
						 */
						if ( result.length > 1 ) {

							var value = result[ 1 ].trim().toLowerCase();
							state.object.smooth = ( value !== '0' && value !== 'off' );

						} else {

							// ZBrush can produce "s" lines #11707
							state.object.smooth = true;

						}
						var material = state.object.currentMaterial();
						if ( material ) material.smooth = state.object.smooth;

					} else {

						// Handle null terminated files without exception
						if ( line === '\0' ) continue;

						throw new Error( 'THREE.OBJLoader: Unexpected line: "' + line + '"' );

					}

				}

				state.finalize();

				var container = new THREE.Group();
				container.materialLibraries = [].concat( state.materialLibraries );

				for ( var i = 0, l = state.objects.length; i < l; i ++ ) {

					var object = state.objects[ i ];
					var geometry = object.geometry;
					var materials = object.materials;
					var isLine = ( geometry.type === 'Line' );
					var isPoints = ( geometry.type === 'Points' );
					var hasVertexColors = false;

					// Skip o/g line declarations that did not follow with any faces
					if ( geometry.vertices.length === 0 ) continue;

					var buffergeometry = new THREE.BufferGeometry();

					buffergeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( geometry.vertices, 3 ) );

					if ( geometry.normals.length > 0 ) {

						buffergeometry.addAttribute( 'normal', new THREE.Float32BufferAttribute( geometry.normals, 3 ) );

					} else {

						buffergeometry.computeVertexNormals();

					}

					if ( geometry.colors.length > 0 ) {

						hasVertexColors = true;
						buffergeometry.addAttribute( 'color', new THREE.Float32BufferAttribute( geometry.colors, 3 ) );

					}

					if ( geometry.uvs.length > 0 ) {

						buffergeometry.addAttribute( 'uv', new THREE.Float32BufferAttribute( geometry.uvs, 2 ) );

					}

					// Create materials

					var createdMaterials = [];

					for ( var mi = 0, miLen = materials.length; mi < miLen; mi ++ ) {

						var sourceMaterial = materials[ mi ];
						var material = undefined;

						if ( this.materials !== null ) {

							material = this.materials.create( sourceMaterial.name );

							// mtl etc. loaders probably can't create line materials correctly, copy properties to a line material.
							if ( isLine && material && ! ( material instanceof THREE.LineBasicMaterial ) ) {

								var materialLine = new THREE.LineBasicMaterial();
								materialLine.copy( material );
								materialLine.lights = false; // TOFIX
								material = materialLine;

							} else if ( isPoints && material && ! ( material instanceof THREE.PointsMaterial ) ) {

								var materialPoints = new THREE.PointsMaterial( { size: 10, sizeAttenuation: false } );
								materialLine.copy( material );
								material = materialPoints;

							}

						}

						if ( ! material ) {

							if ( isLine ) {

								material = new THREE.LineBasicMaterial();

							} else if ( isPoints ) {

								material = new THREE.PointsMaterial( { size: 1, sizeAttenuation: false } );

							} else {

								material = new THREE.MeshPhongMaterial();

							}

							material.name = sourceMaterial.name;

						}

						material.flatShading = sourceMaterial.smooth ? false : true;
						material.vertexColors = hasVertexColors ? THREE.VertexColors : THREE.NoColors;

						createdMaterials.push( material );

					}

					// Create mesh

					var mesh;

					if ( createdMaterials.length > 1 ) {

						for ( var mi = 0, miLen = materials.length; mi < miLen; mi ++ ) {

							var sourceMaterial = materials[ mi ];
							buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi );

						}

						if ( isLine ) {

							mesh = new THREE.LineSegments( buffergeometry, createdMaterials );

						} else if ( isPoints ) {

							mesh = new THREE.Points( buffergeometry, createdMaterials );

						} else {

							mesh = new THREE.Mesh( buffergeometry, createdMaterials );

						}

					} else {

						if ( isLine ) {

							mesh = new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] );

						} else if ( isPoints ) {

							mesh = new THREE.Points( buffergeometry, createdMaterials[ 0 ] );

						} else {

							mesh = new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] );

						}

					}

					mesh.name = object.name;

					container.add( mesh );

				}

				console.timeEnd( 'OBJLoader' );

				return container;

			}

		};

		return OBJLoader;

	} )();

	/**
	 * @author mrdoob / http://mrdoob.com/
	 * @author Mugen87 / https://github.com/Mugen87
	 */

	THREE.PlayCanvasLoader = function ( manager ) {

		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

	};

	THREE.PlayCanvasLoader.prototype = {

		constructor: THREE.PlayCanvasLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			var scope = this;

			var loader = new THREE.FileLoader( scope.manager );
			loader.load( url, function ( text ) {

				onLoad( scope.parse( JSON.parse( text ) ) );

			}, onProgress, onError );

		},

		parse: function ( json ) {

			function parseVertices( data ) {

				var attributes = {};

				// create a buffer attribute for each array that contains vertex information

				for ( var name in data ) {

					var array = data[ name ];

					var type = array.type;
					var size = array.components;

					var attribute;

					switch ( type ) {

						case 'float32':
							attribute = new THREE.Float32BufferAttribute( array.data, size );
							break;

						case 'uint8':
							attribute = new THREE.Uint8BufferAttribute( array.data, size );
							break;

						case 'uint16':
							attribute = new THREE.Uint16BufferAttribute( array.data, size );
							break;

						default:
							console.log( 'THREE.PlayCanvasLoader: Array type "%s" not yet supported.', type );

					}

					attributes[ name ] = attribute;

				}

				data._attributes = attributes;

			}

			function parseMeshes( data ) {

				// create buffer geometry

				var geometry = new THREE.BufferGeometry();

				geometry.setIndex( data.indices );

				var attributes = model.vertices[ data.vertices ]._attributes;

				for ( var name in attributes ) {

					var attribute = attributes[ name ];

					if ( name === 'texCoord0' ) name = 'uv';

					geometry.addAttribute( name, attribute );

				}

				data._geometry = geometry;

			}

			function parseMeshInstances( data ) {

				var node = model.nodes[ data.node ];
				var mesh = model.meshes[ data.mesh ];

				if ( node._geometries === undefined ) {

					node._geometries = [];

				}

				node._geometries.push( mesh._geometry );

			}

			function parseNodes( data ) {

				var object = new THREE.Group();

				var geometries = data._geometries;

				if ( geometries !== undefined ) {

					var material = new THREE.MeshPhongMaterial();

					for ( var i = 0, l = geometries.length; i < l; i ++ ) {

						var geometry = geometries[ i ];

						object.add( new THREE.Mesh( geometry, material ) );

					}

				}

				for ( var i = 0, l = data.rotation.length; i < l; i ++ ) {

					data.rotation[ i ] *= Math.PI / 180;

				}

				//

				object.name = data.name;

				object.position.fromArray( data.position );
				object.quaternion.setFromEuler( new THREE.Euler().fromArray( data.rotation ) );
				object.scale.fromArray( data.scale );

				data._object = object;

			}

			//

			var model = json.model;

			for ( var i = 0, l = model.vertices.length; i < l; i ++ ) {

				parseVertices( model.vertices[ i ] );

			}

			for ( var i = 0, l = model.meshes.length; i < l; i ++ ) {

				parseMeshes( model.meshes[ i ] );

			}

			for ( var i = 0, l = model.meshInstances.length; i < l; i ++ ) {

				parseMeshInstances( model.meshInstances[ i ] );

			}

			for ( var i = 0, l = model.nodes.length; i < l; i ++ ) {

				parseNodes( model.nodes[ i ] );

			}

			// setup scene hierarchy

			for ( var i = 0, l = model.parents.length; i < l; i ++ ) {

				var parent = model.parents[ i ];

				if ( parent === - 1 ) continue;

				model.nodes[ parent ]._object.add( model.nodes[ i ]._object );


			}

			return model.nodes[ 0 ]._object;

		}

	};

	/**
	 * @author Wei Meng / http://about.me/menway
	 *
	 * Description: A THREE loader for PLY ASCII files (known as the Polygon
	 * File Format or the Stanford Triangle Format).
	 *
	 * Limitations: ASCII decoding assumes file is UTF-8.
	 *
	 * Usage:
	 *	var loader = new THREE.PLYLoader();
	 *	loader.load('./models/ply/ascii/dolphins.ply', function (geometry) {
	 *
	 *		scene.add( new THREE.Mesh( geometry ) );
	 *
	 *	} );
	 *
	 * If the PLY file uses non standard property names, they can be mapped while
	 * loading. For example, the following maps the properties
	 * “diffuse_(red|green|blue)” in the file to standard color names.
	 *
	 * loader.setPropertyNameMapping( {
	 *	diffuse_red: 'red',
	 *	diffuse_green: 'green',
	 *	diffuse_blue: 'blue'
	 * } );
	 *
	 */


	THREE.PLYLoader = function ( manager ) {

		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

		this.propertyNameMapping = {};

	};

	THREE.PLYLoader.prototype = {

		constructor: THREE.PLYLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			var scope = this;

			var loader = new THREE.FileLoader( this.manager );
			loader.setResponseType( 'arraybuffer' );
			loader.load( url, function ( text ) {

				onLoad( scope.parse( text ) );

			}, onProgress, onError );

		},

		setPropertyNameMapping: function ( mapping ) {

			this.propertyNameMapping = mapping;

		},

		parse: function ( data ) {

			function parseHeader( data ) {

				var patternHeader = /ply([\s\S]*)end_header\s/;
				var headerText = '';
				var headerLength = 0;
				var result = patternHeader.exec( data );

				if ( result !== null ) {

					headerText = result[ 1 ];
					headerLength = result[ 0 ].length;

				}

				var header = {
					comments: [],
					elements: [],
					headerLength: headerLength
				};

				var lines = headerText.split( '\n' );
				var currentElement;
				var lineType, lineValues;

				function make_ply_element_property( propertValues, propertyNameMapping ) {

					var property = { type: propertValues[ 0 ] };

					if ( property.type === 'list' ) {

						property.name = propertValues[ 3 ];
						property.countType = propertValues[ 1 ];
						property.itemType = propertValues[ 2 ];

					} else {

						property.name = propertValues[ 1 ];

					}

					if ( property.name in propertyNameMapping ) {

						property.name = propertyNameMapping[ property.name ];

					}

					return property;

				}

				for ( var i = 0; i < lines.length; i ++ ) {

					var line = lines[ i ];
					line = line.trim();

					if ( line === '' ) continue;

					lineValues = line.split( /\s+/ );
					lineType = lineValues.shift();
					line = lineValues.join( ' ' );

					switch ( lineType ) {

						case 'format':

							header.format = lineValues[ 0 ];
							header.version = lineValues[ 1 ];

							break;

						case 'comment':

							header.comments.push( line );

							break;

						case 'element':

							if ( currentElement !== undefined ) {

								header.elements.push( currentElement );

							}

							currentElement = {};
							currentElement.name = lineValues[ 0 ];
							currentElement.count = parseInt( lineValues[ 1 ] );
							currentElement.properties = [];

							break;

						case 'property':

							currentElement.properties.push( make_ply_element_property( lineValues, scope.propertyNameMapping ) );

							break;


						default:

							console.log( 'unhandled', lineType, lineValues );

					}

				}

				if ( currentElement !== undefined ) {

					header.elements.push( currentElement );

				}

				return header;

			}

			function parseASCIINumber( n, type ) {

				switch ( type ) {

					case 'char': case 'uchar': case 'short': case 'ushort': case 'int': case 'uint':
					case 'int8': case 'uint8': case 'int16': case 'uint16': case 'int32': case 'uint32':

						return parseInt( n );

					case 'float': case 'double': case 'float32': case 'float64':

						return parseFloat( n );

				}

			}

			function parseASCIIElement( properties, line ) {

				var values = line.split( /\s+/ );

				var element = {};

				for ( var i = 0; i < properties.length; i ++ ) {

					if ( properties[ i ].type === 'list' ) {

						var list = [];
						var n = parseASCIINumber( values.shift(), properties[ i ].countType );

						for ( var j = 0; j < n; j ++ ) {

							list.push( parseASCIINumber( values.shift(), properties[ i ].itemType ) );

						}

						element[ properties[ i ].name ] = list;

					} else {

						element[ properties[ i ].name ] = parseASCIINumber( values.shift(), properties[ i ].type );

					}

				}

				return element;

			}

			function parseASCII( data, header ) {

				// PLY ascii format specification, as per http://en.wikipedia.org/wiki/PLY_(file_format)

				var buffer = {
					indices: [],
					vertices: [],
					normals: [],
					uvs: [],
					colors: []
				};

				var result;

				var patternBody = /end_header\s([\s\S]*)$/;
				var body = '';
				if ( ( result = patternBody.exec( data ) ) !== null ) {

					body = result[ 1 ];

				}

				var lines = body.split( '\n' );
				var currentElement = 0;
				var currentElementCount = 0;

				for ( var i = 0; i < lines.length; i ++ ) {

					var line = lines[ i ];
					line = line.trim();
					if ( line === '' ) {

						continue;

					}

					if ( currentElementCount >= header.elements[ currentElement ].count ) {

						currentElement ++;
						currentElementCount = 0;

					}

					var element = parseASCIIElement( header.elements[ currentElement ].properties, line );

					handleElement( buffer, header.elements[ currentElement ].name, element );

					currentElementCount ++;

				}

				return postProcess( buffer );

			}

			function postProcess( buffer ) {

				var geometry = new THREE.BufferGeometry();

				// mandatory buffer data

				if ( buffer.indices.length > 0 ) {

					geometry.setIndex( buffer.indices );

				}

				geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( buffer.vertices, 3 ) );

				// optional buffer data

				if ( buffer.normals.length > 0 ) {

					geometry.addAttribute( 'normal', new THREE.Float32BufferAttribute( buffer.normals, 3 ) );

				}

				if ( buffer.uvs.length > 0 ) {

					geometry.addAttribute( 'uv', new THREE.Float32BufferAttribute( buffer.uvs, 2 ) );

				}

				if ( buffer.colors.length > 0 ) {

					geometry.addAttribute( 'color', new THREE.Float32BufferAttribute( buffer.colors, 3 ) );

				}

				geometry.computeBoundingSphere();

				return geometry;

			}

			function handleElement( buffer, elementName, element ) {

				if ( elementName === 'vertex' ) {

					buffer.vertices.push( element.x, element.y, element.z );

					if ( 'nx' in element && 'ny' in element && 'nz' in element ) {

						buffer.normals.push( element.nx, element.ny, element.nz );

					}

					if ( 's' in element && 't' in element ) {

						buffer.uvs.push( element.s, element.t );

					}

					if ( 'red' in element && 'green' in element && 'blue' in element ) {

						buffer.colors.push( element.red / 255.0, element.green / 255.0, element.blue / 255.0 );

					}

				} else if ( elementName === 'face' ) {

					var vertex_indices = element.vertex_indices || element.vertex_index; // issue #9338

					if ( vertex_indices.length === 3 ) {

						buffer.indices.push( vertex_indices[ 0 ], vertex_indices[ 1 ], vertex_indices[ 2 ] );

					} else if ( vertex_indices.length === 4 ) {

						buffer.indices.push( vertex_indices[ 0 ], vertex_indices[ 1 ], vertex_indices[ 3 ] );
						buffer.indices.push( vertex_indices[ 1 ], vertex_indices[ 2 ], vertex_indices[ 3 ] );

					}

				}

			}

			function binaryRead( dataview, at, type, little_endian ) {

				switch ( type ) {

					// corespondences for non-specific length types here match rply:
					case 'int8':		case 'char':	 return [ dataview.getInt8( at ), 1 ];
					case 'uint8':		case 'uchar':	 return [ dataview.getUint8( at ), 1 ];
					case 'int16':		case 'short':	 return [ dataview.getInt16( at, little_endian ), 2 ];
					case 'uint16':	case 'ushort': return [ dataview.getUint16( at, little_endian ), 2 ];
					case 'int32':		case 'int':		 return [ dataview.getInt32( at, little_endian ), 4 ];
					case 'uint32':	case 'uint':	 return [ dataview.getUint32( at, little_endian ), 4 ];
					case 'float32': case 'float':	 return [ dataview.getFloat32( at, little_endian ), 4 ];
					case 'float64': case 'double': return [ dataview.getFloat64( at, little_endian ), 8 ];

				}

			}

			function binaryReadElement( dataview, at, properties, little_endian ) {

				var element = {};
				var result, read = 0;

				for ( var i = 0; i < properties.length; i ++ ) {

					if ( properties[ i ].type === 'list' ) {

						var list = [];

						result = binaryRead( dataview, at + read, properties[ i ].countType, little_endian );
						var n = result[ 0 ];
						read += result[ 1 ];

						for ( var j = 0; j < n; j ++ ) {

							result = binaryRead( dataview, at + read, properties[ i ].itemType, little_endian );
							list.push( result[ 0 ] );
							read += result[ 1 ];

						}

						element[ properties[ i ].name ] = list;

					} else {

						result = binaryRead( dataview, at + read, properties[ i ].type, little_endian );
						element[ properties[ i ].name ] = result[ 0 ];
						read += result[ 1 ];

					}

				}

				return [ element, read ];

			}

			function parseBinary( data, header ) {

				var buffer = {
					indices: [],
					vertices: [],
					normals: [],
					uvs: [],
					colors: []
				};

				var little_endian = ( header.format === 'binary_little_endian' );
				var body = new DataView( data, header.headerLength );
				var result, loc = 0;

				for ( var currentElement = 0; currentElement < header.elements.length; currentElement ++ ) {

					for ( var currentElementCount = 0; currentElementCount < header.elements[ currentElement ].count; currentElementCount ++ ) {

						result = binaryReadElement( body, loc, header.elements[ currentElement ].properties, little_endian );
						loc += result[ 1 ];
						var element = result[ 0 ];

						handleElement( buffer, header.elements[ currentElement ].name, element );

					}

				}

				return postProcess( buffer );

			}

			//

			var geometry;
			var scope = this;

			if ( data instanceof ArrayBuffer ) {

				var text = THREE.LoaderUtils.decodeText( new Uint8Array( data ) );
				var header = parseHeader( text );

				geometry = header.format === 'ascii' ? parseASCII( text, header ) : parseBinary( data, header );

			} else {

				geometry = parseASCII( data, parseHeader( data ) );

			}

			return geometry;

		}

	};

	/**
	 * @author aleeper / http://adamleeper.com/
	 * @author mrdoob / http://mrdoob.com/
	 * @author gero3 / https://github.com/gero3
	 * @author Mugen87 / https://github.com/Mugen87
	 *
	 * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs.
	 *
	 * Supports both binary and ASCII encoded files, with automatic detection of type.
	 *
	 * The loader returns a non-indexed buffer geometry.
	 *
	 * Limitations:
	 *  Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL).
	 *  There is perhaps some question as to how valid it is to always assume little-endian-ness.
	 *  ASCII decoding assumes file is UTF-8.
	 *
	 * Usage:
	 *  var loader = new THREE.STLLoader();
	 *  loader.load( './models/stl/slotted_disk.stl', function ( geometry ) {
	 *    scene.add( new THREE.Mesh( geometry ) );
	 *  });
	 *
	 * For binary STLs geometry might contain colors for vertices. To use it:
	 *  // use the same code to load STL as above
	 *  if (geometry.hasColors) {
	 *    material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: THREE.VertexColors });
	 *  } else { .... }
	 *  var mesh = new THREE.Mesh( geometry, material );
	 */


	THREE.STLLoader = function ( manager ) {

		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

	};

	THREE.STLLoader.prototype = {

		constructor: THREE.STLLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			var scope = this;

			var loader = new THREE.FileLoader( scope.manager );
			loader.setResponseType( 'arraybuffer' );
			loader.load( url, function ( text ) {

				try {

					onLoad( scope.parse( text ) );

				} catch ( exception ) {

					if ( onError ) {

						onError( exception );

					}

				}

			}, onProgress, onError );

		},

		parse: function ( data ) {

			function isBinary( data ) {

				var expect, face_size, n_faces, reader;
				reader = new DataView( data );
				face_size = ( 32 / 8 * 3 ) + ( ( 32 / 8 * 3 ) * 3 ) + ( 16 / 8 );
				n_faces = reader.getUint32( 80, true );
				expect = 80 + ( 32 / 8 ) + ( n_faces * face_size );

				if ( expect === reader.byteLength ) {

					return true;

				}

				// An ASCII STL data must begin with 'solid ' as the first six bytes.
				// However, ASCII STLs lacking the SPACE after the 'd' are known to be
				// plentiful.  So, check the first 5 bytes for 'solid'.

				// US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd'

				var solid = [ 115, 111, 108, 105, 100 ];

				for ( var i = 0; i < 5; i ++ ) {

					// If solid[ i ] does not match the i-th byte, then it is not an
					// ASCII STL; hence, it is binary and return true.

					if ( solid[ i ] != reader.getUint8( i, false ) ) return true;

	 			}

				// First 5 bytes read "solid"; declare it to be an ASCII STL

				return false;

			}

			function parseBinary( data ) {

				var reader = new DataView( data );
				var faces = reader.getUint32( 80, true );

				var r, g, b, hasColors = false, colors;
				var defaultR, defaultG, defaultB, alpha;

				// process STL header
				// check for default color in header ("COLOR=rgba" sequence).

				for ( var index = 0; index < 80 - 10; index ++ ) {

					if ( ( reader.getUint32( index, false ) == 0x434F4C4F /*COLO*/ ) &&
						( reader.getUint8( index + 4 ) == 0x52 /*'R'*/ ) &&
						( reader.getUint8( index + 5 ) == 0x3D /*'='*/ ) ) {

						hasColors = true;
						colors = [];

						defaultR = reader.getUint8( index + 6 ) / 255;
						defaultG = reader.getUint8( index + 7 ) / 255;
						defaultB = reader.getUint8( index + 8 ) / 255;
						alpha = reader.getUint8( index + 9 ) / 255;

					}

				}

				var dataOffset = 84;
				var faceLength = 12 * 4 + 2;

				var geometry = new THREE.BufferGeometry();

				var vertices = [];
				var normals = [];

				for ( var face = 0; face < faces; face ++ ) {

					var start = dataOffset + face * faceLength;
					var normalX = reader.getFloat32( start, true );
					var normalY = reader.getFloat32( start + 4, true );
					var normalZ = reader.getFloat32( start + 8, true );

					if ( hasColors ) {

						var packedColor = reader.getUint16( start + 48, true );

						if ( ( packedColor & 0x8000 ) === 0 ) {

							// facet has its own unique color

							r = ( packedColor & 0x1F ) / 31;
							g = ( ( packedColor >> 5 ) & 0x1F ) / 31;
							b = ( ( packedColor >> 10 ) & 0x1F ) / 31;

						} else {

							r = defaultR;
							g = defaultG;
							b = defaultB;

						}

					}

					for ( var i = 1; i <= 3; i ++ ) {

						var vertexstart = start + i * 12;

						vertices.push( reader.getFloat32( vertexstart, true ) );
						vertices.push( reader.getFloat32( vertexstart + 4, true ) );
						vertices.push( reader.getFloat32( vertexstart + 8, true ) );

						normals.push( normalX, normalY, normalZ );

						if ( hasColors ) {

							colors.push( r, g, b );

						}

					}

				}

				geometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( vertices ), 3 ) );
				geometry.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( normals ), 3 ) );

				if ( hasColors ) {

					geometry.addAttribute( 'color', new THREE.BufferAttribute( new Float32Array( colors ), 3 ) );
					geometry.hasColors = true;
					geometry.alpha = alpha;

				}

				return geometry;

			}

			function parseASCII( data ) {

				var geometry = new THREE.BufferGeometry();
				var patternFace = /facet([\s\S]*?)endfacet/g;
				var faceCounter = 0;

				var patternFloat = /[\s]+([+-]?(?:\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/.source;
				var patternVertex = new RegExp( 'vertex' + patternFloat + patternFloat + patternFloat, 'g' );
				var patternNormal = new RegExp( 'normal' + patternFloat + patternFloat + patternFloat, 'g' );

				var vertices = [];
				var normals = [];

				var normal = new THREE.Vector3();

				var result;

				while ( ( result = patternFace.exec( data ) ) !== null ) {

					var vertexCountPerFace = 0;
					var normalCountPerFace = 0;

					var text = result[ 0 ];

					while ( ( result = patternNormal.exec( text ) ) !== null ) {

						normal.x = parseFloat( result[ 1 ] );
						normal.y = parseFloat( result[ 2 ] );
						normal.z = parseFloat( result[ 3 ] );
						normalCountPerFace ++;

					}

					while ( ( result = patternVertex.exec( text ) ) !== null ) {

						vertices.push( parseFloat( result[ 1 ] ), parseFloat( result[ 2 ] ), parseFloat( result[ 3 ] ) );
						normals.push( normal.x, normal.y, normal.z );
						vertexCountPerFace ++;

					}

					// every face have to own ONE valid normal

					if ( normalCountPerFace !== 1 ) {

						console.error( 'THREE.STLLoader: Something isn\'t right with the normal of face number ' + faceCounter );

					}

					// each face have to own THREE valid vertices

					if ( vertexCountPerFace !== 3 ) {

						console.error( 'THREE.STLLoader: Something isn\'t right with the vertices of face number ' + faceCounter );

					}

					faceCounter ++;

				}

				geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
				geometry.addAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) );

				return geometry;

			}

			function ensureString( buffer ) {

				if ( typeof buffer !== 'string' ) {

					return THREE.LoaderUtils.decodeText( new Uint8Array( buffer ) );

				}

				return buffer;

			}

			function ensureBinary( buffer ) {

				if ( typeof buffer === 'string' ) {

					var array_buffer = new Uint8Array( buffer.length );
					for ( var i = 0; i < buffer.length; i ++ ) {

						array_buffer[ i ] = buffer.charCodeAt( i ) & 0xff; // implicitly assumes little-endian

					}
					return array_buffer.buffer || array_buffer;

				} else {

					return buffer;

				}

			}

			// start

			var binData = ensureBinary( data );

			return isBinary( binData ) ? parseBinary( binData ) : parseASCII( ensureString( data ) );

		}

	};

	/*
	 * @author Daosheng Mu / https://github.com/DaoshengMu/
	 * @author mrdoob / http://mrdoob.com/
	 * @author takahirox / https://github.com/takahirox/
	 */

	THREE.TGALoader = function ( manager ) {

		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

	};

	THREE.TGALoader.prototype = {

		constructor: THREE.TGALoader,

		load: function ( url, onLoad, onProgress, onError ) {

			var scope = this;

			var texture = new THREE.Texture();

			var loader = new THREE.FileLoader( this.manager );
			loader.setResponseType( 'arraybuffer' );

			loader.load( url, function ( buffer ) {

				texture.image = scope.parse( buffer );
				texture.needsUpdate = true;

				if ( onLoad !== undefined ) {

					onLoad( texture );

				}

			}, onProgress, onError );

			return texture;

		},

		parse: function ( buffer ) {

			// reference from vthibault, https://github.com/vthibault/roBrowser/blob/master/src/Loaders/Targa.js

			function tgaCheckHeader( header ) {

				switch ( header.image_type ) {

					// check indexed type

					case TGA_TYPE_INDEXED:
					case TGA_TYPE_RLE_INDEXED:
						if ( header.colormap_length > 256 || header.colormap_size !== 24 || header.colormap_type !== 1 ) {

							console.error( 'THREE.TGALoader: Invalid type colormap data for indexed type.' );

						}
						break;

					// check colormap type

					case TGA_TYPE_RGB:
					case TGA_TYPE_GREY:
					case TGA_TYPE_RLE_RGB:
					case TGA_TYPE_RLE_GREY:
						if ( header.colormap_type ) {

							console.error( 'THREE.TGALoader: Invalid type colormap data for colormap type.' );

						}
						break;

					// What the need of a file without data ?

					case TGA_TYPE_NO_DATA:
						console.error( 'THREE.TGALoader: No data.' );

					// Invalid type ?

					default:
						console.error( 'THREE.TGALoader: Invalid type "%s".', header.image_type );

				}

				// check image width and height

				if ( header.width <= 0 || header.height <= 0 ) {

					console.error( 'THREE.TGALoader: Invalid image size.' );

				}

				// check image pixel size

				if ( header.pixel_size !== 8 && header.pixel_size !== 16 &&
					header.pixel_size !== 24 && header.pixel_size !== 32 ) {

					console.error( 'THREE.TGALoader: Invalid pixel size "%s".', header.pixel_size );

				}

			}

			// parse tga image buffer

			function tgaParse( use_rle, use_pal, header, offset, data ) {

				var pixel_data,
					pixel_size,
					pixel_total,
					palettes;

				pixel_size = header.pixel_size >> 3;
				pixel_total = header.width * header.height * pixel_size;

				 // read palettes

				 if ( use_pal ) {

					 palettes = data.subarray( offset, offset += header.colormap_length * ( header.colormap_size >> 3 ) );

				 }

				 // read RLE

				 if ( use_rle ) {

					 pixel_data = new Uint8Array( pixel_total );

					var c, count, i;
					var shift = 0;
					var pixels = new Uint8Array( pixel_size );

					while ( shift < pixel_total ) {

						c = data[ offset ++ ];
						count = ( c & 0x7f ) + 1;

						// RLE pixels

						if ( c & 0x80 ) {

							// bind pixel tmp array

							for ( i = 0; i < pixel_size; ++ i ) {

								pixels[ i ] = data[ offset ++ ];

							}

							// copy pixel array

							for ( i = 0; i < count; ++ i ) {

								pixel_data.set( pixels, shift + i * pixel_size );

							}

							shift += pixel_size * count;

						} else {

							// raw pixels

							count *= pixel_size;
							for ( i = 0; i < count; ++ i ) {

								pixel_data[ shift + i ] = data[ offset ++ ];

							}
							shift += count;

						}

					}

				 } else {

					// raw pixels

					pixel_data = data.subarray(
						 offset, offset += ( use_pal ? header.width * header.height : pixel_total )
					);

				 }

				 return {
					pixel_data: pixel_data,
					palettes: palettes
				 };

			}

			function tgaGetImageData8bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image, palettes ) {

				var colormap = palettes;
				var color, i = 0, x, y;
				var width = header.width;

				for ( y = y_start; y !== y_end; y += y_step ) {

					for ( x = x_start; x !== x_end; x += x_step, i ++ ) {

						color = image[ i ];
						imageData[ ( x + width * y ) * 4 + 3 ] = 255;
						imageData[ ( x + width * y ) * 4 + 2 ] = colormap[ ( color * 3 ) + 0 ];
						imageData[ ( x + width * y ) * 4 + 1 ] = colormap[ ( color * 3 ) + 1 ];
						imageData[ ( x + width * y ) * 4 + 0 ] = colormap[ ( color * 3 ) + 2 ];

					}

				}

				return imageData;

			}

			function tgaGetImageData16bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image ) {

				var color, i = 0, x, y;
				var width = header.width;

				for ( y = y_start; y !== y_end; y += y_step ) {

					for ( x = x_start; x !== x_end; x += x_step, i += 2 ) {

						color = image[ i + 0 ] + ( image[ i + 1 ] << 8 ); // Inversed ?
						imageData[ ( x + width * y ) * 4 + 0 ] = ( color & 0x7C00 ) >> 7;
						imageData[ ( x + width * y ) * 4 + 1 ] = ( color & 0x03E0 ) >> 2;
						imageData[ ( x + width * y ) * 4 + 2 ] = ( color & 0x001F ) >> 3;
						imageData[ ( x + width * y ) * 4 + 3 ] = ( color & 0x8000 ) ? 0 : 255;

					}

				}

				return imageData;

			}

			function tgaGetImageData24bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image ) {

				var i = 0, x, y;
				var width = header.width;

				for ( y = y_start; y !== y_end; y += y_step ) {

					for ( x = x_start; x !== x_end; x += x_step, i += 3 ) {

						imageData[ ( x + width * y ) * 4 + 3 ] = 255;
						imageData[ ( x + width * y ) * 4 + 2 ] = image[ i + 0 ];
						imageData[ ( x + width * y ) * 4 + 1 ] = image[ i + 1 ];
						imageData[ ( x + width * y ) * 4 + 0 ] = image[ i + 2 ];

					}

				}

				return imageData;

			}

			function tgaGetImageData32bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image ) {

				var i = 0, x, y;
				var width = header.width;

				for ( y = y_start; y !== y_end; y += y_step ) {

					for ( x = x_start; x !== x_end; x += x_step, i += 4 ) {

						imageData[ ( x + width * y ) * 4 + 2 ] = image[ i + 0 ];
						imageData[ ( x + width * y ) * 4 + 1 ] = image[ i + 1 ];
						imageData[ ( x + width * y ) * 4 + 0 ] = image[ i + 2 ];
						imageData[ ( x + width * y ) * 4 + 3 ] = image[ i + 3 ];

					}

				}

				return imageData;

			}

			function tgaGetImageDataGrey8bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image ) {

				var color, i = 0, x, y;
				var width = header.width;

				for ( y = y_start; y !== y_end; y += y_step ) {

					for ( x = x_start; x !== x_end; x += x_step, i ++ ) {

						color = image[ i ];
						imageData[ ( x + width * y ) * 4 + 0 ] = color;
						imageData[ ( x + width * y ) * 4 + 1 ] = color;
						imageData[ ( x + width * y ) * 4 + 2 ] = color;
						imageData[ ( x + width * y ) * 4 + 3 ] = 255;

					}

				}

				return imageData;

			}

			function tgaGetImageDataGrey16bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image ) {

				var i = 0, x, y;
				var width = header.width;

				for ( y = y_start; y !== y_end; y += y_step ) {

					for ( x = x_start; x !== x_end; x += x_step, i += 2 ) {

						imageData[ ( x + width * y ) * 4 + 0 ] = image[ i + 0 ];
						imageData[ ( x + width * y ) * 4 + 1 ] = image[ i + 0 ];
						imageData[ ( x + width * y ) * 4 + 2 ] = image[ i + 0 ];
						imageData[ ( x + width * y ) * 4 + 3 ] = image[ i + 1 ];

					}

				}

				return imageData;

			}

			function getTgaRGBA( data, width, height, image, palette ) {

				var x_start,
					y_start,
					x_step,
					y_step,
					x_end,
					y_end;

				switch ( ( header.flags & TGA_ORIGIN_MASK ) >> TGA_ORIGIN_SHIFT ) {

					default:
					case TGA_ORIGIN_UL:
						x_start = 0;
						x_step = 1;
						x_end = width;
						y_start = 0;
						y_step = 1;
						y_end = height;
						break;

					case TGA_ORIGIN_BL:
						x_start = 0;
						x_step = 1;
						x_end = width;
						y_start = height - 1;
						y_step = - 1;
						y_end = - 1;
						break;

					case TGA_ORIGIN_UR:
						x_start = width - 1;
						x_step = - 1;
						x_end = - 1;
						y_start = 0;
						y_step = 1;
						y_end = height;
						break;

					case TGA_ORIGIN_BR:
						x_start = width - 1;
						x_step = - 1;
						x_end = - 1;
						y_start = height - 1;
						y_step = - 1;
						y_end = - 1;
						break;

				}

				if ( use_grey ) {

					switch ( header.pixel_size ) {

						case 8:
							tgaGetImageDataGrey8bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image );
							break;

						case 16:
							tgaGetImageDataGrey16bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image );
							break;

						default:
							console.error( 'THREE.TGALoader: Format not supported.' );
							break;

					}

				} else {

					switch ( header.pixel_size ) {

						case 8:
							tgaGetImageData8bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image, palette );
							break;

						case 16:
							tgaGetImageData16bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image );
							break;

						case 24:
							tgaGetImageData24bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image );
							break;

						case 32:
							tgaGetImageData32bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image );
							break;

						default:
							console.error( 'THREE.TGALoader: Format not supported.' );
							break;

					}

				}

				// Load image data according to specific method
				// var func = 'tgaGetImageData' + (use_grey ? 'Grey' : '') + (header.pixel_size) + 'bits';
				// func(data, y_start, y_step, y_end, x_start, x_step, x_end, width, image, palette );
				return data;

			}

			// TGA constants

			var TGA_TYPE_NO_DATA = 0,
				TGA_TYPE_INDEXED = 1,
				TGA_TYPE_RGB = 2,
				TGA_TYPE_GREY = 3,
				TGA_TYPE_RLE_INDEXED = 9,
				TGA_TYPE_RLE_RGB = 10,
				TGA_TYPE_RLE_GREY = 11,

				TGA_ORIGIN_MASK = 0x30,
				TGA_ORIGIN_SHIFT = 0x04,
				TGA_ORIGIN_BL = 0x00,
				TGA_ORIGIN_BR = 0x01,
				TGA_ORIGIN_UL = 0x02,
				TGA_ORIGIN_UR = 0x03;

			if ( buffer.length < 19 ) console.error( 'THREE.TGALoader: Not enough data to contain header.' );

			var content = new Uint8Array( buffer ),
				offset = 0,
				header = {
					id_length: content[ offset ++ ],
					colormap_type: content[ offset ++ ],
					image_type: content[ offset ++ ],
					colormap_index: content[ offset ++ ] | content[ offset ++ ] << 8,
					colormap_length: content[ offset ++ ] | content[ offset ++ ] << 8,
					colormap_size: content[ offset ++ ],
					origin: [
						content[ offset ++ ] | content[ offset ++ ] << 8,
						content[ offset ++ ] | content[ offset ++ ] << 8
					],
					width: content[ offset ++ ] | content[ offset ++ ] << 8,
					height: content[ offset ++ ] | content[ offset ++ ] << 8,
					pixel_size: content[ offset ++ ],
					flags: content[ offset ++ ]
				};

				// check tga if it is valid format

			tgaCheckHeader( header );

			if ( header.id_length + offset > buffer.length ) {

				console.error( 'THREE.TGALoader: No data.' );

			}

			// skip the needn't data

			offset += header.id_length;

			// get targa information about RLE compression and palette

			var use_rle = false,
				use_pal = false,
				use_grey = false;

			switch ( header.image_type ) {

				case TGA_TYPE_RLE_INDEXED:
					use_rle = true;
					use_pal = true;
					break;

				case TGA_TYPE_INDEXED:
					use_pal = true;
					break;

				case TGA_TYPE_RLE_RGB:
					use_rle = true;
					break;

				case TGA_TYPE_RGB:
					break;

				case TGA_TYPE_RLE_GREY:
					use_rle = true;
					use_grey = true;
					break;

				case TGA_TYPE_GREY:
					use_grey = true;
					break;

			}

			//

			var canvas = document.createElement( 'canvas' );
			canvas.width = header.width;
			canvas.height = header.height;

			var context = canvas.getContext( '2d' );
			var imageData = context.createImageData( header.width, header.height );

			var result = tgaParse( use_rle, use_pal, header, offset, content );
			var rgbaData = getTgaRGBA( imageData.data, header.width, header.height, result.pixel_data, result.palettes );

			context.putImageData( imageData, 0, 0 );

			return canvas;

		}

	};

	/**
	 * Loader for UTF8 version2 (after r51) encoded models generated by:
	 *	http://code.google.com/p/webgl-loader/
	 *
	 * Code to load/decompress mesh is taken from r100 of this webgl-loader
	 */

	THREE.UTF8Loader = function () {};

	/**
	 * Load UTF8 encoded model
	 * @param jsonUrl - URL from which to load json containing information about model
	 * @param callback - Callback(THREE.Object3D) on successful loading of model
	 * @param options - options on how to load model (see THREE.MTLLoader.MaterialCreator for basic options)
	 *                  Additional options include
	 *                   geometryBase: Base url from which to load referenced geometries
	 *                   materialBase: Base url from which to load referenced textures
	 */

	THREE.UTF8Loader.prototype.load = function ( jsonUrl, callback, options ) {

		this.downloadModelJson( jsonUrl, callback, options );

	};

	// BufferGeometryCreator

	THREE.UTF8Loader.BufferGeometryCreator = function () {
	};

	THREE.UTF8Loader.BufferGeometryCreator.prototype.create = function ( attribArray, indices ) {

		var ntris = indices.length / 3;

		var geometry = new THREE.BufferGeometry();

		var positions = new Float32Array( ntris * 3 * 3 );
		var normals = new Float32Array( ntris * 3 * 3 );
		var uvs = new Float32Array( ntris * 3 * 2 );

		var i, j, offset;

		var end = attribArray.length;
		var stride = 8;

		// extract positions

		j = 0;
		offset = 0;

		for ( i = offset; i < end; i += stride ) {

			positions[ j ++ ] = attribArray[ i ];
			positions[ j ++ ] = attribArray[ i + 1 ];
			positions[ j ++ ] = attribArray[ i + 2 ];

		}

		// extract uvs

		j = 0;
		offset = 3;

		for ( i = offset; i < end; i += stride ) {

			uvs[ j ++ ] = attribArray[ i ];
			uvs[ j ++ ] = attribArray[ i + 1 ];

		}

		// extract normals

		j = 0;
		offset = 5;

		for ( i = offset; i < end; i += stride ) {

			normals[ j ++ ] = attribArray[ i ];
			normals[ j ++ ] = attribArray[ i + 1 ];
			normals[ j ++ ] = attribArray[ i + 2 ];

		}

		geometry.setIndex( new THREE.BufferAttribute( indices, 1 ) );
		geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
		geometry.addAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );
		geometry.addAttribute( 'uv', new THREE.BufferAttribute( uvs, 2 ) );

		geometry.computeBoundingSphere();

		return geometry;

	};


	// UTF-8 decoder from webgl-loader (r100)
	// http://code.google.com/p/webgl-loader/

	// Model manifest description. Contains objects like:
	// name: {
	//   materials: { 'material_name': { ... } ... },
	//   decodeParams: {
	//     decodeOffsets: [ ... ],
	//     decodeScales: [ ... ],
	//   },
	//   urls: {
	//     'url': [
	//       { material: 'material_name',
	//         attribRange: [#, #],
	//         indexRange: [#, #],
	//         names: [ 'object names' ... ],
	//         lengths: [#, #, # ... ]
	//       }
	//     ],
	//     ...
	//   }
	// }

	var DEFAULT_DECODE_PARAMS = {

		decodeOffsets: [ - 4095, - 4095, - 4095, 0, 0, - 511, - 511, - 511 ],
		decodeScales: [ 1 / 8191, 1 / 8191, 1 / 8191, 1 / 1023, 1 / 1023, 1 / 1023, 1 / 1023, 1 / 1023 ]

		// TODO: normal decoding? (see walt.js)
		// needs to know: input, output (from vertex format!)
		//
		// Should split attrib/index.
		// 1) Decode position and non-normal attributes.
		// 2) Decode indices, computing normals
		// 3) Maybe normalize normals? Only necessary for refinement, or fixed?
		// 4) Maybe refine normals? Should this be part of regular refinement?
		// 5) Morphing

	};

	// Triangle strips!

	// TODO: will it be an optimization to specialize this method at
	// runtime for different combinations of stride, decodeOffset and
	// decodeScale?

	THREE.UTF8Loader.prototype.decompressAttribsInner_ = function ( str, inputStart, inputEnd,
		output, outputStart, stride, decodeOffset, decodeScale ) {

		var prev = 0;

		for ( var j = inputStart; j < inputEnd; j ++ ) {

			var code = str.charCodeAt( j );
			prev += ( code >> 1 ) ^ ( - ( code & 1 ) );

			output[ outputStart ] = decodeScale * ( prev + decodeOffset );
			outputStart += stride;

		}

	};

	THREE.UTF8Loader.prototype.decompressIndices_ = function ( str, inputStart, numIndices, output, outputStart ) {

		var highest = 0;

		for ( var i = 0; i < numIndices; i ++ ) {

			var code = str.charCodeAt( inputStart ++ );

			output[ outputStart ++ ] = highest - code;

			if ( code === 0 ) {

				highest ++;

			}

		}

	};

	THREE.UTF8Loader.prototype.decompressAABBs_ = function ( str, inputStart, numBBoxen, decodeOffsets, decodeScales ) {

		var numFloats = 6 * numBBoxen;

		var inputEnd = inputStart + numFloats;
		var outputStart = 0;

		var bboxen = new Float32Array( numFloats );

		for ( var i = inputStart; i < inputEnd; i += 6 ) {

			var minX = str.charCodeAt( i + 0 ) + decodeOffsets[ 0 ];
			var minY = str.charCodeAt( i + 1 ) + decodeOffsets[ 1 ];
			var minZ = str.charCodeAt( i + 2 ) + decodeOffsets[ 2 ];

			var radiusX = ( str.charCodeAt( i + 3 ) + 1 ) >> 1;
			var radiusY = ( str.charCodeAt( i + 4 ) + 1 ) >> 1;
			var radiusZ = ( str.charCodeAt( i + 5 ) + 1 ) >> 1;

			bboxen[ outputStart ++ ] = decodeScales[ 0 ] * ( minX + radiusX );
			bboxen[ outputStart ++ ] = decodeScales[ 1 ] * ( minY + radiusY );
			bboxen[ outputStart ++ ] = decodeScales[ 2 ] * ( minZ + radiusZ );

			bboxen[ outputStart ++ ] = decodeScales[ 0 ] * radiusX;
			bboxen[ outputStart ++ ] = decodeScales[ 1 ] * radiusY;
			bboxen[ outputStart ++ ] = decodeScales[ 2 ] * radiusZ;

		}

		return bboxen;

	};

	THREE.UTF8Loader.prototype.decompressMesh = function ( str, meshParams, decodeParams, name, idx, callback ) {

		// Extract conversion parameters from attribArrays.

		var stride = decodeParams.decodeScales.length;

		var decodeOffsets = decodeParams.decodeOffsets;
		var decodeScales = decodeParams.decodeScales;

		var attribStart = meshParams.attribRange[ 0 ];
		var numVerts = meshParams.attribRange[ 1 ];

		// Decode attributes.

		var inputOffset = attribStart;
		var attribsOut = new Float32Array( stride * numVerts );

		for ( var j = 0; j < stride; j ++ ) {

			var end = inputOffset + numVerts;

			var decodeScale = decodeScales[ j ];

			if ( decodeScale ) {

				// Assume if decodeScale is never set, simply ignore the
				// attribute.

				this.decompressAttribsInner_( str, inputOffset, end, attribsOut, j, stride, decodeOffsets[ j ], decodeScale );

			}

			inputOffset = end;

		}

		var numIndices = 3 * meshParams.indexRange[ 1 ];

		var indicesOut = new Uint16Array( numIndices );

		this.decompressIndices_( str, inputOffset, numIndices, indicesOut, 0 );

		// Decode bboxen.

		var bboxen = undefined;
		var bboxOffset = meshParams.bboxes;

		if ( bboxOffset ) {

			bboxen = this.decompressAABBs_( str, bboxOffset, meshParams.names.length, decodeOffsets, decodeScales );

		}

		callback( name, idx, attribsOut, indicesOut, bboxen, meshParams );

	};

	THREE.UTF8Loader.prototype.copyAttrib = function ( stride, attribsOutFixed, lastAttrib, index ) {

		for ( var j = 0; j < stride; j ++ ) {

			lastAttrib[ j ] = attribsOutFixed[ stride * index + j ];

		}

	};

	THREE.UTF8Loader.prototype.decodeAttrib2 = function ( str, stride, decodeOffsets, decodeScales, deltaStart,
		numVerts, attribsOut, attribsOutFixed, lastAttrib, index ) {

		for ( var j = 0; j < 5; j ++ ) {

			var code = str.charCodeAt( deltaStart + numVerts * j + index );
			var delta = ( code >> 1 ) ^ ( - ( code & 1 ) );

			lastAttrib[ j ] += delta;
			attribsOutFixed[ stride * index + j ] = lastAttrib[ j ];
			attribsOut[ stride * index + j ] = decodeScales[ j ] * ( lastAttrib[ j ] + decodeOffsets[ j ] );

		}

	};

	THREE.UTF8Loader.prototype.accumulateNormal = function ( i0, i1, i2, attribsOutFixed, crosses ) {

		var p0x = attribsOutFixed[ 8 * i0 ];
		var p0y = attribsOutFixed[ 8 * i0 + 1 ];
		var p0z = attribsOutFixed[ 8 * i0 + 2 ];

		var p1x = attribsOutFixed[ 8 * i1 ];
		var p1y = attribsOutFixed[ 8 * i1 + 1 ];
		var p1z = attribsOutFixed[ 8 * i1 + 2 ];

		var p2x = attribsOutFixed[ 8 * i2 ];
		var p2y = attribsOutFixed[ 8 * i2 + 1 ];
		var p2z = attribsOutFixed[ 8 * i2 + 2 ];

		p1x -= p0x;
		p1y -= p0y;
		p1z -= p0z;

		p2x -= p0x;
		p2y -= p0y;
		p2z -= p0z;

		p0x = p1y * p2z - p1z * p2y;
		p0y = p1z * p2x - p1x * p2z;
		p0z = p1x * p2y - p1y * p2x;

		crosses[ 3 * i0 ] += p0x;
		crosses[ 3 * i0 + 1 ] += p0y;
		crosses[ 3 * i0 + 2 ] += p0z;

		crosses[ 3 * i1 ] += p0x;
		crosses[ 3 * i1 + 1 ] += p0y;
		crosses[ 3 * i1 + 2 ] += p0z;

		crosses[ 3 * i2 ] += p0x;
		crosses[ 3 * i2 + 1 ] += p0y;
		crosses[ 3 * i2 + 2 ] += p0z;

	};

	THREE.UTF8Loader.prototype.decompressMesh2 = function ( str, meshParams, decodeParams, name, idx, callback ) {

		var MAX_BACKREF = 96;

		// Extract conversion parameters from attribArrays.

		var stride = decodeParams.decodeScales.length;

		var decodeOffsets = decodeParams.decodeOffsets;
		var decodeScales = decodeParams.decodeScales;

		var deltaStart = meshParams.attribRange[ 0 ];
		var numVerts = meshParams.attribRange[ 1 ];

		var codeStart = meshParams.codeRange[ 0 ];

		var numIndices = 3 * meshParams.codeRange[ 2 ];

		var indicesOut = new Uint16Array( numIndices );

		var crosses = new Int32Array( 3 * numVerts );

		var lastAttrib = new Uint16Array( stride );

		var attribsOutFixed = new Uint16Array( stride * numVerts );
		var attribsOut = new Float32Array( stride * numVerts );

		var highest = 0;
		var outputStart = 0;

		for ( var i = 0; i < numIndices; i += 3 ) {

			var code = str.charCodeAt( codeStart ++ );

			var max_backref = Math.min( i, MAX_BACKREF );

			if ( code < max_backref ) {

	  		// Parallelogram

				var winding = code % 3;
				var backref = i - ( code - winding );
				var i0, i1, i2;

				switch ( winding ) {

					case 0:

						i0 = indicesOut[ backref + 2 ];
						i1 = indicesOut[ backref + 1 ];
						i2 = indicesOut[ backref + 0 ];
						break;

					case 1:

						i0 = indicesOut[ backref + 0 ];
						i1 = indicesOut[ backref + 2 ];
						i2 = indicesOut[ backref + 1 ];
						break;

					case 2:

						i0 = indicesOut[ backref + 1 ];
						i1 = indicesOut[ backref + 0 ];
						i2 = indicesOut[ backref + 2 ];
						break;

				}

				indicesOut[ outputStart ++ ] = i0;
				indicesOut[ outputStart ++ ] = i1;

				code = str.charCodeAt( codeStart ++ );

				var index = highest - code;
				indicesOut[ outputStart ++ ] = index;

				if ( code === 0 ) {

					for ( var j = 0; j < 5; j ++ ) {

						var deltaCode = str.charCodeAt( deltaStart + numVerts * j + highest );

						var prediction = ( ( deltaCode >> 1 ) ^ ( - ( deltaCode & 1 ) ) ) +
							attribsOutFixed[ stride * i0 + j ] +
							attribsOutFixed[ stride * i1 + j ] -
							attribsOutFixed[ stride * i2 + j ];

						lastAttrib[ j ] = prediction;

						attribsOutFixed[ stride * highest + j ] = prediction;
						attribsOut[ stride * highest + j ] = decodeScales[ j ] * ( prediction + decodeOffsets[ j ] );

					}

					highest ++;

				} else {

					this.copyAttrib( stride, attribsOutFixed, lastAttrib, index );

				}

				this.accumulateNormal( i0, i1, index, attribsOutFixed, crosses );

			} else {

				// Simple

				var index0 = highest - ( code - max_backref );

				indicesOut[ outputStart ++ ] = index0;

				if ( code === max_backref ) {

					this.decodeAttrib2( str, stride, decodeOffsets, decodeScales, deltaStart,
						numVerts, attribsOut, attribsOutFixed, lastAttrib, highest ++ );

				} else {

					this.copyAttrib( stride, attribsOutFixed, lastAttrib, index0 );

				}

				code = str.charCodeAt( codeStart ++ );

				var index1 = highest - code;
				indicesOut[ outputStart ++ ] = index1;

				if ( code === 0 ) {

					this.decodeAttrib2( str, stride, decodeOffsets, decodeScales, deltaStart,
						numVerts, attribsOut, attribsOutFixed, lastAttrib, highest ++ );

				} else {

					this.copyAttrib( stride, attribsOutFixed, lastAttrib, index1 );

				}

				code = str.charCodeAt( codeStart ++ );

				var index2 = highest - code;
				indicesOut[ outputStart ++ ] = index2;

				if ( code === 0 ) {

					for ( var j = 0; j < 5; j ++ ) {

						lastAttrib[ j ] = ( attribsOutFixed[ stride * index0 + j ] + attribsOutFixed[ stride * index1 + j ] ) / 2;

					}

					this.decodeAttrib2( str, stride, decodeOffsets, decodeScales, deltaStart,
						numVerts, attribsOut, attribsOutFixed, lastAttrib, highest ++ );

				} else {

					this.copyAttrib( stride, attribsOutFixed, lastAttrib, index2 );

				}

				this.accumulateNormal( index0, index1, index2, attribsOutFixed, crosses );

			}

		}

		for ( var i = 0; i < numVerts; i ++ ) {

			var nx = crosses[ 3 * i ];
			var ny = crosses[ 3 * i + 1 ];
			var nz = crosses[ 3 * i + 2 ];

			var norm = 511.0 / Math.sqrt( nx * nx + ny * ny + nz * nz );

			var cx = str.charCodeAt( deltaStart + 5 * numVerts + i );
			var cy = str.charCodeAt( deltaStart + 6 * numVerts + i );
			var cz = str.charCodeAt( deltaStart + 7 * numVerts + i );

			attribsOut[ stride * i + 5 ] = norm * nx + ( ( cx >> 1 ) ^ ( - ( cx & 1 ) ) );
			attribsOut[ stride * i + 6 ] = norm * ny + ( ( cy >> 1 ) ^ ( - ( cy & 1 ) ) );
			attribsOut[ stride * i + 7 ] = norm * nz + ( ( cz >> 1 ) ^ ( - ( cz & 1 ) ) );

		}

		callback( name, idx, attribsOut, indicesOut, undefined, meshParams );

	};

	THREE.UTF8Loader.prototype.downloadMesh = function ( path, name, meshEntry, decodeParams, callback ) {

		var loader = this;
		var idx = 0;

		function onprogress( data ) {

			while ( idx < meshEntry.length ) {

				var meshParams = meshEntry[ idx ];
				var indexRange = meshParams.indexRange;

				if ( indexRange ) {

					var meshEnd = indexRange[ 0 ] + 3 * indexRange[ 1 ];

					if ( data.length < meshEnd ) break;

					loader.decompressMesh( data, meshParams, decodeParams, name, idx, callback );

				} else {

					var codeRange = meshParams.codeRange;
					var meshEnd = codeRange[ 0 ] + codeRange[ 1 ];

					if ( data.length < meshEnd ) break;

					loader.decompressMesh2( data, meshParams, decodeParams, name, idx, callback );

				}

				++ idx;

			}

		}

		getHttpRequest( path, function ( data ) {

			onprogress( data );

			// TODO: handle errors.

		} );

	};

	THREE.UTF8Loader.prototype.downloadMeshes = function ( path, meshUrlMap, decodeParams, callback ) {

		for ( var url in meshUrlMap ) {

			var meshEntry = meshUrlMap[ url ];
			this.downloadMesh( path + url, url, meshEntry, decodeParams, callback );

		}

	};

	THREE.UTF8Loader.prototype.createMeshCallback = function ( materialBaseUrl, loadModelInfo, allDoneCallback ) {

		var nCompletedUrls = 0;
		var nExpectedUrls = 0;

		var expectedMeshesPerUrl = {};
		var decodedMeshesPerUrl = {};

		var modelParts = {};

		var meshUrlMap = loadModelInfo.urls;

		for ( var url in meshUrlMap ) {

			expectedMeshesPerUrl[ url ] = meshUrlMap[ url ].length;
			decodedMeshesPerUrl[ url ] = 0;

			nExpectedUrls ++;

			modelParts[ url ] = new THREE.Object3D();

		}

		var model = new THREE.Object3D();

		// Prepare materials first...

		var materialCreator = new THREE.MTLLoader.MaterialCreator( materialBaseUrl, loadModelInfo.options );
		materialCreator.setMaterials( loadModelInfo.materials );

		materialCreator.preload();

		// Create callback for creating mesh parts

		var bufferGeometryCreator = new THREE.UTF8Loader.BufferGeometryCreator();

		var meshCallback = function ( name, idx, attribArray, indexArray, bboxen, meshParams ) {

			// Got ourselves a new mesh

			// name identifies this part of the model (url)
			// idx is the mesh index of this mesh of the part
			// attribArray defines the vertices
			// indexArray defines the faces
			// bboxen defines the bounding box
			// meshParams contains the material info

			var geometry = bufferGeometryCreator.create( attribArray, indexArray );
			var material = materialCreator.create( meshParams.material );

			var mesh = new THREE.Mesh( geometry, material );
			modelParts[ name ].add( mesh );

			//model.add(new THREE.Mesh(geometry, material));

			decodedMeshesPerUrl[ name ] ++;

			if ( decodedMeshesPerUrl[ name ] === expectedMeshesPerUrl[ name ] ) {

				nCompletedUrls ++;

				model.add( modelParts[ name ] );

				if ( nCompletedUrls === nExpectedUrls ) {

					// ALL DONE!!!

					allDoneCallback( model );

				}

			}

		};

		return meshCallback;

	};

	THREE.UTF8Loader.prototype.downloadModel = function ( geometryBase, materialBase, model, callback ) {

		var meshCallback = this.createMeshCallback( materialBase, model, callback );
		this.downloadMeshes( geometryBase, model.urls, model.decodeParams, meshCallback );

	};

	THREE.UTF8Loader.prototype.downloadModelJson = function ( jsonUrl, callback, options ) {

		getJsonRequest( jsonUrl, function ( loaded ) {

			if ( ! loaded.decodeParams ) {

				if ( options && options.decodeParams ) {

					loaded.decodeParams = options.decodeParams;

				} else {

					loaded.decodeParams = DEFAULT_DECODE_PARAMS;

				}

			}

			loaded.options = options;

			var geometryBase = jsonUrl.substr( 0, jsonUrl.lastIndexOf( "/" ) + 1 );
			var materialBase = geometryBase;

			if ( options && options.geometryBase ) {

				geometryBase = options.geometryBase;

				if ( geometryBase.charAt( geometryBase.length - 1 ) !== "/" ) {

					geometryBase = geometryBase + "/";

				}

			}

			if ( options && options.materialBase ) {

				materialBase = options.materialBase;

				if ( materialBase.charAt( materialBase.length - 1 ) !== "/" ) {

					materialBase = materialBase + "/";

				}

			}

			this.downloadModel( geometryBase, materialBase, loaded, callback );

		}.bind( this ) );

	};

	// XMLHttpRequest stuff

	function getHttpRequest( url, onload, opt_onprogress ) {

		var req = new THREE.FileLoader();
		req.load( url, onload, opt_onprogress );

	}

	function getJsonRequest( url, onjson ) {

		getHttpRequest( url, function ( e ) {

			onjson( JSON.parse( e ) );

		},
		function () {} );

	}

	/**
	 * @author mrdoob / http://mrdoob.com/
	 */

	THREE.VRMLLoader = function ( manager ) {

		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

	};

	THREE.VRMLLoader.prototype = {

		constructor: THREE.VRMLLoader,

		// for IndexedFaceSet support
		isRecordingPoints: false,
		isRecordingFaces: false,
		points: [],
		indexes: [],

		// for Background support
		isRecordingAngles: false,
		isRecordingColors: false,
		angles: [],
		colors: [],

		recordingFieldname: null,

		crossOrigin: 'Anonymous',

		load: function ( url, onLoad, onProgress, onError ) {

			var scope = this;

			var loader = new THREE.FileLoader( this.manager );
			loader.load( url, function ( text ) {

				onLoad( scope.parse( text ) );

			}, onProgress, onError );

		},

		setCrossOrigin: function ( value ) {

			this.crossOrigin = value;

		},

		parse: function ( data ) {

			var texturePath = this.texturePath || '';

			var textureLoader = new THREE.TextureLoader( this.manager );
			textureLoader.setCrossOrigin( this.crossOrigin );

			function parseV2( lines, scene ) {

				var defines = {};
				var float_pattern = /(\b|\-|\+)([\d\.e]+)/;
				var float2_pattern = /([\d\.\+\-e]+)\s+([\d\.\+\-e]+)/g;
				var float3_pattern = /([\d\.\+\-e]+)\s+([\d\.\+\-e]+)\s+([\d\.\+\-e]+)/g;

				/**
				 * Vertically paints the faces interpolating between the
				 * specified colors at the specified angels. This is used for the Background
				 * node, but could be applied to other nodes with multiple faces as well.
				 *
				 * When used with the Background node, default is directionIsDown is true if
				 * interpolating the skyColor down from the Zenith. When interpolationg up from
				 * the Nadir i.e. interpolating the groundColor, the directionIsDown is false.
				 *
				 * The first angle is never specified, it is the Zenith (0 rad). Angles are specified
				 * in radians. The geometry is thought a sphere, but could be anything. The color interpolation
				 * is linear along the Y axis in any case.
				 *
				 * You must specify one more color than you have angles at the beginning of the colors array.
				 * This is the color of the Zenith (the top of the shape).
				 *
				 * @param geometry
				 * @param radius
				 * @param angles
				 * @param colors
				 * @param boolean topDown Whether to work top down or bottom up.
				 */
				function paintFaces( geometry, radius, angles, colors, topDown ) {

					var direction = ( topDown === true ) ? 1 : - 1;

					var coord = [], A = {}, B = {}, applyColor = false;

					for ( var k = 0; k < angles.length; k ++ ) {

						// push the vector at which the color changes

						var vec = {
							x: direction * ( Math.cos( angles[ k ] ) * radius ),
							y: direction * ( Math.sin( angles[ k ] ) * radius )
						};

						coord.push( vec );

					}

					var index = geometry.index;
					var positionAttribute = geometry.attributes.position;
					var colorAttribute = new THREE.BufferAttribute( new Float32Array( geometry.attributes.position.count * 3 ), 3 );

					var position = new THREE.Vector3();
					var color = new THREE.Color();

					for ( var i = 0; i < index.count; i ++ ) {

						var vertexIndex = index.getX( i );

						position.fromBufferAttribute( positionAttribute, vertexIndex );

						for ( var j = 0; j < colors.length; j ++ ) {

							// linear interpolation between aColor and bColor, calculate proportion
							// A is previous point (angle)

							if ( j === 0 ) {

								A.x = 0;
								A.y = ( topDown === true ) ? radius : - 1 * radius;

							} else {

								A.x = coord[ j - 1 ].x;
								A.y = coord[ j - 1 ].y;

							}

							// B is current point (angle)

							B = coord[ j ];

							if ( B !== undefined ) {

								// p has to be between the points A and B which we interpolate

								applyColor = ( topDown === true ) ? ( position.y <= A.y && position.y > B.y ) : ( position.y >= A.y && position.y < B.y );

								if ( applyColor === true ) {

									var aColor = colors[ j ];
									var bColor = colors[ j + 1 ];

									// below is simple linear interpolation

									var t = Math.abs( position.y - A.y ) / ( A.y - B.y );

									// to make it faster, you can only calculate this if the y coord changes, the color is the same for points with the same y

									color.copy( aColor ).lerp( bColor, t );

									colorAttribute.setXYZ( vertexIndex, color.r, color.g, color.b );

								} else {

									var colorIndex = ( topDown === true ) ? colors.length - 1 : 0;
									var c = colors[ colorIndex ];
									colorAttribute.setXYZ( vertexIndex, c.r, c.g, c.b );

								}

							}

						}

					}

					geometry.addAttribute( 'color', colorAttribute );

				}

				var index = [];

				function parseProperty( node, line ) {

					var parts = [], part, property = {}, fieldName;

					/**
					 * Expression for matching relevant information, such as a name or value, but not the separators
					 * @type {RegExp}
					 */
					var regex = /[^\s,\[\]]+/g;

					var point;

					while ( null !== ( part = regex.exec( line ) ) ) {

						parts.push( part[ 0 ] );

					}

					fieldName = parts[ 0 ];


					// trigger several recorders
					switch ( fieldName ) {

						case 'skyAngle':
						case 'groundAngle':
							this.recordingFieldname = fieldName;
							this.isRecordingAngles = true;
							this.angles = [];
							break;

						case 'skyColor':
						case 'groundColor':
							this.recordingFieldname = fieldName;
							this.isRecordingColors = true;
							this.colors = [];
							break;

						case 'point':
							this.recordingFieldname = fieldName;
							this.isRecordingPoints = true;
							this.points = [];
							break;

						case 'coordIndex':
						case 'texCoordIndex':
							this.recordingFieldname = fieldName;
							this.isRecordingFaces = true;
							this.indexes = [];
							break;

					}

					if ( this.isRecordingFaces ) {

						// the parts hold the indexes as strings
						if ( parts.length > 0 ) {

							for ( var ind = 0; ind < parts.length; ind ++ ) {

								// the part should either be positive integer or -1
								if ( ! /(-?\d+)/.test( parts[ ind ] ) ) {

									continue;

								}

								// end of current face
								if ( parts[ ind ] === '-1' ) {

									if ( index.length > 0 ) {

										this.indexes.push( index );

									}

									// start new one
									index = [];

								} else {

									index.push( parseInt( parts[ ind ] ) );

								}

							}

						}

						// end
						if ( /]/.exec( line ) ) {

							if ( index.length > 0 ) {

								this.indexes.push( index );

							}

							// start new one
							index = [];

							this.isRecordingFaces = false;
							node[ this.recordingFieldname ] = this.indexes;

						}

					} else if ( this.isRecordingPoints ) {

						if ( node.nodeType == 'Coordinate' ) {

							while ( null !== ( parts = float3_pattern.exec( line ) ) ) {

								point = {
									x: parseFloat( parts[ 1 ] ),
									y: parseFloat( parts[ 2 ] ),
									z: parseFloat( parts[ 3 ] )
								};

								this.points.push( point );

							}

						}

						if ( node.nodeType == 'TextureCoordinate' ) {

							while ( null !== ( parts = float2_pattern.exec( line ) ) ) {

								point = {
									x: parseFloat( parts[ 1 ] ),
									y: parseFloat( parts[ 2 ] )
								};

								this.points.push( point );

							}

						}

						// end
						if ( /]/.exec( line ) ) {

							this.isRecordingPoints = false;
							node.points = this.points;

						}

					} else if ( this.isRecordingAngles ) {

						// the parts hold the angles as strings
						if ( parts.length > 0 ) {

							for ( var ind = 0; ind < parts.length; ind ++ ) {

								// the part should be a float
								if ( ! float_pattern.test( parts[ ind ] ) ) {

									continue;

								}

								this.angles.push( parseFloat( parts[ ind ] ) );

							}

						}

						// end
						if ( /]/.exec( line ) ) {

							this.isRecordingAngles = false;
							node[ this.recordingFieldname ] = this.angles;

						}

					} else if ( this.isRecordingColors ) {

						while ( null !== ( parts = float3_pattern.exec( line ) ) ) {

							var color = {
								r: parseFloat( parts[ 1 ] ),
								g: parseFloat( parts[ 2 ] ),
								b: parseFloat( parts[ 3 ] )
							};

							this.colors.push( color );

						}

						// end
						if ( /]/.exec( line ) ) {

							this.isRecordingColors = false;
							node[ this.recordingFieldname ] = this.colors;

						}

					} else if ( parts[ parts.length - 1 ] !== 'NULL' && fieldName !== 'children' ) {

						switch ( fieldName ) {

							case 'diffuseColor':
							case 'emissiveColor':
							case 'specularColor':
							case 'color':

								if ( parts.length !== 4 ) {

									console.warn( 'THREE.VRMLLoader: Invalid color format detected for %s.', fieldName );
									break;

								}

								property = {
									r: parseFloat( parts[ 1 ] ),
									g: parseFloat( parts[ 2 ] ),
									b: parseFloat( parts[ 3 ] )
								};

								break;

							case 'location':
							case 'direction':
							case 'translation':
							case 'scale':
							case 'size':
								if ( parts.length !== 4 ) {

									console.warn( 'THREE.VRMLLoader: Invalid vector format detected for %s.', fieldName );
									break;

								}

								property = {
									x: parseFloat( parts[ 1 ] ),
									y: parseFloat( parts[ 2 ] ),
									z: parseFloat( parts[ 3 ] )
								};

								break;

							case 'intensity':
							case 'cutOffAngle':
							case 'radius':
							case 'topRadius':
							case 'bottomRadius':
							case 'height':
							case 'transparency':
							case 'shininess':
							case 'ambientIntensity':
								if ( parts.length !== 2 ) {

									console.warn( 'THREE.VRMLLoader: Invalid single float value specification detected for %s.', fieldName );
									break;

								}

								property = parseFloat( parts[ 1 ] );

								break;

							case 'rotation':
								if ( parts.length !== 5 ) {

									console.warn( 'THREE.VRMLLoader: Invalid quaternion format detected for %s.', fieldName );
									break;

								}

								property = {
									x: parseFloat( parts[ 1 ] ),
									y: parseFloat( parts[ 2 ] ),
									z: parseFloat( parts[ 3 ] ),
									w: parseFloat( parts[ 4 ] )
								};

								break;

							case 'on':
							case 'ccw':
							case 'solid':
							case 'colorPerVertex':
							case 'convex':
								if ( parts.length !== 2 ) {

									console.warn( 'THREE.VRMLLoader: Invalid format detected for %s.', fieldName );
									break;

								}

								property = parts[ 1 ] === 'TRUE' ? true : false;

								break;

						}

						node[ fieldName ] = property;

					}

					return property;

				}

				function getTree( lines ) {

					var tree = { 'string': 'Scene', children: [] };
					var current = tree;
					var matches;
					var specification;

					for ( var i = 0; i < lines.length; i ++ ) {

						var comment = '';

						var line = lines[ i ];

						// omit whitespace only lines
						if ( null !== ( /^\s+?$/g.exec( line ) ) ) {

							continue;

						}

						line = line.trim();

						// skip empty lines
						if ( line === '' ) {

							continue;

						}

						if ( /#/.exec( line ) ) {

							var parts = line.split( '#' );

							// discard everything after the #, it is a comment
							line = parts[ 0 ];

							// well, let's also keep the comment
							comment = parts[ 1 ];

						}

						if ( matches = /([^\s]*){1}(?:\s+)?{/.exec( line ) ) {

							// first subpattern should match the Node name

							var block = { 'nodeType': matches[ 1 ], 'string': line, 'parent': current, 'children': [], 'comment': comment };
							current.children.push( block );
							current = block;

							if ( /}/.exec( line ) ) {

								// example: geometry Box { size 1 1 1 } # all on the same line
								specification = /{(.*)}/.exec( line )[ 1 ];

								// todo: remove once new parsing is complete?
								block.children.push( specification );

								parseProperty( current, specification );

								current = current.parent;

							}

						} else if ( /}/.exec( line ) ) {

							current = current.parent;

						} else if ( line !== '' ) {

							parseProperty( current, line );
							// todo: remove once new parsing is complete? we still do not parse geometry and appearance the new way
							current.children.push( line );

						}

					}

					return tree;

				}

				function parseNode( data, parent ) {

					var object;

					if ( typeof data === 'string' ) {

						if ( /USE/.exec( data ) ) {

							var defineKey = /USE\s+?([^\s]+)/.exec( data )[ 1 ];

							if ( undefined == defines[ defineKey ] ) {

								console.warn( 'THREE.VRMLLoader: %s is not defined.', defineKey );

							} else {

								if ( /appearance/.exec( data ) && defineKey ) {

									parent.material = defines[ defineKey ].clone();

								} else if ( /geometry/.exec( data ) && defineKey ) {

									parent.geometry = defines[ defineKey ].clone();

									// the solid property is not cloned with clone(), is only needed for VRML loading, so we need to transfer it
									if ( undefined !== defines[ defineKey ].solid && defines[ defineKey ].solid === false ) {

										parent.geometry.solid = false;
										parent.material.side = THREE.DoubleSide;

									}

								} else if ( defineKey ) {

									object = defines[ defineKey ].clone();
									parent.add( object );

								}

							}

						}

						return;

					}

					object = parent;

					if ( data.string.indexOf( 'AmbientLight' ) > - 1 && data.nodeType === 'PointLight' ) {

						data.nodeType = 'AmbientLight';

					}

					var l_visible = data.on !== undefined ? data.on : true;
					var l_intensity = data.intensity !== undefined ? data.intensity : 1;
					var l_color = new THREE.Color();

					if ( data.color ) {

						l_color.copy( data.color );

					}

					if ( data.nodeType === 'AmbientLight' ) {

						object = new THREE.AmbientLight( l_color, l_intensity );
						object.visible = l_visible;

						parent.add( object );

					} else if ( data.nodeType === 'PointLight' ) {

						var l_distance = 0;

						if ( data.radius !== undefined && data.radius < 1000 ) {

							l_distance = data.radius;

						}

						object = new THREE.PointLight( l_color, l_intensity, l_distance );
						object.visible = l_visible;

						parent.add( object );

					} else if ( data.nodeType === 'SpotLight' ) {

						var l_intensity = 1;
						var l_distance = 0;
						var l_angle = Math.PI / 3;
						var l_penumbra = 0;
						var l_visible = true;

						if ( data.radius !== undefined && data.radius < 1000 ) {

							l_distance = data.radius;

						}

						if ( data.cutOffAngle !== undefined ) {

							l_angle = data.cutOffAngle;

						}

						object = new THREE.SpotLight( l_color, l_intensity, l_distance, l_angle, l_penumbra );
						object.visible = l_visible;

						parent.add( object );

					} else if ( data.nodeType === 'Transform' || data.nodeType === 'Group' ) {

						object = new THREE.Object3D();

						if ( /DEF/.exec( data.string ) ) {

							object.name = /DEF\s+([^\s]+)/.exec( data.string )[ 1 ];
							defines[ object.name ] = object;

						}

						if ( data.translation !== undefined ) {

							var t = data.translation;

							object.position.set( t.x, t.y, t.z );

						}

						if ( data.rotation !== undefined ) {

							var r = data.rotation;

							object.quaternion.setFromAxisAngle( new THREE.Vector3( r.x, r.y, r.z ), r.w );

						}

						if ( data.scale !== undefined ) {

							var s = data.scale;

							object.scale.set( s.x, s.y, s.z );

						}

						parent.add( object );

					} else if ( data.nodeType === 'Shape' ) {

						object = new THREE.Mesh();

						if ( /DEF/.exec( data.string ) ) {

							object.name = /DEF\s+([^\s]+)/.exec( data.string )[ 1 ];

							defines[ object.name ] = object;

						}

						parent.add( object );

					} else if ( data.nodeType === 'Background' ) {

						var segments = 20;

						// sky (full sphere):

						var radius = 2e4;

						var skyGeometry = new THREE.SphereBufferGeometry( radius, segments, segments );
						var skyMaterial = new THREE.MeshBasicMaterial( { fog: false, side: THREE.BackSide } );

						if ( data.skyColor.length > 1 ) {

							paintFaces( skyGeometry, radius, data.skyAngle, data.skyColor, true );

							skyMaterial.vertexColors = THREE.VertexColors;

						} else {

							var color = data.skyColor[ 0 ];
							skyMaterial.color.setRGB( color.r, color.b, color.g );

						}

						scene.add( new THREE.Mesh( skyGeometry, skyMaterial ) );

						// ground (half sphere):

						if ( data.groundColor !== undefined ) {

							radius = 1.2e4;

							var groundGeometry = new THREE.SphereBufferGeometry( radius, segments, segments, 0, 2 * Math.PI, 0.5 * Math.PI, 1.5 * Math.PI );
							var groundMaterial = new THREE.MeshBasicMaterial( { fog: false, side: THREE.BackSide, vertexColors: THREE.VertexColors } );

							paintFaces( groundGeometry, radius, data.groundAngle, data.groundColor, false );

							scene.add( new THREE.Mesh( groundGeometry, groundMaterial ) );

						}

					} else if ( /geometry/.exec( data.string ) ) {

						if ( data.nodeType === 'Box' ) {

							var s = data.size;

							parent.geometry = new THREE.BoxBufferGeometry( s.x, s.y, s.z );

						} else if ( data.nodeType === 'Cylinder' ) {

							parent.geometry = new THREE.CylinderBufferGeometry( data.radius, data.radius, data.height );

						} else if ( data.nodeType === 'Cone' ) {

							parent.geometry = new THREE.CylinderBufferGeometry( data.topRadius, data.bottomRadius, data.height );

						} else if ( data.nodeType === 'Sphere' ) {

							parent.geometry = new THREE.SphereBufferGeometry( data.radius );

						} else if ( data.nodeType === 'IndexedFaceSet' ) {

							var geometry = new THREE.BufferGeometry();

							var positions = [];
							var uvs = [];

							var position, uv;

							var i, il, j, jl;

							for ( i = 0, il = data.children.length; i < il; i ++ ) {

								var child = data.children[ i ];

								// uvs

								if ( child.nodeType === 'TextureCoordinate' ) {

									if ( child.points ) {

										for ( j = 0, jl = child.points.length; j < jl; j ++ ) {

											uv = child.points[ j ];
											uvs.push( uv.x, uv.y );

										}

									}

								}

								// positions

								if ( child.nodeType === 'Coordinate' ) {

									if ( child.points ) {

										for ( j = 0, jl = child.points.length; j < jl; j ++ ) {

											position = child.points[ j ];
											positions.push( position.x, position.y, position.z );

										}

									}

									if ( child.string.indexOf( 'DEF' ) > - 1 ) {

										var name = /DEF\s+([^\s]+)/.exec( child.string )[ 1 ];

										defines[ name ] = positions.slice( 0 );

									}

									if ( child.string.indexOf( 'USE' ) > - 1 ) {

										var defineKey = /USE\s+([^\s]+)/.exec( child.string )[ 1 ];

										positions = defines[ defineKey ];

									}

								}

							}

							var skip = 0;

							// some shapes only have vertices for use in other shapes

							if ( data.coordIndex ) {

								var newPositions = [];
								var newUvs = [];

								position = new THREE.Vector3();
								uv = new THREE.Vector2();

								for ( i = 0, il = data.coordIndex.length; i < il; i ++ ) {

									var indexes = data.coordIndex[ i ];

									// VRML support multipoint indexed face sets (more then 3 vertices). You must calculate the composing triangles here

									skip = 0;

									while ( indexes.length >= 3 && skip < ( indexes.length - 2 ) ) {

										if ( data.ccw === undefined ) data.ccw = true; // ccw is true by default

										var i1 = indexes[ 0 ];
										var i2 = indexes[ skip + ( data.ccw ? 1 : 2 ) ];
										var i3 = indexes[ skip + ( data.ccw ? 2 : 1 ) ];

										// create non indexed geometry, necessary for face normal generation

										position.fromArray( positions, i1 * 3 );
										uv.fromArray( uvs, i1 * 2 );
										newPositions.push( position.x, position.y, position.z );
										newUvs.push( uv.x, uv.y );

										position.fromArray( positions, i2 * 3 );
										uv.fromArray( uvs, i2 * 2 );
										newPositions.push( position.x, position.y, position.z );
										newUvs.push( uv.x, uv.y );

										position.fromArray( positions, i3 * 3 );
										uv.fromArray( uvs, i3 * 2 );
										newPositions.push( position.x, position.y, position.z );
										newUvs.push( uv.x, uv.y );

										skip ++;

									}

								}

								positions = newPositions;
								uvs = newUvs;

							} else {

								// do not add dummy mesh to the scene

								parent.parent.remove( parent );

							}

							if ( false === data.solid ) {

								parent.material.side = THREE.DoubleSide;

							}

							// we need to store it on the geometry for use with defines
							geometry.solid = data.solid;

							geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );

							if ( uvs.length > 0 ) {

								geometry.addAttribute( 'uv', new THREE.Float32BufferAttribute( uvs, 2 ) );

							}

							geometry.computeVertexNormals();
							geometry.computeBoundingSphere();

							// see if it's a define
							if ( /DEF/.exec( data.string ) ) {

								geometry.name = /DEF ([^\s]+)/.exec( data.string )[ 1 ];
								defines[ geometry.name ] = geometry;

							}

							parent.geometry = geometry;

						}

						return;

					} else if ( /appearance/.exec( data.string ) ) {

						for ( var i = 0; i < data.children.length; i ++ ) {

							var child = data.children[ i ];

							if ( child.nodeType === 'Material' ) {

								var material = new THREE.MeshPhongMaterial();

								if ( child.diffuseColor !== undefined ) {

									var d = child.diffuseColor;

									material.color.setRGB( d.r, d.g, d.b );

								}

								if ( child.emissiveColor !== undefined ) {

									var e = child.emissiveColor;

									material.emissive.setRGB( e.r, e.g, e.b );

								}

								if ( child.specularColor !== undefined ) {

									var s = child.specularColor;

									material.specular.setRGB( s.r, s.g, s.b );

								}

								if ( child.transparency !== undefined ) {

									var t = child.transparency;

									// transparency is opposite of opacity
									material.opacity = Math.abs( 1 - t );

									material.transparent = true;

								}

								if ( /DEF/.exec( data.string ) ) {

									material.name = /DEF ([^\s]+)/.exec( data.string )[ 1 ];

									defines[ material.name ] = material;

								}

								parent.material = material;

							}

							if ( child.nodeType === 'ImageTexture' ) {

								var textureName = /"([^"]+)"/.exec( child.children[ 0 ] );

								if ( textureName ) {

									parent.material.name = textureName[ 1 ];

									parent.material.map = textureLoader.load( texturePath + textureName[ 1 ] );

								}

							}

						}

						return;

					}

					for ( var i = 0, l = data.children.length; i < l; i ++ ) {

						parseNode( data.children[ i ], object );

					}

				}

				parseNode( getTree( lines ), scene );

			}

			var scene = new THREE.Scene();

			var lines = data.split( '\n' );

			// some lines do not have breaks

			for ( var i = lines.length - 1; i > - 1; i -- ) {

				var line = lines[ i ];

				// split lines with {..{ or {..[ - some have both
				if ( /{.*[{\[]/.test( line ) ) {

					var parts = line.split( '{' ).join( '{\n' ).split( '\n' );
					parts.unshift( 1 );
					parts.unshift( i );
					lines.splice.apply( lines, parts );

				} else if ( /\].*}/.test( line ) ) {

					// split lines with ]..}
					var parts = line.split( ']' ).join( ']\n' ).split( '\n' );
					parts.unshift( 1 );
					parts.unshift( i );
					lines.splice.apply( lines, parts );

				}

				if ( /}.*}/.test( line ) ) {

					// split lines with }..}
					var parts = line.split( '}' ).join( '}\n' ).split( '\n' );
					parts.unshift( 1 );
					parts.unshift( i );
					lines.splice.apply( lines, parts );

				}

				if ( /^\b[^\s]+\b$/.test( line.trim() ) ) {

					// prevent lines with single words like "coord" or "geometry", see #12209
					lines[ i + 1 ] = line + ' ' + lines[ i + 1 ].trim();
					lines.splice( i, 1 );

				} else if ( ( line.indexOf( 'coord' ) > - 1 ) && ( line.indexOf( '[' ) < 0 ) && ( line.indexOf( '{' ) < 0 ) ) {

					// force the parser to create Coordinate node for empty coords
					// coord USE something -> coord USE something Coordinate {}

					lines[ i ] += ' Coordinate {}';

				}

			}

			var header = lines.shift();

			if ( /V1.0/.exec( header ) ) {

				console.warn( 'THREE.VRMLLoader: V1.0 not supported yet.' );

			} else if ( /V2.0/.exec( header ) ) {

				parseV2( lines, scene );

			}

			return scene;

		}

	};

	/**
	 * @author mrdoob / http://mrdoob.com/
	 * @author Alex Pletzer
	 *
	 * Updated on 22.03.2017
	 * VTK header is now parsed and used to extract all the compressed data
	 * @author Andrii Iudin https://github.com/andreyyudin
	 * @author Paul Kibet Korir https://github.com/polarise
	 * @author Sriram Somasundharam https://github.com/raamssundar
	 */

	THREE.VTKLoader = function ( manager ) {

		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

	};

	Object.assign( THREE.VTKLoader.prototype, THREE.EventDispatcher.prototype, {

		load: function ( url, onLoad, onProgress, onError ) {

			var scope = this;

			var loader = new THREE.FileLoader( scope.manager );
			loader.setResponseType( 'arraybuffer' );
			loader.load( url, function ( text ) {

				onLoad( scope.parse( text ) );

			}, onProgress, onError );

		},

		parse: function ( data ) {

			function parseASCII( data ) {

				// connectivity of the triangles
				var indices = [];

				// triangles vertices
				var positions = [];

				// red, green, blue colors in the range 0 to 1
				var colors = [];

				// normal vector, one per vertex
				var normals = [];

				var result;

				// pattern for reading vertices, 3 floats or integers
				var pat3Floats = /(\-?\d+\.?[\d\-\+e]*)\s+(\-?\d+\.?[\d\-\+e]*)\s+(\-?\d+\.?[\d\-\+e]*)/g;

				// pattern for connectivity, an integer followed by any number of ints
				// the first integer is the number of polygon nodes
				var patConnectivity = /^(\d+)\s+([\s\d]*)/;

				// indicates start of vertex data section
				var patPOINTS = /^POINTS /;

				// indicates start of polygon connectivity section
				var patPOLYGONS = /^POLYGONS /;

				// indicates start of triangle strips section
				var patTRIANGLE_STRIPS = /^TRIANGLE_STRIPS /;

				// POINT_DATA number_of_values
				var patPOINT_DATA = /^POINT_DATA[ ]+(\d+)/;

				// CELL_DATA number_of_polys
				var patCELL_DATA = /^CELL_DATA[ ]+(\d+)/;

				// Start of color section
				var patCOLOR_SCALARS = /^COLOR_SCALARS[ ]+(\w+)[ ]+3/;

				// NORMALS Normals float
				var patNORMALS = /^NORMALS[ ]+(\w+)[ ]+(\w+)/;

				var inPointsSection = false;
				var inPolygonsSection = false;
				var inTriangleStripSection = false;
				var inPointDataSection = false;
				var inCellDataSection = false;
				var inColorSection = false;
				var inNormalsSection = false;

				var lines = data.split( '\n' );

				for ( var i in lines ) {

					var line = lines[ i ];

					if ( inPointsSection ) {

						// get the vertices
						while ( ( result = pat3Floats.exec( line ) ) !== null ) {

							var x = parseFloat( result[ 1 ] );
							var y = parseFloat( result[ 2 ] );
							var z = parseFloat( result[ 3 ] );
							positions.push( x, y, z );

						}

					} else if ( inPolygonsSection ) {

						if ( ( result = patConnectivity.exec( line ) ) !== null ) {

							// numVertices i0 i1 i2 ...
							var numVertices = parseInt( result[ 1 ] );
							var inds = result[ 2 ].split( /\s+/ );

							if ( numVertices >= 3 ) {

								var i0 = parseInt( inds[ 0 ] );
								var i1, i2;
								var k = 1;
								// split the polygon in numVertices - 2 triangles
								for ( var j = 0; j < numVertices - 2; ++ j ) {

									i1 = parseInt( inds[ k ] );
									i2 = parseInt( inds[ k + 1 ] );
									indices.push( i0, i1, i2 );
									k ++;

								}

							}

						}

					} else if ( inTriangleStripSection ) {

						if ( ( result = patConnectivity.exec( line ) ) !== null ) {

							// numVertices i0 i1 i2 ...
							var numVertices = parseInt( result[ 1 ] );
							var inds = result[ 2 ].split( /\s+/ );

							if ( numVertices >= 3 ) {

								var i0, i1, i2;
								// split the polygon in numVertices - 2 triangles
								for ( var j = 0; j < numVertices - 2; j ++ ) {

									if ( j % 2 === 1 ) {

										i0 = parseInt( inds[ j ] );
										i1 = parseInt( inds[ j + 2 ] );
										i2 = parseInt( inds[ j + 1 ] );
										indices.push( i0, i1, i2 );

									} else {

										i0 = parseInt( inds[ j ] );
										i1 = parseInt( inds[ j + 1 ] );
										i2 = parseInt( inds[ j + 2 ] );
										indices.push( i0, i1, i2 );

									}

								}

							}

						}

					} else if ( inPointDataSection || inCellDataSection ) {

						if ( inColorSection ) {

							// Get the colors

							while ( ( result = pat3Floats.exec( line ) ) !== null ) {

								var r = parseFloat( result[ 1 ] );
								var g = parseFloat( result[ 2 ] );
								var b = parseFloat( result[ 3 ] );
								colors.push( r, g, b );

							}

						} else if ( inNormalsSection ) {

							// Get the normal vectors

							while ( ( result = pat3Floats.exec( line ) ) !== null ) {

								var nx = parseFloat( result[ 1 ] );
								var ny = parseFloat( result[ 2 ] );
								var nz = parseFloat( result[ 3 ] );
								normals.push( nx, ny, nz );

							}

						}

					}

					if ( patPOLYGONS.exec( line ) !== null ) {

						inPolygonsSection = true;
						inPointsSection = false;
						inTriangleStripSection = false;

					} else if ( patPOINTS.exec( line ) !== null ) {

						inPolygonsSection = false;
						inPointsSection = true;
						inTriangleStripSection = false;

					} else if ( patTRIANGLE_STRIPS.exec( line ) !== null ) {

						inPolygonsSection = false;
						inPointsSection = false;
						inTriangleStripSection = true;

					} else if ( patPOINT_DATA.exec( line ) !== null ) {

						inPointDataSection = true;
						inPointsSection = false;
						inPolygonsSection = false;
						inTriangleStripSection = false;

					} else if ( patCELL_DATA.exec( line ) !== null ) {

						inCellDataSection = true;
						inPointsSection = false;
						inPolygonsSection = false;
						inTriangleStripSection = false;

					} else if ( patCOLOR_SCALARS.exec( line ) !== null ) {

						inColorSection = true;
						inNormalsSection = false;
						inPointsSection = false;
						inPolygonsSection = false;
						inTriangleStripSection = false;

					} else if ( patNORMALS.exec( line ) !== null ) {

						inNormalsSection = true;
						inColorSection = false;
						inPointsSection = false;
						inPolygonsSection = false;
						inTriangleStripSection = false;

					}

				}

				var geometry;
				var stagger = 'point';

				if ( colors.length === indices.length ) {

					stagger = 'cell';

				}

				if ( stagger === 'point' ) {

					// Nodal. Use BufferGeometry
					geometry = new THREE.BufferGeometry();
					geometry.setIndex( new THREE.BufferAttribute( new Uint32Array( indices ), 1 ) );
					geometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( positions ), 3 ) );

					if ( colors.length === positions.length ) {

						geometry.addAttribute( 'color', new THREE.BufferAttribute( new Float32Array( colors ), 3 ) );

					}

					if ( normals.length === positions.length ) {

						geometry.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( normals ), 3 ) );

					}

				} else {

					// Cell centered colors. The only way to attach a solid color to each triangle
					// is to use Geometry, which is less efficient than BufferGeometry
					geometry = new THREE.Geometry();

					var numTriangles = indices.length / 3;
					var numPoints = positions.length / 3;
					var face;
					var ia, ib, ic;
					var x, y, z;
					var r, g, b;

					for ( var j = 0; j < numPoints; ++ j ) {

						x = positions[ 3 * j + 0 ];
						y = positions[ 3 * j + 1 ];
						z = positions[ 3 * j + 2 ];
						geometry.vertices.push( new THREE.Vector3( x, y, z ) );

					}

					for ( var i = 0; i < numTriangles; ++ i ) {

						ia = indices[ 3 * i + 0 ];
						ib = indices[ 3 * i + 1 ];
						ic = indices[ 3 * i + 2 ];
						geometry.faces.push( new THREE.Face3( ia, ib, ic ) );

					}

					if ( colors.length === numTriangles * 3 ) {

						for ( var i = 0; i < numTriangles; ++ i ) {

							face = geometry.faces[ i ];
							r = colors[ 3 * i + 0 ];
							g = colors[ 3 * i + 1 ];
							b = colors[ 3 * i + 2 ];
							face.color = new THREE.Color().setRGB( r, g, b );

						}

					}

				}

				return geometry;

			}

			function parseBinary( data ) {

				var count, pointIndex, i, numberOfPoints, s;
				var buffer = new Uint8Array( data );
				var dataView = new DataView( data );

				// Points and normals, by default, are empty
				var points = [];
				var normals = [];
				var indices = [];
				var index = 0;

				function findString( buffer, start ) {

					var index = start;
					var c = buffer[ index ];
					var s = [];
					while ( c !== 10 ) {

						s.push( String.fromCharCode( c ) );
						index ++;
						c = buffer[ index ];

					}

					return { start: start,
						end: index,
						next: index + 1,
						parsedString: s.join( '' ) };

				}

				var state, line;

				while ( true ) {

					// Get a string
					state = findString( buffer, index );
					line = state.parsedString;

					if ( line.indexOf( 'POINTS' ) === 0 ) {
						// Add the points
						numberOfPoints = parseInt( line.split( ' ' )[ 1 ], 10 );

						// Each point is 3 4-byte floats
						count = numberOfPoints * 4 * 3;

						points = new Float32Array( numberOfPoints * 3 );

						pointIndex = state.next;
						for ( i = 0; i < numberOfPoints; i ++ ) {

							points[ 3 * i ] = dataView.getFloat32( pointIndex, false );
							points[ 3 * i + 1 ] = dataView.getFloat32( pointIndex + 4, false );
							points[ 3 * i + 2 ] = dataView.getFloat32( pointIndex + 8, false );
							pointIndex = pointIndex + 12;

						}
						// increment our next pointer
						state.next = state.next + count + 1;

					} else if ( line.indexOf( 'TRIANGLE_STRIPS' ) === 0 ) {

						var numberOfStrips = parseInt( line.split( ' ' )[ 1 ], 10 );
						var size = parseInt( line.split( ' ' )[ 2 ], 10 );
						// 4 byte integers
						count = size * 4;

						indices = new Uint32Array( 3 * size - 9 * numberOfStrips );
						var indicesIndex = 0;

						pointIndex = state.next;
						for ( i = 0; i < numberOfStrips; i ++ ) {

							// For each strip, read the first value, then record that many more points
							var indexCount = dataView.getInt32( pointIndex, false );
							var strip = [];
							pointIndex += 4;
							for ( s = 0; s < indexCount; s ++ ) {

								strip.push( dataView.getInt32( pointIndex, false ) );
								pointIndex += 4;

							}

							// retrieves the n-2 triangles from the triangle strip
							for ( var j = 0; j < indexCount - 2; j ++ ) {

								if ( j % 2 ) {

									indices[ indicesIndex ++ ] = strip[ j ];
									indices[ indicesIndex ++ ] = strip[ j + 2 ];
									indices[ indicesIndex ++ ] = strip[ j + 1 ];

								} else {


									indices[ indicesIndex ++ ] = strip[ j ];
									indices[ indicesIndex ++ ] = strip[ j + 1 ];
									indices[ indicesIndex ++ ] = strip[ j + 2 ];

								}

							}

						}
						// increment our next pointer
						state.next = state.next + count + 1;

					} else if ( line.indexOf( 'POLYGONS' ) === 0 ) {

						var numberOfStrips = parseInt( line.split( ' ' )[ 1 ], 10 );
						var size = parseInt( line.split( ' ' )[ 2 ], 10 );
						// 4 byte integers
						count = size * 4;

						indices = new Uint32Array( 3 * size - 9 * numberOfStrips );
						var indicesIndex = 0;

						pointIndex = state.next;
						for ( i = 0; i < numberOfStrips; i ++ ) {

							// For each strip, read the first value, then record that many more points
							var indexCount = dataView.getInt32( pointIndex, false );
							var strip = [];
							pointIndex += 4;
							for ( s = 0; s < indexCount; s ++ ) {

								strip.push( dataView.getInt32( pointIndex, false ) );
								pointIndex += 4;

							}

							// divide the polygon in n-2 triangle
							for ( var j = 1; j < indexCount - 1; j ++ ) {

								indices[ indicesIndex ++ ] = strip[ 0 ];
								indices[ indicesIndex ++ ] = strip[ j ];
								indices[ indicesIndex ++ ] = strip[ j + 1 ];

							}

						}
						// increment our next pointer
						state.next = state.next + count + 1;

					} else if ( line.indexOf( 'POINT_DATA' ) === 0 ) {

						numberOfPoints = parseInt( line.split( ' ' )[ 1 ], 10 );

						// Grab the next line
						state = findString( buffer, state.next );

						// Now grab the binary data
						count = numberOfPoints * 4 * 3;

						normals = new Float32Array( numberOfPoints * 3 );
						pointIndex = state.next;
						for ( i = 0; i < numberOfPoints; i ++ ) {

							normals[ 3 * i ] = dataView.getFloat32( pointIndex, false );
							normals[ 3 * i + 1 ] = dataView.getFloat32( pointIndex + 4, false );
							normals[ 3 * i + 2 ] = dataView.getFloat32( pointIndex + 8, false );
							pointIndex += 12;

						}

						// Increment past our data
						state.next = state.next + count;

					}

					// Increment index
					index = state.next;

					if ( index >= buffer.byteLength ) {

						break;

					}

				}

				var geometry = new THREE.BufferGeometry();
				geometry.setIndex( new THREE.BufferAttribute( indices, 1 ) );
				geometry.addAttribute( 'position', new THREE.BufferAttribute( points, 3 ) );

				if ( normals.length === points.length ) {

					geometry.addAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );

				}

				return geometry;

			}

			function Float32Concat( first, second ) {

			    var firstLength = first.length, result = new Float32Array( firstLength + second.length );

			    result.set( first );
			    result.set( second, firstLength );

			    return result;

			}

			function Int32Concat( first, second ) {

			    var firstLength = first.length, result = new Int32Array( firstLength + second.length );

			    result.set( first );
			    result.set( second, firstLength );

			    return result;

			}

			function parseXML( stringFile ) {

				// Changes XML to JSON, based on https://davidwalsh.name/convert-xml-json

				function xmlToJson( xml ) {

					// Create the return object
					var obj = {};

					if ( xml.nodeType === 1 ) { // element

						// do attributes

						if ( xml.attributes ) {

							if ( xml.attributes.length > 0 ) {

								obj[ 'attributes' ] = {};

								for ( var j = 0; j < xml.attributes.length; j ++ ) {

									var attribute = xml.attributes.item( j );
									obj[ 'attributes' ][ attribute.nodeName ] = attribute.nodeValue.trim();

								}

							}

						}

					} else if ( xml.nodeType === 3 ) { // text

						obj = xml.nodeValue.trim();

					}

					// do children
					if ( xml.hasChildNodes() ) {

						for ( var i = 0; i < xml.childNodes.length; i ++ ) {

							var item = xml.childNodes.item( i );
							var nodeName = item.nodeName;

							if ( typeof obj[ nodeName ] === 'undefined' ) {

								var tmp = xmlToJson( item );

								if ( tmp !== '' ) obj[ nodeName ] = tmp;

							} else {

								if ( typeof obj[ nodeName ].push === 'undefined' ) {

									var old = obj[ nodeName ];
									obj[ nodeName ] = [ old ];

								}

								var tmp = xmlToJson( item );

								if ( tmp !== '' ) obj[ nodeName ].push( tmp );

							}

						}

					}

					return obj;

				}

				// Taken from Base64-js
				function Base64toByteArray( b64 ) {

					var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array;
					var i;
					var lookup = [];
					var revLookup = [];
					var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
					var len = code.length;

					for ( i = 0; i < len; i ++ ) {

						lookup[ i ] = code[ i ];

					}

					for ( i = 0; i < len; ++ i ) {

						revLookup[ code.charCodeAt( i ) ] = i;

					}

					revLookup[ '-'.charCodeAt( 0 ) ] = 62;
					revLookup[ '_'.charCodeAt( 0 ) ] = 63;

					var j, l, tmp, placeHolders, arr;
					var len = b64.length;

					if ( len % 4 > 0 ) {

						throw new Error( 'Invalid string. Length must be a multiple of 4' );

					}

					placeHolders = b64[ len - 2 ] === '=' ? 2 : b64[ len - 1 ] === '=' ? 1 : 0;
					arr = new Arr( len * 3 / 4 - placeHolders );
					l = placeHolders > 0 ? len - 4 : len;

					var L = 0;

					for ( i = 0, j = 0; i < l; i += 4, j += 3 ) {

						tmp = ( revLookup[ b64.charCodeAt( i ) ] << 18 ) | ( revLookup[ b64.charCodeAt( i + 1 ) ] << 12 ) | ( revLookup[ b64.charCodeAt( i + 2 ) ] << 6 ) | revLookup[ b64.charCodeAt( i + 3 ) ];
						arr[ L ++ ] = ( tmp & 0xFF0000 ) >> 16;
						arr[ L ++ ] = ( tmp & 0xFF00 ) >> 8;
						arr[ L ++ ] = tmp & 0xFF;

					}

					if ( placeHolders === 2 ) {

						tmp = ( revLookup[ b64.charCodeAt( i ) ] << 2 ) | ( revLookup[ b64.charCodeAt( i + 1 ) ] >> 4 );
						arr[ L ++ ] = tmp & 0xFF;

					} else if ( placeHolders === 1 ) {

						tmp = ( revLookup[ b64.charCodeAt( i ) ] << 10 ) | ( revLookup[ b64.charCodeAt( i + 1 ) ] << 4 ) | ( revLookup[ b64.charCodeAt( i + 2 ) ] >> 2 );
						arr[ L ++ ] = ( tmp >> 8 ) & 0xFF;
						arr[ L ++ ] = tmp & 0xFF;

					}

					return arr;

				}

				function parseDataArray( ele, compressed ) {

					var numBytes = 0;

					if ( json.attributes.header_type === 'UInt64' ) {

						numBytes = 8;

					}	else if ( json.attributes.header_type === 'UInt32' ) {

						numBytes = 4;

					}


					// Check the format
					if ( ele.attributes.format === 'binary' && compressed ) {

						var rawData, content, byteData, blocks, cSizeStart, headerSize, padding, dataOffsets, currentOffset;

						if ( ele.attributes.type === 'Float32' ) {

							var txt = new Float32Array( );

						} else if ( ele.attributes.type === 'Int64' ) {

							var txt = new Int32Array( );

						}

						// VTP data with the header has the following structure:
						// [#blocks][#u-size][#p-size][#c-size-1][#c-size-2]...[#c-size-#blocks][DATA]
						//
						// Each token is an integer value whose type is specified by "header_type" at the top of the file (UInt32 if no type specified). The token meanings are:
						// [#blocks] = Number of blocks
						// [#u-size] = Block size before compression
						// [#p-size] = Size of last partial block (zero if it not needed)
						// [#c-size-i] = Size in bytes of block i after compression
						//
						// The [DATA] portion stores contiguously every block appended together. The offset from the beginning of the data section to the beginning of a block is
						// computed by summing the compressed block sizes from preceding blocks according to the header.

						rawData = ele[ '#text' ];

						byteData = Base64toByteArray( rawData );

						blocks = byteData[ 0 ];
						for ( var i = 1; i < numBytes - 1; i ++ ) {

							blocks = blocks | ( byteData[ i ] << ( i * numBytes ) );

						}

						headerSize = ( blocks + 3 ) * numBytes;
						padding = ( ( headerSize % 3 ) > 0 ) ? 3 - ( headerSize % 3 ) : 0;
						headerSize = headerSize + padding;

						dataOffsets = [];
						currentOffset = headerSize;
						dataOffsets.push( currentOffset );

						// Get the blocks sizes after the compression.
						// There are three blocks before c-size-i, so we skip 3*numBytes
						cSizeStart = 3 * numBytes;

						for ( var i = 0; i < blocks; i ++ ) {

							var currentBlockSize = byteData[ i * numBytes + cSizeStart ];

							for ( var j = 1; j < numBytes - 1; j ++ ) {

								// Each data point consists of 8 bytes regardless of the header type
								currentBlockSize = currentBlockSize | ( byteData[ i * numBytes + cSizeStart + j ] << ( j * 8 ) );

							}

							currentOffset = currentOffset + currentBlockSize;
							dataOffsets.push( currentOffset );

						}

						for ( var i = 0; i < dataOffsets.length - 1; i ++ ) {

							var inflate = new Zlib.Inflate( byteData.slice( dataOffsets[ i ], dataOffsets[ i + 1 ] ), { resize: true, verify: true } ); // eslint-disable-line no-undef
							content = inflate.decompress();
							content = content.buffer;

							if ( ele.attributes.type === 'Float32' ) {

								content = new Float32Array( content );
								txt = Float32Concat( txt, content );

							} else if ( ele.attributes.type === 'Int64' ) {

								content = new Int32Array( content );
								txt = Int32Concat( txt, content );

							}

						}

						delete ele[ '#text' ];

						// Get the content and optimize it
						if ( ele.attributes.type === 'Float32' ) {

							if ( ele.attributes.format === 'binary' ) {

								if ( ! compressed ) {

									txt = txt.filter( function ( el, idx ) {

										if ( idx !== 0 ) return true;

									} );

								}

							}

						} else if ( ele.attributes.type === 'Int64' ) {

							if ( ele.attributes.format === 'binary' ) {

								if ( ! compressed ) {

									txt = txt.filter( function ( el, idx ) {

										if ( idx !== 0 ) return true;

									} );

								}

								txt = txt.filter( function ( el, idx ) {

									if ( idx % 2 !== 1 ) return true;

								} );

							}

						}

					} else {

						if ( ele.attributes.format === 'binary' && ! compressed ) {

							var content = Base64toByteArray( ele[ '#text' ] );

							//  VTP data for the uncompressed case has the following structure:
							// [#bytes][DATA]
							// where "[#bytes]" is an integer value specifying the number of bytes in the block of data following it.
							content = content.slice( numBytes ).buffer;

						} else {

							if ( ele[ '#text' ] ) {

								var content = ele[ '#text' ].split( /\s+/ ).filter( function ( el ) {

									if ( el !== '' ) return el;

								} );

							} else {

								var content = new Int32Array( 0 ).buffer;

							}

						}

						delete ele[ '#text' ];

						// Get the content and optimize it
						if ( ele.attributes.type === 'Float32' ) {

							var txt = new Float32Array( content );

						} else if ( ele.attributes.type === 'Int32' ) {

							var txt = new Int32Array( content );

						} else if ( ele.attributes.type === 'Int64' ) {

							var txt = new Int32Array( content );

							if ( ele.attributes.format === 'binary' ) {

								txt = txt.filter( function ( el, idx ) {

									if ( idx % 2 !== 1 ) return true;

								} );

							}

						}

					} // endif ( ele.attributes.format === 'binary' && compressed )

					return txt;

				}

				// Main part
				// Get Dom
				var dom = null;

				if ( window.DOMParser ) {

					try {

						dom = ( new DOMParser() ).parseFromString( stringFile, 'text/xml' );

					} catch ( e ) {

						dom = null;

					}

				} else if ( window.ActiveXObject ) {

					try {

						dom = new ActiveXObject( 'Microsoft.XMLDOM' ); // eslint-disable-line no-undef
						dom.async = false;

						if ( ! dom.loadXML( /* xml */ ) ) {

							throw new Error( dom.parseError.reason + dom.parseError.srcText );

						}

					} catch ( e ) {

						dom = null;

					}

				} else {

					throw new Error( 'Cannot parse xml string!' );

				}

				// Get the doc
				var doc = dom.documentElement;
				// Convert to json
				var json = xmlToJson( doc );
				var points = [];
				var normals = [];
				var indices = [];

				if ( json.PolyData ) {

					var piece = json.PolyData.Piece;
					var compressed = json.attributes.hasOwnProperty( 'compressor' );

					// Can be optimized
					// Loop through the sections
					var sections = [ 'PointData', 'Points', 'Strips', 'Polys' ];// +['CellData', 'Verts', 'Lines'];
					var sectionIndex = 0, numberOfSections = sections.length;

					while ( sectionIndex < numberOfSections ) {

						var section = piece[ sections[ sectionIndex ] ];

						// If it has a DataArray in it

						if ( section && section.DataArray ) {

							// Depending on the number of DataArrays

							if ( Object.prototype.toString.call( section.DataArray ) === '[object Array]' ) {

								var arr = section.DataArray;

							} else {

								var arr = [ section.DataArray ];

							}

							var dataArrayIndex = 0, numberOfDataArrays = arr.length;

							while ( dataArrayIndex < numberOfDataArrays ) {

								// Parse the DataArray
								if ( ( '#text' in arr[ dataArrayIndex ] ) && ( arr[ dataArrayIndex ][ '#text' ].length > 0 ) ) {

									arr[ dataArrayIndex ].text = parseDataArray( arr[ dataArrayIndex ], compressed );

								}

								dataArrayIndex ++;

							}

							switch ( sections[ sectionIndex ] ) {

								// if iti is point data
								case 'PointData':

									var numberOfPoints = parseInt( piece.attributes.NumberOfPoints );
									var normalsName = section.attributes.Normals;

									if ( numberOfPoints > 0 ) {

										for ( var i = 0, len = arr.length; i < len; i ++ ) {

											if ( normalsName === arr[ i ].attributes.Name ) {

												var components = arr[ i ].attributes.NumberOfComponents;
												normals = new Float32Array( numberOfPoints * components );
												normals.set( arr[ i ].text, 0 );

											}

										}

									}

									break;

								// if it is points
								case 'Points':

									var numberOfPoints = parseInt( piece.attributes.NumberOfPoints );

									if ( numberOfPoints > 0 ) {

										var components = section.DataArray.attributes.NumberOfComponents;
										points = new Float32Array( numberOfPoints * components );
										points.set( section.DataArray.text, 0 );

									}

									break;

								// if it is strips
								case 'Strips':

									var numberOfStrips = parseInt( piece.attributes.NumberOfStrips );

									if ( numberOfStrips > 0 ) {

										var connectivity = new Int32Array( section.DataArray[ 0 ].text.length );
										var offset = new Int32Array( section.DataArray[ 1 ].text.length );
										connectivity.set( section.DataArray[ 0 ].text, 0 );
										offset.set( section.DataArray[ 1 ].text, 0 );

										var size = numberOfStrips + connectivity.length;
										indices = new Uint32Array( 3 * size - 9 * numberOfStrips );

										var indicesIndex = 0;

										for ( var i = 0, len = numberOfStrips; i < len; i ++ ) {

											var strip = [];

											for ( var s = 0, len1 = offset[ i ], len0 = 0; s < len1 - len0; s ++ ) {

												strip.push( connectivity[ s ] );

												if ( i > 0 ) len0 = offset[ i - 1 ];

											}

											for ( var j = 0, len1 = offset[ i ], len0 = 0; j < len1 - len0 - 2; j ++ ) {

												if ( j % 2 ) {

													indices[ indicesIndex ++ ] = strip[ j ];
													indices[ indicesIndex ++ ] = strip[ j + 2 ];
													indices[ indicesIndex ++ ] = strip[ j + 1 ];

												} else {

													indices[ indicesIndex ++ ] = strip[ j ];
													indices[ indicesIndex ++ ] = strip[ j + 1 ];
													indices[ indicesIndex ++ ] = strip[ j + 2 ];

												}

												if ( i > 0 ) len0 = offset[ i - 1 ];

											}

										}

									}

									break;

								// if it is polys
								case 'Polys':

									var numberOfPolys = parseInt( piece.attributes.NumberOfPolys );

									if ( numberOfPolys > 0 ) {

										var connectivity = new Int32Array( section.DataArray[ 0 ].text.length );
										var offset = new Int32Array( section.DataArray[ 1 ].text.length );
										connectivity.set( section.DataArray[ 0 ].text, 0 );
										offset.set( section.DataArray[ 1 ].text, 0 );

										var size = numberOfPolys + connectivity.length;
										indices = new Uint32Array( 3 * size - 9 * numberOfPolys );
										var indicesIndex = 0, connectivityIndex = 0;
										var i = 0, len = numberOfPolys, len0 = 0;

										while ( i < len ) {

											var poly = [];
											var s = 0, len1 = offset[ i ];

											while ( s < len1 - len0 ) {

												poly.push( connectivity[ connectivityIndex ++ ] );
												s ++;

											}

											var j = 1;

											while ( j < len1 - len0 - 1 ) {

												indices[ indicesIndex ++ ] = poly[ 0 ];
												indices[ indicesIndex ++ ] = poly[ j ];
												indices[ indicesIndex ++ ] = poly[ j + 1 ];
												j ++;

											}

											i ++;
											len0 = offset[ i - 1 ];

										}

									}

									break;

								default:
									break;

							}

						}

						sectionIndex ++;

					}

					var geometry = new THREE.BufferGeometry();
					geometry.setIndex( new THREE.BufferAttribute( indices, 1 ) );
					geometry.addAttribute( 'position', new THREE.BufferAttribute( points, 3 ) );

					if ( normals.length === points.length ) {

						geometry.addAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );

					}

					return geometry;

				}

			}

			function getStringFile( data ) {

				var stringFile = '';
				var charArray = new Uint8Array( data );
				var i = 0;
				var len = charArray.length;

				while ( len -- ) {

					stringFile += String.fromCharCode( charArray[ i ++ ] );

				}

				return stringFile;

			}

			// get the 5 first lines of the files to check if there is the key word binary
			var meta = THREE.LoaderUtils.decodeText( new Uint8Array( data, 0, 250 ) ).split( '\n' );

			if ( meta[ 0 ].indexOf( 'xml' ) !== - 1 ) {

				return parseXML( getStringFile( data ) );

			} else if ( meta[ 2 ].includes( 'ASCII' ) ) {

				return parseASCII( getStringFile( data ) );

			} else {

				return parseBinary( data );

			}

		}

	} );

	/**
	 * Loader for CTM encoded models generated by OpenCTM tools:
	 *	http://openctm.sourceforge.net/
	 *
	 * Uses js-openctm library by Juan Mellado
	 *	http://code.google.com/p/js-openctm/
	 *
	 * @author alteredq / http://alteredqualia.com/
	 */

	THREE.CTMLoader = function () {

		THREE.Loader.call( this );

	};

	THREE.CTMLoader.prototype = Object.create( THREE.Loader.prototype );
	THREE.CTMLoader.prototype.constructor = THREE.CTMLoader;

	// Load multiple CTM parts defined in JSON

	THREE.CTMLoader.prototype.loadParts = function ( url, callback, parameters ) {

		parameters = parameters || {};

		var scope = this;

		var xhr = new XMLHttpRequest();

		var basePath = parameters.basePath ? parameters.basePath : THREE.LoaderUtils.extractUrlBase( url );

		xhr.onreadystatechange = function () {

			if ( xhr.readyState === 4 ) {

				if ( xhr.status === 200 || xhr.status === 0 ) {

					var jsonObject = JSON.parse( xhr.responseText );

					var materials = [], geometries = [], counter = 0;

					function callbackFinal( geometry ) {

						counter += 1;

						geometries.push( geometry );

						if ( counter === jsonObject.offsets.length ) {

							callback( geometries, materials );

						}

					}


					// init materials

					for ( var i = 0; i < jsonObject.materials.length; i ++ ) {

						materials[ i ] = scope.createMaterial( jsonObject.materials[ i ], basePath );

					}

					// load joined CTM file

					var partUrl = basePath + jsonObject.data;
					var parametersPart = { useWorker: parameters.useWorker, worker: parameters.worker, offsets: jsonObject.offsets };
					scope.load( partUrl, callbackFinal, parametersPart );

				}

			}

		};

		xhr.open( "GET", url, true );
		xhr.setRequestHeader( "Content-Type", "text/plain" );
		xhr.send( null );

	};

	// Load CTMLoader compressed models
	//	- parameters
	//		- url (required)
	//		- callback (required)

	THREE.CTMLoader.prototype.load = function ( url, callback, parameters ) {

		parameters = parameters || {};

		var scope = this;

		var offsets = parameters.offsets !== undefined ? parameters.offsets : [ 0 ];

		var xhr = new XMLHttpRequest();


		var length = 0;

		xhr.onreadystatechange = function () {

			if ( xhr.readyState === 4 ) {

				if ( xhr.status === 200 || xhr.status === 0 ) {

					var binaryData = new Uint8Array( xhr.response );

					var s = Date.now();

					if ( parameters.useWorker ) {

						var worker = parameters.worker || new Worker( 'js/loaders/ctm/CTMWorker.js' );

						worker.onmessage = function ( event ) {

							var files = event.data;

							for ( var i = 0; i < files.length; i ++ ) {

								var ctmFile = files[ i ];

								var e1 = Date.now();
								// console.log( "CTM data parse time [worker]: " + (e1-s) + " ms" );

								scope.createModel( ctmFile, callback );

								var e = Date.now();
								console.log( "model load time [worker]: " + ( e - e1 ) + " ms, total: " + ( e - s ) );

							}


						};

						worker.postMessage( { "data": binaryData, "offsets": offsets }, [ binaryData.buffer ] );

					} else {

						for ( var i = 0; i < offsets.length; i ++ ) {

							var stream = new CTM.Stream( binaryData );
							stream.offset = offsets[ i ];

							var ctmFile = new CTM.File( stream );

							scope.createModel( ctmFile, callback );

						}

						//var e = Date.now();
						//console.log( "CTM data parse time [inline]: " + (e-s) + " ms" );

					}

				} else {

					console.error( "Couldn't load [" + url + "] [" + xhr.status + "]" );

				}

			} else if ( xhr.readyState === 3 ) ; else if ( xhr.readyState === 2 ) {

				length = xhr.getResponseHeader( "Content-Length" );

			}

		};

		xhr.open( "GET", url, true );
		xhr.responseType = "arraybuffer";

		xhr.send( null );

	};


	THREE.CTMLoader.prototype.createModel = function ( file, callback ) {

		var Model = function () {

			THREE.BufferGeometry.call( this );

			this.materials = [];

			var indices = file.body.indices;
			var positions = file.body.vertices;
			var normals = file.body.normals;

			var uvs, colors;

			var uvMaps = file.body.uvMaps;

			if ( uvMaps !== undefined && uvMaps.length > 0 ) {

				uvs = uvMaps[ 0 ].uv;

			}

			var attrMaps = file.body.attrMaps;

			if ( attrMaps !== undefined && attrMaps.length > 0 && attrMaps[ 0 ].name === 'Color' ) {

				colors = attrMaps[ 0 ].attr;

			}

			this.setIndex( new THREE.BufferAttribute( indices, 1 ) );
			this.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );

			if ( normals !== undefined ) {

				this.addAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );

			}

			if ( uvs !== undefined ) {

				this.addAttribute( 'uv', new THREE.BufferAttribute( uvs, 2 ) );

			}

			if ( colors !== undefined ) {

				this.addAttribute( 'color', new THREE.BufferAttribute( colors, 4 ) );

			}

		};

		Model.prototype = Object.create( THREE.BufferGeometry.prototype );
		Model.prototype.constructor = Model;

		var geometry = new Model();

		// compute vertex normals if not present in the CTM model
		if ( geometry.attributes.normal === undefined ) {

			geometry.computeVertexNormals();

		}

		callback( geometry );

	};

	/**
	 * @author fernandojsg / http://fernandojsg.com
	 * @author Don McCurdy / https://www.donmccurdy.com
	 * @author Takahiro / https://github.com/takahirox
	 */

	//------------------------------------------------------------------------------
	// Constants
	//------------------------------------------------------------------------------
	var WEBGL_CONSTANTS = {
		POINTS: 0x0000,
		LINES: 0x0001,
		LINE_LOOP: 0x0002,
		LINE_STRIP: 0x0003,
		TRIANGLES: 0x0004,
		TRIANGLE_STRIP: 0x0005,
		TRIANGLE_FAN: 0x0006,

		UNSIGNED_BYTE: 0x1401,
		UNSIGNED_SHORT: 0x1403,
		FLOAT: 0x1406,
		UNSIGNED_INT: 0x1405,
		ARRAY_BUFFER: 0x8892,
		ELEMENT_ARRAY_BUFFER: 0x8893,

		NEAREST: 0x2600,
		LINEAR: 0x2601,
		NEAREST_MIPMAP_NEAREST: 0x2700,
		LINEAR_MIPMAP_NEAREST: 0x2701,
		NEAREST_MIPMAP_LINEAR: 0x2702,
		LINEAR_MIPMAP_LINEAR: 0x2703
	};

	var THREE_TO_WEBGL = {
		// @TODO Replace with computed property name [THREE.*] when available on es6
		1003: WEBGL_CONSTANTS.NEAREST,
		1004: WEBGL_CONSTANTS.NEAREST_MIPMAP_NEAREST,
		1005: WEBGL_CONSTANTS.NEAREST_MIPMAP_LINEAR,
		1006: WEBGL_CONSTANTS.LINEAR,
		1007: WEBGL_CONSTANTS.LINEAR_MIPMAP_NEAREST,
		1008: WEBGL_CONSTANTS.LINEAR_MIPMAP_LINEAR
	};

	var PATH_PROPERTIES = {
		scale: 'scale',
		position: 'translation',
		quaternion: 'rotation',
		morphTargetInfluences: 'weights'
	};

	//------------------------------------------------------------------------------
	// GLTF Exporter
	//------------------------------------------------------------------------------
	THREE.GLTFExporter = function () {};

	THREE.GLTFExporter.prototype = {

		constructor: THREE.GLTFExporter,

		/**
		 * Parse scenes and generate GLTF output
		 * @param  {THREE.Scene or [THREE.Scenes]} input   THREE.Scene or Array of THREE.Scenes
		 * @param  {Function} onDone  Callback on completed
		 * @param  {Object} options options
		 */
		parse: function ( input, onDone, options ) {

			var DEFAULT_OPTIONS = {
				binary: false,
				trs: false,
				onlyVisible: true,
				truncateDrawRange: true,
				embedImages: true,
				animations: [],
				forceIndices: false,
				forcePowerOfTwoTextures: false
			};

			options = Object.assign( {}, DEFAULT_OPTIONS, options );

			if ( options.animations.length > 0 ) {

				// Only TRS properties, and not matrices, may be targeted by animation.
				options.trs = true;

			}

			var outputJSON = {

				asset: {

					version: "2.0",
					generator: "THREE.GLTFExporter"

				}

			};

			var byteOffset = 0;
			var buffers = [];
			var pending = [];
			var nodeMap = new Map();
			var skins = [];
			var extensionsUsed = {};
			var cachedData = {

				attributes: new Map(),
				materials: new Map(),
				textures: new Map()

			};

			var cachedCanvas;

			/**
			 * Compare two arrays
			 */
			/**
			 * Compare two arrays
			 * @param  {Array} array1 Array 1 to compare
			 * @param  {Array} array2 Array 2 to compare
			 * @return {Boolean}        Returns true if both arrays are equal
			 */
			function equalArray( array1, array2 ) {

				return ( array1.length === array2.length ) && array1.every( function ( element, index ) {

					return element === array2[ index ];

				} );

			}

			/**
			 * Converts a string to an ArrayBuffer.
			 * @param  {string} text
			 * @return {ArrayBuffer}
			 */
			function stringToArrayBuffer( text ) {

				if ( window.TextEncoder !== undefined ) {

					return new TextEncoder().encode( text ).buffer;

				}

				var array = new Uint8Array( new ArrayBuffer( text.length ) );

				for ( var i = 0, il = text.length; i < il; i ++ ) {

					var value = text.charCodeAt( i );

					// Replacing multi-byte character with space(0x20).
					array[ i ] = value > 0xFF ? 0x20 : value;

				}

				return array.buffer;

			}

			/**
			 * Get the min and max vectors from the given attribute
			 * @param  {THREE.BufferAttribute} attribute Attribute to find the min/max in range from start to start + count
			 * @param  {Integer} start
			 * @param  {Integer} count
			 * @return {Object} Object containing the `min` and `max` values (As an array of attribute.itemSize components)
			 */
			function getMinMax( attribute, start, count ) {

				var output = {

					min: new Array( attribute.itemSize ).fill( Number.POSITIVE_INFINITY ),
					max: new Array( attribute.itemSize ).fill( Number.NEGATIVE_INFINITY )

				};

				for ( var i = start; i < start + count; i ++ ) {

					for ( var a = 0; a < attribute.itemSize; a ++ ) {

						var value = attribute.array[ i * attribute.itemSize + a ];
						output.min[ a ] = Math.min( output.min[ a ], value );
						output.max[ a ] = Math.max( output.max[ a ], value );

					}

				}

				return output;

			}

			/**
			 * Checks if image size is POT.
			 *
			 * @param {Image} image The image to be checked.
			 * @returns {Boolean} Returns true if image size is POT.
			 *
			 */
			function isPowerOfTwo( image ) {

				return THREE.Math.isPowerOfTwo( image.width ) && THREE.Math.isPowerOfTwo( image.height );

			}

			/**
			 * Checks if normal attribute values are normalized.
			 *
			 * @param {THREE.BufferAttribute} normal
			 * @returns {Boolean}
			 *
			 */
			function isNormalizedNormalAttribute( normal ) {

				if ( cachedData.attributes.has( normal ) ) {

					return false;

				}

				var v = new THREE.Vector3();

				for ( var i = 0, il = normal.count; i < il; i ++ ) {

					// 0.0005 is from glTF-validator
					if ( Math.abs( v.fromArray( normal.array, i * 3 ).length() - 1.0 ) > 0.0005 ) return false;

				}

				return true;

			}

			/**
			 * Creates normalized normal buffer attribute.
			 *
			 * @param {THREE.BufferAttribute} normal
			 * @returns {THREE.BufferAttribute}
			 *
			 */
			function createNormalizedNormalAttribute( normal ) {

				if ( cachedData.attributes.has( normal ) ) {

					return cachedData.textures.get( normal );

				}

				var attribute = normal.clone();

				var v = new THREE.Vector3();

				for ( var i = 0, il = attribute.count; i < il; i ++ ) {

					v.fromArray( attribute.array, i * 3 );

					if ( v.x === 0 && v.y === 0 && v.z === 0 ) {

						// if values can't be normalized set (1, 0, 0)
						v.setX( 1.0 );

					} else {

						v.normalize();

					}

					v.toArray( attribute.array, i * 3 );

				}

				cachedData.attributes.set( normal, attribute );

				return attribute;

			}

			/**
			 * Get the required size + padding for a buffer, rounded to the next 4-byte boundary.
			 * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment
			 *
			 * @param {Integer} bufferSize The size the original buffer.
			 * @returns {Integer} new buffer size with required padding.
			 *
			 */
			function getPaddedBufferSize( bufferSize ) {

				return Math.ceil( bufferSize / 4 ) * 4;

			}

			/**
			 * Returns a buffer aligned to 4-byte boundary.
			 *
			 * @param {ArrayBuffer} arrayBuffer Buffer to pad
			 * @param {Integer} paddingByte (Optional)
			 * @returns {ArrayBuffer} The same buffer if it's already aligned to 4-byte boundary or a new buffer
			 */
			function getPaddedArrayBuffer( arrayBuffer, paddingByte ) {

				paddingByte = paddingByte || 0;

				var paddedLength = getPaddedBufferSize( arrayBuffer.byteLength );

				if ( paddedLength !== arrayBuffer.byteLength ) {

					var array = new Uint8Array( paddedLength );
					array.set( new Uint8Array( arrayBuffer ) );

					if ( paddingByte !== 0 ) {

						for ( var i = arrayBuffer.byteLength; i < paddedLength; i ++ ) {

							array[ i ] = paddingByte;

						}

					}

					return array.buffer;

				}

				return arrayBuffer;

			}

			/**
			 * Serializes a userData.
			 *
			 * @param {THREE.Object3D|THREE.Material} object
			 * @returns {Object}
			 */
			function serializeUserData( object ) {

				try {

					return JSON.parse( JSON.stringify( object.userData ) );

				} catch ( error ) {

					console.warn( 'THREE.GLTFExporter: userData of \'' + object.name + '\' ' +
						'won\'t be serialized because of JSON.stringify error - ' + error.message );

					return {};

				}

			}

			/**
			 * Process a buffer to append to the default one.
			 * @param  {ArrayBuffer} buffer
			 * @return {Integer}
			 */
			function processBuffer( buffer ) {

				if ( ! outputJSON.buffers ) {

					outputJSON.buffers = [ { byteLength: 0 } ];

				}

				// All buffers are merged before export.
				buffers.push( buffer );

				return 0;

			}

			/**
			 * Process and generate a BufferView
			 * @param  {THREE.BufferAttribute} attribute
			 * @param  {number} componentType
			 * @param  {number} start
			 * @param  {number} count
			 * @param  {number} target (Optional) Target usage of the BufferView
			 * @return {Object}
			 */
			function processBufferView( attribute, componentType, start, count, target ) {

				if ( ! outputJSON.bufferViews ) {

					outputJSON.bufferViews = [];

				}

				// Create a new dataview and dump the attribute's array into it

				var componentSize;

				if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) {

					componentSize = 1;

				} else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) {

					componentSize = 2;

				} else {

					componentSize = 4;

				}

				var byteLength = getPaddedBufferSize( count * attribute.itemSize * componentSize );
				var dataView = new DataView( new ArrayBuffer( byteLength ) );
				var offset = 0;

				for ( var i = start; i < start + count; i ++ ) {

					for ( var a = 0; a < attribute.itemSize; a ++ ) {

						// @TODO Fails on InterleavedBufferAttribute, and could probably be
						// optimized for normal BufferAttribute.
						var value = attribute.array[ i * attribute.itemSize + a ];

						if ( componentType === WEBGL_CONSTANTS.FLOAT ) {

							dataView.setFloat32( offset, value, true );

						} else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_INT ) {

							dataView.setUint32( offset, value, true );

						} else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) {

							dataView.setUint16( offset, value, true );

						} else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) {

							dataView.setUint8( offset, value );

						}

						offset += componentSize;

					}

				}

				var gltfBufferView = {

					buffer: processBuffer( dataView.buffer ),
					byteOffset: byteOffset,
					byteLength: byteLength

				};

				if ( target !== undefined ) gltfBufferView.target = target;

				if ( target === WEBGL_CONSTANTS.ARRAY_BUFFER ) {

					// Only define byteStride for vertex attributes.
					gltfBufferView.byteStride = attribute.itemSize * componentSize;

				}

				byteOffset += byteLength;

				outputJSON.bufferViews.push( gltfBufferView );

				// @TODO Merge bufferViews where possible.
				var output = {

					id: outputJSON.bufferViews.length - 1,
					byteLength: 0

				};

				return output;

			}

			/**
			 * Process and generate a BufferView from an image Blob.
			 * @param {Blob} blob
			 * @return {Promise<Integer>}
			 */
			function processBufferViewImage( blob ) {

				if ( ! outputJSON.bufferViews ) {

					outputJSON.bufferViews = [];

				}

				return new Promise( function ( resolve ) {

					var reader = new window.FileReader();
					reader.readAsArrayBuffer( blob );
					reader.onloadend = function () {

						var buffer = getPaddedArrayBuffer( reader.result );

						var bufferView = {
							buffer: processBuffer( buffer ),
							byteOffset: byteOffset,
							byteLength: buffer.byteLength
						};

						byteOffset += buffer.byteLength;

						outputJSON.bufferViews.push( bufferView );

						resolve( outputJSON.bufferViews.length - 1 );

					};

				} );

			}

			/**
			 * Process attribute to generate an accessor
			 * @param  {THREE.BufferAttribute} attribute Attribute to process
			 * @param  {THREE.BufferGeometry} geometry (Optional) Geometry used for truncated draw range
			 * @param  {Integer} start (Optional)
			 * @param  {Integer} count (Optional)
			 * @return {Integer}           Index of the processed accessor on the "accessors" array
			 */
			function processAccessor( attribute, geometry, start, count ) {

				var types = {

					1: 'SCALAR',
					2: 'VEC2',
					3: 'VEC3',
					4: 'VEC4',
					16: 'MAT4'

				};

				var componentType;

				// Detect the component type of the attribute array (float, uint or ushort)
				if ( attribute.array.constructor === Float32Array ) {

					componentType = WEBGL_CONSTANTS.FLOAT;

				} else if ( attribute.array.constructor === Uint32Array ) {

					componentType = WEBGL_CONSTANTS.UNSIGNED_INT;

				} else if ( attribute.array.constructor === Uint16Array ) {

					componentType = WEBGL_CONSTANTS.UNSIGNED_SHORT;

				} else if ( attribute.array.constructor === Uint8Array ) {

					componentType = WEBGL_CONSTANTS.UNSIGNED_BYTE;

				} else {

					throw new Error( 'THREE.GLTFExporter: Unsupported bufferAttribute component type.' );

				}

				if ( start === undefined ) start = 0;
				if ( count === undefined ) count = attribute.count;

				// @TODO Indexed buffer geometry with drawRange not supported yet
				if ( options.truncateDrawRange && geometry !== undefined && geometry.index === null ) {

					var end = start + count;
					var end2 = geometry.drawRange.count === Infinity
							? attribute.count
							: geometry.drawRange.start + geometry.drawRange.count;

					start = Math.max( start, geometry.drawRange.start );
					count = Math.min( end, end2 ) - start;

					if ( count < 0 ) count = 0;

				}

				// Skip creating an accessor if the attribute doesn't have data to export
				if ( count === 0 ) {

					return null;

				}

				var minMax = getMinMax( attribute, start, count );

				var bufferViewTarget;

				// If geometry isn't provided, don't infer the target usage of the bufferView. For
				// animation samplers, target must not be set.
				if ( geometry !== undefined ) {

					bufferViewTarget = attribute === geometry.index ? WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER : WEBGL_CONSTANTS.ARRAY_BUFFER;

				}

				var bufferView = processBufferView( attribute, componentType, start, count, bufferViewTarget );

				var gltfAccessor = {

					bufferView: bufferView.id,
					byteOffset: bufferView.byteOffset,
					componentType: componentType,
					count: count,
					max: minMax.max,
					min: minMax.min,
					type: types[ attribute.itemSize ]

				};

				if ( ! outputJSON.accessors ) {

					outputJSON.accessors = [];

				}

				outputJSON.accessors.push( gltfAccessor );

				return outputJSON.accessors.length - 1;

			}

			/**
			 * Process image
			 * @param  {Texture} map Texture to process
			 * @return {Integer}     Index of the processed texture in the "images" array
			 */
			function processImage( map ) {

				// @TODO Cache

				if ( ! outputJSON.images ) {

					outputJSON.images = [];

				}

				var mimeType = map.format === THREE.RGBAFormat ? 'image/png' : 'image/jpeg';
				var gltfImage = { mimeType: mimeType };

				if ( options.embedImages ) {

					var canvas = cachedCanvas = cachedCanvas || document.createElement( 'canvas' );

					canvas.width = map.image.width;
					canvas.height = map.image.height;

					if ( options.forcePowerOfTwoTextures && ! isPowerOfTwo( map.image ) ) {

						console.warn( 'GLTFExporter: Resized non-power-of-two image.', map.image );

						canvas.width = THREE.Math.floorPowerOfTwo( canvas.width );
						canvas.height = THREE.Math.floorPowerOfTwo( canvas.height );

					}

					var ctx = canvas.getContext( '2d' );

					if ( map.flipY === true ) {

						ctx.translate( 0, canvas.height );
						ctx.scale( 1, - 1 );

					}

					ctx.drawImage( map.image, 0, 0, canvas.width, canvas.height );

					if ( options.binary === true ) {

						pending.push( new Promise( function ( resolve ) {

							canvas.toBlob( function ( blob ) {

								processBufferViewImage( blob ).then( function ( bufferViewIndex ) {

									gltfImage.bufferView = bufferViewIndex;

									resolve();

								} );

							}, mimeType );

						} ) );

					} else {

						gltfImage.uri = canvas.toDataURL( mimeType );

					}

				} else {

					gltfImage.uri = map.image.src;

				}

				outputJSON.images.push( gltfImage );

				return outputJSON.images.length - 1;

			}

			/**
			 * Process sampler
			 * @param  {Texture} map Texture to process
			 * @return {Integer}     Index of the processed texture in the "samplers" array
			 */
			function processSampler( map ) {

				if ( ! outputJSON.samplers ) {

					outputJSON.samplers = [];

				}

				var gltfSampler = {

					magFilter: THREE_TO_WEBGL[ map.magFilter ],
					minFilter: THREE_TO_WEBGL[ map.minFilter ],
					wrapS: THREE_TO_WEBGL[ map.wrapS ],
					wrapT: THREE_TO_WEBGL[ map.wrapT ]

				};

				outputJSON.samplers.push( gltfSampler );

				return outputJSON.samplers.length - 1;

			}

			/**
			 * Process texture
			 * @param  {Texture} map Map to process
			 * @return {Integer}     Index of the processed texture in the "textures" array
			 */
			function processTexture( map ) {

				if ( cachedData.textures.has( map ) ) {

					return cachedData.textures.get( map );

				}

				if ( ! outputJSON.textures ) {

					outputJSON.textures = [];

				}

				var gltfTexture = {

					sampler: processSampler( map ),
					source: processImage( map )

				};

				outputJSON.textures.push( gltfTexture );

				var index = outputJSON.textures.length - 1;
				cachedData.textures.set( map, index );

				return index;

			}

			/**
			 * Process material
			 * @param  {THREE.Material} material Material to process
			 * @return {Integer}      Index of the processed material in the "materials" array
			 */
			function processMaterial( material ) {

				if ( cachedData.materials.has( material ) ) {

					return cachedData.materials.get( material );

				}

				if ( ! outputJSON.materials ) {

					outputJSON.materials = [];

				}

				if ( material.isShaderMaterial ) {

					console.warn( 'GLTFExporter: THREE.ShaderMaterial not supported.' );
					return null;

				}

				// @QUESTION Should we avoid including any attribute that has the default value?
				var gltfMaterial = {

					pbrMetallicRoughness: {}

				};

				if ( material.isMeshBasicMaterial ) {

					gltfMaterial.extensions = { KHR_materials_unlit: {} };

					extensionsUsed[ 'KHR_materials_unlit' ] = true;

				} else if ( ! material.isMeshStandardMaterial ) {

					console.warn( 'GLTFExporter: Use MeshStandardMaterial or MeshBasicMaterial for best results.' );

				}

				// pbrMetallicRoughness.baseColorFactor
				var color = material.color.toArray().concat( [ material.opacity ] );

				if ( ! equalArray( color, [ 1, 1, 1, 1 ] ) ) {

					gltfMaterial.pbrMetallicRoughness.baseColorFactor = color;

				}

				if ( material.isMeshStandardMaterial ) {

					gltfMaterial.pbrMetallicRoughness.metallicFactor = material.metalness;
					gltfMaterial.pbrMetallicRoughness.roughnessFactor = material.roughness;

				} else if ( material.isMeshBasicMaterial ) {

					gltfMaterial.pbrMetallicRoughness.metallicFactor = 0.0;
					gltfMaterial.pbrMetallicRoughness.roughnessFactor = 0.9;

				} else {

					gltfMaterial.pbrMetallicRoughness.metallicFactor = 0.5;
					gltfMaterial.pbrMetallicRoughness.roughnessFactor = 0.5;

				}

				// pbrMetallicRoughness.metallicRoughnessTexture
				if ( material.metalnessMap || material.roughnessMap ) {

					if ( material.metalnessMap === material.roughnessMap ) {

						gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture = {

							index: processTexture( material.metalnessMap )

						};

					} else {

						console.warn( 'THREE.GLTFExporter: Ignoring metalnessMap and roughnessMap because they are not the same Texture.' );

					}

				}

				// pbrMetallicRoughness.baseColorTexture
				if ( material.map ) {

					gltfMaterial.pbrMetallicRoughness.baseColorTexture = {

						index: processTexture( material.map )

					};

				}

				if ( material.isMeshBasicMaterial ||
					material.isLineBasicMaterial ||
					material.isPointsMaterial ) ; else {

					// emissiveFactor
					var emissive = material.emissive.clone().multiplyScalar( material.emissiveIntensity ).toArray();

					if ( ! equalArray( emissive, [ 0, 0, 0 ] ) ) {

						gltfMaterial.emissiveFactor = emissive;

					}

					// emissiveTexture
					if ( material.emissiveMap ) {

						gltfMaterial.emissiveTexture = {

							index: processTexture( material.emissiveMap )

						};

					}

				}

				// normalTexture
				if ( material.normalMap ) {

					gltfMaterial.normalTexture = {

						index: processTexture( material.normalMap )

					};

					if ( material.normalScale.x !== - 1 ) {

						if ( material.normalScale.x !== material.normalScale.y ) {

							console.warn( 'THREE.GLTFExporter: Normal scale components are different, ignoring Y and exporting X.' );

						}

						gltfMaterial.normalTexture.scale = material.normalScale.x;

					}

				}

				// occlusionTexture
				if ( material.aoMap ) {

					gltfMaterial.occlusionTexture = {

						index: processTexture( material.aoMap )

					};

					if ( material.aoMapIntensity !== 1.0 ) {

						gltfMaterial.occlusionTexture.strength = material.aoMapIntensity;

					}

				}

				// alphaMode
				if ( material.transparent || material.alphaTest > 0.0 ) {

					gltfMaterial.alphaMode = material.opacity < 1.0 ? 'BLEND' : 'MASK';

					// Write alphaCutoff if it's non-zero and different from the default (0.5).
					if ( material.alphaTest > 0.0 && material.alphaTest !== 0.5 ) {

						gltfMaterial.alphaCutoff = material.alphaTest;

					}

				}

				// doubleSided
				if ( material.side === THREE.DoubleSide ) {

					gltfMaterial.doubleSided = true;

				}

				if ( material.name !== '' ) {

					gltfMaterial.name = material.name;

				}

				if ( Object.keys( material.userData ).length > 0 ) {

					gltfMaterial.extras = serializeUserData( material );

				}

				outputJSON.materials.push( gltfMaterial );

				var index = outputJSON.materials.length - 1;
				cachedData.materials.set( material, index );

				return index;

			}

			/**
			 * Process mesh
			 * @param  {THREE.Mesh} mesh Mesh to process
			 * @return {Integer}      Index of the processed mesh in the "meshes" array
			 */
			function processMesh( mesh ) {

				var geometry = mesh.geometry;

				var mode;

				// Use the correct mode
				if ( mesh.isLineSegments ) {

					mode = WEBGL_CONSTANTS.LINES;

				} else if ( mesh.isLineLoop ) {

					mode = WEBGL_CONSTANTS.LINE_LOOP;

				} else if ( mesh.isLine ) {

					mode = WEBGL_CONSTANTS.LINE_STRIP;

				} else if ( mesh.isPoints ) {

					mode = WEBGL_CONSTANTS.POINTS;

				} else {

					if ( ! geometry.isBufferGeometry ) {

						var geometryTemp = new THREE.BufferGeometry();
						geometryTemp.fromGeometry( geometry );
						geometry = geometryTemp;

					}

					if ( mesh.drawMode === THREE.TriangleFanDrawMode ) {

						console.warn( 'GLTFExporter: TriangleFanDrawMode and wireframe incompatible.' );
						mode = WEBGL_CONSTANTS.TRIANGLE_FAN;

					} else if ( mesh.drawMode === THREE.TriangleStripDrawMode ) {

						mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINE_STRIP : WEBGL_CONSTANTS.TRIANGLE_STRIP;

					} else {

						mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINES : WEBGL_CONSTANTS.TRIANGLES;

					}

				}

				var gltfMesh = {};

				var attributes = {};
				var primitives = [];
				var targets = [];

				// Conversion between attributes names in threejs and gltf spec
				var nameConversion = {

					uv: 'TEXCOORD_0',
					uv2: 'TEXCOORD_1',
					color: 'COLOR_0',
					skinWeight: 'WEIGHTS_0',
					skinIndex: 'JOINTS_0'

				};

				var originalNormal = geometry.getAttribute( 'normal' );

				if ( originalNormal !== undefined && ! isNormalizedNormalAttribute( originalNormal ) ) {

					console.warn( 'THREE.GLTFExporter: Creating normalized normal attribute from the non-normalized one.' );

					geometry.addAttribute( 'normal', createNormalizedNormalAttribute( originalNormal ) );

				}

				// @QUESTION Detect if .vertexColors = THREE.VertexColors?
				// For every attribute create an accessor
				for ( var attributeName in geometry.attributes ) {

					var attribute = geometry.attributes[ attributeName ];
					attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase();

					// JOINTS_0 must be UNSIGNED_BYTE or UNSIGNED_SHORT.
					var array = attribute.array;
					if ( attributeName === 'JOINTS_0' &&
						! ( array instanceof Uint16Array ) &&
						! ( array instanceof Uint8Array ) ) {

						console.warn( 'GLTFExporter: Attribute "skinIndex" converted to type UNSIGNED_SHORT.' );
						attribute = new THREE.BufferAttribute( new Uint16Array( array ), attribute.itemSize, attribute.normalized );

					}

					if ( attributeName.substr( 0, 5 ) !== 'MORPH' ) {

						var accessor = processAccessor( attribute, geometry );
						if ( accessor !== null ) {

							attributes[ attributeName ] = accessor;

						}

					}

				}

				if ( originalNormal !== undefined ) geometry.addAttribute( 'normal', originalNormal );

				// Skip if no exportable attributes found
				if ( Object.keys( attributes ).length === 0 ) {

					return null;

				}

				// Morph targets
				if ( mesh.morphTargetInfluences !== undefined && mesh.morphTargetInfluences.length > 0 ) {

					var weights = [];
					var targetNames = [];
					var reverseDictionary = {};

					if ( mesh.morphTargetDictionary !== undefined ) {

						for ( var key in mesh.morphTargetDictionary ) {

							reverseDictionary[ mesh.morphTargetDictionary[ key ] ] = key;

						}

					}

					for ( var i = 0; i < mesh.morphTargetInfluences.length; ++ i ) {

						var target = {};

						var warned = false;

						for ( var attributeName in geometry.morphAttributes ) {

							// glTF 2.0 morph supports only POSITION/NORMAL/TANGENT.
							// Three.js doesn't support TANGENT yet.

							if ( attributeName !== 'position' && attributeName !== 'normal' ) {

								if ( ! warned ) {

									console.warn( 'GLTFExporter: Only POSITION and NORMAL morph are supported.' );
									warned = true;

								}

								continue;

							}

							var attribute = geometry.morphAttributes[ attributeName ][ i ];

							// Three.js morph attribute has absolute values while the one of glTF has relative values.
							//
							// glTF 2.0 Specification:
							// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#morph-targets

							var baseAttribute = geometry.attributes[ attributeName ];
							// Clones attribute not to override
							var relativeAttribute = attribute.clone();

							for ( var j = 0, jl = attribute.count; j < jl; j ++ ) {

								relativeAttribute.setXYZ(
									j,
									attribute.getX( j ) - baseAttribute.getX( j ),
									attribute.getY( j ) - baseAttribute.getY( j ),
									attribute.getZ( j ) - baseAttribute.getZ( j )
								);

							}

							target[ attributeName.toUpperCase() ] = processAccessor( relativeAttribute, geometry );

						}

						targets.push( target );

						weights.push( mesh.morphTargetInfluences[ i ] );
						if ( mesh.morphTargetDictionary !== undefined ) targetNames.push( reverseDictionary[ i ] );

					}

					gltfMesh.weights = weights;

					if ( targetNames.length > 0 ) {

						gltfMesh.extras = {};
						gltfMesh.extras.targetNames = targetNames;

					}

				}

				var extras = ( Object.keys( geometry.userData ).length > 0 ) ? serializeUserData( geometry ) : undefined;

				var forceIndices = options.forceIndices;
				var isMultiMaterial = Array.isArray( mesh.material );

				if ( isMultiMaterial && mesh.geometry.groups.length === 0 ) return null;

				if ( ! forceIndices && geometry.index === null && isMultiMaterial ) {

					// temporal workaround.
					console.warn( 'THREE.GLTFExporter: Creating index for non-indexed multi-material mesh.' );
					forceIndices = true;

				}

				var didForceIndices = false;

				if ( geometry.index === null && forceIndices ) {

					var indices = [];

					for ( var i = 0, il = geometry.attributes.position.count; i < il; i ++ ) {

						indices[ i ] = i;

					}

					geometry.setIndex( indices );

					didForceIndices = true;

				}

				var materials = isMultiMaterial ? mesh.material : [ mesh.material ];
				var groups = isMultiMaterial ? mesh.geometry.groups : [ { materialIndex: 0, start: undefined, count: undefined } ];

				for ( var i = 0, il = groups.length; i < il; i ++ ) {

					var primitive = {
						mode: mode,
						attributes: attributes,
					};

					if ( extras ) primitive.extras = extras;

					if ( targets.length > 0 ) primitive.targets = targets;

					if ( geometry.index !== null ) {

						primitive.indices = processAccessor( geometry.index, geometry, groups[ i ].start, groups[ i ].count );

					}

					var material = processMaterial( materials[ groups[ i ].materialIndex ] );

					if ( material !== null ) {

						primitive.material = material;

					}

					primitives.push( primitive );

				}

				if ( didForceIndices ) {

					geometry.setIndex( null );

				}

				gltfMesh.primitives = primitives;

				if ( ! outputJSON.meshes ) {

					outputJSON.meshes = [];

				}

				outputJSON.meshes.push( gltfMesh );

				return outputJSON.meshes.length - 1;

			}

			/**
			 * Process camera
			 * @param  {THREE.Camera} camera Camera to process
			 * @return {Integer}      Index of the processed mesh in the "camera" array
			 */
			function processCamera( camera ) {

				if ( ! outputJSON.cameras ) {

					outputJSON.cameras = [];

				}

				var isOrtho = camera.isOrthographicCamera;

				var gltfCamera = {

					type: isOrtho ? 'orthographic' : 'perspective'

				};

				if ( isOrtho ) {

					gltfCamera.orthographic = {

						xmag: camera.right * 2,
						ymag: camera.top * 2,
						zfar: camera.far <= 0 ? 0.001 : camera.far,
						znear: camera.near < 0 ? 0 : camera.near

					};

				} else {

					gltfCamera.perspective = {

						aspectRatio: camera.aspect,
						yfov: THREE.Math.degToRad( camera.fov ) / camera.aspect,
						zfar: camera.far <= 0 ? 0.001 : camera.far,
						znear: camera.near < 0 ? 0 : camera.near

					};

				}

				if ( camera.name !== '' ) {

					gltfCamera.name = camera.type;

				}

				outputJSON.cameras.push( gltfCamera );

				return outputJSON.cameras.length - 1;

			}

			/**
			 * Creates glTF animation entry from AnimationClip object.
			 *
			 * Status:
			 * - Only properties listed in PATH_PROPERTIES may be animated.
			 *
			 * @param {THREE.AnimationClip} clip
			 * @param {THREE.Object3D} root
			 * @return {number}
			 */
			function processAnimation( clip, root ) {

				if ( ! outputJSON.animations ) {

					outputJSON.animations = [];

				}

				var channels = [];
				var samplers = [];

				for ( var i = 0; i < clip.tracks.length; ++ i ) {

					var track = clip.tracks[ i ];
					var trackBinding = THREE.PropertyBinding.parseTrackName( track.name );
					var trackNode = THREE.PropertyBinding.findNode( root, trackBinding.nodeName );
					var trackProperty = PATH_PROPERTIES[ trackBinding.propertyName ];

					if ( trackBinding.objectName === 'bones' ) {

						if ( trackNode.isSkinnedMesh === true ) {

							trackNode = trackNode.skeleton.getBoneByName( trackBinding.objectIndex );

						} else {

							trackNode = undefined;

						}

					}

					if ( ! trackNode || ! trackProperty ) {

						console.warn( 'THREE.GLTFExporter: Could not export animation track "%s".', track.name );
						return null;

					}

					var inputItemSize = 1;
					var outputItemSize = track.values.length / track.times.length;

					if ( trackProperty === PATH_PROPERTIES.morphTargetInfluences ) {

						outputItemSize /= trackNode.morphTargetInfluences.length;

					}

					var interpolation;

					// @TODO export CubicInterpolant(InterpolateSmooth) as CUBICSPLINE

					// Detecting glTF cubic spline interpolant by checking factory method's special property
					// GLTFCubicSplineInterpolant is a custom interpolant and track doesn't return
					// valid value from .getInterpolation().
					if ( track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline === true ) {

						interpolation = 'CUBICSPLINE';

						// itemSize of CUBICSPLINE keyframe is 9
						// (VEC3 * 3: inTangent, splineVertex, and outTangent)
						// but needs to be stored as VEC3 so dividing by 3 here.
						outputItemSize /= 3;

					} else if ( track.getInterpolation() === THREE.InterpolateDiscrete ) {

						interpolation = 'STEP';

					} else {

						interpolation = 'LINEAR';

					}

					samplers.push( {

						input: processAccessor( new THREE.BufferAttribute( track.times, inputItemSize ) ),
						output: processAccessor( new THREE.BufferAttribute( track.values, outputItemSize ) ),
						interpolation: interpolation

					} );

					channels.push( {

						sampler: samplers.length - 1,
						target: {
							node: nodeMap.get( trackNode ),
							path: trackProperty
						}

					} );

				}

				outputJSON.animations.push( {

					name: clip.name || 'clip_' + outputJSON.animations.length,
					samplers: samplers,
					channels: channels

				} );

				return outputJSON.animations.length - 1;

			}

			function processSkin( object ) {

				var node = outputJSON.nodes[ nodeMap.get( object ) ];

				var skeleton = object.skeleton;
				var rootJoint = object.skeleton.bones[ 0 ];

				if ( rootJoint === undefined ) return null;

				var joints = [];
				var inverseBindMatrices = new Float32Array( skeleton.bones.length * 16 );

				for ( var i = 0; i < skeleton.bones.length; ++ i ) {

					joints.push( nodeMap.get( skeleton.bones[ i ] ) );

					skeleton.boneInverses[ i ].toArray( inverseBindMatrices, i * 16 );

				}

				if ( outputJSON.skins === undefined ) {

					outputJSON.skins = [];

				}

				outputJSON.skins.push( {

					inverseBindMatrices: processAccessor( new THREE.BufferAttribute( inverseBindMatrices, 16 ) ),
					joints: joints,
					skeleton: nodeMap.get( rootJoint )

				} );

				var skinIndex = node.skin = outputJSON.skins.length - 1;

				return skinIndex;

			}

			/**
			 * Process Object3D node
			 * @param  {THREE.Object3D} node Object3D to processNode
			 * @return {Integer}      Index of the node in the nodes list
			 */
			function processNode( object ) {

				if ( object.isLight ) {

					console.warn( 'GLTFExporter: Unsupported node type:', object.constructor.name );
					return null;

				}

				if ( ! outputJSON.nodes ) {

					outputJSON.nodes = [];

				}

				var gltfNode = {};

				if ( options.trs ) {

					var rotation = object.quaternion.toArray();
					var position = object.position.toArray();
					var scale = object.scale.toArray();

					if ( ! equalArray( rotation, [ 0, 0, 0, 1 ] ) ) {

						gltfNode.rotation = rotation;

					}

					if ( ! equalArray( position, [ 0, 0, 0 ] ) ) {

						gltfNode.translation = position;

					}

					if ( ! equalArray( scale, [ 1, 1, 1 ] ) ) {

						gltfNode.scale = scale;

					}

				} else {

					object.updateMatrix();
					if ( ! equalArray( object.matrix.elements, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] ) ) {

						gltfNode.matrix = object.matrix.elements;

					}

				}

				// We don't export empty strings name because it represents no-name in Three.js.
				if ( object.name !== '' ) {

					gltfNode.name = String( object.name );

				}

				if ( object.userData && Object.keys( object.userData ).length > 0 ) {

					gltfNode.extras = serializeUserData( object );

				}

				if ( object.isMesh || object.isLine || object.isPoints ) {

					var mesh = processMesh( object );

					if ( mesh !== null ) {

						gltfNode.mesh = mesh;

					}

				} else if ( object.isCamera ) {

					gltfNode.camera = processCamera( object );

				}

				if ( object.isSkinnedMesh ) {

					skins.push( object );

				}

				if ( object.children.length > 0 ) {

					var children = [];

					for ( var i = 0, l = object.children.length; i < l; i ++ ) {

						var child = object.children[ i ];

						if ( child.visible || options.onlyVisible === false ) {

							var node = processNode( child );

							if ( node !== null ) {

								children.push( node );

							}

						}

					}

					if ( children.length > 0 ) {

						gltfNode.children = children;

					}


				}

				outputJSON.nodes.push( gltfNode );

				var nodeIndex = outputJSON.nodes.length - 1;
				nodeMap.set( object, nodeIndex );

				return nodeIndex;

			}

			/**
			 * Process Scene
			 * @param  {THREE.Scene} node Scene to process
			 */
			function processScene( scene ) {

				if ( ! outputJSON.scenes ) {

					outputJSON.scenes = [];
					outputJSON.scene = 0;

				}

				var gltfScene = {

					nodes: []

				};

				if ( scene.name !== '' ) {

					gltfScene.name = scene.name;

				}

				outputJSON.scenes.push( gltfScene );

				var nodes = [];

				for ( var i = 0, l = scene.children.length; i < l; i ++ ) {

					var child = scene.children[ i ];

					if ( child.visible || options.onlyVisible === false ) {

						var node = processNode( child );

						if ( node !== null ) {

							nodes.push( node );

						}

					}

				}

				if ( nodes.length > 0 ) {

					gltfScene.nodes = nodes;

				}

			}

			/**
			 * Creates a THREE.Scene to hold a list of objects and parse it
			 * @param  {Array} objects List of objects to process
			 */
			function processObjects( objects ) {

				var scene = new THREE.Scene();
				scene.name = 'AuxScene';

				for ( var i = 0; i < objects.length; i ++ ) {

					// We push directly to children instead of calling `add` to prevent
					// modify the .parent and break its original scene and hierarchy
					scene.children.push( objects[ i ] );

				}

				processScene( scene );

			}

			function processInput( input ) {

				input = input instanceof Array ? input : [ input ];

				var objectsWithoutScene = [];

				for ( var i = 0; i < input.length; i ++ ) {

					if ( input[ i ] instanceof THREE.Scene ) {

						processScene( input[ i ] );

					} else {

						objectsWithoutScene.push( input[ i ] );

					}

				}

				if ( objectsWithoutScene.length > 0 ) {

					processObjects( objectsWithoutScene );

				}

				for ( var i = 0; i < skins.length; ++ i ) {

					processSkin( skins[ i ] );

				}

				for ( var i = 0; i < options.animations.length; ++ i ) {

					processAnimation( options.animations[ i ], input[ 0 ] );

				}

			}

			processInput( input );

			Promise.all( pending ).then( function () {

				// Merge buffers.
				var blob = new Blob( buffers, { type: 'application/octet-stream' } );

				// Declare extensions.
				var extensionsUsedList = Object.keys( extensionsUsed );
				if ( extensionsUsedList.length > 0 ) outputJSON.extensionsUsed = extensionsUsedList;

				if ( outputJSON.buffers && outputJSON.buffers.length > 0 ) {

					// Update bytelength of the single buffer.
					outputJSON.buffers[ 0 ].byteLength = blob.size;

					var reader = new window.FileReader();

					if ( options.binary === true ) {

						// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification

						var GLB_HEADER_BYTES = 12;
						var GLB_HEADER_MAGIC = 0x46546C67;
						var GLB_VERSION = 2;

						var GLB_CHUNK_PREFIX_BYTES = 8;
						var GLB_CHUNK_TYPE_JSON = 0x4E4F534A;
						var GLB_CHUNK_TYPE_BIN = 0x004E4942;

						reader.readAsArrayBuffer( blob );
						reader.onloadend = function () {

							// Binary chunk.
							var binaryChunk = getPaddedArrayBuffer( reader.result );
							var binaryChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) );
							binaryChunkPrefix.setUint32( 0, binaryChunk.byteLength, true );
							binaryChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_BIN, true );

							// JSON chunk.
							var jsonChunk = getPaddedArrayBuffer( stringToArrayBuffer( JSON.stringify( outputJSON ) ), 0x20 );
							var jsonChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) );
							jsonChunkPrefix.setUint32( 0, jsonChunk.byteLength, true );
							jsonChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_JSON, true );

							// GLB header.
							var header = new ArrayBuffer( GLB_HEADER_BYTES );
							var headerView = new DataView( header );
							headerView.setUint32( 0, GLB_HEADER_MAGIC, true );
							headerView.setUint32( 4, GLB_VERSION, true );
							var totalByteLength = GLB_HEADER_BYTES
								+ jsonChunkPrefix.byteLength + jsonChunk.byteLength
								+ binaryChunkPrefix.byteLength + binaryChunk.byteLength;
							headerView.setUint32( 8, totalByteLength, true );

							var glbBlob = new Blob( [
								header,
								jsonChunkPrefix,
								jsonChunk,
								binaryChunkPrefix,
								binaryChunk
							], { type: 'application/octet-stream' } );

							var glbReader = new window.FileReader();
							glbReader.readAsArrayBuffer( glbBlob );
							glbReader.onloadend = function () {

								onDone( glbReader.result );

							};

						};

					} else {

						reader.readAsDataURL( blob );
						reader.onloadend = function () {

							var base64data = reader.result;
							outputJSON.buffers[ 0 ].uri = base64data;
							onDone( outputJSON );

						};

					}

				} else {

					onDone( outputJSON );

				}

			} );

		}

	};

	/**
	 * @author takahiro / http://github.com/takahirox
	 *
	 * Dependencies
	 *  - mmd-parser https://github.com/takahirox/mmd-parser
	 */

	THREE.MMDExporter = function () {

		// Unicode to Shift_JIS table
		var u2sTable;

		function unicodeToShiftjis( str ) {

			if ( u2sTable === undefined ) {

				var encoder = new MMDParser.CharsetEncoder();
				var table = encoder.s2uTable;
				u2sTable = {};

				var keys = Object.keys( table );

				for ( var i = 0, il = keys.length; i < il; i ++ ) {

					var key = keys[ i ];

					var value = table[ key ];
					key = parseInt( key );

					u2sTable[ value ] = key;

				}

			}

			var array = [];

			for ( var i = 0, il = str.length; i < il; i ++ ) {

				var code = str.charCodeAt( i );

				var value = u2sTable[ code ];

				if ( value === undefined ) {

					throw 'cannot convert charcode 0x' + code.toString( 16 );

				} else if ( value > 0xff ) {

					array.push( ( value >> 8 ) & 0xff );
					array.push( value & 0xff );

				} else {

					array.push( value & 0xff );

				}

			}

			return new Uint8Array( array );

		}

		function getBindBones( skin ) {

			// any more efficient ways?
			var poseSkin = skin.clone();
			poseSkin.pose();
			return poseSkin.skeleton.bones;

		}

		/* TODO: implement
		// mesh -> pmd
		this.parsePmd = function ( object ) {

		};
		*/

		/* TODO: implement
		// mesh -> pmx
		this.parsePmx = function ( object ) {

		};
		*/

		/*
		 * skeleton -> vpd
		 * Returns Shift_JIS encoded Uint8Array. Otherwise return strings.
		 */
		this.parseVpd = function ( skin, outputShiftJis, useOriginalBones ) {

			if ( skin.isSkinnedMesh !== true ) {

				console.warn( 'THREE.MMDExporter: parseVpd() requires SkinnedMesh instance.' );
				return null;

			}

			function toStringsFromNumber( num ) {

				if ( Math.abs( num ) < 1e-6 ) num = 0;

				var a = num.toString();

				if ( a.indexOf( '.' ) === - 1 ) {

					a += '.';

				}

				a += '000000';

				var index = a.indexOf( '.' );

				var d = a.slice( 0, index );
				var p = a.slice( index + 1, index + 7 );

				return d + '.' + p;

			}

			function toStringsFromArray( array ) {

				var a = [];

				for ( var i = 0, il = array.length; i < il; i ++ ) {

					a.push( toStringsFromNumber( array[ i ] ) );

				}

				return a.join( ',' );

			}

			skin.updateMatrixWorld( true );

			var bones = skin.skeleton.bones;
			var bones2 = getBindBones( skin );

			var position = new THREE.Vector3();
			var quaternion = new THREE.Quaternion();
			var quaternion2 = new THREE.Quaternion();
			var matrix = new THREE.Matrix4();

			var array = [];
			array.push( 'Vocaloid Pose Data file' );
			array.push( '' );
			array.push( ( skin.name !== '' ? skin.name.replace( /\s/g, '_' ) : 'skin' ) + '.osm;' );
			array.push( bones.length + ';' );
			array.push( '' );

			for ( var i = 0, il = bones.length; i < il; i ++ ) {

				var bone = bones[ i ];
				var bone2 = bones2[ i ];

				/*
				 * use the bone matrix saved before solving IK.
				 * see CCDIKSolver for the detail.
				 */
				if ( useOriginalBones === true &&
					bone.userData.ik !== undefined &&
					bone.userData.ik.originalMatrix !== undefined ) {

					matrix.fromArray( bone.userData.ik.originalMatrix );

				} else {

					matrix.copy( bone.matrix );

				}

				position.setFromMatrixPosition( matrix );
				quaternion.setFromRotationMatrix( matrix );

				var pArray = position.sub( bone2.position ).toArray();
				var qArray = quaternion2.copy( bone2.quaternion ).conjugate().multiply( quaternion ).toArray();

				// right to left
				pArray[ 2 ] = - pArray[ 2 ];
				qArray[ 0 ] = - qArray[ 0 ];
				qArray[ 1 ] = - qArray[ 1 ];

				array.push( 'Bone' + i + '{' + bone.name );
				array.push( '  ' + toStringsFromArray( pArray ) + ';' );
				array.push( '  ' + toStringsFromArray( qArray ) + ';' );
				array.push( '}' );
				array.push( '' );

			}

			array.push( '' );

			var lines = array.join( '\n' );

			return ( outputShiftJis === true ) ? unicodeToShiftjis( lines ) : lines;

		};

		/* TODO: implement
		// animation + skeleton -> vmd
		this.parseVmd = function ( object ) {

		};
		*/

	};

	/**
	 * @author mrdoob / http://mrdoob.com/
	 */

	THREE.OBJExporter = function () {};

	THREE.OBJExporter.prototype = {

		constructor: THREE.OBJExporter,

		parse: function ( object ) {

			var output = '';

			var indexVertex = 0;
			var indexVertexUvs = 0;
			var indexNormals = 0;

			var vertex = new THREE.Vector3();
			var normal = new THREE.Vector3();
			var uv = new THREE.Vector2();

			var i, j, k, l, m, face = [];

			var parseMesh = function ( mesh ) {

				var nbVertex = 0;
				var nbNormals = 0;
				var nbVertexUvs = 0;

				var geometry = mesh.geometry;

				var normalMatrixWorld = new THREE.Matrix3();

				if ( geometry instanceof THREE.Geometry ) {

					geometry = new THREE.BufferGeometry().setFromObject( mesh );

				}

				if ( geometry instanceof THREE.BufferGeometry ) {

					// shortcuts
					var vertices = geometry.getAttribute( 'position' );
					var normals = geometry.getAttribute( 'normal' );
					var uvs = geometry.getAttribute( 'uv' );
					var indices = geometry.getIndex();

					// name of the mesh object
					output += 'o ' + mesh.name + '\n';

					// name of the mesh material
					if ( mesh.material && mesh.material.name ) {

						output += 'usemtl ' + mesh.material.name + '\n';

					}

					// vertices

					if ( vertices !== undefined ) {

						for ( i = 0, l = vertices.count; i < l; i ++, nbVertex ++ ) {

							vertex.x = vertices.getX( i );
							vertex.y = vertices.getY( i );
							vertex.z = vertices.getZ( i );

							// transfrom the vertex to world space
							vertex.applyMatrix4( mesh.matrixWorld );

							// transform the vertex to export format
							output += 'v ' + vertex.x + ' ' + vertex.y + ' ' + vertex.z + '\n';

						}

					}

					// uvs

					if ( uvs !== undefined ) {

						for ( i = 0, l = uvs.count; i < l; i ++, nbVertexUvs ++ ) {

							uv.x = uvs.getX( i );
							uv.y = uvs.getY( i );

							// transform the uv to export format
							output += 'vt ' + uv.x + ' ' + uv.y + '\n';

						}

					}

					// normals

					if ( normals !== undefined ) {

						normalMatrixWorld.getNormalMatrix( mesh.matrixWorld );

						for ( i = 0, l = normals.count; i < l; i ++, nbNormals ++ ) {

							normal.x = normals.getX( i );
							normal.y = normals.getY( i );
							normal.z = normals.getZ( i );

							// transfrom the normal to world space
							normal.applyMatrix3( normalMatrixWorld );

							// transform the normal to export format
							output += 'vn ' + normal.x + ' ' + normal.y + ' ' + normal.z + '\n';

						}

					}

					// faces

					if ( indices !== null ) {

						for ( i = 0, l = indices.count; i < l; i += 3 ) {

							for ( m = 0; m < 3; m ++ ) {

								j = indices.getX( i + m ) + 1;

								face[ m ] = ( indexVertex + j ) + ( normals || uvs ? '/' + ( uvs ? ( indexVertexUvs + j ) : '' ) + ( normals ? '/' + ( indexNormals + j ) : '' ) : '' );

							}

							// transform the face to export format
							output += 'f ' + face.join( ' ' ) + "\n";

						}

					} else {

						for ( i = 0, l = vertices.count; i < l; i += 3 ) {

							for ( m = 0; m < 3; m ++ ) {

								j = i + m + 1;

								face[ m ] = ( indexVertex + j ) + ( normals || uvs ? '/' + ( uvs ? ( indexVertexUvs + j ) : '' ) + ( normals ? '/' + ( indexNormals + j ) : '' ) : '' );

							}

							// transform the face to export format
							output += 'f ' + face.join( ' ' ) + "\n";

						}

					}

				} else {

					console.warn( 'THREE.OBJExporter.parseMesh(): geometry type unsupported', geometry );

				}

				// update index
				indexVertex += nbVertex;
				indexVertexUvs += nbVertexUvs;
				indexNormals += nbNormals;

			};

			var parseLine = function ( line ) {

				var nbVertex = 0;

				var geometry = line.geometry;
				var type = line.type;

				if ( geometry instanceof THREE.Geometry ) {

					geometry = new THREE.BufferGeometry().setFromObject( line );

				}

				if ( geometry instanceof THREE.BufferGeometry ) {

					// shortcuts
					var vertices = geometry.getAttribute( 'position' );

					// name of the line object
					output += 'o ' + line.name + '\n';

					if ( vertices !== undefined ) {

						for ( i = 0, l = vertices.count; i < l; i ++, nbVertex ++ ) {

							vertex.x = vertices.getX( i );
							vertex.y = vertices.getY( i );
							vertex.z = vertices.getZ( i );

							// transfrom the vertex to world space
							vertex.applyMatrix4( line.matrixWorld );

							// transform the vertex to export format
							output += 'v ' + vertex.x + ' ' + vertex.y + ' ' + vertex.z + '\n';

						}

					}

					if ( type === 'Line' ) {

						output += 'l ';

						for ( j = 1, l = vertices.count; j <= l; j ++ ) {

							output += ( indexVertex + j ) + ' ';

						}

						output += '\n';

					}

					if ( type === 'LineSegments' ) {

						for ( j = 1, k = j + 1, l = vertices.count; j < l; j += 2, k = j + 1 ) {

							output += 'l ' + ( indexVertex + j ) + ' ' + ( indexVertex + k ) + '\n';

						}

					}

				} else {

					console.warn( 'THREE.OBJExporter.parseLine(): geometry type unsupported', geometry );

				}

				// update index
				indexVertex += nbVertex;

			};

			object.traverse( function ( child ) {

				if ( child instanceof THREE.Mesh ) {

					parseMesh( child );

				}

				if ( child instanceof THREE.Line ) {

					parseLine( child );

				}

			} );

			return output;

		}

	};

	/**
	 * @author Garrett Johnson / http://gkjohnson.github.io/
	 * https://github.com/gkjohnson/ply-exporter-js
	 *
	 * Usage:
	 *  var exporter = new THREE.PLYExporter();
	 *
	 *  // second argument is a list of options
	 *  var data = exporter.parse( mesh, { binary: true, excludeAttributes: [ 'color' ] } );
	 *
	 * Format Definition:
	 *  http://paulbourke.net/dataformats/ply/
	 */

	THREE.PLYExporter = function () {};

	THREE.PLYExporter.prototype = {

		constructor: THREE.PLYExporter,

		parse: function ( object, options ) {

			// Iterate over the valid meshes in the object
			function traverseMeshes( cb ) {

				object.traverse( function ( child ) {

					if ( child.isMesh === true ) {

						var mesh = child;
						var geometry = mesh.geometry;

						if ( geometry.isGeometry === true ) {

							geometry = geomToBufferGeom.get( geometry );

						}

						if ( geometry.isBufferGeometry === true ) {

							if ( geometry.getAttribute( 'position' ) !== undefined ) {

								cb( mesh, geometry );

							}

						}

					}

				} );

			}

			// Default options
			var defaultOptions = {
				binary: false,
				excludeAttributes: [] // normal, uv, color, index
			};

			options = Object.assign( defaultOptions, options );

			var excludeAttributes = options.excludeAttributes;
			var geomToBufferGeom = new WeakMap();
			var includeNormals = false;
			var includeColors = false;
			var includeUVs = false;
			var includeIndices = true;

			// count the vertices, check which properties are used,
			// and cache the BufferGeometry
			var vertexCount = 0;
			var faceCount = 0;
			object.traverse( function ( child ) {

				if ( child.isMesh === true ) {

					var mesh = child;
					var geometry = mesh.geometry;

					if ( geometry.isGeometry === true ) {

						var bufferGeometry = geomToBufferGeom.get( geometry ) || new THREE.BufferGeometry().setFromObject( mesh );
						geomToBufferGeom.set( geometry, bufferGeometry );
						geometry = bufferGeometry;

					}

					if ( geometry.isBufferGeometry === true ) {

						var vertices = geometry.getAttribute( 'position' );
						var normals = geometry.getAttribute( 'normal' );
						var uvs = geometry.getAttribute( 'uv' );
						var colors = geometry.getAttribute( 'color' );
						var indices = geometry.getIndex();

						if ( vertices === undefined ) {

							return;

						}

						vertexCount += vertices.count;
						faceCount += indices ? indices.count / 3 : vertices.count / 3;

						if ( normals !== undefined ) includeNormals = true;

						if ( uvs !== undefined ) includeUVs = true;

						if ( colors !== undefined ) includeColors = true;

					}

				}

			} );

			includeNormals = includeNormals && excludeAttributes.indexOf( 'normal' ) === - 1;
			includeColors = includeColors && excludeAttributes.indexOf( 'color' ) === - 1;
			includeUVs = includeUVs && excludeAttributes.indexOf( 'uv' ) === - 1;
			includeIndices = includeIndices && excludeAttributes.indexOf( 'index' ) === - 1;


			if ( includeIndices && faceCount !== Math.floor( faceCount ) ) {

				// point cloud meshes will not have an index array and may not have a
				// number of vertices that is divisble by 3 (and therefore representable
				// as triangles)
				console.error(

					'PLYExporter: Failed to generate a valid PLY file with triangle indices because the ' +
					'number of indices is not divisible by 3.'

				);

				return null;

			}

			// get how many bytes will be needed to save out the faces
			// so we can use a minimal amount of memory / data
			var indexByteCount = 1;

			if ( vertexCount > 256 ) { // 2^8 bits

				indexByteCount = 2;

			}

			if ( vertexCount > 65536 ) { // 2^16 bits

				indexByteCount = 4;

			}


			var header =
				'ply\n' +
				`format ${ options.binary ? 'binary_big_endian' : 'ascii' } 1.0\n` +
				`element vertex ${vertexCount}\n` +

				// position
				'property float x\n' +
				'property float y\n' +
				'property float z\n';

			if ( includeNormals === true ) {

				// normal
				header +=
					'property float nx\n' +
					'property float ny\n' +
					'property float nz\n';

			}

			if ( includeUVs === true ) {

				// uvs
				header +=
					'property float s\n' +
					'property float t\n';

			}

			if ( includeColors === true ) {

				// colors
				header +=
					'property uchar red\n' +
					'property uchar green\n' +
					'property uchar blue\n';

			}

			if ( includeIndices === true ) {

				// faces
				header +=
					`element face ${faceCount}\n` +
					`property list uchar uint${ indexByteCount * 8 } vertex_index\n`;

			}

			header += 'end_header\n';


			// Generate attribute data
			var vertex = new THREE.Vector3();
			var normalMatrixWorld = new THREE.Matrix3();

			if ( options.binary === true ) {

				// Binary File Generation
				var headerBin = new TextEncoder().encode( header );

				// 3 position values at 4 bytes
				// 3 normal values at 4 bytes
				// 3 color channels with 1 byte
				// 2 uv values at 4 bytes
				var vertexListLength = vertexCount * ( 4 * 3 + ( includeNormals ? 4 * 3 : 0 ) + ( includeColors ? 3 : 0 ) + ( includeUVs ? 4 * 2 : 0 ) );

				// 1 byte shape desciptor
				// 3 vertex indices at ${indexByteCount} bytes
				var faceListLength = includeIndices ? faceCount * ( indexByteCount * 3 + 1 ) : 0;
				var output = new DataView( new ArrayBuffer( headerBin.length + vertexListLength + faceListLength ) );
				new Uint8Array( output.buffer ).set( headerBin, 0 );


				var vOffset = headerBin.length;
				var fOffset = headerBin.length + vertexListLength;
				var writtenVertices = 0;
				traverseMeshes( function ( mesh, geometry ) {

					var vertices = geometry.getAttribute( 'position' );
					var normals = geometry.getAttribute( 'normal' );
					var uvs = geometry.getAttribute( 'uv' );
					var colors = geometry.getAttribute( 'color' );
					var indices = geometry.getIndex();

					normalMatrixWorld.getNormalMatrix( mesh.matrixWorld );

					for ( var i = 0, l = vertices.count; i < l; i ++ ) {

						vertex.x = vertices.getX( i );
						vertex.y = vertices.getY( i );
						vertex.z = vertices.getZ( i );

						vertex.applyMatrix4( mesh.matrixWorld );


						// Position information
						output.setFloat32( vOffset, vertex.x );
						vOffset += 4;

						output.setFloat32( vOffset, vertex.y );
						vOffset += 4;

						output.setFloat32( vOffset, vertex.z );
						vOffset += 4;

						// Normal information
						if ( includeNormals === true ) {

							if ( normals != null ) {

								vertex.x = normals.getX( i );
								vertex.y = normals.getY( i );
								vertex.z = normals.getZ( i );

								vertex.applyMatrix3( normalMatrixWorld );

								output.setFloat32( vOffset, vertex.x );
								vOffset += 4;

								output.setFloat32( vOffset, vertex.y );
								vOffset += 4;

								output.setFloat32( vOffset, vertex.z );
								vOffset += 4;

							} else {

								output.setFloat32( vOffset, 0 );
								vOffset += 4;

								output.setFloat32( vOffset, 0 );
								vOffset += 4;

								output.setFloat32( vOffset, 0 );
								vOffset += 4;

							}

						}

						// UV information
						if ( includeUVs === true ) {

							if ( uvs != null ) {

								output.setFloat32( vOffset, uvs.getX( i ) );
								vOffset += 4;

								output.setFloat32( vOffset, uvs.getY( i ) );
								vOffset += 4;

							} else if ( includeUVs !== false ) {

								output.setFloat32( vOffset, 0 );
								vOffset += 4;

								output.setFloat32( vOffset, 0 );
								vOffset += 4;

							}

						}

						// Color information
						if ( includeColors === true ) {

							if ( colors != null ) {

								output.setUint8( vOffset, Math.floor( colors.getX( i ) * 255 ) );
								vOffset += 1;

								output.setUint8( vOffset, Math.floor( colors.getY( i ) * 255 ) );
								vOffset += 1;

								output.setUint8( vOffset, Math.floor( colors.getZ( i ) * 255 ) );
								vOffset += 1;

							} else {

								output.setUint8( vOffset, 255 );
								vOffset += 1;

								output.setUint8( vOffset, 255 );
								vOffset += 1;

								output.setUint8( vOffset, 255 );
								vOffset += 1;

							}

						}

					}

					if ( includeIndices === true ) {

						// Create the face list
						var faceIndexFunc = `setUint${indexByteCount * 8}`;
						if ( indices !== null ) {

							for ( var i = 0, l = indices.count; i < l; i += 3 ) {

								output.setUint8( fOffset, 3 );
								fOffset += 1;

								output[ faceIndexFunc ]( fOffset, indices.getX( i + 0 ) + writtenVertices );
								fOffset += indexByteCount;

								output[ faceIndexFunc ]( fOffset, indices.getX( i + 1 ) + writtenVertices );
								fOffset += indexByteCount;

								output[ faceIndexFunc ]( fOffset, indices.getX( i + 2 ) + writtenVertices );
								fOffset += indexByteCount;

							}

						} else {

							for ( var i = 0, l = vertices.count; i < l; i += 3 ) {

								output.setUint8( fOffset, 3 );
								fOffset += 1;

								output[ faceIndexFunc ]( fOffset, writtenVertices + i );
								fOffset += indexByteCount;

								output[ faceIndexFunc ]( fOffset, writtenVertices + i + 1 );
								fOffset += indexByteCount;

								output[ faceIndexFunc ]( fOffset, writtenVertices + i + 2 );
								fOffset += indexByteCount;

							}

						}

					}


					// Save the amount of verts we've already written so we can offset
					// the face index on the next mesh
					writtenVertices += vertices.count;

				} );

				return output;

			} else {

				// Ascii File Generation
				// count the number of vertices
				var writtenVertices = 0;
				var vertexList = '';
				var faceList = '';

				traverseMeshes( function ( mesh, geometry ) {

					var vertices = geometry.getAttribute( 'position' );
					var normals = geometry.getAttribute( 'normal' );
					var uvs = geometry.getAttribute( 'uv' );
					var colors = geometry.getAttribute( 'color' );
					var indices = geometry.getIndex();

					normalMatrixWorld.getNormalMatrix( mesh.matrixWorld );

					// form each line
					for ( var i = 0, l = vertices.count; i < l; i ++ ) {

						vertex.x = vertices.getX( i );
						vertex.y = vertices.getY( i );
						vertex.z = vertices.getZ( i );

						vertex.applyMatrix4( mesh.matrixWorld );


						// Position information
						var line =
							vertex.x + ' ' +
							vertex.y + ' ' +
							vertex.z;

						// Normal information
						if ( includeNormals === true ) {

							if ( normals != null ) {

								vertex.x = normals.getX( i );
								vertex.y = normals.getY( i );
								vertex.z = normals.getZ( i );

								vertex.applyMatrix3( normalMatrixWorld );

								line += ' ' +
									vertex.x + ' ' +
									vertex.y + ' ' +
									vertex.z;

							} else {

								line += ' 0 0 0';

							}

						}

						// UV information
						if ( includeUVs === true ) {

							if ( uvs != null ) {

								line += ' ' +
									uvs.getX( i ) + ' ' +
									uvs.getY( i );

							} else if ( includeUVs !== false ) {

								line += ' 0 0';

							}

						}

						// Color information
						if ( includeColors === true ) {

							if ( colors != null ) {

								line += ' ' +
									Math.floor( colors.getX( i ) * 255 ) + ' ' +
									Math.floor( colors.getY( i ) * 255 ) + ' ' +
									Math.floor( colors.getZ( i ) * 255 );

							} else {

								line += ' 255 255 255';

							}

						}

						vertexList += line + '\n';

					}

					// Create the face list
					if ( includeIndices === true ) {

						if ( indices !== null ) {

							for ( var i = 0, l = indices.count; i < l; i += 3 ) {

								faceList += `3 ${ indices.getX( i + 0 ) + writtenVertices }`;
								faceList += ` ${ indices.getX( i + 1 ) + writtenVertices }`;
								faceList += ` ${ indices.getX( i + 2 ) + writtenVertices }\n`;

							}

						} else {

							for ( var i = 0, l = vertices.count; i < l; i += 3 ) {

								faceList += `3 ${ writtenVertices + i } ${ writtenVertices + i + 1 } ${ writtenVertices + i + 2 }\n`;

							}

						}

						faceCount += indices ? indices.count / 3 : vertices.count / 3;

					}

					writtenVertices += vertices.count;

				} );

				return `${ header }${vertexList}\n${ includeIndices ? `${faceList}\n` : '' }`;

			}

		}

	};

	/**
	 * @author kovacsv / http://kovacsv.hu/
	 * @author mrdoob / http://mrdoob.com/
	 * @author mudcube / http://mudcu.be/
	 */

	THREE.STLBinaryExporter = function () {};

	THREE.STLBinaryExporter.prototype = {

		constructor: THREE.STLBinaryExporter,

		parse: ( function () {

			var vector = new THREE.Vector3();
			var normalMatrixWorld = new THREE.Matrix3();

			return function parse( scene ) {

				// We collect objects first, as we may need to convert from BufferGeometry to Geometry
				var objects = [];
				var triangles = 0;
				scene.traverse( function ( object ) {

					if ( ! ( object instanceof THREE.Mesh ) ) return;

					var geometry = object.geometry;
					if ( geometry instanceof THREE.BufferGeometry ) {

						geometry = new THREE.Geometry().fromBufferGeometry( geometry );

					}

					if ( ! ( geometry instanceof THREE.Geometry ) ) return;
					triangles += geometry.faces.length;

					objects.push( {

						geometry: geometry,
						matrix: object.matrixWorld

					} );

				} );

				var offset = 80; // skip header
				var bufferLength = triangles * 2 + triangles * 3 * 4 * 4 + 80 + 4;
				var arrayBuffer = new ArrayBuffer( bufferLength );
				var output = new DataView( arrayBuffer );
				output.setUint32( offset, triangles, true ); offset += 4;

				// Traversing our collected objects
				objects.forEach( function ( object ) {

					var vertices = object.geometry.vertices;
					var faces = object.geometry.faces;

					normalMatrixWorld.getNormalMatrix( object.matrix );

					for ( var i = 0, l = faces.length; i < l; i ++ ) {

						var face = faces[ i ];

						vector.copy( face.normal ).applyMatrix3( normalMatrixWorld ).normalize();

						output.setFloat32( offset, vector.x, true ); offset += 4; // normal
						output.setFloat32( offset, vector.y, true ); offset += 4;
						output.setFloat32( offset, vector.z, true ); offset += 4;

						var indices = [ face.a, face.b, face.c ];

						for ( var j = 0; j < 3; j ++ ) {

							vector.copy( vertices[ indices[ j ] ] ).applyMatrix4( object.matrix );

							output.setFloat32( offset, vector.x, true ); offset += 4; // vertices
							output.setFloat32( offset, vector.y, true ); offset += 4;
							output.setFloat32( offset, vector.z, true ); offset += 4;

						}

						output.setUint16( offset, 0, true ); offset += 2; // attribute byte count

					}

				} );

				return output;

			};

		}() )

	};

	/**
	 * @author kovacsv / http://kovacsv.hu/
	 * @author mrdoob / http://mrdoob.com/
	 * @author mudcube / http://mudcu.be/
	 * @author Mugen87 / https://github.com/Mugen87
	 *
	 * Usage:
	 *  var exporter = new THREE.STLExporter();
	 *
	 *  // second argument is a list of options
	 *  var data = exporter.parse( mesh, { binary: true } );
	 *
	 */

	THREE.STLExporter = function () {};

	THREE.STLExporter.prototype = {

		constructor: THREE.STLExporter,

		parse: ( function () {

			var vector = new THREE.Vector3();
			var normalMatrixWorld = new THREE.Matrix3();

			return function parse( scene, options ) {

				if ( options === undefined ) options = {};

				var binary = options.binary !== undefined ? options.binary : false;

				//

				var objects = [];
				var triangles = 0;

				scene.traverse( function ( object ) {

					if ( object.isMesh ) {

						var geometry = object.geometry;

						if ( geometry.isBufferGeometry ) {

							geometry = new THREE.Geometry().fromBufferGeometry( geometry );

						}

						if ( geometry.isGeometry ) {

							triangles += geometry.faces.length;

							objects.push( {

								geometry: geometry,
								matrixWorld: object.matrixWorld

							} );

						}

					}

				} );

				if ( binary ) {

					var offset = 80; // skip header
					var bufferLength = triangles * 2 + triangles * 3 * 4 * 4 + 80 + 4;
					var arrayBuffer = new ArrayBuffer( bufferLength );
					var output = new DataView( arrayBuffer );
					output.setUint32( offset, triangles, true ); offset += 4;

					for ( var i = 0, il = objects.length; i < il; i ++ ) {

						var object = objects[ i ];

						var vertices = object.geometry.vertices;
						var faces = object.geometry.faces;
						var matrixWorld = object.matrixWorld;

						normalMatrixWorld.getNormalMatrix( matrixWorld );

						for ( var j = 0, jl = faces.length; j < jl; j ++ ) {

							var face = faces[ j ];

							vector.copy( face.normal ).applyMatrix3( normalMatrixWorld ).normalize();

							output.setFloat32( offset, vector.x, true ); offset += 4; // normal
							output.setFloat32( offset, vector.y, true ); offset += 4;
							output.setFloat32( offset, vector.z, true ); offset += 4;

							var indices = [ face.a, face.b, face.c ];

							for ( var k = 0; k < 3; k ++ ) {

								vector.copy( vertices[ indices[ k ] ] ).applyMatrix4( matrixWorld );

								output.setFloat32( offset, vector.x, true ); offset += 4; // vertices
								output.setFloat32( offset, vector.y, true ); offset += 4;
								output.setFloat32( offset, vector.z, true ); offset += 4;

							}

							output.setUint16( offset, 0, true ); offset += 2; // attribute byte count

						}

					}

					return output;

				} else {

					var output = '';

					output += 'solid exported\n';

					for ( var i = 0, il = objects.length; i < il; i ++ ) {

						var object = objects[ i ];

						var vertices = object.geometry.vertices;
						var faces = object.geometry.faces;
						var matrixWorld = object.matrixWorld;

						normalMatrixWorld.getNormalMatrix( matrixWorld );

						for ( var j = 0, jl = faces.length; j < jl; j ++ ) {

							var face = faces[ j ];

							vector.copy( face.normal ).applyMatrix3( normalMatrixWorld ).normalize();

							output += '\tfacet normal ' + vector.x + ' ' + vector.y + ' ' + vector.z + '\n';
							output += '\t\touter loop\n';

							var indices = [ face.a, face.b, face.c ];

							for ( var k = 0; k < 3; k ++ ) {

								vector.copy( vertices[ indices[ k ] ] ).applyMatrix4( matrixWorld );

								output += '\t\t\tvertex ' + vector.x + ' ' + vector.y + ' ' + vector.z + '\n';

							}

							output += '\t\tendloop\n';
							output += '\tendfacet\n';

						}

					}

					output += 'endsolid exported\n';

					return output;

				}

			};

		}() )

	};

	/**
	 * @author alteredq / http://alteredqualia.com/
	 */

	THREE.SceneLoader = function (manager) {

		this.onLoadStart = function () { };
		this.onLoadProgress = function () { };
		this.onLoadComplete = function () { };

		this.callbackSync = function () { };
		this.callbackProgress = function () { };

		this.geometryHandlers = {};
		this.hierarchyHandlers = {};

		this.addGeometryHandler("ascii", THREE.JSONLoader);

		this.manager = (manager !== undefined) ? manager : THREE.DefaultLoadingManager;

	};

	THREE.SceneLoader.prototype = {

		constructor: THREE.SceneLoader,

		load: function (url, onLoad, onProgress, onError) {

			var scope = this;

			var loader = new THREE.FileLoader(scope.manager);
			loader.load(url, function (text) {

				scope.parse(JSON.parse(text), onLoad, url);

			}, onProgress, onError);

		},

		addGeometryHandler: function (typeID, loaderClass) {

			this.geometryHandlers[typeID] = { "loaderClass": loaderClass };

		},

		addHierarchyHandler: function (typeID, loaderClass) {

			this.hierarchyHandlers[typeID] = { "loaderClass": loaderClass };

		},

		parse: function (json, callbackFinished, url) {

			var scope = this;

			var urlBase = THREE.Loader.prototype.extractUrlBase(url);

			var geometry, material, camera, fog,
				texture, color,
				light, counter_models, counter_textures,
				total_models, total_textures,
				result;

			var target_array = [];

			var data = json;

			// async geometry loaders

			for (var typeID in this.geometryHandlers) {

				var loaderClass = this.geometryHandlers[typeID]["loaderClass"];
				this.geometryHandlers[typeID]["loaderObject"] = new loaderClass();

			}

			// async hierachy loaders

			for (var typeID in this.hierarchyHandlers) {

				var loaderClass = this.hierarchyHandlers[typeID]["loaderClass"];
				this.hierarchyHandlers[typeID]["loaderObject"] = new loaderClass();

			}

			counter_models = 0;
			counter_textures = 0;

			result = {

				scene: new THREE.Scene(),
				geometries: {},
				face_materials: {},
				materials: {},
				textures: {},
				objects: {},
				cameras: {},
				lights: {},
				fogs: {},
				empties: {},
				groups: {}

			};

			if (data.transform) {

				var position = data.transform.position,
					rotation = data.transform.rotation,
					scale = data.transform.scale;

				if (position) {

					result.scene.position.fromArray(position);

				}

				if (rotation) {

					result.scene.rotation.fromArray(rotation);

				}

				if (scale) {

					result.scene.scale.fromArray(scale);

				}

				if (position || rotation || scale) {

					result.scene.updateMatrix();
					result.scene.updateMatrixWorld();

				}

			}

			function get_url(source_url, url_type) {

				if (url_type == "relativeToHTML") {

					return source_url;

				} else {

					return urlBase + source_url;

				}

			}

			// toplevel loader function, delegates to handle_children

			function handle_objects() {

				handle_children(result.scene, data.objects);

			}

			// handle all the children from the loaded json and attach them to given parent

			function handle_children(parent, children) {

				var mat, pos, rot, scl, quat;

				for (var objID in children) {

					// check by id if child has already been handled,
					// if not, create new object

					var object = result.objects[objID];
					var objJSON = children[objID];

					if (object === undefined) {

						// meshes

						if (objJSON.type && (objJSON.type in scope.hierarchyHandlers)) {

							if (objJSON.loading === undefined) {

								material = result.materials[objJSON.material];

								objJSON.loading = true;

								var loader = scope.hierarchyHandlers[objJSON.type]["loaderObject"];

								// ColladaLoader

								if (loader.options) {

									loader.load(get_url(objJSON.url, data.urlBaseType), create_callback_hierachy(objID, parent, material, objJSON));

									// UTF8Loader
									// OBJLoader

								} else {

									loader.load(get_url(objJSON.url, data.urlBaseType), create_callback_hierachy(objID, parent, material, objJSON));

								}

							}

						} else if (objJSON.geometry !== undefined) {

							geometry = result.geometries[objJSON.geometry];

							// geometry already loaded

							if (geometry) {

								material = result.materials[objJSON.material];

								pos = objJSON.position;
								rot = objJSON.rotation;
								scl = objJSON.scale;
								mat = objJSON.matrix;
								quat = objJSON.quaternion;

								// use materials from the model file
								// if there is no material specified in the object

								if (!objJSON.material) {

									material = new THREE.MultiMaterial(result.face_materials[objJSON.geometry]);

								}

								// use materials from the model file
								// if there is just empty face material
								// (must create new material as each model has its own face material)

								if ((material instanceof THREE.MultiMaterial) && material.materials.length === 0) {

									material = new THREE.MultiMaterial(result.face_materials[objJSON.geometry]);

								}

								if (objJSON.skin) {

									object = new THREE.SkinnedMesh(geometry, material);

								} else if (objJSON.morph) {

									object = new THREE.MorphAnimMesh(geometry, material);

									if (objJSON.duration !== undefined) {

										object.duration = objJSON.duration;

									}

									if (objJSON.time !== undefined) {

										object.time = objJSON.time;

									}

									if (objJSON.mirroredLoop !== undefined) {

										object.mirroredLoop = objJSON.mirroredLoop;

									}

									if (material.morphNormals) {

										geometry.computeMorphNormals();

									}

								} else {

									object = new THREE.Mesh(geometry, material);

								}

								object.name = objID;

								if (mat) {

									object.matrixAutoUpdate = false;
									object.matrix.set(
										mat[0], mat[1], mat[2], mat[3],
										mat[4], mat[5], mat[6], mat[7],
										mat[8], mat[9], mat[10], mat[11],
										mat[12], mat[13], mat[14], mat[15]
									);

								} else {

									object.position.fromArray(pos);

									if (quat) {

										object.quaternion.fromArray(quat);

									} else {

										object.rotation.fromArray(rot);

									}

									object.scale.fromArray(scl);

								}

								object.visible = objJSON.visible;
								object.castShadow = objJSON.castShadow;
								object.receiveShadow = objJSON.receiveShadow;

								parent.add(object);

								result.objects[objID] = object;

							}

							// lights

						} else if (objJSON.type === "AmbientLight" || objJSON.type === "PointLight" ||
							objJSON.type === "DirectionalLight" || objJSON.type === "SpotLight" ||
							objJSON.type === "HemisphereLight") {

							var color = objJSON.color;
							var intensity = objJSON.intensity;
							var distance = objJSON.distance;
							var position = objJSON.position;
							var rotation = objJSON.rotation;

							switch (objJSON.type) {

								case 'AmbientLight':
									light = new THREE.AmbientLight(color);
									break;

								case 'PointLight':
									light = new THREE.PointLight(color, intensity, distance);
									light.position.fromArray(position);
									break;

								case 'DirectionalLight':
									light = new THREE.DirectionalLight(color, intensity);
									light.position.fromArray(objJSON.direction);
									break;

								case 'SpotLight':
									light = new THREE.SpotLight(color, intensity, distance);
									light.angle = objJSON.angle;
									light.position.fromArray(position);
									light.target.set(position[0], position[1] - distance, position[2]);
									light.target.applyEuler(new THREE.Euler(rotation[0], rotation[1], rotation[2], 'XYZ'));
									break;

								case 'HemisphereLight':
									light = new THREE.DirectionalLight(color, intensity, distance);
									light.target.set(position[0], position[1] - distance, position[2]);
									light.target.applyEuler(new THREE.Euler(rotation[0], rotation[1], rotation[2], 'XYZ'));
									break;

							}

							parent.add(light);

							light.name = objID;
							result.lights[objID] = light;
							result.objects[objID] = light;

							// cameras

						} else if (objJSON.type === "PerspectiveCamera" || objJSON.type === "OrthographicCamera") {

							pos = objJSON.position;
							rot = objJSON.rotation;
							quat = objJSON.quaternion;

							if (objJSON.type === "PerspectiveCamera") {

								camera = new THREE.PerspectiveCamera(objJSON.fov, objJSON.aspect, objJSON.near, objJSON.far);

							} else if (objJSON.type === "OrthographicCamera") {

								camera = new THREE.OrthographicCamera(objJSON.left, objJSON.right, objJSON.top, objJSON.bottom, objJSON.near, objJSON.far);

							}

							camera.name = objID;
							camera.position.fromArray(pos);

							if (quat !== undefined) {

								camera.quaternion.fromArray(quat);

							} else if (rot !== undefined) {

								camera.rotation.fromArray(rot);

							} else if (objJSON.target) {

								camera.lookAt(new THREE.Vector3().fromArray(objJSON.target));

							}

							parent.add(camera);

							result.cameras[objID] = camera;
							result.objects[objID] = camera;

							// pure Object3D

						} else {

							pos = objJSON.position;
							rot = objJSON.rotation;
							scl = objJSON.scale;
							quat = objJSON.quaternion;

							object = new THREE.Object3D();
							object.name = objID;
							object.position.fromArray(pos);

							if (quat) {

								object.quaternion.fromArray(quat);

							} else {

								object.rotation.fromArray(rot);

							}

							object.scale.fromArray(scl);
							object.visible = (objJSON.visible !== undefined) ? objJSON.visible : false;

							parent.add(object);

							result.objects[objID] = object;
							result.empties[objID] = object;

						}

						if (object) {

							if (objJSON.userData !== undefined) {

								for (var key in objJSON.userData) {

									var value = objJSON.userData[key];
									object.userData[key] = value;

								}

							}

							if (objJSON.groups !== undefined) {

								for (var i = 0; i < objJSON.groups.length; i++) {

									var groupID = objJSON.groups[i];

									if (result.groups[groupID] === undefined) {

										result.groups[groupID] = [];

									}

									result.groups[groupID].push(objID);

								}

							}

						}

					}

					if (object !== undefined && objJSON.children !== undefined) {

						handle_children(object, objJSON.children);

					}

				}

			}

			function handle_mesh(geo, mat, id) {

				result.geometries[id] = geo;
				result.face_materials[id] = mat;
				handle_objects();

			}

			function handle_hierarchy(node, id, parent, material, obj) {

				var p = obj.position;
				var r = obj.rotation;
				var q = obj.quaternion;
				var s = obj.scale;

				node.position.fromArray(p);

				if (q) {

					node.quaternion.fromArray(q);

				} else {

					node.rotation.fromArray(r);

				}

				node.scale.fromArray(s);

				// override children materials
				// if object material was specified in JSON explicitly

				if (material) {

					node.traverse(function (child) {

						child.material = material;

					});

				}

				// override children visibility
				// with root node visibility as specified in JSON

				var visible = (obj.visible !== undefined) ? obj.visible : true;

				node.traverse(function (child) {

					child.visible = visible;

				});

				parent.add(node);

				node.name = id;

				result.objects[id] = node;
				handle_objects();

			}

			function create_callback_geometry(id) {

				return function (geo, mat) {

					geo.name = id;

					handle_mesh(geo, mat, id);

					counter_models -= 1;

					scope.onLoadComplete();

					async_callback_gate();

				}

			}

			function create_callback_hierachy(id, parent, material, obj) {

				return function (event) {

					var result;

					// loaders which use EventDispatcher

					if (event.content) {

						result = event.content;

						// ColladaLoader

					} else if (event.dae) {

						result = event.scene;


						// UTF8Loader

					} else {

						result = event;

					}

					handle_hierarchy(result, id, parent, material, obj);

					counter_models -= 1;

					scope.onLoadComplete();

					async_callback_gate();

				}

			}

			function create_callback_embed(id) {

				return function (geo, mat) {

					geo.name = id;

					result.geometries[id] = geo;
					result.face_materials[id] = mat;

				}

			}

			function async_callback_gate() {

				var progress = {

					totalModels: total_models,
					totalTextures: total_textures,
					loadedModels: total_models - counter_models,
					loadedTextures: total_textures - counter_textures

				};

				scope.callbackProgress(progress, result);

				scope.onLoadProgress();

				if (counter_models === 0 && counter_textures === 0) {

					finalize();
					callbackFinished(result);

				}

			}

			function finalize() {

				// take care of targets which could be asynchronously loaded objects

				for (var i = 0; i < target_array.length; i++) {

					var ta = target_array[i];

					var target = result.objects[ta.targetName];

					if (target) {

						ta.object.target = target;

					} else {

						// if there was error and target of specified name doesn't exist in the scene file
						// create instead dummy target
						// (target must be added to scene explicitly as parent is already added)

						ta.object.target = new THREE.Object3D();
						result.scene.add(ta.object.target);

					}

					ta.object.target.userData.targetInverse = ta.object;

				}

			}

			var callbackTexture = function (count) {

				counter_textures -= count;
				async_callback_gate();

				scope.onLoadComplete();

			};

			// must use this instead of just directly calling callbackTexture
			// because of closure in the calling context loop

			var generateTextureCallback = function (count) {

				return function () {

					callbackTexture(count);

				};

			};

			function traverse_json_hierarchy(objJSON, callback) {

				callback(objJSON);

				if (objJSON.children !== undefined) {

					for (var objChildID in objJSON.children) {

						traverse_json_hierarchy(objJSON.children[objChildID], callback);

					}

				}

			}

			// first go synchronous elements

			// fogs

			var fogID, fogJSON;

			for (fogID in data.fogs) {

				fogJSON = data.fogs[fogID];

				if (fogJSON.type === "linear") {

					fog = new THREE.Fog(0x000000, fogJSON.near, fogJSON.far);

				} else if (fogJSON.type === "exp2") {

					fog = new THREE.FogExp2(0x000000, fogJSON.density);

				}

				color = fogJSON.color;
				fog.color.setRGB(color[0], color[1], color[2]);

				result.fogs[fogID] = fog;

			}

			// now come potentially asynchronous elements

			// geometries

			// count how many geometries will be loaded asynchronously

			var geoID, geoJSON;

			for (geoID in data.geometries) {

				geoJSON = data.geometries[geoID];

				if (geoJSON.type in this.geometryHandlers) {

					counter_models += 1;

					scope.onLoadStart();

				}

			}

			// count how many hierarchies will be loaded asynchronously

			for (var objID in data.objects) {

				traverse_json_hierarchy(data.objects[objID], function (objJSON) {

					if (objJSON.type && (objJSON.type in scope.hierarchyHandlers)) {

						counter_models += 1;

						scope.onLoadStart();

					}

				});

			}

			total_models = counter_models;

			for (geoID in data.geometries) {

				geoJSON = data.geometries[geoID];

				if (geoJSON.type === "cube") {

					geometry = new THREE.BoxGeometry(geoJSON.width, geoJSON.height, geoJSON.depth, geoJSON.widthSegments, geoJSON.heightSegments, geoJSON.depthSegments);
					geometry.name = geoID;
					result.geometries[geoID] = geometry;

				} else if (geoJSON.type === "plane") {

					geometry = new THREE.PlaneGeometry(geoJSON.width, geoJSON.height, geoJSON.widthSegments, geoJSON.heightSegments);
					geometry.name = geoID;
					result.geometries[geoID] = geometry;

				} else if (geoJSON.type === "sphere") {

					geometry = new THREE.SphereGeometry(geoJSON.radius, geoJSON.widthSegments, geoJSON.heightSegments);
					geometry.name = geoID;
					result.geometries[geoID] = geometry;

				} else if (geoJSON.type === "cylinder") {

					geometry = new THREE.CylinderGeometry(geoJSON.topRad, geoJSON.botRad, geoJSON.height, geoJSON.radSegs, geoJSON.heightSegs);
					geometry.name = geoID;
					result.geometries[geoID] = geometry;

				} else if (geoJSON.type === "torus") {

					geometry = new THREE.TorusGeometry(geoJSON.radius, geoJSON.tube, geoJSON.segmentsR, geoJSON.segmentsT);
					geometry.name = geoID;
					result.geometries[geoID] = geometry;

				} else if (geoJSON.type === "icosahedron") {

					geometry = new THREE.IcosahedronGeometry(geoJSON.radius, geoJSON.subdivisions);
					geometry.name = geoID;
					result.geometries[geoID] = geometry;

				} else if (geoJSON.type in this.geometryHandlers) {

					var loader = this.geometryHandlers[geoJSON.type]["loaderObject"];
					loader.load(get_url(geoJSON.url, data.urlBaseType), create_callback_geometry(geoID));

				} else if (geoJSON.type === "embedded") {

					var modelJson = data.embeds[geoJSON.id],
						texture_path = "";

					// pass metadata along to jsonLoader so it knows the format version

					modelJson.metadata = data.metadata;

					if (modelJson) {

						var jsonLoader = this.geometryHandlers["ascii"]["loaderObject"];
						var model = jsonLoader.parse(modelJson, texture_path);
						create_callback_embed(geoID)(model.geometry, model.materials);

					}

				}

			}

			// textures

			// count how many textures will be loaded asynchronously

			var textureID, textureJSON;

			for (textureID in data.textures) {

				textureJSON = data.textures[textureID];

				if (Array.isArray(textureJSON.url)) {

					counter_textures += textureJSON.url.length;

					for (var n = 0; n < textureJSON.url.length; n++) {

						scope.onLoadStart();

					}

				} else {

					counter_textures += 1;

					scope.onLoadStart();

				}

			}

			total_textures = counter_textures;

			for (textureID in data.textures) {

				textureJSON = data.textures[textureID];

				if (textureJSON.mapping !== undefined && THREE[textureJSON.mapping] !== undefined) {

					textureJSON.mapping = THREE[textureJSON.mapping];

				}

				var texture;

				if (Array.isArray(textureJSON.url)) {

					var count = textureJSON.url.length;
					var urls = [];

					for (var i = 0; i < count; i++) {

						urls[i] = get_url(textureJSON.url[i], data.urlBaseType);

					}

					var loader = THREE.Loader.Handlers.get(urls[0]);

					if (loader !== null) {

						texture = loader.load(urls, generateTextureCallback(count));

						if (textureJSON.mapping !== undefined)
							texture.mapping = textureJSON.mapping;

					} else {

						texture = new THREE.CubeTextureLoader().load(urls, generateTextureCallback(count));
						texture.mapping = textureJSON.mapping;

					}

				} else {

					var fullUrl = get_url(textureJSON.url, data.urlBaseType);
					var textureCallback = generateTextureCallback(1);

					var loader = THREE.Loader.Handlers.get(fullUrl);

					if (loader !== null) {

						texture = loader.load(fullUrl, textureCallback);

					} else {

						texture = new THREE.Texture();
						loader = new THREE.ImageLoader();

						(function (texture) {

							loader.load(fullUrl, function (image) {

								texture.image = image;
								texture.needsUpdate = true;

								textureCallback();

							});

						})(texture);


					}

					if (textureJSON.mapping !== undefined)
						texture.mapping = textureJSON.mapping;

					if (THREE[textureJSON.minFilter] !== undefined)
						texture.minFilter = THREE[textureJSON.minFilter];

					if (THREE[textureJSON.magFilter] !== undefined)
						texture.magFilter = THREE[textureJSON.magFilter];

					if (textureJSON.anisotropy) texture.anisotropy = textureJSON.anisotropy;

					if (textureJSON.repeat) {

						texture.repeat.set(textureJSON.repeat[0], textureJSON.repeat[1]);

						if (textureJSON.repeat[0] !== 1) texture.wrapS = THREE.RepeatWrapping;
						if (textureJSON.repeat[1] !== 1) texture.wrapT = THREE.RepeatWrapping;

					}

					if (textureJSON.offset) {

						texture.offset.set(textureJSON.offset[0], textureJSON.offset[1]);

					}

					// handle wrap after repeat so that default repeat can be overriden

					if (textureJSON.wrap) {

						var wrapMap = {
							"repeat": THREE.RepeatWrapping,
							"mirror": THREE.MirroredRepeatWrapping
						};

						if (wrapMap[textureJSON.wrap[0]] !== undefined) texture.wrapS = wrapMap[textureJSON.wrap[0]];
						if (wrapMap[textureJSON.wrap[1]] !== undefined) texture.wrapT = wrapMap[textureJSON.wrap[1]];

					}

				}

				result.textures[textureID] = texture;

			}

			// materials

			var matID, matJSON;
			var parID;

			for (matID in data.materials) {

				matJSON = data.materials[matID];

				for (parID in matJSON.parameters) {

					if (parID === "envMap" || parID === "map" || parID === "lightMap" || parID === "bumpMap" || parID === "normalMap" || parID === "alphaMap") {

						matJSON.parameters[parID] = result.textures[matJSON.parameters[parID]];

					} else if (parID === "shading") {

						matJSON.parameters[parID] = (matJSON.parameters[parID] === "flat") ? THREE.FlatShading : THREE.SmoothShading;

					} else if (parID === "side") {

						if (matJSON.parameters[parID] == "double") {

							matJSON.parameters[parID] = THREE.DoubleSide;

						} else if (matJSON.parameters[parID] == "back") {

							matJSON.parameters[parID] = THREE.BackSide;

						} else {

							matJSON.parameters[parID] = THREE.FrontSide;

						}

					} else if (parID === "blending") {

						matJSON.parameters[parID] = matJSON.parameters[parID] in THREE ? THREE[matJSON.parameters[parID]] : THREE.NormalBlending;

					} else if (parID === "combine") {

						matJSON.parameters[parID] = matJSON.parameters[parID] in THREE ? THREE[matJSON.parameters[parID]] : THREE.MultiplyOperation;

					} else if (parID === "vertexColors") {

						if (matJSON.parameters[parID] == "face") {

							matJSON.parameters[parID] = THREE.FaceColors;

							// default to vertex colors if "vertexColors" is anything else face colors or 0 / null / false

						} else if (matJSON.parameters[parID]) {

							matJSON.parameters[parID] = THREE.VertexColors;

						}

					} else if (parID === "wrapRGB") {

						var v3 = matJSON.parameters[parID];
						matJSON.parameters[parID] = new THREE.Vector3(v3[0], v3[1], v3[2]);

					} else if (parID === "normalScale") {

						var v2 = matJSON.parameters[parID];
						matJSON.parameters[parID] = new THREE.Vector2(v2[0], v2[1]);

					}

				}

				if (matJSON.parameters.opacity !== undefined && matJSON.parameters.opacity < 1.0) {

					matJSON.parameters.transparent = true;

				}

				material = new THREE[matJSON.type](matJSON.parameters);
				material.name = matID;

				result.materials[matID] = material;

			}

			// second pass through all materials to initialize MultiMaterials
			// that could be referring to other materials out of order

			for (matID in data.materials) {

				matJSON = data.materials[matID];

				if (matJSON.parameters.materials) {

					var materialArray = [];

					for (var i = 0; i < matJSON.parameters.materials.length; i++) {

						var label = matJSON.parameters.materials[i];
						materialArray.push(result.materials[label]);

					}

					result.materials[matID].materials = materialArray;

				}

			}

			// objects ( synchronous init of procedural primitives )

			handle_objects();

			// defaults

			if (result.cameras && data.defaults.camera) {

				result.currentCamera = result.cameras[data.defaults.camera];

			}

			if (result.fogs && data.defaults.fog) {

				result.scene.fog = result.fogs[data.defaults.fog];

			}

			// synchronous callback

			scope.callbackSync(result);

			// just in case there are no async elements

			async_callback_gate();

		}

	};

	/**
	 * @author mrdoob / http://mrdoob.com/
	 * @author supereggbert / http://www.paulbrunt.co.uk/
	 * @author julianwa / https://github.com/julianwa
	 */

	THREE.RenderableObject = function () {

		this.id = 0;

		this.object = null;
		this.z = 0;
		this.renderOrder = 0;

	};

	//

	THREE.RenderableFace = function () {

		this.id = 0;

		this.v1 = new THREE.RenderableVertex();
		this.v2 = new THREE.RenderableVertex();
		this.v3 = new THREE.RenderableVertex();

		this.normalModel = new THREE.Vector3();

		this.vertexNormalsModel = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
		this.vertexNormalsLength = 0;

		this.color = new THREE.Color();
		this.material = null;
		this.uvs = [ new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2() ];

		this.z = 0;
		this.renderOrder = 0;

	};

	//

	THREE.RenderableVertex = function () {

		this.position = new THREE.Vector3();
		this.positionWorld = new THREE.Vector3();
		this.positionScreen = new THREE.Vector4();

		this.visible = true;

	};

	THREE.RenderableVertex.prototype.copy = function ( vertex ) {

		this.positionWorld.copy( vertex.positionWorld );
		this.positionScreen.copy( vertex.positionScreen );

	};

	//

	THREE.RenderableLine = function () {

		this.id = 0;

		this.v1 = new THREE.RenderableVertex();
		this.v2 = new THREE.RenderableVertex();

		this.vertexColors = [ new THREE.Color(), new THREE.Color() ];
		this.material = null;

		this.z = 0;
		this.renderOrder = 0;

	};

	//

	THREE.RenderableSprite = function () {

		this.id = 0;

		this.object = null;

		this.x = 0;
		this.y = 0;
		this.z = 0;

		this.rotation = 0;
		this.scale = new THREE.Vector2();

		this.material = null;
		this.renderOrder = 0;

	};

	//

	THREE.Projector = function () {

		var _object, _objectCount, _objectPool = [], _objectPoolLength = 0,
			_vertex, _vertexCount, _vertexPool = [], _vertexPoolLength = 0,
			_face, _faceCount, _facePool = [], _facePoolLength = 0,
			_line, _lineCount, _linePool = [], _linePoolLength = 0,
			_sprite, _spriteCount, _spritePool = [], _spritePoolLength = 0,

			_renderData = { objects: [], lights: [], elements: [] },

			_vector3 = new THREE.Vector3(),
			_vector4 = new THREE.Vector4(),

			_clipBox = new THREE.Box3( new THREE.Vector3( - 1, - 1, - 1 ), new THREE.Vector3( 1, 1, 1 ) ),
			_boundingBox = new THREE.Box3(),
			_points3 = new Array( 3 ),

			_viewMatrix = new THREE.Matrix4(),
			_viewProjectionMatrix = new THREE.Matrix4(),

			_modelMatrix,
			_modelViewProjectionMatrix = new THREE.Matrix4(),

			_normalMatrix = new THREE.Matrix3(),

			_frustum = new THREE.Frustum(),

			_clippedVertex1PositionScreen = new THREE.Vector4(),
			_clippedVertex2PositionScreen = new THREE.Vector4();

		//

		this.projectVector = function ( vector, camera ) {

			console.warn( 'THREE.Projector: .projectVector() is now vector.project().' );
			vector.project( camera );

		};

		this.unprojectVector = function ( vector, camera ) {

			console.warn( 'THREE.Projector: .unprojectVector() is now vector.unproject().' );
			vector.unproject( camera );

		};

		this.pickingRay = function () {

			console.error( 'THREE.Projector: .pickingRay() is now raycaster.setFromCamera().' );

		};

		//

		var RenderList = function () {

			var normals = [];
			var colors = [];
			var uvs = [];

			var object = null;
			var material = null;

			var normalMatrix = new THREE.Matrix3();

			function setObject( value ) {

				object = value;
				material = object.material;

				normalMatrix.getNormalMatrix( object.matrixWorld );

				normals.length = 0;
				colors.length = 0;
				uvs.length = 0;

			}

			function projectVertex( vertex ) {

				var position = vertex.position;
				var positionWorld = vertex.positionWorld;
				var positionScreen = vertex.positionScreen;

				positionWorld.copy( position ).applyMatrix4( _modelMatrix );
				positionScreen.copy( positionWorld ).applyMatrix4( _viewProjectionMatrix );

				var invW = 1 / positionScreen.w;

				positionScreen.x *= invW;
				positionScreen.y *= invW;
				positionScreen.z *= invW;

				vertex.visible = positionScreen.x >= - 1 && positionScreen.x <= 1 &&
						 positionScreen.y >= - 1 && positionScreen.y <= 1 &&
						 positionScreen.z >= - 1 && positionScreen.z <= 1;

			}

			function pushVertex( x, y, z ) {

				_vertex = getNextVertexInPool();
				_vertex.position.set( x, y, z );

				projectVertex( _vertex );

			}

			function pushNormal( x, y, z ) {

				normals.push( x, y, z );

			}

			function pushColor( r, g, b ) {

				colors.push( r, g, b );

			}

			function pushUv( x, y ) {

				uvs.push( x, y );

			}

			function checkTriangleVisibility( v1, v2, v3 ) {

				if ( v1.visible === true || v2.visible === true || v3.visible === true ) return true;

				_points3[ 0 ] = v1.positionScreen;
				_points3[ 1 ] = v2.positionScreen;
				_points3[ 2 ] = v3.positionScreen;

				return _clipBox.intersectsBox( _boundingBox.setFromPoints( _points3 ) );

			}

			function checkBackfaceCulling( v1, v2, v3 ) {

				return ( ( v3.positionScreen.x - v1.positionScreen.x ) *
					    ( v2.positionScreen.y - v1.positionScreen.y ) -
					    ( v3.positionScreen.y - v1.positionScreen.y ) *
					    ( v2.positionScreen.x - v1.positionScreen.x ) ) < 0;

			}

			function pushLine( a, b ) {

				var v1 = _vertexPool[ a ];
				var v2 = _vertexPool[ b ];

				// Clip

				v1.positionScreen.copy( v1.position ).applyMatrix4( _modelViewProjectionMatrix );
				v2.positionScreen.copy( v2.position ).applyMatrix4( _modelViewProjectionMatrix );

				if ( clipLine( v1.positionScreen, v2.positionScreen ) === true ) {

					// Perform the perspective divide
					v1.positionScreen.multiplyScalar( 1 / v1.positionScreen.w );
					v2.positionScreen.multiplyScalar( 1 / v2.positionScreen.w );

					_line = getNextLineInPool();
					_line.id = object.id;
					_line.v1.copy( v1 );
					_line.v2.copy( v2 );
					_line.z = Math.max( v1.positionScreen.z, v2.positionScreen.z );
					_line.renderOrder = object.renderOrder;

					_line.material = object.material;

					if ( object.material.vertexColors === THREE.VertexColors ) {

						_line.vertexColors[ 0 ].fromArray( colors, a * 3 );
						_line.vertexColors[ 1 ].fromArray( colors, b * 3 );

					}

					_renderData.elements.push( _line );

				}

			}

			function pushTriangle( a, b, c ) {

				var v1 = _vertexPool[ a ];
				var v2 = _vertexPool[ b ];
				var v3 = _vertexPool[ c ];

				if ( checkTriangleVisibility( v1, v2, v3 ) === false ) return;

				if ( material.side === THREE.DoubleSide || checkBackfaceCulling( v1, v2, v3 ) === true ) {

					_face = getNextFaceInPool();

					_face.id = object.id;
					_face.v1.copy( v1 );
					_face.v2.copy( v2 );
					_face.v3.copy( v3 );
					_face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3;
					_face.renderOrder = object.renderOrder;

					// use first vertex normal as face normal

					_face.normalModel.fromArray( normals, a * 3 );
					_face.normalModel.applyMatrix3( normalMatrix ).normalize();

					for ( var i = 0; i < 3; i ++ ) {

						var normal = _face.vertexNormalsModel[ i ];
						normal.fromArray( normals, arguments[ i ] * 3 );
						normal.applyMatrix3( normalMatrix ).normalize();

						var uv = _face.uvs[ i ];
						uv.fromArray( uvs, arguments[ i ] * 2 );

					}

					_face.vertexNormalsLength = 3;

					_face.material = object.material;

					_renderData.elements.push( _face );

				}

			}

			return {
				setObject: setObject,
				projectVertex: projectVertex,
				checkTriangleVisibility: checkTriangleVisibility,
				checkBackfaceCulling: checkBackfaceCulling,
				pushVertex: pushVertex,
				pushNormal: pushNormal,
				pushColor: pushColor,
				pushUv: pushUv,
				pushLine: pushLine,
				pushTriangle: pushTriangle
			};

		};

		var renderList = new RenderList();

		function projectObject( object ) {

			if ( object.visible === false ) return;

			if ( object instanceof THREE.Light ) {

				_renderData.lights.push( object );

			} else if ( object instanceof THREE.Mesh || object instanceof THREE.Line || object instanceof THREE.Points ) {

				if ( object.material.visible === false ) return;
				if ( object.frustumCulled === true && _frustum.intersectsObject( object ) === false ) return;

				addObject( object );

			} else if ( object instanceof THREE.Sprite ) {

				if ( object.material.visible === false ) return;
				if ( object.frustumCulled === true && _frustum.intersectsSprite( object ) === false ) return;

				addObject( object );

			}

			var children = object.children;

			for ( var i = 0, l = children.length; i < l; i ++ ) {

				projectObject( children[ i ] );

			}

		}

		function addObject( object ) {

			_object = getNextObjectInPool();
			_object.id = object.id;
			_object.object = object;

			_vector3.setFromMatrixPosition( object.matrixWorld );
			_vector3.applyMatrix4( _viewProjectionMatrix );
			_object.z = _vector3.z;
			_object.renderOrder = object.renderOrder;

			_renderData.objects.push( _object );

		}

		this.projectScene = function ( scene, camera, sortObjects, sortElements ) {

			_faceCount = 0;
			_lineCount = 0;
			_spriteCount = 0;

			_renderData.elements.length = 0;

			if ( scene.autoUpdate === true ) scene.updateMatrixWorld();
			if ( camera.parent === null ) camera.updateMatrixWorld();

			_viewMatrix.copy( camera.matrixWorldInverse );
			_viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, _viewMatrix );

			_frustum.setFromMatrix( _viewProjectionMatrix );

			//

			_objectCount = 0;

			_renderData.objects.length = 0;
			_renderData.lights.length = 0;

			projectObject( scene );

			if ( sortObjects === true ) {

				_renderData.objects.sort( painterSort );

			}

			//

			var objects = _renderData.objects;

			for ( var o = 0, ol = objects.length; o < ol; o ++ ) {

				var object = objects[ o ].object;
				var geometry = object.geometry;

				renderList.setObject( object );

				_modelMatrix = object.matrixWorld;

				_vertexCount = 0;

				if ( object instanceof THREE.Mesh ) {

					if ( geometry instanceof THREE.BufferGeometry ) {

						var attributes = geometry.attributes;
						var groups = geometry.groups;

						if ( attributes.position === undefined ) continue;

						var positions = attributes.position.array;

						for ( var i = 0, l = positions.length; i < l; i += 3 ) {

							renderList.pushVertex( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] );

						}

						if ( attributes.normal !== undefined ) {

							var normals = attributes.normal.array;

							for ( var i = 0, l = normals.length; i < l; i += 3 ) {

								renderList.pushNormal( normals[ i ], normals[ i + 1 ], normals[ i + 2 ] );

							}

						}

						if ( attributes.uv !== undefined ) {

							var uvs = attributes.uv.array;

							for ( var i = 0, l = uvs.length; i < l; i += 2 ) {

								renderList.pushUv( uvs[ i ], uvs[ i + 1 ] );

							}

						}

						if ( geometry.index !== null ) {

							var indices = geometry.index.array;

							if ( groups.length > 0 ) {

								for ( var g = 0; g < groups.length; g ++ ) {

									var group = groups[ g ];

									for ( var i = group.start, l = group.start + group.count; i < l; i += 3 ) {

										renderList.pushTriangle( indices[ i ], indices[ i + 1 ], indices[ i + 2 ] );

									}

								}

							} else {

								for ( var i = 0, l = indices.length; i < l; i += 3 ) {

									renderList.pushTriangle( indices[ i ], indices[ i + 1 ], indices[ i + 2 ] );

								}

							}

						} else {

							for ( var i = 0, l = positions.length / 3; i < l; i += 3 ) {

								renderList.pushTriangle( i, i + 1, i + 2 );

							}

						}

					} else if ( geometry instanceof THREE.Geometry ) {

						var vertices = geometry.vertices;
						var faces = geometry.faces;
						var faceVertexUvs = geometry.faceVertexUvs[ 0 ];

						_normalMatrix.getNormalMatrix( _modelMatrix );

						var material = object.material;

						var isMultiMaterial = Array.isArray( material );

						for ( var v = 0, vl = vertices.length; v < vl; v ++ ) {

							var vertex = vertices[ v ];

							_vector3.copy( vertex );

							if ( material.morphTargets === true ) {

								var morphTargets = geometry.morphTargets;
								var morphInfluences = object.morphTargetInfluences;

								for ( var t = 0, tl = morphTargets.length; t < tl; t ++ ) {

									var influence = morphInfluences[ t ];

									if ( influence === 0 ) continue;

									var target = morphTargets[ t ];
									var targetVertex = target.vertices[ v ];

									_vector3.x += ( targetVertex.x - vertex.x ) * influence;
									_vector3.y += ( targetVertex.y - vertex.y ) * influence;
									_vector3.z += ( targetVertex.z - vertex.z ) * influence;

								}

							}

							renderList.pushVertex( _vector3.x, _vector3.y, _vector3.z );

						}

						for ( var f = 0, fl = faces.length; f < fl; f ++ ) {

							var face = faces[ f ];

							material = isMultiMaterial === true
								 ? object.material[ face.materialIndex ]
								 : object.material;

							if ( material === undefined ) continue;

							var side = material.side;

							var v1 = _vertexPool[ face.a ];
							var v2 = _vertexPool[ face.b ];
							var v3 = _vertexPool[ face.c ];

							if ( renderList.checkTriangleVisibility( v1, v2, v3 ) === false ) continue;

							var visible = renderList.checkBackfaceCulling( v1, v2, v3 );

							if ( side !== THREE.DoubleSide ) {

								if ( side === THREE.FrontSide && visible === false ) continue;
								if ( side === THREE.BackSide && visible === true ) continue;

							}

							_face = getNextFaceInPool();

							_face.id = object.id;
							_face.v1.copy( v1 );
							_face.v2.copy( v2 );
							_face.v3.copy( v3 );

							_face.normalModel.copy( face.normal );

							if ( visible === false && ( side === THREE.BackSide || side === THREE.DoubleSide ) ) {

								_face.normalModel.negate();

							}

							_face.normalModel.applyMatrix3( _normalMatrix ).normalize();

							var faceVertexNormals = face.vertexNormals;

							for ( var n = 0, nl = Math.min( faceVertexNormals.length, 3 ); n < nl; n ++ ) {

								var normalModel = _face.vertexNormalsModel[ n ];
								normalModel.copy( faceVertexNormals[ n ] );

								if ( visible === false && ( side === THREE.BackSide || side === THREE.DoubleSide ) ) {

									normalModel.negate();

								}

								normalModel.applyMatrix3( _normalMatrix ).normalize();

							}

							_face.vertexNormalsLength = faceVertexNormals.length;

							var vertexUvs = faceVertexUvs[ f ];

							if ( vertexUvs !== undefined ) {

								for ( var u = 0; u < 3; u ++ ) {

									_face.uvs[ u ].copy( vertexUvs[ u ] );

								}

							}

							_face.color = face.color;
							_face.material = material;

							_face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3;
							_face.renderOrder = object.renderOrder;

							_renderData.elements.push( _face );

						}

					}

				} else if ( object instanceof THREE.Line ) {

					_modelViewProjectionMatrix.multiplyMatrices( _viewProjectionMatrix, _modelMatrix );

					if ( geometry instanceof THREE.BufferGeometry ) {

						var attributes = geometry.attributes;

						if ( attributes.position !== undefined ) {

							var positions = attributes.position.array;

							for ( var i = 0, l = positions.length; i < l; i += 3 ) {

								renderList.pushVertex( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] );

							}

							if ( attributes.color !== undefined ) {

								var colors = attributes.color.array;

								for ( var i = 0, l = colors.length; i < l; i += 3 ) {

									renderList.pushColor( colors[ i ], colors[ i + 1 ], colors[ i + 2 ] );

								}

							}

							if ( geometry.index !== null ) {

								var indices = geometry.index.array;

								for ( var i = 0, l = indices.length; i < l; i += 2 ) {

									renderList.pushLine( indices[ i ], indices[ i + 1 ] );

								}

							} else {

								var step = object instanceof THREE.LineSegments ? 2 : 1;

								for ( var i = 0, l = ( positions.length / 3 ) - 1; i < l; i += step ) {

									renderList.pushLine( i, i + 1 );

								}

							}

						}

					} else if ( geometry instanceof THREE.Geometry ) {

						var vertices = object.geometry.vertices;

						if ( vertices.length === 0 ) continue;

						v1 = getNextVertexInPool();
						v1.positionScreen.copy( vertices[ 0 ] ).applyMatrix4( _modelViewProjectionMatrix );

						var step = object instanceof THREE.LineSegments ? 2 : 1;

						for ( var v = 1, vl = vertices.length; v < vl; v ++ ) {

							v1 = getNextVertexInPool();
							v1.positionScreen.copy( vertices[ v ] ).applyMatrix4( _modelViewProjectionMatrix );

							if ( ( v + 1 ) % step > 0 ) continue;

							v2 = _vertexPool[ _vertexCount - 2 ];

							_clippedVertex1PositionScreen.copy( v1.positionScreen );
							_clippedVertex2PositionScreen.copy( v2.positionScreen );

							if ( clipLine( _clippedVertex1PositionScreen, _clippedVertex2PositionScreen ) === true ) {

								// Perform the perspective divide
								_clippedVertex1PositionScreen.multiplyScalar( 1 / _clippedVertex1PositionScreen.w );
								_clippedVertex2PositionScreen.multiplyScalar( 1 / _clippedVertex2PositionScreen.w );

								_line = getNextLineInPool();

								_line.id = object.id;
								_line.v1.positionScreen.copy( _clippedVertex1PositionScreen );
								_line.v2.positionScreen.copy( _clippedVertex2PositionScreen );

								_line.z = Math.max( _clippedVertex1PositionScreen.z, _clippedVertex2PositionScreen.z );
								_line.renderOrder = object.renderOrder;

								_line.material = object.material;

								if ( object.material.vertexColors === THREE.VertexColors ) {

									_line.vertexColors[ 0 ].copy( object.geometry.colors[ v ] );
									_line.vertexColors[ 1 ].copy( object.geometry.colors[ v - 1 ] );

								}

								_renderData.elements.push( _line );

							}

						}

					}

				} else if ( object instanceof THREE.Points ) {

					_modelViewProjectionMatrix.multiplyMatrices( _viewProjectionMatrix, _modelMatrix );

					if ( geometry instanceof THREE.Geometry ) {

						var vertices = object.geometry.vertices;

						for ( var v = 0, vl = vertices.length; v < vl; v ++ ) {

							var vertex = vertices[ v ];

							_vector4.set( vertex.x, vertex.y, vertex.z, 1 );
							_vector4.applyMatrix4( _modelViewProjectionMatrix );

							pushPoint( _vector4, object, camera );

						}

					} else if ( geometry instanceof THREE.BufferGeometry ) {

						var attributes = geometry.attributes;

						if ( attributes.position !== undefined ) {

							var positions = attributes.position.array;

							for ( var i = 0, l = positions.length; i < l; i += 3 ) {

								_vector4.set( positions[ i ], positions[ i + 1 ], positions[ i + 2 ], 1 );
								_vector4.applyMatrix4( _modelViewProjectionMatrix );

								pushPoint( _vector4, object, camera );

							}

						}

					}

				} else if ( object instanceof THREE.Sprite ) {

					_vector4.set( _modelMatrix.elements[ 12 ], _modelMatrix.elements[ 13 ], _modelMatrix.elements[ 14 ], 1 );
					_vector4.applyMatrix4( _viewProjectionMatrix );

					pushPoint( _vector4, object, camera );

				}

			}

			if ( sortElements === true ) {

				_renderData.elements.sort( painterSort );

			}

			return _renderData;

		};

		function pushPoint( _vector4, object, camera ) {

			var invW = 1 / _vector4.w;

			_vector4.z *= invW;

			if ( _vector4.z >= - 1 && _vector4.z <= 1 ) {

				_sprite = getNextSpriteInPool();
				_sprite.id = object.id;
				_sprite.x = _vector4.x * invW;
				_sprite.y = _vector4.y * invW;
				_sprite.z = _vector4.z;
				_sprite.renderOrder = object.renderOrder;
				_sprite.object = object;

				_sprite.rotation = object.rotation;

				_sprite.scale.x = object.scale.x * Math.abs( _sprite.x - ( _vector4.x + camera.projectionMatrix.elements[ 0 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 12 ] ) );
				_sprite.scale.y = object.scale.y * Math.abs( _sprite.y - ( _vector4.y + camera.projectionMatrix.elements[ 5 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 13 ] ) );

				_sprite.material = object.material;

				_renderData.elements.push( _sprite );

			}

		}

		// Pools

		function getNextObjectInPool() {

			if ( _objectCount === _objectPoolLength ) {

				var object = new THREE.RenderableObject();
				_objectPool.push( object );
				_objectPoolLength ++;
				_objectCount ++;
				return object;

			}

			return _objectPool[ _objectCount ++ ];

		}

		function getNextVertexInPool() {

			if ( _vertexCount === _vertexPoolLength ) {

				var vertex = new THREE.RenderableVertex();
				_vertexPool.push( vertex );
				_vertexPoolLength ++;
				_vertexCount ++;
				return vertex;

			}

			return _vertexPool[ _vertexCount ++ ];

		}

		function getNextFaceInPool() {

			if ( _faceCount === _facePoolLength ) {

				var face = new THREE.RenderableFace();
				_facePool.push( face );
				_facePoolLength ++;
				_faceCount ++;
				return face;

			}

			return _facePool[ _faceCount ++ ];


		}

		function getNextLineInPool() {

			if ( _lineCount === _linePoolLength ) {

				var line = new THREE.RenderableLine();
				_linePool.push( line );
				_linePoolLength ++;
				_lineCount ++;
				return line;

			}

			return _linePool[ _lineCount ++ ];

		}

		function getNextSpriteInPool() {

			if ( _spriteCount === _spritePoolLength ) {

				var sprite = new THREE.RenderableSprite();
				_spritePool.push( sprite );
				_spritePoolLength ++;
				_spriteCount ++;
				return sprite;

			}

			return _spritePool[ _spriteCount ++ ];

		}

		//

		function painterSort( a, b ) {

			if ( a.renderOrder !== b.renderOrder ) {

				return a.renderOrder - b.renderOrder;

			} else if ( a.z !== b.z ) {

				return b.z - a.z;

			} else if ( a.id !== b.id ) {

				return a.id - b.id;

			} else {

				return 0;

			}

		}

		function clipLine( s1, s2 ) {

			var alpha1 = 0, alpha2 = 1,

			// Calculate the boundary coordinate of each vertex for the near and far clip planes,
			// Z = -1 and Z = +1, respectively.

				bc1near = s1.z + s1.w,
				bc2near = s2.z + s2.w,
				bc1far = - s1.z + s1.w,
				bc2far = - s2.z + s2.w;

			if ( bc1near >= 0 && bc2near >= 0 && bc1far >= 0 && bc2far >= 0 ) {

				// Both vertices lie entirely within all clip planes.
				return true;

			} else if ( ( bc1near < 0 && bc2near < 0 ) || ( bc1far < 0 && bc2far < 0 ) ) {

				// Both vertices lie entirely outside one of the clip planes.
				return false;

			} else {

				// The line segment spans at least one clip plane.

				if ( bc1near < 0 ) {

					// v1 lies outside the near plane, v2 inside
					alpha1 = Math.max( alpha1, bc1near / ( bc1near - bc2near ) );

				} else if ( bc2near < 0 ) {

					// v2 lies outside the near plane, v1 inside
					alpha2 = Math.min( alpha2, bc1near / ( bc1near - bc2near ) );

				}

				if ( bc1far < 0 ) {

					// v1 lies outside the far plane, v2 inside
					alpha1 = Math.max( alpha1, bc1far / ( bc1far - bc2far ) );

				} else if ( bc2far < 0 ) {

					// v2 lies outside the far plane, v2 inside
					alpha2 = Math.min( alpha2, bc1far / ( bc1far - bc2far ) );

				}

				if ( alpha2 < alpha1 ) {

					// The line segment spans two boundaries, but is outside both of them.
					// (This can't happen when we're only clipping against just near/far but good
					//  to leave the check here for future usage if other clip planes are added.)
					return false;

				} else {

					// Update the s1 and s2 vertices to match the clipped line segment.
					s1.lerp( s2, alpha1 );
					s2.lerp( s1, 1 - alpha2 );

					return true;

				}

			}

		}

	};

	/**
	 * @author mrdoob / http://mrdoob.com/
	 */

	THREE.SpriteCanvasMaterial = function ( parameters ) {

		THREE.Material.call( this );

		this.type = 'SpriteCanvasMaterial';

		this.color = new THREE.Color( 0xffffff );
		this.program = function () {};

		this.setValues( parameters );

	};

	THREE.SpriteCanvasMaterial.prototype = Object.create( THREE.Material.prototype );
	THREE.SpriteCanvasMaterial.prototype.constructor = THREE.SpriteCanvasMaterial;
	THREE.SpriteCanvasMaterial.prototype.isSpriteCanvasMaterial = true;

	THREE.SpriteCanvasMaterial.prototype.clone = function () {

		var material = new THREE.SpriteCanvasMaterial();

		material.copy( this );
		material.color.copy( this.color );
		material.program = this.program;

		return material;

	};

	//

	THREE.CanvasRenderer = function ( parameters ) {

		console.log( 'THREE.CanvasRenderer', THREE.REVISION );

		parameters = parameters || {};

		var _this = this,
			_renderData, _elements, _lights,
			_projector = new THREE.Projector(),

			_canvas = parameters.canvas !== undefined
					 ? parameters.canvas
					 : document.createElement( 'canvas' ),

			_canvasWidth = _canvas.width,
			_canvasHeight = _canvas.height,
			_canvasWidthHalf = Math.floor( _canvasWidth / 2 ),
			_canvasHeightHalf = Math.floor( _canvasHeight / 2 ),

			_viewportX = 0,
			_viewportY = 0,
			_viewportWidth = _canvasWidth,
			_viewportHeight = _canvasHeight,

			_pixelRatio = 1,

			_context = _canvas.getContext( '2d', {
				alpha: parameters.alpha === true
			} ),

			_clearColor = new THREE.Color( 0x000000 ),
			_clearAlpha = parameters.alpha === true ? 0 : 1,

			_contextGlobalAlpha = 1,
			_contextGlobalCompositeOperation = 0,
			_contextStrokeStyle = null,
			_contextFillStyle = null,
			_contextLineWidth = null,
			_contextLineCap = null,
			_contextLineJoin = null,
			_contextLineDash = [],

			_v1, _v2, _v3,

			_v1x, _v1y, _v2x, _v2y, _v3x, _v3y,

			_color = new THREE.Color(),

			_diffuseColor = new THREE.Color(),
			_emissiveColor = new THREE.Color(),

			_lightColor = new THREE.Color(),

			_patterns = {},

			_uvs,
			_uv1x, _uv1y, _uv2x, _uv2y, _uv3x, _uv3y,

			_clipBox = new THREE.Box2(),
			_clearBox = new THREE.Box2(),
			_elemBox = new THREE.Box2(),

			_ambientLight = new THREE.Color(),
			_directionalLights = new THREE.Color(),
			_pointLights = new THREE.Color(),

			_vector3 = new THREE.Vector3(), // Needed for PointLight
			_centroid = new THREE.Vector3(),
			_normal = new THREE.Vector3(),
			_normalViewMatrix = new THREE.Matrix3();

		/* TODO
		_canvas.mozImageSmoothingEnabled = false;
		_canvas.webkitImageSmoothingEnabled = false;
		_canvas.msImageSmoothingEnabled = false;
		_canvas.imageSmoothingEnabled = false;
		*/

		// dash+gap fallbacks for Firefox and everything else

		if ( _context.setLineDash === undefined ) {

			_context.setLineDash = function () {};

		}

		this.domElement = _canvas;

		this.autoClear = true;
		this.sortObjects = true;
		this.sortElements = true;

		this.info = {

			render: {

				vertices: 0,
				faces: 0

			}

		};

		// API

		this.getContext = function () {

			return _context;

		};

		this.getContextAttributes = function () {

			return _context.getContextAttributes();

		};

		this.getPixelRatio = function () {

			return _pixelRatio;

		};

		this.setPixelRatio = function ( value ) {

			if ( value !== undefined ) _pixelRatio = value;

		};

		this.setSize = function ( width, height, updateStyle ) {

			_canvasWidth = width * _pixelRatio;
			_canvasHeight = height * _pixelRatio;

			_canvas.width = _canvasWidth;
			_canvas.height = _canvasHeight;

			_canvasWidthHalf = Math.floor( _canvasWidth / 2 );
			_canvasHeightHalf = Math.floor( _canvasHeight / 2 );

			if ( updateStyle !== false ) {

				_canvas.style.width = width + 'px';
				_canvas.style.height = height + 'px';

			}

			_clipBox.min.set( - _canvasWidthHalf, - _canvasHeightHalf );
			_clipBox.max.set(   _canvasWidthHalf,   _canvasHeightHalf );

			_clearBox.min.set( - _canvasWidthHalf, - _canvasHeightHalf );
			_clearBox.max.set(   _canvasWidthHalf,   _canvasHeightHalf );

			_contextGlobalAlpha = 1;
			_contextGlobalCompositeOperation = 0;
			_contextStrokeStyle = null;
			_contextFillStyle = null;
			_contextLineWidth = null;
			_contextLineCap = null;
			_contextLineJoin = null;

			this.setViewport( 0, 0, width, height );

		};

		this.setViewport = function ( x, y, width, height ) {

			_viewportX = x * _pixelRatio;
			_viewportY = y * _pixelRatio;

			_viewportWidth = width * _pixelRatio;
			_viewportHeight = height * _pixelRatio;

		};

		this.setScissor = function () {};
		this.setScissorTest = function () {};

		this.setClearColor = function ( color, alpha ) {

			_clearColor.set( color );
			_clearAlpha = alpha !== undefined ? alpha : 1;

			_clearBox.min.set( - _canvasWidthHalf, - _canvasHeightHalf );
			_clearBox.max.set(   _canvasWidthHalf,   _canvasHeightHalf );

		};

		this.setClearColorHex = function ( hex, alpha ) {

			console.warn( 'THREE.CanvasRenderer: .setClearColorHex() is being removed. Use .setClearColor() instead.' );
			this.setClearColor( hex, alpha );

		};

		this.getClearColor = function () {

			return _clearColor;

		};

		this.getClearAlpha = function () {

			return _clearAlpha;

		};

		this.getMaxAnisotropy = function () {

			return 0;

		};

		this.clear = function () {

			if ( _clearBox.isEmpty() === false ) {

				_clearBox.intersect( _clipBox );
				_clearBox.expandByScalar( 2 );

				_clearBox.min.x =   _clearBox.min.x + _canvasWidthHalf;
				_clearBox.min.y = - _clearBox.min.y + _canvasHeightHalf;		// higher y value !
				_clearBox.max.x =   _clearBox.max.x + _canvasWidthHalf;
				_clearBox.max.y = - _clearBox.max.y + _canvasHeightHalf;		// lower y value !

				if ( _clearAlpha < 1 ) {

					_context.clearRect(
						_clearBox.min.x | 0,
						_clearBox.max.y | 0,
						( _clearBox.max.x - _clearBox.min.x ) | 0,
						( _clearBox.min.y - _clearBox.max.y ) | 0
					);

				}

				if ( _clearAlpha > 0 ) {

					setOpacity( 1 );
					setBlending( THREE.NormalBlending );

					setFillStyle( 'rgba(' + Math.floor( _clearColor.r * 255 ) + ',' + Math.floor( _clearColor.g * 255 ) + ',' + Math.floor( _clearColor.b * 255 ) + ',' + _clearAlpha + ')' );

					_context.fillRect(
						_clearBox.min.x | 0,
						_clearBox.max.y | 0,
						( _clearBox.max.x - _clearBox.min.x ) | 0,
						( _clearBox.min.y - _clearBox.max.y ) | 0
					);

				}

				_clearBox.makeEmpty();

			}

		};

		// compatibility

		this.clearColor = function () {};
		this.clearDepth = function () {};
		this.clearStencil = function () {};

		this.render = function ( scene, camera ) {

			if ( camera.isCamera === undefined ) {

				console.error( 'THREE.CanvasRenderer.render: camera is not an instance of THREE.Camera.' );
				return;

			}

			var background = scene.background;

			if ( background && background.isColor ) {

				setOpacity( 1 );
				setBlending( THREE.NormalBlending );

				setFillStyle( background.getStyle() );
				_context.fillRect( 0, 0, _canvasWidth, _canvasHeight );

			} else if ( this.autoClear === true ) {

				this.clear();

			}

			_this.info.render.vertices = 0;
			_this.info.render.faces = 0;

			_context.setTransform( _viewportWidth / _canvasWidth, 0, 0, - _viewportHeight / _canvasHeight, _viewportX, _canvasHeight - _viewportY );
			_context.translate( _canvasWidthHalf, _canvasHeightHalf );

			_renderData = _projector.projectScene( scene, camera, this.sortObjects, this.sortElements );
			_elements = _renderData.elements;
			_lights = _renderData.lights;

			_normalViewMatrix.getNormalMatrix( camera.matrixWorldInverse );

			/* DEBUG
			setFillStyle( 'rgba( 0, 255, 255, 0.5 )' );
			_context.fillRect( _clipBox.min.x, _clipBox.min.y, _clipBox.max.x - _clipBox.min.x, _clipBox.max.y - _clipBox.min.y );
			*/

			calculateLights();

			for ( var e = 0, el = _elements.length; e < el; e ++ ) {

				var element = _elements[ e ];

				var material = element.material;

				if ( material === undefined || material.opacity === 0 ) continue;

				_elemBox.makeEmpty();

				if ( element instanceof THREE.RenderableSprite ) {

					_v1 = element;
					_v1.x *= _canvasWidthHalf; _v1.y *= _canvasHeightHalf;

					renderSprite( _v1, element, material );

				} else if ( element instanceof THREE.RenderableLine ) {

					_v1 = element.v1; _v2 = element.v2;

					_v1.positionScreen.x *= _canvasWidthHalf; _v1.positionScreen.y *= _canvasHeightHalf;
					_v2.positionScreen.x *= _canvasWidthHalf; _v2.positionScreen.y *= _canvasHeightHalf;

					_elemBox.setFromPoints( [
						_v1.positionScreen,
						_v2.positionScreen
					] );

					if ( _clipBox.intersectsBox( _elemBox ) === true ) {

						renderLine( _v1, _v2, element, material );

					}

				} else if ( element instanceof THREE.RenderableFace ) {

					_v1 = element.v1; _v2 = element.v2; _v3 = element.v3;

					if ( _v1.positionScreen.z < - 1 || _v1.positionScreen.z > 1 ) continue;
					if ( _v2.positionScreen.z < - 1 || _v2.positionScreen.z > 1 ) continue;
					if ( _v3.positionScreen.z < - 1 || _v3.positionScreen.z > 1 ) continue;

					_v1.positionScreen.x *= _canvasWidthHalf; _v1.positionScreen.y *= _canvasHeightHalf;
					_v2.positionScreen.x *= _canvasWidthHalf; _v2.positionScreen.y *= _canvasHeightHalf;
					_v3.positionScreen.x *= _canvasWidthHalf; _v3.positionScreen.y *= _canvasHeightHalf;

					if ( material.overdraw > 0 ) {

						expand( _v1.positionScreen, _v2.positionScreen, material.overdraw );
						expand( _v2.positionScreen, _v3.positionScreen, material.overdraw );
						expand( _v3.positionScreen, _v1.positionScreen, material.overdraw );

					}

					_elemBox.setFromPoints( [
						_v1.positionScreen,
						_v2.positionScreen,
						_v3.positionScreen
					] );

					if ( _clipBox.intersectsBox( _elemBox ) === true ) {

						renderFace3( _v1, _v2, _v3, 0, 1, 2, element, material );

					}

				}

				/* DEBUG
				setLineWidth( 1 );
				setStrokeStyle( 'rgba( 0, 255, 0, 0.5 )' );
				_context.strokeRect( _elemBox.min.x, _elemBox.min.y, _elemBox.max.x - _elemBox.min.x, _elemBox.max.y - _elemBox.min.y );
				*/

				_clearBox.union( _elemBox );

			}

			/* DEBUG
			setLineWidth( 1 );
			setStrokeStyle( 'rgba( 255, 0, 0, 0.5 )' );
			_context.strokeRect( _clearBox.min.x, _clearBox.min.y, _clearBox.max.x - _clearBox.min.x, _clearBox.max.y - _clearBox.min.y );
			*/

			_context.setTransform( 1, 0, 0, 1, 0, 0 );

		};

		//

		function calculateLights() {

			_ambientLight.setRGB( 0, 0, 0 );
			_directionalLights.setRGB( 0, 0, 0 );
			_pointLights.setRGB( 0, 0, 0 );

			for ( var l = 0, ll = _lights.length; l < ll; l ++ ) {

				var light = _lights[ l ];
				var lightColor = light.color;

				if ( light.isAmbientLight ) {

					_ambientLight.add( lightColor );

				} else if ( light.isDirectionalLight ) {

					// for sprites

					_directionalLights.add( lightColor );

				} else if ( light.isPointLight ) {

					// for sprites

					_pointLights.add( lightColor );

				}

			}

		}

		function calculateLight( position, normal, color ) {

			for ( var l = 0, ll = _lights.length; l < ll; l ++ ) {

				var light = _lights[ l ];

				_lightColor.copy( light.color );

				if ( light.isDirectionalLight ) {

					var lightPosition = _vector3.setFromMatrixPosition( light.matrixWorld ).normalize();

					var amount = normal.dot( lightPosition );

					if ( amount <= 0 ) continue;

					amount *= light.intensity;

					color.add( _lightColor.multiplyScalar( amount ) );

				} else if ( light.isPointLight ) {

					var lightPosition = _vector3.setFromMatrixPosition( light.matrixWorld );

					var amount = normal.dot( _vector3.subVectors( lightPosition, position ).normalize() );

					if ( amount <= 0 ) continue;

					amount *= light.distance == 0 ? 1 : 1 - Math.min( position.distanceTo( lightPosition ) / light.distance, 1 );

					if ( amount == 0 ) continue;

					amount *= light.intensity;

					color.add( _lightColor.multiplyScalar( amount ) );

				}

			}

		}

		function renderSprite( v1, element, material ) {

			setOpacity( material.opacity );
			setBlending( material.blending );

			var scaleX = element.scale.x * _canvasWidthHalf;
			var scaleY = element.scale.y * _canvasHeightHalf;

			var dist = Math.sqrt( scaleX * scaleX + scaleY * scaleY ); // allow for rotated sprite
			_elemBox.min.set( v1.x - dist, v1.y - dist );
			_elemBox.max.set( v1.x + dist, v1.y + dist );

			if ( material.isSpriteMaterial ) {

				var texture = material.map;

				if ( texture !== null ) {

					var pattern = _patterns[ texture.id ];

					if ( pattern === undefined || pattern.version !== texture.version ) {

						pattern = textureToPattern( texture );
						_patterns[ texture.id ] = pattern;

					}

					if ( pattern.canvas !== undefined ) {

						setFillStyle( pattern.canvas );

						var bitmap = texture.image;

						var ox = bitmap.width * texture.offset.x;
						var oy = bitmap.height * texture.offset.y;

						var sx = bitmap.width * texture.repeat.x;
						var sy = bitmap.height * texture.repeat.y;

						var cx = scaleX / sx;
						var cy = scaleY / sy;

						_context.save();
						_context.translate( v1.x, v1.y );
						if ( material.rotation !== 0 ) _context.rotate( material.rotation );
						_context.translate( - scaleX / 2, - scaleY / 2 );
						_context.scale( cx, cy );
						_context.translate( - ox, - oy );
						_context.fillRect( ox, oy, sx, sy );
						_context.restore();

					}

				} else {

					// no texture

					setFillStyle( material.color.getStyle() );

					_context.save();
					_context.translate( v1.x, v1.y );
					if ( material.rotation !== 0 ) _context.rotate( material.rotation );
					_context.scale( scaleX, - scaleY );
					_context.fillRect( - 0.5, - 0.5, 1, 1 );
					_context.restore();

				}

			} else if ( material.isSpriteCanvasMaterial ) {

				setStrokeStyle( material.color.getStyle() );
				setFillStyle( material.color.getStyle() );

				_context.save();
				_context.translate( v1.x, v1.y );
				if ( material.rotation !== 0 ) _context.rotate( material.rotation );
				_context.scale( scaleX, scaleY );

				material.program( _context );

				_context.restore();

			} else if ( material.isPointsMaterial ) {

				setFillStyle( material.color.getStyle() );

				_context.save();
				_context.translate( v1.x, v1.y );
				if ( material.rotation !== 0 ) _context.rotate( material.rotation );
				_context.scale( scaleX * material.size, - scaleY * material.size );
				_context.fillRect( - 0.5, - 0.5, 1, 1 );
				_context.restore();

			}

			/* DEBUG
			setStrokeStyle( 'rgb(255,255,0)' );
			_context.beginPath();
			_context.moveTo( v1.x - 10, v1.y );
			_context.lineTo( v1.x + 10, v1.y );
			_context.moveTo( v1.x, v1.y - 10 );
			_context.lineTo( v1.x, v1.y + 10 );
			_context.stroke();
			*/

		}

		function renderLine( v1, v2, element, material ) {

			setOpacity( material.opacity );
			setBlending( material.blending );

			_context.beginPath();
			_context.moveTo( v1.positionScreen.x, v1.positionScreen.y );
			_context.lineTo( v2.positionScreen.x, v2.positionScreen.y );

			if ( material.isLineBasicMaterial ) {

				setLineWidth( material.linewidth );
				setLineCap( material.linecap );
				setLineJoin( material.linejoin );

				if ( material.vertexColors !== THREE.VertexColors ) {

					setStrokeStyle( material.color.getStyle() );

				} else {

					var colorStyle1 = element.vertexColors[ 0 ].getStyle();
					var colorStyle2 = element.vertexColors[ 1 ].getStyle();

					if ( colorStyle1 === colorStyle2 ) {

						setStrokeStyle( colorStyle1 );

					} else {

						try {

							var grad = _context.createLinearGradient(
								v1.positionScreen.x,
								v1.positionScreen.y,
								v2.positionScreen.x,
								v2.positionScreen.y
							);
							grad.addColorStop( 0, colorStyle1 );
							grad.addColorStop( 1, colorStyle2 );

						} catch ( exception ) {

							grad = colorStyle1;

						}

						setStrokeStyle( grad );

					}

				}

				if ( material.isLineDashedMaterial ) {

					setLineDash( [ material.dashSize, material.gapSize ] );

				}

				_context.stroke();
				_elemBox.expandByScalar( material.linewidth * 2 );

				if ( material.isLineDashedMaterial ) {

					setLineDash( [] );

				}

			}

		}

		function renderFace3( v1, v2, v3, uv1, uv2, uv3, element, material ) {

			_this.info.render.vertices += 3;
			_this.info.render.faces ++;

			setOpacity( material.opacity );
			setBlending( material.blending );

			_v1x = v1.positionScreen.x; _v1y = v1.positionScreen.y;
			_v2x = v2.positionScreen.x; _v2y = v2.positionScreen.y;
			_v3x = v3.positionScreen.x; _v3y = v3.positionScreen.y;

			drawTriangle( _v1x, _v1y, _v2x, _v2y, _v3x, _v3y );

			if ( ( material.isMeshLambertMaterial || material.isMeshPhongMaterial || material.isMeshStandardMaterial ) && material.map === null ) {

				_diffuseColor.copy( material.color );
				_emissiveColor.copy( material.emissive );

				if ( material.vertexColors === THREE.FaceColors ) {

					_diffuseColor.multiply( element.color );

				}

				_color.copy( _ambientLight );

				_centroid.copy( v1.positionWorld ).add( v2.positionWorld ).add( v3.positionWorld ).divideScalar( 3 );

				calculateLight( _centroid, element.normalModel, _color );

				_color.multiply( _diffuseColor ).add( _emissiveColor );

				material.wireframe === true
					 ? strokePath( _color, material.wireframeLinewidth, material.wireframeLinecap, material.wireframeLinejoin )
					 : fillPath( _color );

			} else if ( material.isMeshBasicMaterial || material.isMeshLambertMaterial || material.isMeshPhongMaterial || material.isMeshStandardMaterial ) {

				if ( material.map !== null ) {

					var mapping = material.map.mapping;

					if ( mapping === THREE.UVMapping ) {

						_uvs = element.uvs;
						patternPath( _v1x, _v1y, _v2x, _v2y, _v3x, _v3y, _uvs[ uv1 ].x, _uvs[ uv1 ].y, _uvs[ uv2 ].x, _uvs[ uv2 ].y, _uvs[ uv3 ].x, _uvs[ uv3 ].y, material.map );

					}

				} else if ( material.envMap !== null ) {

					if ( material.envMap.mapping === THREE.SphericalReflectionMapping ) {

						_normal.copy( element.vertexNormalsModel[ uv1 ] ).applyMatrix3( _normalViewMatrix );
						_uv1x = 0.5 * _normal.x + 0.5;
						_uv1y = 0.5 * _normal.y + 0.5;

						_normal.copy( element.vertexNormalsModel[ uv2 ] ).applyMatrix3( _normalViewMatrix );
						_uv2x = 0.5 * _normal.x + 0.5;
						_uv2y = 0.5 * _normal.y + 0.5;

						_normal.copy( element.vertexNormalsModel[ uv3 ] ).applyMatrix3( _normalViewMatrix );
						_uv3x = 0.5 * _normal.x + 0.5;
						_uv3y = 0.5 * _normal.y + 0.5;

						patternPath( _v1x, _v1y, _v2x, _v2y, _v3x, _v3y, _uv1x, _uv1y, _uv2x, _uv2y, _uv3x, _uv3y, material.envMap );

					}

				} else {

					_color.copy( material.color );

					if ( material.vertexColors === THREE.FaceColors ) {

						_color.multiply( element.color );

					}

					material.wireframe === true
						 ? strokePath( _color, material.wireframeLinewidth, material.wireframeLinecap, material.wireframeLinejoin )
						 : fillPath( _color );

				}

			} else if ( material.isMeshNormalMaterial ) {

				_normal.copy( element.normalModel ).applyMatrix3( _normalViewMatrix );

				_color.setRGB( _normal.x, _normal.y, _normal.z ).multiplyScalar( 0.5 ).addScalar( 0.5 );

				material.wireframe === true
					 ? strokePath( _color, material.wireframeLinewidth, material.wireframeLinecap, material.wireframeLinejoin )
					 : fillPath( _color );

			} else {

				_color.setRGB( 1, 1, 1 );

				material.wireframe === true
					 ? strokePath( _color, material.wireframeLinewidth, material.wireframeLinecap, material.wireframeLinejoin )
					 : fillPath( _color );

			}

		}

		//

		function drawTriangle( x0, y0, x1, y1, x2, y2 ) {

			_context.beginPath();
			_context.moveTo( x0, y0 );
			_context.lineTo( x1, y1 );
			_context.lineTo( x2, y2 );
			_context.closePath();

		}

		function strokePath( color, linewidth, linecap, linejoin ) {

			setLineWidth( linewidth );
			setLineCap( linecap );
			setLineJoin( linejoin );
			setStrokeStyle( color.getStyle() );

			_context.stroke();

			_elemBox.expandByScalar( linewidth * 2 );

		}

		function fillPath( color ) {

			setFillStyle( color.getStyle() );
			_context.fill();

		}

		function textureToPattern( texture ) {

			if ( texture.version === 0 ||
				texture instanceof THREE.CompressedTexture ||
				texture instanceof THREE.DataTexture ) {

				return {
					canvas: undefined,
					version: texture.version
				};

			}

			var image = texture.image;

			if ( image.complete === false ) {

				return {
					canvas: undefined,
					version: 0
				};

			}

			var repeatX = texture.wrapS === THREE.RepeatWrapping || texture.wrapS === THREE.MirroredRepeatWrapping;
			var repeatY = texture.wrapT === THREE.RepeatWrapping || texture.wrapT === THREE.MirroredRepeatWrapping;

			var mirrorX = texture.wrapS === THREE.MirroredRepeatWrapping;
			var mirrorY = texture.wrapT === THREE.MirroredRepeatWrapping;

			//

			var canvas = document.createElement( 'canvas' );
			canvas.width = image.width * ( mirrorX ? 2 : 1 );
			canvas.height = image.height * ( mirrorY ? 2 : 1 );

			var context = canvas.getContext( '2d' );
			context.setTransform( 1, 0, 0, - 1, 0, image.height );
			context.drawImage( image, 0, 0 );

			if ( mirrorX === true ) {

				context.setTransform( - 1, 0, 0, - 1, image.width, image.height );
				context.drawImage( image, - image.width, 0 );

			}

			if ( mirrorY === true ) {

				context.setTransform( 1, 0, 0, 1, 0, 0 );
				context.drawImage( image, 0, image.height );

			}

			if ( mirrorX === true && mirrorY === true ) {

				context.setTransform( - 1, 0, 0, 1, image.width, 0 );
				context.drawImage( image, - image.width, image.height );

			}

			var repeat = 'no-repeat';

			if ( repeatX === true && repeatY === true ) {

				repeat = 'repeat';

			} else if ( repeatX === true ) {

				repeat = 'repeat-x';

			} else if ( repeatY === true ) {

				repeat = 'repeat-y';

			}

			var pattern = _context.createPattern( canvas, repeat );

			if ( texture.onUpdate ) texture.onUpdate( texture );

			return {
				canvas: pattern,
				version: texture.version
			};

		}

		function patternPath( x0, y0, x1, y1, x2, y2, u0, v0, u1, v1, u2, v2, texture ) {

			var pattern = _patterns[ texture.id ];

			if ( pattern === undefined || pattern.version !== texture.version ) {

				pattern = textureToPattern( texture );
				_patterns[ texture.id ] = pattern;

			}

			if ( pattern.canvas !== undefined ) {

				setFillStyle( pattern.canvas );

			} else {

				setFillStyle( 'rgba( 0, 0, 0, 1)' );
				_context.fill();
				return;

			}

			// http://extremelysatisfactorytotalitarianism.com/blog/?p=2120

			var a, b, c, d, e, f, det, idet,
				offsetX = texture.offset.x / texture.repeat.x,
				offsetY = texture.offset.y / texture.repeat.y,
				width = texture.image.width * texture.repeat.x,
				height = texture.image.height * texture.repeat.y;

			u0 = ( u0 + offsetX ) * width;
			v0 = ( v0 + offsetY ) * height;

			u1 = ( u1 + offsetX ) * width;
			v1 = ( v1 + offsetY ) * height;

			u2 = ( u2 + offsetX ) * width;
			v2 = ( v2 + offsetY ) * height;

			x1 -= x0; y1 -= y0;
			x2 -= x0; y2 -= y0;

			u1 -= u0; v1 -= v0;
			u2 -= u0; v2 -= v0;

			det = u1 * v2 - u2 * v1;

			if ( det === 0 ) return;

			idet = 1 / det;

			a = ( v2 * x1 - v1 * x2 ) * idet;
			b = ( v2 * y1 - v1 * y2 ) * idet;
			c = ( u1 * x2 - u2 * x1 ) * idet;
			d = ( u1 * y2 - u2 * y1 ) * idet;

			e = x0 - a * u0 - c * v0;
			f = y0 - b * u0 - d * v0;

			_context.save();
			_context.transform( a, b, c, d, e, f );
			_context.fill();
			_context.restore();

		}

		/*
		function clipImage( x0, y0, x1, y1, x2, y2, u0, v0, u1, v1, u2, v2, image ) {

			// http://extremelysatisfactorytotalitarianism.com/blog/?p=2120

			var a, b, c, d, e, f, det, idet,
			width = image.width - 1,
			height = image.height - 1;

			u0 *= width; v0 *= height;
			u1 *= width; v1 *= height;
			u2 *= width; v2 *= height;

			x1 -= x0; y1 -= y0;
			x2 -= x0; y2 -= y0;

			u1 -= u0; v1 -= v0;
			u2 -= u0; v2 -= v0;

			det = u1 * v2 - u2 * v1;

			idet = 1 / det;

			a = ( v2 * x1 - v1 * x2 ) * idet;
			b = ( v2 * y1 - v1 * y2 ) * idet;
			c = ( u1 * x2 - u2 * x1 ) * idet;
			d = ( u1 * y2 - u2 * y1 ) * idet;

			e = x0 - a * u0 - c * v0;
			f = y0 - b * u0 - d * v0;

			_context.save();
			_context.transform( a, b, c, d, e, f );
			_context.clip();
			_context.drawImage( image, 0, 0 );
			_context.restore();

		}
		*/

		// Hide anti-alias gaps

		function expand( v1, v2, pixels ) {

			var x = v2.x - v1.x, y = v2.y - v1.y,
				det = x * x + y * y, idet;

			if ( det === 0 ) return;

			idet = pixels / Math.sqrt( det );

			x *= idet; y *= idet;

			v2.x += x; v2.y += y;
			v1.x -= x; v1.y -= y;

		}

		// Context cached methods.

		function setOpacity( value ) {

			if ( _contextGlobalAlpha !== value ) {

				_context.globalAlpha = value;
				_contextGlobalAlpha = value;

			}

		}

		function setBlending( value ) {

			if ( _contextGlobalCompositeOperation !== value ) {

				if ( value === THREE.NormalBlending ) {

					_context.globalCompositeOperation = 'source-over';

				} else if ( value === THREE.AdditiveBlending ) {

					_context.globalCompositeOperation = 'lighter';

				} else if ( value === THREE.SubtractiveBlending ) {

					_context.globalCompositeOperation = 'darker';

				} else if ( value === THREE.MultiplyBlending ) {

					_context.globalCompositeOperation = 'multiply';

				}

				_contextGlobalCompositeOperation = value;

			}

		}

		function setLineWidth( value ) {

			if ( _contextLineWidth !== value ) {

				_context.lineWidth = value;
				_contextLineWidth = value;

			}

		}

		function setLineCap( value ) {

			// "butt", "round", "square"

			if ( _contextLineCap !== value ) {

				_context.lineCap = value;
				_contextLineCap = value;

			}

		}

		function setLineJoin( value ) {

			// "round", "bevel", "miter"

			if ( _contextLineJoin !== value ) {

				_context.lineJoin = value;
				_contextLineJoin = value;

			}

		}

		function setStrokeStyle( value ) {

			if ( _contextStrokeStyle !== value ) {

				_context.strokeStyle = value;
				_contextStrokeStyle = value;

			}

		}

		function setFillStyle( value ) {

			if ( _contextFillStyle !== value ) {

				_context.fillStyle = value;
				_contextFillStyle = value;

			}

		}

		function setLineDash( value ) {

			if ( _contextLineDash.length !== value.length ) {

				_context.setLineDash( value );
				_contextLineDash = value;

			}

		}

	};

	/**
	 * RaytracingRenderer renders by raytracing it's scene. However, it does not
	 * compute the pixels itself but it hands off and coordinates the taks for workers.
	 * The workers compute the pixel values and this renderer simply paints it to the Canvas.
	 *
	 * @author zz85 / http://github.com/zz85
	 */

	THREE.RaytracingRenderer = function ( parameters ) {

		console.log( 'THREE.RaytracingRenderer', THREE.REVISION );

		parameters = parameters || {};

		var scope = this;
		var pool = [];
		var renderering = false;

		var canvas = document.createElement( 'canvas' );
		var context = canvas.getContext( '2d', {
			alpha: parameters.alpha === true
		} );

		var canvasWidth, canvasHeight;

		var clearColor = new THREE.Color( 0x000000 );

		this.domElement = canvas;

		this.autoClear = true;

		var workers = parameters.workers;
		var blockSize = parameters.blockSize || 64;
		this.randomize = parameters.randomize;

		var toRender = [], workerId = 0, sceneId = 0;

		console.log( '%cSpinning off ' + workers + ' Workers ', 'font-size: 20px; background: black; color: white; font-family: monospace;' );

		this.setWorkers = function( w ) {

			workers = w || navigator.hardwareConcurrency || 4;

			while ( pool.length < workers ) {
				var worker = new Worker( parameters.workerPath );
				worker.id = workerId++;

				worker.onmessage = function( e ) {

					var data = e.data;

					if ( ! data ) return;

					if ( data.blockSize && sceneId == data.sceneId ) { // we match sceneId here to be sure

						var imagedata = new ImageData( new Uint8ClampedArray( data.data ), data.blockSize, data.blockSize );
						context.putImageData( imagedata, data.blockX, data.blockY );

						// completed

						console.log( 'Worker ' + this.id, data.time / 1000, ( Date.now() - reallyThen ) / 1000 + ' s' );

						if ( pool.length > workers ) {

							pool.splice( pool.indexOf( this ), 1 );
							return this.terminate();

						}

						renderNext( this );

					}

				};

				worker.color = new THREE.Color().setHSL( Math.random() , 0.8, 0.8 ).getHexString();
				pool.push( worker );

				updateSettings( worker );

				if ( renderering ) {

					worker.postMessage( {
						scene: sceneJSON,
						camera: cameraJSON,
						annex: materials,
						sceneId: sceneId
					} );

					renderNext( worker );

				}

			}

			if ( ! renderering ) {

				while ( pool.length > workers ) {

					pool.pop().terminate();

				}

			}

		};

		this.setWorkers( workers );

		this.setClearColor = function ( color, alpha ) {

			clearColor.set( color );

		};

		this.setPixelRatio = function () {};

		this.setSize = function ( width, height ) {

			canvas.width = width;
			canvas.height = height;

			canvasWidth = canvas.width;
			canvasHeight = canvas.height;

			context.fillStyle = 'white';

			pool.forEach( updateSettings );

		};

		this.setSize( canvas.width, canvas.height );

		this.clear = function () {

		};

		//

		var totalBlocks, xblocks, yblocks;

		function updateSettings( worker ) {

			worker.postMessage( {

				init: [ canvasWidth, canvasHeight ],
				worker: worker.id,
				// workers: pool.length,
				blockSize: blockSize

			} );

		}

		function renderNext( worker ) {
			if ( ! toRender.length ) {

				renderering = false;
				return scope.dispatchEvent( { type: "complete" } );

			}

			var current = toRender.pop();

			var blockX = ( current % xblocks ) * blockSize;
			var blockY = ( current / xblocks | 0 ) * blockSize;

			worker.postMessage( {
				render: true,
				x: blockX,
				y: blockY,
				sceneId: sceneId
			} );

			context.fillStyle = '#' + worker.color;

			context.fillRect( blockX, blockY, blockSize, blockSize );

		}

		var materials = {};

		var sceneJSON, cameraJSON, reallyThen;

		// additional properties that were not serialize automatically

		var _annex = {

			mirror: 1,
			reflectivity: 1,
			refractionRatio: 1,
			glass: 1

		};

		function serializeObject( o ) {

			var mat = o.material;

			if ( ! mat || mat.uuid in materials ) return;

			var props = {};
			for ( var m in _annex ) {

				if ( mat[ m ] !== undefined ) {

					props[ m ] = mat[ m ];

				}

			}

			materials[ mat.uuid ] = props;
		}

		this.render = function ( scene, camera ) {

			renderering = true;

			// update scene graph

			if ( scene.autoUpdate === true ) scene.updateMatrixWorld();

			// update camera matrices

			if ( camera.parent === null ) camera.updateMatrixWorld();


			sceneJSON = scene.toJSON();
			cameraJSON = camera.toJSON();
			++ sceneId;

			scene.traverse( serializeObject );

			pool.forEach( function( worker ) {

				worker.postMessage( {
					scene: sceneJSON,
					camera: cameraJSON,
					annex: materials,
					sceneId: sceneId
				} );
			} );

			context.clearRect( 0, 0, canvasWidth, canvasHeight );
			reallyThen = Date.now();

			xblocks = Math.ceil( canvasWidth / blockSize );
			yblocks = Math.ceil( canvasHeight / blockSize );
			totalBlocks = xblocks * yblocks;

			toRender = [];

			for ( var i = 0; i < totalBlocks; i ++ ) {

				toRender.push( i );

			}


			// Randomize painting :)

			if ( scope.randomize ) {

				for ( var i = 0; i < totalBlocks; i ++ ) {

					var swap = Math.random()  * totalBlocks | 0;
					var tmp = toRender[ swap ];
					toRender[ swap ] = toRender[ i ];
					toRender[ i ] = tmp;

				}

			}


			pool.forEach( renderNext );

		};

	};

	Object.assign( THREE.RaytracingRenderer.prototype, THREE.EventDispatcher.prototype );

	/**
	 * @author mrdoob / http://mrdoob.com/
	 * @author ryg / http://farbrausch.de/~fg
	 * @author mraleph / http://mrale.ph/
	 * @author daoshengmu / http://dsmu.me/
	 */

	THREE.SoftwareRenderer = function ( parameters ) {

		console.log( 'THREE.SoftwareRenderer', THREE.REVISION );

		parameters = parameters || {};

		var canvas = parameters.canvas !== undefined
				 ? parameters.canvas
				 : document.createElement( 'canvas' );

		var context = canvas.getContext( '2d', {
			alpha: parameters.alpha === true
		} );

		var shaders = {};
		var textures = {};

		var canvasWidth, canvasHeight;
		var canvasWBlocks, canvasHBlocks;
		var viewportXScale, viewportYScale, viewportZScale;
		var viewportXOffs, viewportYOffs, viewportZOffs;

		var clearColor = new THREE.Color( 0x000000 );
		var clearAlpha = parameters.alpha === true ? 0 : 1;

		var imagedata, data, zbuffer;
		var numBlocks, blockMaxZ, blockFlags;

		var BLOCK_ISCLEAR = ( 1 << 0 );
		var BLOCK_NEEDCLEAR = ( 1 << 1 );

		var subpixelBits = 4;
		var subpixelBias = ( 1 << subpixelBits ) - 1;
		var blockShift = 3;
		var blockSize = 1 << blockShift;
		var maxZVal = ( 1 << 24 ); // Note: You want to size this so you don't get overflows.
		var lineMode = false;
		var lookVector = new THREE.Vector3( 0, 0, 1 );
		var crossVector = new THREE.Vector3();

		var rectx1 = Infinity, recty1 = Infinity;
		var rectx2 = 0, recty2 = 0;

		var prevrectx1 = Infinity, prevrecty1 = Infinity;
		var prevrectx2 = 0, prevrecty2 = 0;

		var projector = new THREE.Projector();

		var spriteV1 = new THREE.Vector4();
		var spriteV2 = new THREE.Vector4();
		var spriteV3 = new THREE.Vector4();

		var spriteUV1 = new THREE.Vector2();
		var spriteUV2 = new THREE.Vector2();
		var spriteUV3 = new THREE.Vector2();

		var mpVPool = [];
		var mpVPoolCount = 0;
		var mpNPool = [];
		var mpNPoolCount = 0;
		var mpUVPool = [];
		var mpUVPoolCount = 0;

		var _this = this;

		this.domElement = canvas;

		this.autoClear = true;

		this.setClearColor = function ( color, alpha ) {

			clearColor.set( color );
			clearAlpha = alpha;
			clearColorBuffer( clearColor );

		};

		this.setPixelRatio = function () {};

		this.setSize = function ( width, height ) {

			canvasWBlocks = Math.floor( width / blockSize );
			canvasHBlocks = Math.floor( height / blockSize );
			canvasWidth = canvasWBlocks * blockSize;
			canvasHeight = canvasHBlocks * blockSize;

			var fixScale = 1 << subpixelBits;

			viewportXScale = fixScale * canvasWidth / 2;
			viewportYScale = - fixScale * canvasHeight / 2;
			viewportZScale = maxZVal / 2;

			viewportXOffs = fixScale * canvasWidth / 2 + 0.5;
			viewportYOffs = fixScale * canvasHeight / 2 + 0.5;
			viewportZOffs = maxZVal / 2 + 0.5;

			canvas.width = canvasWidth;
			canvas.height = canvasHeight;

			imagedata = context.getImageData( 0, 0, canvasWidth, canvasHeight );
			data = imagedata.data;

			zbuffer = new Int32Array( data.length / 4 );

			numBlocks = canvasWBlocks * canvasHBlocks;
			blockMaxZ = new Int32Array( numBlocks );
			blockFlags = new Uint8Array( numBlocks );

			for ( var i = 0, l = zbuffer.length; i < l; i ++ ) {

				zbuffer[ i ] = maxZVal;

			}

			for ( var i = 0; i < numBlocks; i ++ ) {

				blockFlags[ i ] = BLOCK_ISCLEAR;

			}

			clearColorBuffer( clearColor );

		};

		this.clear = function () {

			rectx1 = Infinity;
			recty1 = Infinity;
			rectx2 = 0;
			recty2 = 0;
			mpVPoolCount = 0;
			mpNPoolCount = 0;
			mpUVPoolCount = 0;

			for ( var i = 0; i < numBlocks; i ++ ) {

				blockMaxZ[ i ] = maxZVal;
				blockFlags[ i ] = ( blockFlags[ i ] & BLOCK_ISCLEAR ) ? BLOCK_ISCLEAR : BLOCK_NEEDCLEAR;

			}

		};


		this.render = function ( scene, camera ) {

			// TODO: Check why autoClear can't be false.
			this.clear();

			var background = scene.background;

			if ( background && background.isColor ) {

				clearColorBuffer( background );

			}

			var renderData = projector.projectScene( scene, camera, false, false );
			var elements = renderData.elements;

			for ( var e = 0, el = elements.length; e < el; e ++ ) {

				var element = elements[ e ];
				var material = element.material;
				var shader = getMaterialShader( material );

				if ( ! shader ) continue;

				if ( element instanceof THREE.RenderableFace ) {

					if ( ! element.uvs ) {

						drawTriangle(
							element.v1.positionScreen,
							element.v2.positionScreen,
							element.v3.positionScreen,
							null, null, null,
							shader, element, material
						);

					} else {

						drawTriangle(
							element.v1.positionScreen,
							element.v2.positionScreen,
							element.v3.positionScreen,
							element.uvs[ 0 ], element.uvs[ 1 ], element.uvs[ 2 ],
							shader, element, material
						);

					}


				} else if ( element instanceof THREE.RenderableSprite ) {

					var scaleX = element.scale.x * 0.5;
					var scaleY = element.scale.y * 0.5;

					spriteV1.copy( element );
					spriteV1.x -= scaleX;
					spriteV1.y += scaleY;

					spriteV2.copy( element );
					spriteV2.x -= scaleX;
					spriteV2.y -= scaleY;

					spriteV3.copy( element );
					spriteV3.x += scaleX;
					spriteV3.y += scaleY;

					if ( material.map ) {

						spriteUV1.set( 0, 1 );
						spriteUV2.set( 0, 0 );
						spriteUV3.set( 1, 1 );

						drawTriangle(
							spriteV1, spriteV2, spriteV3,
							spriteUV1, spriteUV2, spriteUV3,
							shader, element, material
						);

					} else {

						drawTriangle(
							spriteV1, spriteV2, spriteV3,
							null, null, null,
							shader, element, material
						);

					}

					spriteV1.copy( element );
					spriteV1.x += scaleX;
					spriteV1.y += scaleY;

					spriteV2.copy( element );
					spriteV2.x -= scaleX;
					spriteV2.y -= scaleY;

					spriteV3.copy( element );
					spriteV3.x += scaleX;
					spriteV3.y -= scaleY;

					if ( material.map ) {

						spriteUV1.set( 1, 1 );
						spriteUV2.set( 0, 0 );
						spriteUV3.set( 1, 0 );

						drawTriangle(
							spriteV1, spriteV2, spriteV3,
							spriteUV1, spriteUV2, spriteUV3,
							shader, element, material
						);

					} else {

						drawTriangle(
							spriteV1, spriteV2, spriteV3,
							null, null, null,
							shader, element, material
						);

					}

				} else if ( element instanceof THREE.RenderableLine ) {

					var shader = getMaterialShader( material );

					drawLine(
						element.v1.positionScreen,
						element.v2.positionScreen,
						element.vertexColors[ 0 ],
						element.vertexColors[ 1 ],
						shader,
						material
					);

				}

			}

			finishClear();

			var x = Math.min( rectx1, prevrectx1 );
			var y = Math.min( recty1, prevrecty1 );
			var width = Math.max( rectx2, prevrectx2 ) - x;
			var height = Math.max( recty2, prevrecty2 ) - y;

			/*
			// debug; draw zbuffer

			for ( var i = 0, l = zbuffer.length; i < l; i++ ) {

				var o = i * 4;
				var v = (65535 - zbuffer[ i ]) >> 3;
				data[ o + 0 ] = v;
				data[ o + 1 ] = v;
				data[ o + 2 ] = v;
				data[ o + 3 ] = 255;
			}
			*/

			if ( x !== Infinity ) {

				context.putImageData( imagedata, 0, 0, x, y, width, height );

			}

			prevrectx1 = rectx1; prevrecty1 = recty1;
			prevrectx2 = rectx2; prevrecty2 = recty2;

		};

		function getAlpha() {

			return parameters.alpha === true ? clearAlpha : 1;

		}

		function clearColorBuffer( color ) {

			var size = canvasWidth * canvasHeight * 4;

			for ( var i = 0; i < size; i += 4 ) {

				data[ i ] = color.r * 255 | 0;
				data[ i + 1 ] = color.g * 255 | 0;
				data[ i + 2 ] = color.b * 255 | 0;
				data[ i + 3 ] = getAlpha() * 255 | 0;

			}

			context.fillStyle = 'rgba(' + ( ( clearColor.r * 255 ) | 0 ) + ',' + ( ( clearColor.g * 255 ) | 0 ) + ',' + ( ( clearColor.b * 255 ) | 0 ) + ',' + getAlpha() + ')';
			context.fillRect( 0, 0, canvasWidth, canvasHeight );

		}

		function getPalette( material, bSimulateSpecular ) {

			var i = 0, j = 0;
			var diffuseR = material.color.r * 255;
			var diffuseG = material.color.g * 255;
			var diffuseB = material.color.b * 255;
			var palette = new Uint8Array( 256 * 3 );

			if ( bSimulateSpecular ) {

				while ( i < 204 ) {

					palette[ j ++ ] = Math.min( i * diffuseR / 204, 255 );
					palette[ j ++ ] = Math.min( i * diffuseG / 204, 255 );
					palette[ j ++ ] = Math.min( i * diffuseB / 204, 255 );
					++ i;

				}

				while ( i < 256 ) {

					// plus specular highlight
					palette[ j ++ ] = Math.min( diffuseR + ( i - 204 ) * ( 255 - diffuseR ) / 82, 255 );
					palette[ j ++ ] = Math.min( diffuseG + ( i - 204 ) * ( 255 - diffuseG ) / 82, 255 );
					palette[ j ++ ] = Math.min( diffuseB + ( i - 204 ) * ( 255 - diffuseB ) / 82, 255 );
					++ i;

				}

			} else {

				while ( i < 256 ) {

					palette[ j ++ ] = Math.min( i * diffuseR / 255, 255 );
					palette[ j ++ ] = Math.min( i * diffuseG / 255, 255 );
					palette[ j ++ ] = Math.min( i * diffuseB / 255, 255 );
					++ i;

				}

			}

			return palette;

		}

		function basicMaterialShader( buffer, depthBuf, offset, depth, u, v, n, face, material ) {

			var colorOffset = offset * 4;

			var texture = textures[ material.map.id ];

			if ( ! texture.data ) return;

			var tdim = texture.width;
			var isTransparent = material.transparent;
			var tbound = tdim - 1;
			var tdata = texture.data;
			var tIndex = ( ( ( v * tdim ) & tbound ) * tdim + ( ( u * tdim ) & tbound ) ) * 4;

			if ( ! isTransparent ) {

				buffer[ colorOffset ] = tdata[ tIndex ];
				buffer[ colorOffset + 1 ] = tdata[ tIndex + 1 ];
				buffer[ colorOffset + 2 ] = tdata[ tIndex + 2 ];
				buffer[ colorOffset + 3 ] = ( material.opacity << 8 ) - 1;
				depthBuf[ offset ] = depth;

			} else {

				var srcR = tdata[ tIndex ];
				var srcG = tdata[ tIndex + 1 ];
				var srcB = tdata[ tIndex + 2 ];
				var opaci = tdata[ tIndex + 3 ] * material.opacity / 255;
				var destR = buffer[ colorOffset ];
				var destG = buffer[ colorOffset + 1 ];
				var destB = buffer[ colorOffset + 2 ];

				buffer[ colorOffset ] = ( srcR * opaci + destR * ( 1 - opaci ) );
				buffer[ colorOffset + 1 ] = ( srcG * opaci + destG * ( 1 - opaci ) );
				buffer[ colorOffset + 2 ] = ( srcB * opaci + destB * ( 1 - opaci ) );
				buffer[ colorOffset + 3 ] = ( material.opacity << 8 ) - 1;

				// Only opaue pixls write to the depth buffer

				if ( buffer[ colorOffset + 3 ] == 255 )	depthBuf[ offset ] = depth;

			}

		}

		function lightingMaterialShader( buffer, depthBuf, offset, depth, u, v, n, face, material ) {

			var colorOffset = offset * 4;

			var texture = textures[ material.map.id ];

			if ( ! texture.data ) return;

			var tdim = texture.width;
			var isTransparent = material.transparent;
			var cIndex = ( n > 0 ? ( ~ ~ n ) : 0 ) * 3;
			var tbound = tdim - 1;
			var tdata = texture.data;
			var tIndex = ( ( ( v * tdim ) & tbound ) * tdim + ( ( u * tdim ) & tbound ) ) * 4;

			if ( ! isTransparent ) {

				buffer[ colorOffset ] = ( material.palette[ cIndex ] * tdata[ tIndex ] ) >> 8;
				buffer[ colorOffset + 1 ] = ( material.palette[ cIndex + 1 ] * tdata[ tIndex + 1 ] ) >> 8;
				buffer[ colorOffset + 2 ] = ( material.palette[ cIndex + 2 ] * tdata[ tIndex + 2 ] ) >> 8;
				buffer[ colorOffset + 3 ] = ( material.opacity << 8 ) - 1;
				depthBuf[ offset ] = depth;

			} else {

				var foreColorR = material.palette[ cIndex ] * tdata[ tIndex ];
				var foreColorG = material.palette[ cIndex + 1 ] * tdata[ tIndex + 1 ];
				var foreColorB = material.palette[ cIndex + 2 ] * tdata[ tIndex + 2 ];
				var opaci = tdata[ tIndex + 3 ] * material.opacity / 256;
				var destR = buffer[ colorOffset ];
				var destG = buffer[ colorOffset + 1 ];
				var destB = buffer[ colorOffset + 2 ];

				buffer[ colorOffset ] = foreColorR * opaci + destR * ( 1 - opaci );
				buffer[ colorOffset + 1 ] = foreColorG * opaci + destG * ( 1 - opaci );
				buffer[ colorOffset + 2 ] = foreColorB * opaci + destB * ( 1 - opaci );
				buffer[ colorOffset + 3 ] = ( material.opacity << 8 ) - 1;

				// Only opaue pixls write to the depth buffer

				if ( buffer[ colorOffset + 3 ] == 255 ) depthBuf[ offset ] = depth;

			}

		}

		function getMaterialShader( material ) {

			var id = material.id;
			var shader = shaders[ id ];

			if ( shader && material.map && ! textures[ material.map.id ] ) delete shaders[ id ];

			if ( shaders[ id ] === undefined || material.needsUpdate === true ) {

				if ( material instanceof THREE.MeshBasicMaterial ||
					material instanceof THREE.MeshLambertMaterial ||
					material instanceof THREE.MeshPhongMaterial ||
					material instanceof THREE.SpriteMaterial ) {

					if ( material instanceof THREE.MeshLambertMaterial ) {

						// Generate color palette
						if ( ! material.palette ) {

							material.palette = getPalette( material, false );

						}

					} else if ( material instanceof THREE.MeshPhongMaterial ) {

						// Generate color palette
						if ( ! material.palette ) {

							material.palette = getPalette( material, true );

						}

					}

					var string;

					if ( material.map ) {

						var texture = new THREE.SoftwareRenderer.Texture();
						texture.fromImage( material.map.image );

						if ( ! texture.data ) return;

						textures[ material.map.id ] = texture;

						if ( material instanceof THREE.MeshBasicMaterial
							|| material instanceof THREE.SpriteMaterial ) {

							shader = basicMaterialShader;

						} else {

							shader = lightingMaterialShader;

						}


					} else {

						if ( material.vertexColors === THREE.FaceColors ) {

							string = [
								'var colorOffset = offset * 4;',
								'buffer[ colorOffset ] = face.color.r * 255;',
								'buffer[ colorOffset + 1 ] = face.color.g * 255;',
								'buffer[ colorOffset + 2 ] = face.color.b * 255;',
								'buffer[ colorOffset + 3 ] = material.opacity * 255;',
								'depthBuf[ offset ] = depth;'
							].join( '\n' );

						} else {

							string = [
								'var colorOffset = offset * 4;',
								'buffer[ colorOffset ] = material.color.r * 255;',
								'buffer[ colorOffset + 1 ] = material.color.g * 255;',
								'buffer[ colorOffset + 2 ] = material.color.b * 255;',
								'buffer[ colorOffset + 3 ] = material.opacity * 255;',
								'depthBuf[ offset ] = depth;'
							].join( '\n' );

						}

						shader = new Function( 'buffer, depthBuf, offset, depth, u, v, n, face, material', string );

					}

				} else if ( material instanceof THREE.LineBasicMaterial ) {

					var string = [
						'var colorOffset = offset * 4;',
						'buffer[ colorOffset ] = material.color.r * (color1.r+color2.r) * 0.5 * 255;',
						'buffer[ colorOffset + 1 ] = material.color.g * (color1.g+color2.g) * 0.5 * 255;',
						'buffer[ colorOffset + 2 ] = material.color.b * (color1.b+color2.b) * 0.5 * 255;',
						'buffer[ colorOffset + 3 ] = 255;',
						'depthBuf[ offset ] = depth;'
					].join( '\n' );

					shader = new Function( 'buffer, depthBuf, offset, depth, color1, color2, material', string );

				} else {

					var string = [
						'var colorOffset = offset * 4;',
						'buffer[ colorOffset ] = u * 255;',
						'buffer[ colorOffset + 1 ] = v * 255;',
						'buffer[ colorOffset + 2 ] = 0;',
						'buffer[ colorOffset + 3 ] = 255;',
						'depthBuf[ offset ] = depth;'
					].join( '\n' );

					shader = new Function( 'buffer, depthBuf, offset, depth, u, v, n, face, material', string );

				}

				shaders[ id ] = shader;

				material.needsUpdate = false;

			}

			return shader;

		}

		/*
		function clearRectangle( x1, y1, x2, y2 ) {

			var xmin = Math.max( Math.min( x1, x2 ), 0 );
			var xmax = Math.min( Math.max( x1, x2 ), canvasWidth );
			var ymin = Math.max( Math.min( y1, y2 ), 0 );
			var ymax = Math.min( Math.max( y1, y2 ), canvasHeight );

			var offset = ( xmin + ymin * canvasWidth ) * 4 + 3;
			var linestep = ( canvasWidth - ( xmax - xmin ) ) * 4;

			for ( var y = ymin; y < ymax; y ++ ) {

				for ( var x = xmin; x < xmax; x ++ ) {

					data[ offset += 4 ] = 0;

				}

				offset += linestep;

			}

		}
		*/

		function drawTriangle( v1, v2, v3, uv1, uv2, uv3, shader, face, material ) {

			// TODO: Implement per-pixel z-clipping

			if ( v1.z < - 1 || v1.z > 1 || v2.z < - 1 || v2.z > 1 || v3.z < - 1 || v3.z > 1 ) return;

			// https://gist.github.com/2486101
			// explanation: http://pouet.net/topic.php?which=8760&page=1

			var fixscale = ( 1 << subpixelBits );

			// 28.4 fixed-point coordinates

			var x1 = ( v1.x * viewportXScale + viewportXOffs ) | 0;
			var x2 = ( v2.x * viewportXScale + viewportXOffs ) | 0;
			var x3 = ( v3.x * viewportXScale + viewportXOffs ) | 0;

			var y1 = ( v1.y * viewportYScale + viewportYOffs ) | 0;
			var y2 = ( v2.y * viewportYScale + viewportYOffs ) | 0;
			var y3 = ( v3.y * viewportYScale + viewportYOffs ) | 0;

			var bHasNormal = face.vertexNormalsModel && face.vertexNormalsModel.length;
			var bHasUV = uv1 && uv2 && uv3;

			var longestSide = Math.max(
				Math.sqrt( ( x1 - x2 ) * ( x1 - x2 ) + ( y1 - y2 ) * ( y1 - y2 ) ),
				Math.sqrt( ( x2 - x3 ) * ( x2 - x3 ) + ( y2 - y3 ) * ( y2 - y3 ) ),
				Math.sqrt( ( x3 - x1 ) * ( x3 - x1 ) + ( y3 - y1 ) * ( y3 - y1 ) )
			);

			if ( ! ( face instanceof THREE.RenderableSprite ) && ( longestSide > 100 * fixscale ) ) {

				// 1
				// |\
				// |a\
				// |__\
				// |\c|\
				// |b\|d\
				// |__\__\
				// 2      3
				var tempFace = { vertexNormalsModel: [], color: face.color };
				var mpUV12, mpUV23, mpUV31;

				if ( bHasUV ) {

					if ( mpUVPoolCount === mpUVPool.length ) {

						mpUV12 = new THREE.Vector2();
						mpUVPool.push( mpUV12 );
						++ mpUVPoolCount;

						mpUV23 = new THREE.Vector2();
						mpUVPool.push( mpUV23 );
						++ mpUVPoolCount;

						mpUV31 = new THREE.Vector2();
						mpUVPool.push( mpUV31 );
						++ mpUVPoolCount;

					} else {

						mpUV12 = mpUVPool[ mpUVPoolCount ];
						++ mpUVPoolCount;

						mpUV23 = mpUVPool[ mpUVPoolCount ];
						++ mpUVPoolCount;

						mpUV31 = mpUVPool[ mpUVPoolCount ];
						++ mpUVPoolCount;

					}

					var weight;

					weight = ( 1 + v2.z ) * ( v2.w / v1.w ) / ( 1 + v1.z );
					mpUV12.copy( uv1 ).multiplyScalar( weight ).add( uv2 ).multiplyScalar( 1 / ( weight + 1 ) );

					weight = ( 1 + v3.z ) * ( v3.w / v2.w ) / ( 1 + v2.z );
					mpUV23.copy( uv2 ).multiplyScalar( weight ).add( uv3 ).multiplyScalar( 1 / ( weight + 1 ) );

					weight = ( 1 + v1.z ) * ( v1.w / v3.w ) / ( 1 + v3.z );
					mpUV31.copy( uv3 ).multiplyScalar( weight ).add( uv1 ).multiplyScalar( 1 / ( weight + 1 ) );

				}

				var mpV12, mpV23, mpV31;

				if ( mpVPoolCount === mpVPool.length ) {

					mpV12 = new THREE.Vector4();
					mpVPool.push( mpV12 );
					++ mpVPoolCount;

					mpV23 = new THREE.Vector4();
					mpVPool.push( mpV23 );
					++ mpVPoolCount;

					mpV31 = new THREE.Vector4();
					mpVPool.push( mpV31 );
					++ mpVPoolCount;

				} else {

					mpV12 = mpVPool[ mpVPoolCount ];
					++ mpVPoolCount;

					mpV23 = mpVPool[ mpVPoolCount ];
					++ mpVPoolCount;

					mpV31 = mpVPool[ mpVPoolCount ];
					++ mpVPoolCount;

				}

				mpV12.copy( v1 ).add( v2 ).multiplyScalar( 0.5 );
				mpV23.copy( v2 ).add( v3 ).multiplyScalar( 0.5 );
				mpV31.copy( v3 ).add( v1 ).multiplyScalar( 0.5 );

				var mpN12, mpN23, mpN31;

				if ( bHasNormal ) {

					if ( mpNPoolCount === mpNPool.length ) {

						mpN12 = new THREE.Vector3();
						mpNPool.push( mpN12 );
						++ mpNPoolCount;

						mpN23 = new THREE.Vector3();
						mpNPool.push( mpN23 );
						++ mpNPoolCount;

						mpN31 = new THREE.Vector3();
						mpNPool.push( mpN31 );
						++ mpNPoolCount;

					} else {

						mpN12 = mpNPool[ mpNPoolCount ];
						++ mpNPoolCount;

						mpN23 = mpNPool[ mpNPoolCount ];
						++ mpNPoolCount;

						mpN31 = mpNPool[ mpNPoolCount ];
						++ mpNPoolCount;

					}

					mpN12.copy( face.vertexNormalsModel[ 0 ] ).add( face.vertexNormalsModel[ 1 ] ).normalize();
					mpN23.copy( face.vertexNormalsModel[ 1 ] ).add( face.vertexNormalsModel[ 2 ] ).normalize();
					mpN31.copy( face.vertexNormalsModel[ 2 ] ).add( face.vertexNormalsModel[ 0 ] ).normalize();

				}

				// a
				if ( bHasNormal ) {

					tempFace.vertexNormalsModel[ 0 ] = face.vertexNormalsModel[ 0 ];
					tempFace.vertexNormalsModel[ 1 ] = mpN12;
					tempFace.vertexNormalsModel[ 2 ] = mpN31;

				}

				drawTriangle( v1, mpV12, mpV31, uv1, mpUV12, mpUV31, shader, tempFace, material );

				// b
				if ( bHasNormal ) {

					tempFace.vertexNormalsModel[ 0 ] = face.vertexNormalsModel[ 1 ];
					tempFace.vertexNormalsModel[ 1 ] = mpN23;
					tempFace.vertexNormalsModel[ 2 ] = mpN12;

				}

				drawTriangle( v2, mpV23, mpV12, uv2, mpUV23, mpUV12, shader, tempFace, material );

				// c
				if ( bHasNormal ) {

					tempFace.vertexNormalsModel[ 0 ] = mpN12;
					tempFace.vertexNormalsModel[ 1 ] = mpN23;
					tempFace.vertexNormalsModel[ 2 ] = mpN31;

				}

				drawTriangle( mpV12, mpV23, mpV31, mpUV12, mpUV23, mpUV31, shader, tempFace, material );

				// d
				if ( bHasNormal ) {

					tempFace.vertexNormalsModel[ 0 ] = face.vertexNormalsModel[ 2 ];
					tempFace.vertexNormalsModel[ 1 ] = mpN31;
					tempFace.vertexNormalsModel[ 2 ] = mpN23;

				}

				drawTriangle( v3, mpV31, mpV23, uv3, mpUV31, mpUV23, shader, tempFace, material );

				return;

			}

			// Z values (.28 fixed-point)

			var z1 = ( v1.z * viewportZScale + viewportZOffs ) | 0;
			var z2 = ( v2.z * viewportZScale + viewportZOffs ) | 0;
			var z3 = ( v3.z * viewportZScale + viewportZOffs ) | 0;

			// UV values
			var bHasUV = false;
			var tu1, tv1, tu2, tv2, tu3, tv3;

			if ( uv1 && uv2 && uv3 ) {

				bHasUV = true;

				tu1 = uv1.x;
				tv1 = 1 - uv1.y;
				tu2 = uv2.x;
				tv2 = 1 - uv2.y;
				tu3 = uv3.x;
				tv3 = 1 - uv3.y;

			}

			// Normal values
			var n1, n2, n3, nz1, nz2, nz3;

			if ( bHasNormal ) {

				n1 = face.vertexNormalsModel[ 0 ];
				n2 = face.vertexNormalsModel[ 1 ];
				n3 = face.vertexNormalsModel[ 2 ];
				nz1 = n1.z * 255;
				nz2 = n2.z * 255;
				nz3 = n3.z * 255;

			}

			// Deltas

			var dx12 = x1 - x2, dy12 = y2 - y1;
			var dx23 = x2 - x3, dy23 = y3 - y2;
			var dx31 = x3 - x1, dy31 = y1 - y3;

			// Bounding rectangle

			var minx = Math.max( ( Math.min( x1, x2, x3 ) + subpixelBias ) >> subpixelBits, 0 );
			var maxx = Math.min( ( Math.max( x1, x2, x3 ) + subpixelBias ) >> subpixelBits, canvasWidth );
			var miny = Math.max( ( Math.min( y1, y2, y3 ) + subpixelBias ) >> subpixelBits, 0 );
			var maxy = Math.min( ( Math.max( y1, y2, y3 ) + subpixelBias ) >> subpixelBits, canvasHeight );

			rectx1 = Math.min( minx, rectx1 );
			rectx2 = Math.max( maxx, rectx2 );
			recty1 = Math.min( miny, recty1 );
			recty2 = Math.max( maxy, recty2 );

			// Block size, standard 8x8 (must be power of two)

			var q = blockSize;

			// Start in corner of 8x8 block

			minx &= ~ ( q - 1 );
			miny &= ~ ( q - 1 );

			// Constant part of half-edge functions

			var minXfixscale = ( minx << subpixelBits );
			var minYfixscale = ( miny << subpixelBits );

			var c1 = dy12 * ( ( minXfixscale ) - x1 ) + dx12 * ( ( minYfixscale ) - y1 );
			var c2 = dy23 * ( ( minXfixscale ) - x2 ) + dx23 * ( ( minYfixscale ) - y2 );
			var c3 = dy31 * ( ( minXfixscale ) - x3 ) + dx31 * ( ( minYfixscale ) - y3 );

			// Correct for fill convention

			if ( dy12 > 0 || ( dy12 == 0 && dx12 > 0 ) ) c1 ++;
			if ( dy23 > 0 || ( dy23 == 0 && dx23 > 0 ) ) c2 ++;
			if ( dy31 > 0 || ( dy31 == 0 && dx31 > 0 ) ) c3 ++;

			// Note this doesn't kill subpixel precision, but only because we test for >=0 (not >0).
			// It's a bit subtle. :)
			c1 = ( c1 - 1 ) >> subpixelBits;
			c2 = ( c2 - 1 ) >> subpixelBits;
			c3 = ( c3 - 1 ) >> subpixelBits;

			// Z interpolation setup

			var dz12 = z1 - z2, dz31 = z3 - z1;
			var invDet = 1.0 / ( dx12 * dy31 - dx31 * dy12 );
			var dzdx = ( invDet * ( dz12 * dy31 - dz31 * dy12 ) ); // dz per one subpixel step in x
			var dzdy = ( invDet * ( dz12 * dx31 - dx12 * dz31 ) ); // dz per one subpixel step in y

			// Z at top/left corner of rast area

			var cz = ( z1 + ( ( minXfixscale ) - x1 ) * dzdx + ( ( minYfixscale ) - y1 ) * dzdy ) | 0;

			// Z pixel steps

			dzdx = ( dzdx * fixscale ) | 0;
			dzdy = ( dzdy * fixscale ) | 0;

			var dtvdx, dtvdy, cbtu, cbtv;
			if ( bHasUV ) {

				// UV interpolation setup
				var dtu12 = tu1 - tu2, dtu31 = tu3 - tu1;
				var dtudx = ( invDet * ( dtu12 * dy31 - dtu31 * dy12 ) ); // dtu per one subpixel step in x
				var dtudy = ( invDet * ( dtu12 * dx31 - dx12 * dtu31 ) ); // dtu per one subpixel step in y
				var dtv12 = tv1 - tv2, dtv31 = tv3 - tv1;
				dtvdx = ( invDet * ( dtv12 * dy31 - dtv31 * dy12 ) ); // dtv per one subpixel step in x
				dtvdy = ( invDet * ( dtv12 * dx31 - dx12 * dtv31 ) ); // dtv per one subpixel step in y

				// UV at top/left corner of rast area
				cbtu = ( tu1 + ( minXfixscale - x1 ) * dtudx + ( minYfixscale - y1 ) * dtudy );
				cbtv = ( tv1 + ( minXfixscale - x1 ) * dtvdx + ( minYfixscale - y1 ) * dtvdy );

				// UV pixel steps
				dtudx = dtudx * fixscale;
				dtudy = dtudy * fixscale;
				dtvdx = dtvdx * fixscale;
				dtvdy = dtvdy * fixscale;

			}

			var dnzdy, cbnz;

			if ( bHasNormal ) {

				 // Normal interpolation setup
				var dnz12 = nz1 - nz2, dnz31 = nz3 - nz1;
				var dnzdx = ( invDet * ( dnz12 * dy31 - dnz31 * dy12 ) ); // dnz per one subpixel step in x
				var dnzdy = ( invDet * ( dnz12 * dx31 - dx12 * dnz31 ) ); // dnz per one subpixel step in y

				// Normal at top/left corner of rast area
				cbnz = ( nz1 + ( minXfixscale - x1 ) * dnzdx + ( minYfixscale - y1 ) * dnzdy );

				// Normal pixel steps
				dnzdx = ( dnzdx * fixscale );
				dnzdy = ( dnzdy * fixscale );

			}

			// Set up min/max corners
			var qm1 = q - 1; // for convenience
			var nmin1 = 0, nmax1 = 0;
			var nmin2 = 0, nmax2 = 0;
			var nmin3 = 0, nmax3 = 0;
			var nminz = 0, nmaxz = 0;
			if ( dx12 >= 0 ) nmax1 -= qm1 * dx12; else nmin1 -= qm1 * dx12;
			if ( dy12 >= 0 ) nmax1 -= qm1 * dy12; else nmin1 -= qm1 * dy12;
			if ( dx23 >= 0 ) nmax2 -= qm1 * dx23; else nmin2 -= qm1 * dx23;
			if ( dy23 >= 0 ) nmax2 -= qm1 * dy23; else nmin2 -= qm1 * dy23;
			if ( dx31 >= 0 ) nmax3 -= qm1 * dx31; else nmin3 -= qm1 * dx31;
			if ( dy31 >= 0 ) nmax3 -= qm1 * dy31; else nmin3 -= qm1 * dy31;
			if ( dzdx >= 0 ) nmaxz += qm1 * dzdx; else nminz += qm1 * dzdx;
			if ( dzdy >= 0 ) nmaxz += qm1 * dzdy; else nminz += qm1 * dzdy;

			// Loop through blocks
			var linestep = canvasWidth - q;

			var cb1 = c1;
			var cb2 = c2;
			var cb3 = c3;
			var cbz = cz;
			var qstep = - q;
			var e1x = qstep * dy12;
			var e2x = qstep * dy23;
			var e3x = qstep * dy31;
			var ezx = qstep * dzdx;

			var etux, etvx;
			if ( bHasUV ) {

				etux = qstep * dtudx;
				etvx = qstep * dtvdx;

			}

			var enzx;
			if ( bHasNormal ) {

				enzx = qstep * dnzdx;

			}

			var x0 = minx;

			for ( var y0 = miny; y0 < maxy; y0 += q ) {

				// New block line - keep hunting for tri outer edge in old block line dir
				while ( x0 >= minx && x0 < maxx && cb1 >= nmax1 && cb2 >= nmax2 && cb3 >= nmax3 ) {

					x0 += qstep;
					cb1 += e1x;
					cb2 += e2x;
					cb3 += e3x;
					cbz += ezx;

					if ( bHasUV ) {

						cbtu += etux;
						cbtv += etvx;

					}

					if ( bHasNormal ) {

						cbnz += enzx;

					}

				}

				// Okay, we're now in a block we know is outside. Reverse direction and go into main loop.
				qstep = - qstep;
				e1x = - e1x;
				e2x = - e2x;
				e3x = - e3x;
				ezx = - ezx;

				if ( bHasUV ) {

					etux = - etux;
					etvx = - etvx;

				}

				if ( bHasNormal ) {

					enzx = - enzx;

				}

				while ( 1 ) {

					// Step everything
					x0 += qstep;
					cb1 += e1x;
					cb2 += e2x;
					cb3 += e3x;
					cbz += ezx;

					if ( bHasUV ) {

						cbtu += etux;
						cbtv += etvx;

					}

					if ( bHasNormal ) {

						cbnz += enzx;

					}

					// We're done with this block line when at least one edge completely out
					// If an edge function is too small and decreasing in the current traversal
					// dir, we're done with this line.
					if ( x0 < minx || x0 >= maxx ) break;
					if ( cb1 < nmax1 ) if ( e1x < 0 ) break; else continue;
					if ( cb2 < nmax2 ) if ( e2x < 0 ) break; else continue;
					if ( cb3 < nmax3 ) if ( e3x < 0 ) break; else continue;

					// We can skip this block if it's already fully covered
					var blockX = x0 >> blockShift;
					var blockY = y0 >> blockShift;
					var blockId = blockX + blockY * canvasWBlocks;
					var minz = cbz + nminz;

					// farthest point in block closer than closest point in our tri?
					if ( blockMaxZ[ blockId ] < minz ) continue;

					// Need to do a deferred clear?
					var bflags = blockFlags[ blockId ];
					if ( bflags & BLOCK_NEEDCLEAR ) clearBlock( blockX, blockY );
					blockFlags[ blockId ] = bflags & ~ ( BLOCK_ISCLEAR | BLOCK_NEEDCLEAR );

					// Offset at top-left corner
					var offset = x0 + y0 * canvasWidth;

					// Accept whole block when fully covered
					if ( cb1 >= nmin1 && cb2 >= nmin2 && cb3 >= nmin3 ) {

						var maxz = cbz + nmaxz;
						blockMaxZ[ blockId ] = Math.min( blockMaxZ[ blockId ], maxz );

						var cy1 = cb1;
						var cy2 = cb2;
						var cyz = cbz;

						var cytu, cytv;
						if ( bHasUV ) {

							cytu = cbtu;
							cytv = cbtv;

						}

						var cynz;
						if ( bHasNormal ) {

							cynz = cbnz;

						}


						for ( var iy = 0; iy < q; iy ++ ) {

							var cx1 = cy1;
							var cx2 = cy2;
							var cxz = cyz;

							var cxtu;
							var cxtv;
							if ( bHasUV ) {

								cxtu = cytu;
								cxtv = cytv;

							}

							var cxnz;
							if ( bHasNormal ) {

								cxnz = cynz;

							}

							for ( var ix = 0; ix < q; ix ++ ) {

								var z = cxz;

								if ( z < zbuffer[ offset ] ) {

									shader( data, zbuffer, offset, z, cxtu, cxtv, cxnz, face, material );

								}

								cx1 += dy12;
								cx2 += dy23;
								cxz += dzdx;

								if ( bHasUV ) {

									cxtu += dtudx;
									cxtv += dtvdx;

								}

								if ( bHasNormal ) {

									cxnz += dnzdx;

								}

								offset ++;

							}

							cy1 += dx12;
							cy2 += dx23;
							cyz += dzdy;

							if ( bHasUV ) {

								cytu += dtudy;
								cytv += dtvdy;

							}

							if ( bHasNormal ) {

								cynz += dnzdy;

							}

							offset += linestep;

						}

					} else {

						// Partially covered block

						var cy1 = cb1;
						var cy2 = cb2;
						var cy3 = cb3;
						var cyz = cbz;

						var cytu, cytv;
						if ( bHasUV ) {

							cytu = cbtu;
							cytv = cbtv;

						}

						var cynz;
						if ( bHasNormal ) {

							cynz = cbnz;

						}

						for ( var iy = 0; iy < q; iy ++ ) {

							var cx1 = cy1;
							var cx2 = cy2;
							var cx3 = cy3;
							var cxz = cyz;

							var cxtu;
							var cxtv;
							if ( bHasUV ) {

								cxtu = cytu;
								cxtv = cytv;

							}

							var cxnz;
							if ( bHasNormal ) {

								cxnz = cynz;

							}

							for ( var ix = 0; ix < q; ix ++ ) {

								if ( ( cx1 | cx2 | cx3 ) >= 0 ) {

									var z = cxz;

									if ( z < zbuffer[ offset ] ) {

										shader( data, zbuffer, offset, z, cxtu, cxtv, cxnz, face, material );

									}

								}

								cx1 += dy12;
								cx2 += dy23;
								cx3 += dy31;
								cxz += dzdx;

								if ( bHasUV ) {

									cxtu += dtudx;
									cxtv += dtvdx;

								}

								if ( bHasNormal ) {

									cxnz += dnzdx;

								}

								offset ++;

							}

							cy1 += dx12;
							cy2 += dx23;
							cy3 += dx31;
							cyz += dzdy;

							if ( bHasUV ) {

								cytu += dtudy;
								cytv += dtvdy;

							}

							if ( bHasNormal ) {

								cynz += dnzdy;

							}

							offset += linestep;

						}

					}

				}

				// Advance to next row of blocks
				cb1 += q * dx12;
				cb2 += q * dx23;
				cb3 += q * dx31;
				cbz += q * dzdy;

				if ( bHasUV ) {

					cbtu += q * dtudy;
					cbtv += q * dtvdy;

				}

				if ( bHasNormal ) {

					cbnz += q * dnzdy;

				}

			}

		}

		// When drawing line, the blockShiftShift has to be zero. In order to clean pixel
		// Using color1 and color2 to interpolation pixel color
		// LineWidth is according to material.linewidth
		function drawLine( v1, v2, color1, color2, shader, material ) {

			// While the line mode is enable, blockSize has to be changed to 0.
			if ( ! lineMode ) {

				lineMode = true;
				blockShift = 0;
				blockSize = 1 << blockShift;

				_this.setSize( canvas.width, canvas.height );

			}

			// TODO: Implement per-pixel z-clipping
			if ( v1.z < - 1 || v1.z > 1 || v2.z < - 1 || v2.z > 1 ) return;

			var halfLineWidth = Math.floor( ( material.linewidth - 1 ) * 0.5 );

			// https://gist.github.com/2486101
			// explanation: http://pouet.net/topic.php?which=8760&page=1

			// 28.4 fixed-point coordinates
			var x1 = ( v1.x * viewportXScale + viewportXOffs ) | 0;
			var x2 = ( v2.x * viewportXScale + viewportXOffs ) | 0;

			var y1 = ( v1.y * viewportYScale + viewportYOffs ) | 0;
			var y2 = ( v2.y * viewportYScale + viewportYOffs ) | 0;

			var z1 = ( v1.z * viewportZScale + viewportZOffs ) | 0;
			var z2 = ( v2.z * viewportZScale + viewportZOffs ) | 0;

			// Deltas
			var dx12 = x1 - x2, dy12 = y1 - y2, dz12 = z1 - z2;

			// Bounding rectangle
			var minx = Math.max( ( Math.min( x1, x2 ) + subpixelBias ) >> subpixelBits, 0 );
			var maxx = Math.min( ( Math.max( x1, x2 ) + subpixelBias ) >> subpixelBits, canvasWidth );
			var miny = Math.max( ( Math.min( y1, y2 ) + subpixelBias ) >> subpixelBits, 0 );
			var maxy = Math.min( ( Math.max( y1, y2 ) + subpixelBias ) >> subpixelBits, canvasHeight );
			var minz = Math.max( ( Math.min( z1, z2 ) + subpixelBias ) >> subpixelBits, 0 );
			var maxz = ( Math.max( z1, z2 ) + subpixelBias ) >> subpixelBits;

			rectx1 = Math.min( minx, rectx1 );
			rectx2 = Math.max( maxx, rectx2 );
			recty1 = Math.min( miny, recty1 );
			recty2 = Math.max( maxy, recty2 );

			// Get the line's unit vector and cross vector
			var length = Math.sqrt( ( dy12 * dy12 ) + ( dx12 * dx12 ) );
			var unitX = ( dx12 / length );
			var unitY = ( dy12 / length );
			var unitZ = ( dz12 / length );
			var pixelX, pixelY, pixelZ;
			var pX, pY, pZ;
			crossVector.set( unitX, unitY, unitZ );
			crossVector.cross( lookVector );
			crossVector.normalize();

			while ( length > 0 ) {

				// Get this pixel.
				pixelX = x2 + length * unitX;
				pixelY = y2 + length * unitY;
				pixelZ = z2 + length * unitZ;

				pixelX = ( pixelX + subpixelBias ) >> subpixelBits;
				pixelY = ( pixelY + subpixelBias ) >> subpixelBits;
				pZ = ( pixelZ + subpixelBias ) >> subpixelBits;

				// Draw line with line width
				for ( var i = - halfLineWidth; i <= halfLineWidth; ++ i ) {

					// Compute the line pixels.
					// Get the pixels on the vector that crosses to the line vector
					pX = Math.floor( ( pixelX + crossVector.x * i ) );
					pY = Math.floor( ( pixelY + crossVector.y * i ) );

					// if pixel is over the rect. Continue
					if ( rectx1 >= pX || rectx2 <= pX || recty1 >= pY || recty2 <= pY )
						continue;

					// Find this pixel at which block
					var blockX = pX >> blockShift;
					var blockY = pY >> blockShift;
					var blockId = blockX + blockY * canvasWBlocks;

					// Compare the pixel depth width z block.
					if ( blockMaxZ[ blockId ] < minz ) continue;

					blockMaxZ[ blockId ] = Math.min( blockMaxZ[ blockId ], maxz );

					var bflags = blockFlags[ blockId ];
					if ( bflags & BLOCK_NEEDCLEAR ) clearBlock( blockX, blockY );
					blockFlags[ blockId ] = bflags & ~ ( BLOCK_ISCLEAR | BLOCK_NEEDCLEAR );

					// draw pixel
					var offset = pX + pY * canvasWidth;

					if ( pZ < zbuffer[ offset ] ) {

						shader( data, zbuffer, offset, pZ, color1, color2, material );

					}

				}

				-- length;

			}

		}

		function clearBlock( blockX, blockY ) {

			var zoffset = blockX * blockSize + blockY * blockSize * canvasWidth;
			var poffset = zoffset * 4;

			var zlinestep = canvasWidth - blockSize;
			var plinestep = zlinestep * 4;

			for ( var y = 0; y < blockSize; y ++ ) {

				for ( var x = 0; x < blockSize; x ++ ) {

					zbuffer[ zoffset ++ ] = maxZVal;

					data[ poffset ++ ] = clearColor.r * 255 | 0;
					data[ poffset ++ ] = clearColor.g * 255 | 0;
					data[ poffset ++ ] = clearColor.b * 255 | 0;
					data[ poffset ++ ] = getAlpha() * 255 | 0;

				}

				zoffset += zlinestep;
				poffset += plinestep;

			}

		}

		function finishClear( ) {

			var block = 0;

			for ( var y = 0; y < canvasHBlocks; y ++ ) {

				for ( var x = 0; x < canvasWBlocks; x ++ ) {

					if ( blockFlags[ block ] & BLOCK_NEEDCLEAR ) {

						clearBlock( x, y );
						blockFlags[ block ] = BLOCK_ISCLEAR;

					}

					block ++;

				}

			}

		}

	};

	THREE.SoftwareRenderer.Texture = function () {

		var canvas;

		this.fromImage = function ( image ) {

			if ( ! image || image.width <= 0 || image.height <= 0 )
				return;

			if ( canvas === undefined ) {

				canvas = document.createElement( 'canvas' );

			}

			var size = image.width > image.height ? image.width : image.height;
			size = THREE.Math.ceilPowerOfTwo( size );

			if ( canvas.width != size || canvas.height != size ) {

				canvas.width = size;
				canvas.height = size;

			}

			var ctx = canvas.getContext( '2d' );
			ctx.clearRect( 0, 0, size, size );
			ctx.drawImage( image, 0, 0, size, size );

			var imgData = ctx.getImageData( 0, 0, size, size );

			this.data = imgData.data;
			this.width = size;
			this.height = size;
			this.srcUrl = image.src;

		};

	};

	/**
	 * @author mrdoob / http://mrdoob.com/
	 */

	THREE.SVGObject = function ( node ) {

		THREE.Object3D.call( this );

		this.node = node;

	};

	THREE.SVGObject.prototype = Object.create( THREE.Object3D.prototype );
	THREE.SVGObject.prototype.constructor = THREE.SVGObject;

	THREE.SVGRenderer = function () {

		console.log( 'THREE.SVGRenderer', THREE.REVISION );

		var _this = this,
			_renderData, _elements, _lights,
			_projector = new THREE.Projector(),
			_svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ),
			_svgWidth, _svgHeight, _svgWidthHalf, _svgHeightHalf,

			_v1, _v2, _v3,

			_clipBox = new THREE.Box2(),
			_elemBox = new THREE.Box2(),

			_color = new THREE.Color(),
			_diffuseColor = new THREE.Color(),
			_ambientLight = new THREE.Color(),
			_directionalLights = new THREE.Color(),
			_pointLights = new THREE.Color(),
			_clearColor = new THREE.Color(),
			_clearAlpha = 1,

			_vector3 = new THREE.Vector3(), // Needed for PointLight
			_centroid = new THREE.Vector3(),
			_normal = new THREE.Vector3(),
			_normalViewMatrix = new THREE.Matrix3(),

			_viewMatrix = new THREE.Matrix4(),
			_viewProjectionMatrix = new THREE.Matrix4(),

			_svgPathPool = [],
			_svgNode, _pathCount = 0,

			_currentPath, _currentStyle,

			_quality = 1, _precision = null;

		this.domElement = _svg;

		this.autoClear = true;
		this.sortObjects = true;
		this.sortElements = true;

		this.info = {

			render: {

				vertices: 0,
				faces: 0

			}

		};

		this.setQuality = function ( quality ) {

			switch ( quality ) {

				case "high": _quality = 1; break;
				case "low": _quality = 0; break;

			}

		};

		this.setClearColor = function ( color, alpha ) {

			_clearColor.set( color );
			_clearAlpha = alpha !== undefined ? alpha : 1;

		};

		this.setPixelRatio = function () {};

		this.setSize = function ( width, height ) {

			_svgWidth = width; _svgHeight = height;
			_svgWidthHalf = _svgWidth / 2; _svgHeightHalf = _svgHeight / 2;

			_svg.setAttribute( 'viewBox', ( - _svgWidthHalf ) + ' ' + ( - _svgHeightHalf ) + ' ' + _svgWidth + ' ' + _svgHeight );
			_svg.setAttribute( 'width', _svgWidth );
			_svg.setAttribute( 'height', _svgHeight );

			_clipBox.min.set( - _svgWidthHalf, - _svgHeightHalf );
			_clipBox.max.set( _svgWidthHalf, _svgHeightHalf );

		};

		this.setPrecision = function ( precision ) {

			_precision = precision;

		};

		function removeChildNodes() {

			_pathCount = 0;

			while ( _svg.childNodes.length > 0 ) {

				_svg.removeChild( _svg.childNodes[ 0 ] );

			}

		}

		function getSvgColor( color, opacity ) {

			var arg = Math.floor( color.r * 255 ) + ',' + Math.floor( color.g * 255 ) + ',' + Math.floor( color.b * 255 );

			if ( opacity === undefined || opacity === 1 ) return 'rgb(' + arg + ')';

			return 'rgb(' + arg + '); fill-opacity: ' + opacity;

		}

		function convert( c ) {

			return _precision !== null ? c.toFixed( _precision ) : c;

		}

		this.clear = function () {

			removeChildNodes();
			_svg.style.backgroundColor = getSvgColor( _clearColor, _clearAlpha );

		};

		this.render = function ( scene, camera ) {

			if ( camera instanceof THREE.Camera === false ) {

				console.error( 'THREE.SVGRenderer.render: camera is not an instance of THREE.Camera.' );
				return;

			}

			var background = scene.background;

			if ( background && background.isColor ) {

				removeChildNodes();
				_svg.style.backgroundColor = getSvgColor( background );

			} else if ( this.autoClear === true ) {

				this.clear();

			}

			_this.info.render.vertices = 0;
			_this.info.render.faces = 0;

			_viewMatrix.copy( camera.matrixWorldInverse );
			_viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, _viewMatrix );

			_renderData = _projector.projectScene( scene, camera, this.sortObjects, this.sortElements );
			_elements = _renderData.elements;
			_lights = _renderData.lights;

			_normalViewMatrix.getNormalMatrix( camera.matrixWorldInverse );

			calculateLights( _lights );

			 // reset accumulated path

			_currentPath = '';
			_currentStyle = '';

			for ( var e = 0, el = _elements.length; e < el; e ++ ) {

				var element = _elements[ e ];
				var material = element.material;

				if ( material === undefined || material.opacity === 0 ) continue;

				_elemBox.makeEmpty();

				if ( element instanceof THREE.RenderableSprite ) {

					_v1 = element;
					_v1.x *= _svgWidthHalf; _v1.y *= - _svgHeightHalf;

					renderSprite( _v1, element, material );

				} else if ( element instanceof THREE.RenderableLine ) {

					_v1 = element.v1; _v2 = element.v2;

					_v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
					_v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;

					_elemBox.setFromPoints( [ _v1.positionScreen, _v2.positionScreen ] );

					if ( _clipBox.intersectsBox( _elemBox ) === true ) {

						renderLine( _v1, _v2, element, material );

					}

				} else if ( element instanceof THREE.RenderableFace ) {

					_v1 = element.v1; _v2 = element.v2; _v3 = element.v3;

					if ( _v1.positionScreen.z < - 1 || _v1.positionScreen.z > 1 ) continue;
					if ( _v2.positionScreen.z < - 1 || _v2.positionScreen.z > 1 ) continue;
					if ( _v3.positionScreen.z < - 1 || _v3.positionScreen.z > 1 ) continue;

					_v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf;
					_v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf;
					_v3.positionScreen.x *= _svgWidthHalf; _v3.positionScreen.y *= - _svgHeightHalf;

					_elemBox.setFromPoints( [
						_v1.positionScreen,
						_v2.positionScreen,
						_v3.positionScreen
					] );

					if ( _clipBox.intersectsBox( _elemBox ) === true ) {

						renderFace3( _v1, _v2, _v3, element, material );

					}

				}

			}

			flushPath(); // just to flush last svg:path

			scene.traverseVisible( function ( object ) {

				 if ( object instanceof THREE.SVGObject ) {

					_vector3.setFromMatrixPosition( object.matrixWorld );
					_vector3.applyMatrix4( _viewProjectionMatrix );

					var x = _vector3.x * _svgWidthHalf;
					var y = - _vector3.y * _svgHeightHalf;

					var node = object.node;
					node.setAttribute( 'transform', 'translate(' + x + ',' + y + ')' );

					_svg.appendChild( node );

				}

			} );

		};

		function calculateLights( lights ) {

			_ambientLight.setRGB( 0, 0, 0 );
			_directionalLights.setRGB( 0, 0, 0 );
			_pointLights.setRGB( 0, 0, 0 );

			for ( var l = 0, ll = lights.length; l < ll; l ++ ) {

				var light = lights[ l ];
				var lightColor = light.color;

				if ( light.isAmbientLight ) {

					_ambientLight.r += lightColor.r;
					_ambientLight.g += lightColor.g;
					_ambientLight.b += lightColor.b;

				} else if ( light.isDirectionalLight ) {

					_directionalLights.r += lightColor.r;
					_directionalLights.g += lightColor.g;
					_directionalLights.b += lightColor.b;

				} else if ( light.isPointLight ) {

					_pointLights.r += lightColor.r;
					_pointLights.g += lightColor.g;
					_pointLights.b += lightColor.b;

				}

			}

		}

		function calculateLight( lights, position, normal, color ) {

			for ( var l = 0, ll = lights.length; l < ll; l ++ ) {

				var light = lights[ l ];
				var lightColor = light.color;

				if ( light.isDirectionalLight ) {

					var lightPosition = _vector3.setFromMatrixPosition( light.matrixWorld ).normalize();

					var amount = normal.dot( lightPosition );

					if ( amount <= 0 ) continue;

					amount *= light.intensity;

					color.r += lightColor.r * amount;
					color.g += lightColor.g * amount;
					color.b += lightColor.b * amount;

				} else if ( light.isPointLight ) {

					var lightPosition = _vector3.setFromMatrixPosition( light.matrixWorld );

					var amount = normal.dot( _vector3.subVectors( lightPosition, position ).normalize() );

					if ( amount <= 0 ) continue;

					amount *= light.distance == 0 ? 1 : 1 - Math.min( position.distanceTo( lightPosition ) / light.distance, 1 );

					if ( amount == 0 ) continue;

					amount *= light.intensity;

					color.r += lightColor.r * amount;
					color.g += lightColor.g * amount;
					color.b += lightColor.b * amount;

				}

			}

		}

		function renderSprite( v1, element, material ) {

			var scaleX = element.scale.x * _svgWidthHalf;
			var scaleY = element.scale.y * _svgHeightHalf;

			if ( material.isPointsMaterial ) {

				scaleX *= material.size;
				scaleY *= material.size;

			}

			var path = 'M' + convert( v1.x - scaleX * 0.5 ) + ',' + convert( v1.y - scaleY * 0.5 ) + 'h' + convert( scaleX ) + 'v' + convert( scaleY ) + 'h' + convert( - scaleX ) + 'z';
			var style = "";

			if ( material.isSpriteMaterial || material.isPointsMaterial ) {

				style = 'fill:' + getSvgColor( material.color, material.opacity );

			}

			addPath( style, path );

		}

		function renderLine( v1, v2, element, material ) {

			var path = 'M' + convert( v1.positionScreen.x ) + ',' + convert( v1.positionScreen.y ) + 'L' + convert( v2.positionScreen.x ) + ',' + convert( v2.positionScreen.y );

			if ( material.isLineBasicMaterial ) {

				var style = 'fill:none;stroke:' + getSvgColor( material.color, material.opacity ) + ';stroke-width:' + material.linewidth + ';stroke-linecap:' + material.linecap;

				if ( material.isLineDashedMaterial ) {

					style = style + ';stroke-dasharray:' + material.dashSize + "," + material.gapSize;

				}

				addPath( style, path );

			}

		}

		function renderFace3( v1, v2, v3, element, material ) {

			_this.info.render.vertices += 3;
			_this.info.render.faces ++;

			var path = 'M' + convert( v1.positionScreen.x ) + ',' + convert( v1.positionScreen.y ) + 'L' + convert( v2.positionScreen.x ) + ',' + convert( v2.positionScreen.y ) + 'L' + convert( v3.positionScreen.x ) + ',' + convert( v3.positionScreen.y ) + 'z';
			var style = '';

			if ( material.isMeshBasicMaterial ) {

				_color.copy( material.color );

				if ( material.vertexColors === THREE.FaceColors ) {

					_color.multiply( element.color );

				}

			} else if ( material.isMeshLambertMaterial || material.isMeshPhongMaterial || material.isMeshStandardMaterial ) {

				_diffuseColor.copy( material.color );

				if ( material.vertexColors === THREE.FaceColors ) {

					_diffuseColor.multiply( element.color );

				}

				_color.copy( _ambientLight );

				_centroid.copy( v1.positionWorld ).add( v2.positionWorld ).add( v3.positionWorld ).divideScalar( 3 );

				calculateLight( _lights, _centroid, element.normalModel, _color );

				_color.multiply( _diffuseColor ).add( material.emissive );

			} else if ( material.isMeshNormalMaterial ) {

				_normal.copy( element.normalModel ).applyMatrix3( _normalViewMatrix );

				_color.setRGB( _normal.x, _normal.y, _normal.z ).multiplyScalar( 0.5 ).addScalar( 0.5 );

			}

			if ( material.wireframe ) {

				style = 'fill:none;stroke:' + getSvgColor( _color, material.opacity ) + ';stroke-width:' + material.wireframeLinewidth + ';stroke-linecap:' + material.wireframeLinecap + ';stroke-linejoin:' + material.wireframeLinejoin;

			} else {

				style = 'fill:' + getSvgColor( _color, material.opacity );

			}

			addPath( style, path );

		}

		function addPath( style, path ) {

			if ( _currentStyle === style ) {

				_currentPath += path;

			} else {

				flushPath();

				_currentStyle = style;
				_currentPath = path;

			}

		}

		function flushPath() {

			if ( _currentPath ) {

				_svgNode = getPathNode( _pathCount ++ );
				_svgNode.setAttribute( 'd', _currentPath );
				_svgNode.setAttribute( 'style', _currentStyle );
				_svg.appendChild( _svgNode );

			}

			_currentPath = '';
			_currentStyle = '';

		}

		function getPathNode( id ) {

			if ( _svgPathPool[ id ] == null ) {

				_svgPathPool[ id ] = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );

				if ( _quality == 0 ) {

					_svgPathPool[ id ].setAttribute( 'shape-rendering', 'crispEdges' ); //optimizeSpeed

				}

				return _svgPathPool[ id ];

			}

			return _svgPathPool[ id ];

		}

	};

	/**
	 * @author dmarcos / https://github.com/dmarcos
	 * @author mrdoob / http://mrdoob.com
	 *
	 * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html
	 *
	 * Firefox: http://mozvr.com/downloads/
	 * Chromium: https://webvr.info/get-chrome
	 */

	THREE.VREffect = function ( renderer, onError ) {

		var vrDisplay, vrDisplays;
		var eyeTranslationL = new THREE.Vector3();
		var eyeTranslationR = new THREE.Vector3();
		var renderRectL, renderRectR;
		var headMatrix = new THREE.Matrix4();
		var eyeMatrixL = new THREE.Matrix4();
		var eyeMatrixR = new THREE.Matrix4();

		var frameData = null;

		if ( 'VRFrameData' in window ) {

			frameData = new window.VRFrameData();

		}

		function gotVRDisplays( displays ) {

			vrDisplays = displays;

			if ( displays.length > 0 ) {

				vrDisplay = displays[ 0 ];

			} else {

				if ( onError ) onError( 'HMD not available' );

			}

		}

		if ( navigator.getVRDisplays ) {

			navigator.getVRDisplays().then( gotVRDisplays ).catch( function () {

				console.warn( 'THREE.VREffect: Unable to get VR Displays' );

			} );

		}

		//

		this.isPresenting = false;

		var scope = this;

		var rendererSize = renderer.getSize();
		var rendererUpdateStyle = false;
		var rendererPixelRatio = renderer.getPixelRatio();

		this.getVRDisplay = function () {

			return vrDisplay;

		};

		this.setVRDisplay = function ( value ) {

			vrDisplay = value;

		};

		this.getVRDisplays = function () {

			console.warn( 'THREE.VREffect: getVRDisplays() is being deprecated.' );
			return vrDisplays;

		};

		this.setSize = function ( width, height, updateStyle ) {

			rendererSize = { width: width, height: height };
			rendererUpdateStyle = updateStyle;

			if ( scope.isPresenting ) {

				var eyeParamsL = vrDisplay.getEyeParameters( 'left' );
				renderer.setPixelRatio( 1 );
				renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false );

			} else {

				renderer.setPixelRatio( rendererPixelRatio );
				renderer.setSize( width, height, updateStyle );

			}

		};

		// VR presentation

		var canvas = renderer.domElement;
		var defaultLeftBounds = [ 0.0, 0.0, 0.5, 1.0 ];
		var defaultRightBounds = [ 0.5, 0.0, 0.5, 1.0 ];

		function onVRDisplayPresentChange() {

			var wasPresenting = scope.isPresenting;
			scope.isPresenting = vrDisplay !== undefined && vrDisplay.isPresenting;

			if ( scope.isPresenting ) {

				var eyeParamsL = vrDisplay.getEyeParameters( 'left' );
				var eyeWidth = eyeParamsL.renderWidth;
				var eyeHeight = eyeParamsL.renderHeight;

				if ( ! wasPresenting ) {

					rendererPixelRatio = renderer.getPixelRatio();
					rendererSize = renderer.getSize();

					renderer.setPixelRatio( 1 );
					renderer.setSize( eyeWidth * 2, eyeHeight, false );

				}

			} else if ( wasPresenting ) {

				renderer.setPixelRatio( rendererPixelRatio );
				renderer.setSize( rendererSize.width, rendererSize.height, rendererUpdateStyle );

			}

		}

		window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false );

		this.setFullScreen = function ( boolean ) {

			return new Promise( function ( resolve, reject ) {

				if ( vrDisplay === undefined ) {

					reject( new Error( 'No VR hardware found.' ) );
					return;

				}

				if ( scope.isPresenting === boolean ) {

					resolve();
					return;

				}

				if ( boolean ) {

					resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) );

				} else {

					resolve( vrDisplay.exitPresent() );

				}

			} );

		};

		this.requestPresent = function () {

			return this.setFullScreen( true );

		};

		this.exitPresent = function () {

			return this.setFullScreen( false );

		};

		this.requestAnimationFrame = function ( f ) {

			if ( vrDisplay !== undefined ) {

				return vrDisplay.requestAnimationFrame( f );

			} else {

				return window.requestAnimationFrame( f );

			}

		};

		this.cancelAnimationFrame = function ( h ) {

			if ( vrDisplay !== undefined ) {

				vrDisplay.cancelAnimationFrame( h );

			} else {

				window.cancelAnimationFrame( h );

			}

		};

		this.submitFrame = function () {

			if ( vrDisplay !== undefined && scope.isPresenting ) {

				vrDisplay.submitFrame();

			}

		};

		this.autoSubmitFrame = true;

		// render

		var cameraL = new THREE.PerspectiveCamera();
		cameraL.layers.enable( 1 );

		var cameraR = new THREE.PerspectiveCamera();
		cameraR.layers.enable( 2 );

		this.render = function ( scene, camera, renderTarget, forceClear ) {

			if ( vrDisplay && scope.isPresenting ) {

				var autoUpdate = scene.autoUpdate;

				if ( autoUpdate ) {

					scene.updateMatrixWorld();
					scene.autoUpdate = false;

				}

				if ( Array.isArray( scene ) ) {

					console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' );
					scene = scene[ 0 ];

				}

				// When rendering we don't care what the recommended size is, only what the actual size
				// of the backbuffer is.
				var size = renderer.getSize();
				var layers = vrDisplay.getLayers();
				var leftBounds;
				var rightBounds;

				if ( layers.length ) {

					var layer = layers[ 0 ];

					leftBounds = layer.leftBounds !== null && layer.leftBounds.length === 4 ? layer.leftBounds : defaultLeftBounds;
					rightBounds = layer.rightBounds !== null && layer.rightBounds.length === 4 ? layer.rightBounds : defaultRightBounds;

				} else {

					leftBounds = defaultLeftBounds;
					rightBounds = defaultRightBounds;

				}

				renderRectL = {
					x: Math.round( size.width * leftBounds[ 0 ] ),
					y: Math.round( size.height * leftBounds[ 1 ] ),
					width: Math.round( size.width * leftBounds[ 2 ] ),
					height: Math.round( size.height * leftBounds[ 3 ] )
				};
				renderRectR = {
					x: Math.round( size.width * rightBounds[ 0 ] ),
					y: Math.round( size.height * rightBounds[ 1 ] ),
					width: Math.round( size.width * rightBounds[ 2 ] ),
					height: Math.round( size.height * rightBounds[ 3 ] )
				};

				if ( renderTarget ) {

					renderer.setRenderTarget( renderTarget );
					renderTarget.scissorTest = true;

				} else {

					renderer.setRenderTarget( null );
					renderer.setScissorTest( true );

				}

				if ( renderer.autoClear || forceClear ) renderer.clear();

				if ( camera.parent === null ) camera.updateMatrixWorld();

				camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale );

				cameraR.position.copy( cameraL.position );
				cameraR.quaternion.copy( cameraL.quaternion );
				cameraR.scale.copy( cameraL.scale );

				if ( vrDisplay.getFrameData ) {

					vrDisplay.depthNear = camera.near;
					vrDisplay.depthFar = camera.far;

					vrDisplay.getFrameData( frameData );

					cameraL.projectionMatrix.elements = frameData.leftProjectionMatrix;
					cameraR.projectionMatrix.elements = frameData.rightProjectionMatrix;

					getEyeMatrices( frameData );

					cameraL.updateMatrix();
					cameraL.matrix.multiply( eyeMatrixL );
					cameraL.matrix.decompose( cameraL.position, cameraL.quaternion, cameraL.scale );

					cameraR.updateMatrix();
					cameraR.matrix.multiply( eyeMatrixR );
					cameraR.matrix.decompose( cameraR.position, cameraR.quaternion, cameraR.scale );

				} else {

					var eyeParamsL = vrDisplay.getEyeParameters( 'left' );
					var eyeParamsR = vrDisplay.getEyeParameters( 'right' );

					cameraL.projectionMatrix = fovToProjection( eyeParamsL.fieldOfView, true, camera.near, camera.far );
					cameraR.projectionMatrix = fovToProjection( eyeParamsR.fieldOfView, true, camera.near, camera.far );

					eyeTranslationL.fromArray( eyeParamsL.offset );
					eyeTranslationR.fromArray( eyeParamsR.offset );

					cameraL.translateOnAxis( eyeTranslationL, cameraL.scale.x );
					cameraR.translateOnAxis( eyeTranslationR, cameraR.scale.x );

				}

				// render left eye
				if ( renderTarget ) {

					renderTarget.viewport.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );
					renderTarget.scissor.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );

				} else {

					renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );
					renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height );

				}
				renderer.render( scene, cameraL, renderTarget, forceClear );

				// render right eye
				if ( renderTarget ) {

					renderTarget.viewport.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );
					renderTarget.scissor.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );

				} else {

					renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );
					renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height );

				}
				renderer.render( scene, cameraR, renderTarget, forceClear );

				if ( renderTarget ) {

					renderTarget.viewport.set( 0, 0, size.width, size.height );
					renderTarget.scissor.set( 0, 0, size.width, size.height );
					renderTarget.scissorTest = false;
					renderer.setRenderTarget( null );

				} else {

					renderer.setViewport( 0, 0, size.width, size.height );
					renderer.setScissorTest( false );

				}

				if ( autoUpdate ) {

					scene.autoUpdate = true;

				}

				if ( scope.autoSubmitFrame ) {

					scope.submitFrame();

				}

				return;

			}

			// Regular render mode if not HMD

			renderer.render( scene, camera, renderTarget, forceClear );

		};

		this.dispose = function () {

			window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false );

		};

		//

		var poseOrientation = new THREE.Quaternion();
		var posePosition = new THREE.Vector3();

		// Compute model matrices of the eyes with respect to the head.
		function getEyeMatrices( frameData ) {

			// Compute the matrix for the position of the head based on the pose
			if ( frameData.pose.orientation ) {

				poseOrientation.fromArray( frameData.pose.orientation );
				headMatrix.makeRotationFromQuaternion( poseOrientation );

			}	else {

				headMatrix.identity();

			}

			if ( frameData.pose.position ) {

				posePosition.fromArray( frameData.pose.position );
				headMatrix.setPosition( posePosition );

			}

			// The view matrix transforms vertices from sitting space to eye space. As such, the view matrix can be thought of as a product of two matrices:
			// headToEyeMatrix * sittingToHeadMatrix

			// The headMatrix that we've calculated above is the model matrix of the head in sitting space, which is the inverse of sittingToHeadMatrix.
			// So when we multiply the view matrix with headMatrix, we're left with headToEyeMatrix:
			// viewMatrix * headMatrix = headToEyeMatrix * sittingToHeadMatrix * headMatrix = headToEyeMatrix

			eyeMatrixL.fromArray( frameData.leftViewMatrix );
			eyeMatrixL.multiply( headMatrix );
			eyeMatrixR.fromArray( frameData.rightViewMatrix );
			eyeMatrixR.multiply( headMatrix );

			// The eye's model matrix in head space is the inverse of headToEyeMatrix we calculated above.

			eyeMatrixL.getInverse( eyeMatrixL );
			eyeMatrixR.getInverse( eyeMatrixR );

		}

		function fovToNDCScaleOffset( fov ) {

			var pxscale = 2.0 / ( fov.leftTan + fov.rightTan );
			var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5;
			var pyscale = 2.0 / ( fov.upTan + fov.downTan );
			var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5;
			return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] };

		}

		function fovPortToProjection( fov, rightHanded, zNear, zFar ) {

			rightHanded = rightHanded === undefined ? true : rightHanded;
			zNear = zNear === undefined ? 0.01 : zNear;
			zFar = zFar === undefined ? 10000.0 : zFar;

			var handednessScale = rightHanded ? - 1.0 : 1.0;

			// start with an identity matrix
			var mobj = new THREE.Matrix4();
			var m = mobj.elements;

			// and with scale/offset info for normalized device coords
			var scaleAndOffset = fovToNDCScaleOffset( fov );

			// X result, map clip edges to [-w,+w]
			m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ];
			m[ 0 * 4 + 1 ] = 0.0;
			m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale;
			m[ 0 * 4 + 3 ] = 0.0;

			// Y result, map clip edges to [-w,+w]
			// Y offset is negated because this proj matrix transforms from world coords with Y=up,
			// but the NDC scaling has Y=down (thanks D3D?)
			m[ 1 * 4 + 0 ] = 0.0;
			m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ];
			m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale;
			m[ 1 * 4 + 3 ] = 0.0;

			// Z result (up to the app)
			m[ 2 * 4 + 0 ] = 0.0;
			m[ 2 * 4 + 1 ] = 0.0;
			m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale;
			m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar );

			// W result (= Z in)
			m[ 3 * 4 + 0 ] = 0.0;
			m[ 3 * 4 + 1 ] = 0.0;
			m[ 3 * 4 + 2 ] = handednessScale;
			m[ 3 * 4 + 3 ] = 0.0;

			mobj.transpose();
			return mobj;

		}

		function fovToProjection( fov, rightHanded, zNear, zFar ) {

			var DEG2RAD = Math.PI / 180.0;

			var fovPort = {
				upTan: Math.tan( fov.upDegrees * DEG2RAD ),
				downTan: Math.tan( fov.downDegrees * DEG2RAD ),
				leftTan: Math.tan( fov.leftDegrees * DEG2RAD ),
				rightTan: Math.tan( fov.rightDegrees * DEG2RAD )
			};

			return fovPortToProjection( fovPort, rightHanded, zNear, zFar );

		}

	};

	/**
	 * @author dmarcos / https://github.com/dmarcos
	 * @author mrdoob / http://mrdoob.com
	 */

	THREE.VRControls = function ( object, onError ) {

		var scope = this;

		var vrDisplay, vrDisplays;

		var standingMatrix = new THREE.Matrix4();

		var frameData = null;

		if ( 'VRFrameData' in window ) {

			frameData = new VRFrameData();

		}

		function gotVRDisplays( displays ) {

			vrDisplays = displays;

			if ( displays.length > 0 ) {

				vrDisplay = displays[ 0 ];

			} else {

				if ( onError ) onError( 'VR input not available.' );

			}

		}

		if ( navigator.getVRDisplays ) {

			navigator.getVRDisplays().then( gotVRDisplays ).catch( function () {

				console.warn( 'THREE.VRControls: Unable to get VR Displays' );

			} );

		}

		// the Rift SDK returns the position in meters
		// this scale factor allows the user to define how meters
		// are converted to scene units.

		this.scale = 1;

		// If true will use "standing space" coordinate system where y=0 is the
		// floor and x=0, z=0 is the center of the room.
		this.standing = false;

		// Distance from the users eyes to the floor in meters. Used when
		// standing=true but the VRDisplay doesn't provide stageParameters.
		this.userHeight = 1.6;

		this.getVRDisplay = function () {

			return vrDisplay;

		};

		this.setVRDisplay = function ( value ) {

			vrDisplay = value;

		};

		this.getVRDisplays = function () {

			console.warn( 'THREE.VRControls: getVRDisplays() is being deprecated.' );
			return vrDisplays;

		};

		this.getStandingMatrix = function () {

			return standingMatrix;

		};

		this.update = function () {

			if ( vrDisplay ) {

				var pose;

				if ( vrDisplay.getFrameData ) {

					vrDisplay.getFrameData( frameData );
					pose = frameData.pose;

				} else if ( vrDisplay.getPose ) {

					pose = vrDisplay.getPose();

				}

				if ( pose.orientation !== null ) {

					object.quaternion.fromArray( pose.orientation );

				}

				if ( pose.position !== null ) {

					object.position.fromArray( pose.position );

				} else {

					object.position.set( 0, 0, 0 );

				}

				if ( this.standing ) {

					if ( vrDisplay.stageParameters ) {

						object.updateMatrix();

						standingMatrix.fromArray( vrDisplay.stageParameters.sittingToStandingTransform );
						object.applyMatrix( standingMatrix );

					} else {

						object.position.setY( object.position.y + this.userHeight );

					}

				}

				object.position.multiplyScalar( scope.scale );

			}

		};

		this.dispose = function () {

			vrDisplay = null;

		};

	};

	/**
	 * @author mrdoob / http://mrdoob.com
	 * @author Mugen87 / https://github.com/Mugen87
	 *
	 * Based on @tojiro's vr-samples-utils.js
	 */

	var WEBVR = {

		createButton: function ( renderer, options ) {

			function showEnterVR( device ) {

				button.style.display = '';

				button.style.cursor = 'pointer';
				button.style.left = 'calc(50% - 50px)';
				button.style.width = '100px';

				button.textContent = 'ENTER VR';

				button.onmouseenter = function () { button.style.opacity = '1.0'; };
				button.onmouseleave = function () { button.style.opacity = '0.5'; };

				button.onclick = function () {

					device.isPresenting ? device.exitPresent() : device.requestPresent( [ { source: renderer.domElement } ] );

				};

				renderer.vr.setDevice( device );

			}

			function showEnterXR( device ) {

				var currentSession = null;

				function onSessionStarted( session ) {

					if ( options === undefined ) options = {};
					if ( options.frameOfReferenceType === undefined ) options.frameOfReferenceType = 'stage';

					session.addEventListener( 'end', onSessionEnded );

					renderer.vr.setSession( session, options );
					button.textContent = 'EXIT XR';

					currentSession = session;

				}

				function onSessionEnded( event ) {

					currentSession.removeEventListener( 'end', onSessionEnded );

					renderer.vr.setSession( null );
					button.textContent = 'ENTER XR';

					currentSession = null;

				}

				//

				button.style.display = '';

				button.style.cursor = 'pointer';
				button.style.left = 'calc(50% - 50px)';
				button.style.width = '100px';

				button.textContent = 'ENTER XR';

				button.onmouseenter = function () { button.style.opacity = '1.0'; };
				button.onmouseleave = function () { button.style.opacity = '0.5'; };

				button.onclick = function () {

					if ( currentSession === null ) {

						device.requestSession( { exclusive: true } ).then( onSessionStarted );

					} else {

						currentSession.end();

					}

				};

				renderer.vr.setDevice( device );

			}

			function showVRNotFound() {

				button.style.display = '';

				button.style.cursor = 'auto';
				button.style.left = 'calc(50% - 75px)';
				button.style.width = '150px';

				button.textContent = 'VR NOT FOUND';

				button.onmouseenter = null;
				button.onmouseleave = null;

				button.onclick = null;

				renderer.vr.setDevice( null );

			}

			function stylizeElement( element ) {

				element.style.position = 'absolute';
				element.style.bottom = '20px';
				element.style.padding = '12px 6px';
				element.style.border = '1px solid #fff';
				element.style.borderRadius = '4px';
				element.style.background = 'transparent';
				element.style.color = '#fff';
				element.style.font = 'normal 13px sans-serif';
				element.style.textAlign = 'center';
				element.style.opacity = '0.5';
				element.style.outline = 'none';
				element.style.zIndex = '999';

			}

			if ( 'xr' in navigator ) {

				var button = document.createElement( 'button' );
				button.style.display = 'none';

				stylizeElement( button );

				navigator.xr.requestDevice().then( function ( device ) {

					device.supportsSession( { exclusive: true } ).then( function () {

						showEnterXR( device );

					} ).catch( showVRNotFound );

				} ).catch( showVRNotFound );

				return button;

			} else if ( 'getVRDisplays' in navigator ) {

				var button = document.createElement( 'button' );
				button.style.display = 'none';

				stylizeElement( button );

				window.addEventListener( 'vrdisplayconnect', function ( event ) {

					showEnterVR( event.display );

				}, false );

				window.addEventListener( 'vrdisplaydisconnect', function ( event ) {

					showVRNotFound();

				}, false );

				window.addEventListener( 'vrdisplaypresentchange', function ( event ) {

					button.textContent = event.display.isPresenting ? 'EXIT VR' : 'ENTER VR';

				}, false );

				window.addEventListener( 'vrdisplayactivate', function ( event ) {

					event.display.requestPresent( [ { source: renderer.domElement } ] );

				}, false );

				navigator.getVRDisplays()
					.then( function ( displays ) {

						if ( displays.length > 0 ) {

							showEnterVR( displays[ 0 ] );

						} else {

							showVRNotFound();

						}

					} );

				return button;

			} else {

				var message = document.createElement( 'a' );
				message.href = 'https://webvr.info';
				message.innerHTML = 'WEBVR NOT SUPPORTED';

				message.style.left = 'calc(50% - 90px)';
				message.style.width = '180px';
				message.style.textDecoration = 'none';

				stylizeElement( message );

				return message;

			}

		},

		// DEPRECATED

		checkAvailability: function () {
			console.warn( 'WEBVR.checkAvailability has been deprecated.' );
			return new Promise( function () {} );
		},

		getMessageContainer: function () {
			console.warn( 'WEBVR.getMessageContainer has been deprecated.' );
			return document.createElement( 'div' );
		},

		getButton: function () {
			console.warn( 'WEBVR.getButton has been deprecated.' );
			return document.createElement( 'div' );
		},

		getVRDisplay: function () {
			console.warn( 'WEBVR.getVRDisplay has been deprecated.' );
		}

	};

	/**
	 * @author mrdoob / http://mrdoob.com/
	 */

	function html2canvas( element ) {

		var range = document.createRange();

		function Clipper( context ) {

			var clips = [];
			var isClipping = false;

			function doClip() {

				if ( isClipping ) {

					isClipping = false;
					context.restore();

				}

				if ( clips.length === 0 ) return;

				var minX = - Infinity, minY = - Infinity;
				var maxX = Infinity, maxY = Infinity;

				for ( var i = 0; i < clips.length; i ++ ) {

					var clip = clips[ i ];

					minX = Math.max( minX, clip.x );
					minY = Math.max( minY, clip.y );
					maxX = Math.min( maxX, clip.x + clip.width );
					maxY = Math.min( maxY, clip.y + clip.height );

				}

				context.save();
				context.beginPath();
				context.rect( minX, minY, maxX - minX, maxY - minY );
				context.clip();

				isClipping = true;

			}

			return {
				add: function ( clip ) {
					clips.push( clip );
					doClip();
				},
				remove: function () {
					clips.pop();
					doClip();
				}
			};

		}

		function drawText( style, x, y, string ) {

			if ( string !== '' ) {

				context.font = style.fontSize + ' ' + style.fontFamily;
				context.textBaseline = 'top';
				context.fillStyle = style.color;
				context.fillText( string, x, y );

			}

		}

		function drawBorder( style, which, x, y, width, height ) {

			var borderWidth = style[ which + 'Width' ];
			var borderStyle = style[ which + 'Style' ];
			var borderColor = style[ which + 'Color' ];

			if ( borderWidth !== '0px' && borderStyle !== 'none' && borderColor !== 'transparent' && borderColor !== 'rgba(0, 0, 0, 0)' ) {

				context.strokeStyle = borderColor;
				context.beginPath();
				context.moveTo( x, y );
				context.lineTo( x + width, y + height );
				context.stroke();

			}

		}

		function drawElement( element, style ) {

			var x = 0, y = 0, width = 0, height = 0;

			if ( element.nodeType === 3 ) {

				// text

				range.selectNode( element );

				var rect = range.getBoundingClientRect();

				x = rect.left - offset.left - 0.5;
				y = rect.top - offset.top - 0.5;
				width = rect.width;
				height = rect.height;

				drawText( style, x, y, element.nodeValue.trim() );

			} else {

				if ( element.style.display === 'none' ) return;

				var rect = element.getBoundingClientRect();

				x = rect.left - offset.left - 0.5;
				y = rect.top - offset.top - 0.5;
				width = rect.width;
				height = rect.height;

				style = window.getComputedStyle( element );

				var backgroundColor = style.backgroundColor;

				if ( backgroundColor !== 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)' ) {

					context.fillStyle = backgroundColor;
					context.fillRect( x, y, width, height );

				}

				drawBorder( style, 'borderTop', x, y, width, 0 );
				drawBorder( style, 'borderLeft', x, y, 0, height );
				drawBorder( style, 'borderBottom', x, y + height, width, 0 );
				drawBorder( style, 'borderRight', x + width, y, 0, height );

				if ( element.type === 'color' || element.type === 'text' ) {

					clipper.add( { x: x, y: y, width: width, height: height } );

					drawText( style, x + parseInt( style.paddingLeft ), y + parseInt( style.paddingTop ), element.value );

					clipper.remove();

				}

			}

			/*
			// debug

			context.strokeStyle = '#' + Math.random().toString( 16 ).slice( - 3 );
			context.strokeRect( x - 0.5, y - 0.5, width + 1, height + 1 );
			*/

			var isClipping = style.overflow === 'auto' || style.overflow === 'hidden';

			if ( isClipping ) clipper.add( { x: x, y: y, width: width, height: height } );

			for ( var i = 0; i < element.childNodes.length; i ++ ) {

				drawElement( element.childNodes[ i ], style );

			}

			if ( isClipping ) clipper.remove();

		}

		var offset = element.getBoundingClientRect();

		var canvas = document.createElement( 'canvas' );
		canvas.width = offset.width;
		canvas.height = offset.height;

		var context = canvas.getContext( '2d'/*, { alpha: false }*/ );

		var clipper = new Clipper( context );

		console.time( 'drawElement' );

		drawElement( element );

		console.timeEnd( 'drawElement' );

		return canvas;

	}

	/**
	 * @author mrdoob / http://mrdoob.com/
	 */

	 THREE.HTMLGroup = function ( dom ) {

	 	THREE.Group.call( this );

	 	this.type = 'HTMLGroup';

	 	/*
	 	dom.addEventListener( 'mousemove', function ( event ) {

	 		console.log( 'mousemove' );

	 	} );

	 	dom.addEventListener( 'click', function ( event ) {

	 		console.log( 'click' );

	 	} );
	 	*/

	 };

	 THREE.HTMLGroup.prototype = Object.assign( Object.create( THREE.Group.prototype ), {

	 	constructor: THREE.HTMLGroup

	 } );

	 THREE.HTMLMesh = function ( dom ) {

	 	var texture = new THREE.HTMLTexture( dom );

	 	var geometry = new THREE.PlaneGeometry( texture.image.width * 0.05, texture.image.height * 0.05 );
	 	var material = new THREE.MeshBasicMaterial( { map: texture } );

	 	THREE.Mesh.call( this, geometry, material );

	 	this.type = 'HTMLMesh';

	 };

	 THREE.HTMLMesh.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), {

	 	constructor: THREE.HTMLMesh

	 } );

	 THREE.HTMLTexture = function ( dom ) {

	 	THREE.CanvasTexture.call( this, html2canvas( dom ) );

	 	this.dom = dom;

	 	this.anisotropy = 16;

	 };

	 THREE.HTMLTexture.prototype = Object.assign( Object.create( THREE.CanvasTexture.prototype ), {

	 	constructor: THREE.HTMLTexture,

	 	update: function () {

	 		console.log( 'yo!', this, this.dom );

	 		this.image = html2canvas( this.dom );
	 		this.needsUpdate = true;

	 	}

	 } );

	var noop = {value: function() {}};

	function dispatch() {
	  for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {
	    if (!(t = arguments[i] + "") || (t in _)) throw new Error("illegal type: " + t);
	    _[t] = [];
	  }
	  return new Dispatch(_);
	}

	function Dispatch(_) {
	  this._ = _;
	}

	function parseTypenames(typenames, types) {
	  return typenames.trim().split(/^|\s+/).map(function(t) {
	    var name = "", i = t.indexOf(".");
	    if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
	    if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t);
	    return {type: t, name: name};
	  });
	}

	Dispatch.prototype = dispatch.prototype = {
	  constructor: Dispatch,
	  on: function(typename, callback) {
	    var _ = this._,
	        T = parseTypenames(typename + "", _),
	        t,
	        i = -1,
	        n = T.length;

	    // If no callback was specified, return the callback of the given type and name.
	    if (arguments.length < 2) {
	      while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t;
	      return;
	    }

	    // If a type was specified, set the callback for the given type and name.
	    // Otherwise, if a null callback was specified, remove callbacks of the given name.
	    if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback);
	    while (++i < n) {
	      if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback);
	      else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null);
	    }

	    return this;
	  },
	  copy: function() {
	    var copy = {}, _ = this._;
	    for (var t in _) copy[t] = _[t].slice();
	    return new Dispatch(copy);
	  },
	  call: function(type, that) {
	    if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];
	    if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
	    for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
	  },
	  apply: function(type, that, args) {
	    if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
	    for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
	  }
	};

	function get(type, name) {
	  for (var i = 0, n = type.length, c; i < n; ++i) {
	    if ((c = type[i]).name === name) {
	      return c.value;
	    }
	  }
	}

	function set(type, name, callback) {
	  for (var i = 0, n = type.length; i < n; ++i) {
	    if (type[i].name === name) {
	      type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1));
	      break;
	    }
	  }
	  if (callback != null) type.push({name: name, value: callback});
	  return type;
	}

	window.URL = window.URL || window.webkitURL;
	window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;

	Number.prototype.format = function () {
	    return this.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
	};

	/**
	 * 自定义事件列表
	 */
	var EventList = [
	    // dom事件
	    'click', // 点击
	    'contextmenu', // 右键
	    'dblclick', // 双击
	    'keydown', // 按下键盘按键
	    'keyup', // 抬起键盘按键
	    'mousedown', // 按下鼠标按键
	    'mousemove', // 鼠标移动
	    'mouseup', // 抬起鼠标按键
	    'mousewheel', // 鼠标滚轮
	    'resize', // 窗口大小改变
	    'dragover', // 拖入dom
	    'drop', // 放置到dom中
	    'message', // 收到Worker消息

	    // app事件
	    'appStart', // 应用程序开始前调用
	    'appStarted', // 应用程序开始后调用
	    'initApp', // 引用程序初始化
	    'appStop', // 程序开始结束前调用
	    'appStoped', // 程序结束后调用

	    // 菜单栏事件
	    'mNewScene', // 新建
	    'mLoadScene', // 载入
	    'mSaveScene', // 保存
	    'mPublishScene', // 发布

	    'mUndo', // 撤销
	    'mRedo', // 重做
	    'mClearHistory', // 清空历史记录
	    'mClone', // 复制
	    'mDelete', // 删除
	    'mMinifyShader', // 清除着色器

	    'mAddGroup', // 添加组
	    'mAddPlane', // 添加平板
	    'mAddBox', // 添加正方体
	    'mAddCircle', // 添加圆
	    'mAddCylinder', // 添加圆柱体
	    'mAddSphere', // 添加球体
	    'mAddIcosahedron', // 添加二十面体
	    'mAddTorus', // 添加轮胎
	    'mAddTorusKnot', // 添加纽结
	    'mAddTeaport', // 添加茶壶
	    'mAddLathe', // 添加花瓶
	    'mAddSprite', // 添加精灵
	    'mAddPointLight', // 添加点光源
	    'mAddSpotLight', // 添加聚光灯
	    'mAddDirectionalLight', // 添加平行光源
	    'mAddHemisphereLight', // 添加半球光
	    'mAddAmbientLight', // 添加环境光
	    'mAddPerspectiveCamera', // 添加透视相机

	    'mAddAsset', // 添加模型
	    'mImportAsset', // 导入资源
	    'mExportGeometry', // 导出几何体
	    'mExportObject', // 导出物体
	    'mExportScene', // 导出场景
	    'mExportGLTF', // 导出gltf文件
	    'mExportMMD', // 导出mmd文件
	    'mExportOBJ', // 导出obj模型
	    'mExportPLY', // 导出ply文件
	    'mExportSTLB', // 导出stl二进制文件
	    'mExportSTL', // 导出stl模型

	    'mAddPerson', // 添加人
	    'mAddFire', // 添加火焰
	    'mAddSmoke', // 添加烟

	    'mPlay', // 启动

	    'mVRMode', // VR模式

	    'mArkanoid', // 打砖块
	    'mCamera', // 相机
	    'mParticles', // 粒子
	    'mPong', // 乒乓球

	    'mSourceCode', // 源码
	    'mAbout', // 关于

	    // 工具栏事件

	    'changeMode', // 改变模式（select, translate, rotate, scale, delete）

	    // editor事件
	    'setTheme', // 设置编辑器主题
	    'setScene', // 设置编辑器场景
	    'addObject', // 添加物体
	    'moveObject', // 移动物体
	    'nameObject', // 重命名物体
	    'removeObject', // 删除物体
	    'addGeometry', // 添加几何体事件
	    'setGeometryName', // 设置几何体名称事件
	    'addMaterial', // 添加材质事件
	    'setMaterialName', // 设置材质名称
	    'addTexture', // 添加纹理
	    'addHelper', // 添加帮助事件
	    'removeHelper', // 移除脚本
	    'addScript', // 添加脚本
	    'removeScript', // 移除脚本
	    'select', // 选中事件
	    'clear', // 清空场景
	    'load', // 加载场景
	    'save', // 保存场景

	    // signal事件
	    'editScript', // 编辑脚本事件

	    'startPlayer', // 启动播放器事件
	    'stopPlayer', // 停止播放器事件

	    'enterVR', // 进入VR事件
	    'enteredVR', // 已经进入VR事件
	    'exitedVR', // 已经退出VR事件

	    'editorCleared', // 编辑器已经清空事件

	    'savingStarted', // 开始保存事件
	    'savingFinished', // 保存完成事件

	    'themeChanged', // 改变主题事件

	    'snapChanged', // 对齐单元格事件
	    'spaceChanged', // 空间坐标系改变事件
	    'rendererChanged', // 渲染模式改变事件

	    'sceneBackgroundChanged', // 场景背景改变事件
	    'sceneFogChanged', // 场景雾效改变事件
	    'sceneGraphChanged', // 场景内容改变事件

	    'cameraChanged', // 相机改变事件

	    'geometryChanged', // 几何体改变事件

	    'objectSelected', // 物体选中改变
	    'objectFocused', // 物体交点改变事件

	    'objectAdded', // 添加物体事件
	    'objectChanged', // 物体改变事件
	    'objectRemoved', // 物体移除事件

	    'helperAdded', // 添加帮助事件
	    'helperRemoved', // 移除帮助事件

	    'materialChanged', // 材质改变事件

	    'scriptAdded', // 添加脚本事件
	    'scriptChanged', // 脚本改变事件
	    'scriptRemoved', // 脚本移除事件

	    'windowResize', // 窗口大小改变事件

	    'showGridChanged', // 网格显示隐藏改变
	    'refreshSidebarObject3D', // 刷新Object3D侧边栏事件
	    'historyChanged', // 历史改变事件
	    'refreshScriptEditor', // 刷新脚本编辑器事件

	    // 场景编辑区
	    'transformControlsChange', // 变形控件改变
	    'transformControlsMouseDown', // 变形控件按下鼠标键
	    'transformControlsMouseUp', // 变形控件抬起鼠标键
	    'render', // 渲染一次场景
	    'animate', // 进行动画

	    // 侧边栏
	    'newMaterial', // 材质面板新建材质
	    'copyMaterial', // 材质面板复制材质
	    'pasteMaterial', // 材质面板粘贴材质
	    'updateMaterial', // 根据材质面板更新材质
	    'updateMaterialPanel', // 更新材质面板UI

	    'updateScaleX', // 物体面板更新缩放x
	    'updateScaleY', // 物体面板更新缩放y
	    'updateScaleZ', // 物体面板更新缩放z
	    'updateObject', // 更新物体属性
	    'updateObjectPanel', // 更新物体面板

	    'updateRenderer',
	    'selectTab', // 点击选择侧边栏选项卡
	    'selectPropertyTab', // 点击选择属性选项卡

	    'updateScenePanel', // 刷新场景面板
	    'updateScenePanelFog', // 刷新场景面板雾效设置
	    'outlinerChange', // 场景大纲发生改变

	    // 状态栏
	    'gridChange', // 状态栏网格改变事件
	    'codeMirrorChange', // CodeMirror改变事件
	];

	var ID = -1;

	/**
	 * 事件基类
	 */
	function BaseEvent(app) {
	    this.app = app;
	    this.id = 'BaseEvent' + ID--;
	}

	/**
	 * 开始执行
	 */
	BaseEvent.prototype.start = function () {

	};

	/**
	 * 停止执行
	 */
	BaseEvent.prototype.stop = function () {

	};

	/**
	 * 动画事件
	 * @param {*} app 
	 */
	function AnimateEvent(app) {
	    BaseEvent.call(this, app);
	    this.running = false;
	    this.clock = new THREE.Clock();
	}

	AnimateEvent.prototype = Object.create(BaseEvent.prototype);
	AnimateEvent.prototype.constructor = AnimateEvent;

	AnimateEvent.prototype.start = function () {
	    this.running = true;
	    requestAnimationFrame(this.onAnimate.bind(this));
	};

	AnimateEvent.prototype.stop = function () {
	    this.running = false;
	};

	AnimateEvent.prototype.onAnimate = function () {
	    this.app.editor.stats.begin();

	    this.app.call('animate', this, this.clock);
	    this.app.call('render', this);

	    this.app.editor.stats.end();

	    if (this.running) {
	        requestAnimationFrame(this.onAnimate.bind(this));
	    }
	};

	/**
	 * 初始化应用程序事件
	 * @param {*} app 
	 */
	function InitAppEvent(app) {
	    BaseEvent.call(this, app);
	}

	InitAppEvent.prototype = Object.create(BaseEvent.prototype);
	InitAppEvent.prototype.constructor = InitAppEvent;

	InitAppEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('initApp.' + this.id, function () {
	        _this.onInitApp();
	    });
	};

	InitAppEvent.prototype.stop = function () {
	    this.app.on('initApp.' + this.id, null);
	};

	InitAppEvent.prototype.onInitApp = function () {
	    var app = this.app;
	    var editor = app.editor;

	    editor.setTheme(editor.config.getKey('theme'));

	    editor.storage.init(function () {

	        editor.storage.get(function (state) {

	            // 从文件中读取场景时，如果读取缓存，会覆盖从文件中读取的场景，所以直接返回
	            if (app.isLoadingFromHash) {
	                return;
	            }
	            if (state !== undefined) {
	                editor.fromJSON(state);
	            }

	            var selected = editor.config.getKey('selected');

	            if (selected !== undefined) {
	                editor.selectByUuid(selected);
	            }

	        });
	    });
	};

	/**
	 * 拖动事件
	 * @param {*} app 
	 */
	function DragOverEvent(app) {
	    BaseEvent.call(this, app);
	}

	DragOverEvent.prototype = Object.create(BaseEvent.prototype);
	DragOverEvent.prototype.constructor = DragOverEvent;

	DragOverEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('dragover.' + this.id, function (event) {
	        _this.onDragOver(event);
	    });
	};

	DragOverEvent.prototype.stop = function () {
	    this.app.on('dragover.' + this.id, null);
	};

	DragOverEvent.prototype.onDragOver = function (event) {
	    event.preventDefault();
	    event.dataTransfer.dropEffect = 'copy';
	};

	/**
	 * 放置事件
	 * @param {*} app 
	 */
	function DropEvent(app) {
	    BaseEvent.call(this, app);
	}

	DropEvent.prototype = Object.create(BaseEvent.prototype);
	DropEvent.prototype.constructor = DropEvent;

	DropEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('drop.' + this.id, function (event) {
	        _this.onDrop(event);
	    });
	};

	DropEvent.prototype.stop = function () {
	    this.app.on('drop.' + this.id, null);
	};

	DropEvent.prototype.onDrop = function (event) {
	    var editor = this.app.editor;

	    event.preventDefault();

	    if (event.dataTransfer.files.length > 0) {

	        editor.loader.loadFile(event.dataTransfer.files[0]);

	    }
	};

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param editorRef pointer to main editor object used to initialize
	 *        each command object with a reference to the editor
	 * @constructor
	 */

	function Command(editorRef) {

	    this.id = -1;
	    this.inMemory = false;
	    this.updatable = false;
	    this.type = '';
	    this.name = '';

	    if (editorRef !== undefined) {

	        Command.editor = editorRef;

	    }
	    this.editor = Command.editor;


	}
	Command.prototype.toJSON = function () {

	    var output = {};
	    output.type = this.type;
	    output.id = this.id;
	    output.name = this.name;
	    return output;

	};

	Command.prototype.fromJSON = function (json) {

	    this.inMemory = true;
	    this.type = json.type;
	    this.id = json.id;
	    this.name = json.name;

	};

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @constructor
	 */

	function RemoveObjectCommand(object) {

		Command.call(this);

		this.type = 'RemoveObjectCommand';
		this.name = 'Remove Object';

		this.object = object;
		this.parent = (object !== undefined) ? object.parent : undefined;
		if (this.parent !== undefined) {

			this.index = this.parent.children.indexOf(this.object);

		}

	}
	RemoveObjectCommand.prototype = Object.create(Command.prototype);

	Object.assign(RemoveObjectCommand.prototype, {

		constructor: RemoveObjectCommand,

		execute: function () {

			var scope = this.editor;
			this.object.traverse(function (child) {

				scope.removeHelper(child);

			});

			this.parent.remove(this.object);
			this.editor.select(this.parent);

			this.editor.app.call('objectRemoved', this, this.object);
			this.editor.app.call('sceneGraphChanged', this);

		},

		undo: function () {

			var scope = this.editor;

			this.object.traverse(function (child) {

				if (child.geometry !== undefined) scope.addGeometry(child.geometry);
				if (child.material !== undefined) scope.addMaterial(child.material);

				scope.addHelper(child);

			});

			this.parent.children.splice(this.index, 0, this.object);
			this.object.parent = this.parent;
			this.editor.select(this.object);

			this.editor.app.call('objectAdded', this, this.object);
			this.editor.app.call('sceneGraphChanged', this);

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);
			output.object = this.object.toJSON();
			output.index = this.index;
			output.parentUuid = this.parent.uuid;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.parent = this.editor.objectByUuid(json.parentUuid);
			if (this.parent === undefined) {

				this.parent = this.editor.scene;

			}

			this.index = json.index;

			this.object = this.editor.objectByUuid(json.object.object.uuid);
			if (this.object === undefined) {

				var loader = new THREE.ObjectLoader();
				this.object = loader.parse(json.object);

			}

		}

	});

	/**
	 * 键盘按键事件
	 * @param {*} app 
	 */
	function KeyDownEvent(app) {
	    BaseEvent.call(this, app);
	}

	KeyDownEvent.prototype = Object.create(BaseEvent.prototype);
	KeyDownEvent.prototype.constructor = KeyDownEvent;

	KeyDownEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('keydown.' + this.id, function (event) {
	        _this.onKeyDown(event);
	    });
	};

	KeyDownEvent.prototype.stop = function () {
	    this.app.on('keydown.' + this.id, null);
	};

	KeyDownEvent.prototype.onKeyDown = function (event) {

	    var editor = this.app.editor;

	    switch (event.keyCode) {

	        case 8: // backspace

	            event.preventDefault(); // prevent browser back

	        case 46: // delete

	            var object = editor.selected;

	            if (object == null) {
	                return;
	            }

	            if (confirm('删除 ' + object.name + '?') === false) return;

	            var parent = object.parent;
	            if (parent !== null) editor.execute(new RemoveObjectCommand(object));

	            break;

	        case 90: // Register Ctrl-Z for Undo, Ctrl-Shift-Z for Redo

	            if (event.ctrlKey && event.shiftKey) {

	                editor.redo();

	            } else if (event.ctrlKey) {

	                editor.undo();

	            }

	            break;

	        case 87: // Register W for translation transform mode
	            this.app.call('changeMode', this, 'translate');
	            break;

	        case 69: // Register E for rotation transform mode
	            this.app.call('changeMode', this, 'rotate');
	            break;

	        case 82: // Register R for scaling transform mode
	            this.app.call('changeMode', this, 'scale');
	            break;
	    }
	};

	/**
	 * 窗口大小改变事件
	 * @param {*} app 
	 */
	function ResizeEvent(app) {
	    BaseEvent.call(this, app);
	}

	ResizeEvent.prototype = Object.create(BaseEvent.prototype);
	ResizeEvent.prototype.constructor = ResizeEvent;

	ResizeEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('resize.' + this.id, function (event) {
	        _this.onResize(event);
	    });
	};

	ResizeEvent.prototype.stop = function () {
	    this.app.on('resize.' + this.id, null);
	};

	ResizeEvent.prototype.onResize = function (event) {
	    this.app.call('windowResize', this);
	};

	/**
	 * 窗口大小改变事件
	 * @param {*} app 
	 */
	function MessageEvent(app) {
	    BaseEvent.call(this, app);
	}

	MessageEvent.prototype = Object.create(BaseEvent.prototype);
	MessageEvent.prototype.constructor = MessageEvent;

	MessageEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('message.' + this.id, function (event) {
	        _this.onMessage(event);
	    });
	};

	MessageEvent.prototype.stop = function () {
	    this.app.on('message.' + this.id, null);
	};

	MessageEvent.prototype.onMessage = function (event) {

	    var editor = this.app.editor;

	    editor.clear();
	    editor.fromJSON(event.data);

	};

	/**
	 * 从文件中打开场景事件
	 * @param {*} app 
	 */
	function LoadFromHashEvent(app) {
	    BaseEvent.call(this, app);
	}

	LoadFromHashEvent.prototype = Object.create(BaseEvent.prototype);
	LoadFromHashEvent.prototype.constructor = LoadFromHashEvent;

	LoadFromHashEvent.prototype.start = function () {
	    var hash = window.location.hash;
	    var editor = this.app.editor;

	    if (hash.substr(1, 5) === 'file=') {

	        var file = hash.substr(6);

	        if (confirm('未保存场景数据将丢失。确定打开文件？')) {

	            var loader = new THREE.FileLoader();
	            loader.crossOrigin = '';
	            loader.load(file, function (text) {

	                editor.clear();
	                editor.fromJSON(JSON.parse(text));

	            });

	            this.app.isLoadingFromHash = true;

	        }

	    }
	};

	LoadFromHashEvent.prototype.stop = function () {

	};

	/**
	 * 自动保存事件
	 * @param {*} app 
	 */
	function AutoSaveEvent(app) {
	    BaseEvent.call(this, app);

	    this.timeout = null;
	}

	AutoSaveEvent.prototype = Object.create(BaseEvent.prototype);
	AutoSaveEvent.prototype.constructor = AutoSaveEvent;

	AutoSaveEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('geometryChanged.' + this.id, function () {
	        _this.SaveState();
	    });
	    this.app.on('objectAdded.' + this.id, function () {
	        _this.SaveState();
	    });
	    this.app.on('objectChanged.' + this.id, function () {
	        _this.SaveState();
	    });
	    this.app.on('objectRemoved.' + this.id, function () {
	        _this.SaveState();
	    });
	    this.app.on('materialChanged.' + this.id, function () {
	        _this.SaveState();
	    });
	    this.app.on('sceneBackgroundChanged.' + this.id, function () {
	        _this.SaveState();
	    });
	    this.app.on('sceneFogChanged.' + this.id, function () {
	        _this.SaveState();
	    });
	    this.app.on('sceneGraphChanged.' + this.id, function () {
	        _this.SaveState();
	    });
	    this.app.on('scriptChanged.' + this.id, function () {
	        _this.SaveState();
	    });
	    this.app.on('historyChanged.' + this.id, function () {
	        _this.SaveState();
	    });
	};

	AutoSaveEvent.prototype.stop = function () {

	};

	AutoSaveEvent.prototype.SaveState = function () {
	    var editor = this.app.editor;

	    if (editor.config.getKey('autosave') === false) {
	        return;
	    }

	    clearTimeout(this.timeout);

	    var _this = this;

	    this.timeout = setTimeout(function () {
	        _this.app.call('savingStarted', _this);

	        _this.timeout = setTimeout(function () {
	            editor.storage.set(editor.toJSON());
	            _this.app.call('savingFinished', _this);
	        }, 100);

	    }, 1000);
	};

	/**
	 * VR事件
	 * @param {*} app 
	 */
	function VREvent(app) {
	    BaseEvent.call(this, app);
	    this.groupVR = null;
	}

	VREvent.prototype = Object.create(BaseEvent.prototype);
	VREvent.prototype.constructor = VREvent;

	VREvent.prototype.start = function () {
	    var _this = this;
	    var editor = this.app.editor;
	    this.app.on('enterVR.' + this.id, function () {
	        _this.onEnterVR();
	    });
	    this.app.on('exitedVR.' + this.id, function () {
	        _this.onExitedVR();
	    });
	};

	VREvent.prototype.stop = function () {
	    this.app.on('enterVR.' + this.id, null);
	    this.app.on('exitedVR.' + this.id, null);
	};

	VREvent.prototype.onEnterVR = function () {
	    var groupVR = this.groupVR;
	    var editor = this.app.editor;
	    var viewport = this.app.viewport;
	    var sidebar = this.app.sidebar;
	    var vrEffect = editor.vrEffect;

	    if (groupVR == null) {

	        groupVR = new THREE.HTMLGroup(viewport.dom);
	        editor.sceneHelpers.add(groupVR);

	        var mesh = new THREE.HTMLMesh(sidebar.dom);
	        mesh.position.set(15, 0, 15);
	        mesh.rotation.y = -0.5;
	        groupVR.add(mesh);

	        var _this = this;
	        this.app.on('objectSelected.VREvent', function () {
	            _this.updateTexture(mesh);
	        });
	        this.app.on('objectAdded.VREvent', function () {
	            _this.updateTexture(mesh);
	        });
	        this.app.on('objectChanged.VREvent', function () {
	            _this.updateTexture(mesh);
	        });
	        this.app.on('objectRemoved.VREvent', function () {
	            _this.updateTexture(mesh);
	        });
	        this.app.on('sceneGraphChanged.VREvent', function () {
	            _this.updateTexture(mesh);
	        });
	        this.app.on('historyChanged.VREvent', function () {
	            _this.updateTexture(mesh);
	        });

	    }

	    vrEffect.isPresenting ? vrEffect.exitPresent() : vrEffect.requestPresent();

	    groupVR.visible = true;
	};

	VREvent.prototype.updateTexture = function (mesh) {
	    mesh.material.map.update();
	};

	VREvent.prototype.onExitedVR = function () {

	    var groupVR = this.groupVR;

	    if (groupVR !== undefined) {
	        groupVR.visible = false;
	    }

	};

	/**
	 * 设置主题事件
	 * @param {*} app 
	 */
	function SetThemeEvent(app) {
	    BaseEvent.call(this, app);
	}

	SetThemeEvent.prototype = Object.create(BaseEvent.prototype);
	SetThemeEvent.prototype.constructor = SetThemeEvent;

	SetThemeEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('setTheme.' + this.id, function (theme) {
	        _this.onSetTheme(theme);
	    });
	};

	SetThemeEvent.prototype.stop = function () {
	    this.app.on('setTheme.' + this.id, null);
	};

	SetThemeEvent.prototype.onSetTheme = function (theme) {
	    var dom = document.getElementById('theme');
	    if (dom) {
	        dom.href = theme;
	        this.app.call('themeChanged', this, theme);
	    }
	};

	/**
	 * 设置场景事件
	 * @param {*} app 
	 */
	function SetSceneEvent(app) {
	    BaseEvent.call(this, app);
	}

	SetSceneEvent.prototype = Object.create(BaseEvent.prototype);
	SetSceneEvent.prototype.constructor = SetSceneEvent;

	SetSceneEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('setScene.' + this.id, function (scene) {
	        _this.onSetScene(scene);
	    });
	};

	SetSceneEvent.prototype.stop = function () {
	    this.app.on('setScene.' + this.id, null);
	};

	SetSceneEvent.prototype.onSetScene = function (scene) {
	    var editor = this.app.editor;

	    editor.scene.uuid = scene.uuid;
	    editor.scene.name = scene.name;

	    if (scene.background !== null) editor.scene.background = scene.background.clone();
	    if (scene.fog !== null) editor.scene.fog = scene.fog.clone();

	    editor.scene.userData = JSON.parse(JSON.stringify(scene.userData));

	    while (scene.children.length > 0) {
	        editor.addObject(scene.children[0]);
	    }

	    this.app.call('sceneGraphChanged', this);
	};

	/**
	 * 添加物体事件
	 * @param {*} app 
	 */
	function AddObjectEvent(app) {
	    BaseEvent.call(this, app);
	}

	AddObjectEvent.prototype = Object.create(BaseEvent.prototype);
	AddObjectEvent.prototype.constructor = AddObjectEvent;

	AddObjectEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('addObject.' + this.id, function (object) {
	        _this.onAddObject(object);
	    });
	};

	AddObjectEvent.prototype.stop = function () {
	    this.app.on('addObject.' + this.id, null);
	};

	AddObjectEvent.prototype.onAddObject = function (object) {
	    var editor = this.app.editor;

	    object.traverse(function (child) {

	        if (child.geometry !== undefined) editor.addGeometry(child.geometry);
	        if (child.material !== undefined) editor.addMaterial(child.material);

	        editor.addHelper(child);

	    });

	    editor.scene.add(object);

	    editor.app.call('objectAdded', this, object);
	    editor.app.call('sceneGraphChanged', this);
	};

	/**
	 * 移动物体事件
	 * @param {*} app 
	 */
	function MoveObjectEvent(app) {
	    BaseEvent.call(this, app);
	}

	MoveObjectEvent.prototype = Object.create(BaseEvent.prototype);
	MoveObjectEvent.prototype.constructor = MoveObjectEvent;

	MoveObjectEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('moveObject.' + this.id, function (object, parent, before) {
	        _this.onMoveObject(object, parent, before);
	    });
	};

	MoveObjectEvent.prototype.stop = function () {
	    this.app.on('moveObject.' + this.id, null);
	};

	MoveObjectEvent.prototype.onMoveObject = function (object, parent, before) {
	    var editor = this.app.editor;

	    if (parent === undefined) {
	        parent = editor.scene;
	    }

	    parent.add(object);

	    // sort children array
	    if (before !== undefined) {
	        var index = parent.children.indexOf(before);
	        parent.children.splice(index, 0, object);
	        parent.children.pop();
	    }

	    editor.app.call('sceneGraphChanged', this);
	};

	/**
	 * 重命名物体事件
	 * @param {*} app 
	 */
	function NameObjectEvent(app) {
	    BaseEvent.call(this, app);
	}

	NameObjectEvent.prototype = Object.create(BaseEvent.prototype);
	NameObjectEvent.prototype.constructor = NameObjectEvent;

	NameObjectEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('nameObject.' + this.id, function (object, name) {
	        _this.onNameObject(object);
	    });
	};

	NameObjectEvent.prototype.stop = function () {
	    this.app.on('nameObject.' + this.id, null);
	};

	NameObjectEvent.prototype.onNameObject = function (object, name) {
	    var editor = this.app.editor;

	    object.name = name;
	    editor.app.call('sceneGraphChanged', this);
	};

	/**
	 * 删除物体事件
	 * @param {*} app 
	 */
	function RemoveObjectEvent(app) {
	    BaseEvent.call(this, app);
	}

	RemoveObjectEvent.prototype = Object.create(BaseEvent.prototype);
	RemoveObjectEvent.prototype.constructor = RemoveObjectEvent;

	RemoveObjectEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('removeObject.' + this.id, function (object) {
	        _this.onRemoveObject(object);
	    });
	};

	RemoveObjectEvent.prototype.stop = function () {
	    this.app.on('removeObject.' + this.id, null);
	};

	RemoveObjectEvent.prototype.onRemoveObject = function (object) {
	    var editor = this.app.editor;

	    if (object.parent === null) return; // 避免删除相机或场景

	    object.traverse(function (child) {
	        editor.removeHelper(child);
	    });

	    object.parent.remove(object);

	    editor.app.call('objectRemoved', this, object);
	    editor.app.call('sceneGraphChanged', this);
	};

	/**
	 * 添加几何体事件
	 * @param {*} app 
	 */
	function AddGeometryEvent(app) {
	    BaseEvent.call(this, app);
	}

	AddGeometryEvent.prototype = Object.create(BaseEvent.prototype);
	AddGeometryEvent.prototype.constructor = AddGeometryEvent;

	AddGeometryEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('addGeometry.' + this.id, function (geometry) {
	        _this.onAddGeometry(geometry);
	    });
	};

	AddGeometryEvent.prototype.stop = function () {
	    this.app.on('addGeometry.' + this.id, null);
	};

	AddGeometryEvent.prototype.onAddGeometry = function (geometry) {
	    var editor = this.app.editor;
	    editor.geometries[geometry.uuid] = geometry;
	};

	/**
	 * 设置几何体名称事件
	 * @param {*} app 
	 */
	function SetGeometryNameEvent(app) {
	    BaseEvent.call(this, app);
	}

	SetGeometryNameEvent.prototype = Object.create(BaseEvent.prototype);
	SetGeometryNameEvent.prototype.constructor = SetGeometryNameEvent;

	SetGeometryNameEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('setGeometryName.' + this.id, function (geometry, name) {
	        _this.onSetGeometryName(geometry, name);
	    });
	};

	SetGeometryNameEvent.prototype.stop = function () {
	    this.app.on('setGeometryName.' + this.id, null);
	};

	SetGeometryNameEvent.prototype.onSetGeometryName = function (geometry, name) {
	    var editor = this.app.editor;
	    geometry.name = name;
	    editor.app.call('sceneGraphChanged', this);
	};

	/**
	 * 添加材质事件
	 * @param {*} app 
	 */
	function AddMaterialEvent(app) {
	    BaseEvent.call(this, app);
	}

	AddMaterialEvent.prototype = Object.create(BaseEvent.prototype);
	AddMaterialEvent.prototype.constructor = AddMaterialEvent;

	AddMaterialEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('addMaterial.' + this.id, function (material) {
	        _this.onAddMaterial(material);
	    });
	};

	AddMaterialEvent.prototype.stop = function () {
	    this.app.on('addMaterial.' + this.id, null);
	};

	AddMaterialEvent.prototype.onAddMaterial = function (material) {
	    var editor = this.app.editor;

	    editor.materials[material.uuid] = material;
	};

	/**
	 * 设置材质名称事件
	 * @param {*} app 
	 */
	function SetMaterialNameEvent(app) {
	    BaseEvent.call(this, app);
	}

	SetMaterialNameEvent.prototype = Object.create(BaseEvent.prototype);
	SetMaterialNameEvent.prototype.constructor = SetMaterialNameEvent;

	SetMaterialNameEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('setMaterialName.' + this.id, function (material, name) {
	        _this.onSetMaterialName(material, name);
	    });
	};

	SetMaterialNameEvent.prototype.stop = function () {
	    this.app.on('setMaterialName.' + this.id, null);
	};

	SetMaterialNameEvent.prototype.onSetMaterialName = function (material, name) {
	    var editor = this.app.editor;

	    material.name = name;
	    editor.app.call('sceneGraphChanged', this);
	};

	/**
	 * 添加纹理事件
	 * @param {*} app 
	 */
	function AddTextureEvent(app) {
	    BaseEvent.call(this, app);
	}

	AddTextureEvent.prototype = Object.create(BaseEvent.prototype);
	AddTextureEvent.prototype.constructor = AddTextureEvent;

	AddTextureEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('addTexture.' + this.id, function (texture) {
	        _this.onAddTexture(texture);
	    });
	};

	AddTextureEvent.prototype.stop = function () {
	    this.app.on('addTexture.' + this.id, null);
	};

	AddTextureEvent.prototype.onAddTexture = function (texture) {
	    var editor = this.app.editor;

	    editor.textures[texture.uuid] = texture;
	};

	/**
	 * 添加帮助事件
	 * @param {*} app 
	 */
	function AddHelperEvent(app) {
	    BaseEvent.call(this, app);
	}

	AddHelperEvent.prototype = Object.create(BaseEvent.prototype);
	AddHelperEvent.prototype.constructor = AddHelperEvent;

	AddHelperEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('addHelper.' + this.id, function (object) {
	        _this.onAddHelper(object);
	    });
	};

	AddHelperEvent.prototype.stop = function () {
	    this.app.on('addHelper.' + this.id, null);
	};

	AddHelperEvent.prototype.onAddHelper = function (object) {
	    var editor = this.app.editor;

	    var geometry = new THREE.SphereBufferGeometry(2, 4, 2);
	    var material = new THREE.MeshBasicMaterial({
	        color: 0xff0000,
	        visible: false
	    });

	    var helper;

	    if (object instanceof THREE.Camera) { // 相机

	        helper = new THREE.CameraHelper(object, 1);

	    } else if (object instanceof THREE.PointLight) { // 点光源

	        helper = new THREE.PointLightHelper(object, 1);

	    } else if (object instanceof THREE.DirectionalLight) { // 平行光

	        helper = new THREE.DirectionalLightHelper(object, 1);

	    } else if (object instanceof THREE.SpotLight) { // 聚光灯

	        helper = new THREE.SpotLightHelper(object, 1);

	    } else if (object instanceof THREE.HemisphereLight) { // 半球光

	        helper = new THREE.HemisphereLightHelper(object, 1);

	    } else if (object instanceof THREE.SkinnedMesh) { // 带皮肤网格

	        helper = new THREE.SkeletonHelper(object);

	    } else {

	        // no helper for this object type
	        return;

	    }

	    var picker = new THREE.Mesh(geometry, material);
	    picker.name = 'picker';
	    picker.userData.object = object;
	    helper.add(picker);

	    editor.sceneHelpers.add(helper);
	    editor.helpers[object.id] = helper;

	    editor.app.call('helperAdded', this, helper);
	};

	/**
	 * 移除帮助事件
	 * @param {*} app 
	 */
	function RemoveHelperEvent(app) {
	    BaseEvent.call(this, app);
	}

	RemoveHelperEvent.prototype = Object.create(BaseEvent.prototype);
	RemoveHelperEvent.prototype.constructor = RemoveHelperEvent;

	RemoveHelperEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('removeHelper.' + this.id, function (object) {
	        _this.onRemoveHelper(object);
	    });
	};

	RemoveHelperEvent.prototype.stop = function () {
	    this.app.on('removeHelper.' + this.id, null);
	};

	RemoveHelperEvent.prototype.onRemoveHelper = function (object) {
	    var editor = this.app.editor;

	    if (editor.helpers[object.id] !== undefined) {

	        var helper = editor.helpers[object.id];
	        helper.parent.remove(helper);
	        delete editor.helpers[object.id];

	        this.app.call('helperRemoved', this, helper);
	    }
	};

	/**
	 * 添加脚本事件
	 * @param {*} app 
	 */
	function AddScriptEvent(app) {
	    BaseEvent.call(this, app);
	}

	AddScriptEvent.prototype = Object.create(BaseEvent.prototype);
	AddScriptEvent.prototype.constructor = AddScriptEvent;

	AddScriptEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('addScript.' + this.id, function (object, script) {
	        _this.onAddScript(object, script);
	    });
	};

	AddScriptEvent.prototype.stop = function () {
	    this.app.on('addScript.' + this.id, null);
	};

	AddScriptEvent.prototype.onAddScript = function (object, script) {
	    var editor = this.app.editor;

	    if (editor.scripts[object.uuid] === undefined) {

	        editor.scripts[object.uuid] = [];

	    }

	    editor.scripts[object.uuid].push(script);

	    editor.app.call('scriptAdded', this, script);
	};

	/**
	 * 移除脚本事件
	 * @param {*} app 
	 */
	function RemoveScriptEvent(app) {
	    BaseEvent.call(this, app);
	}

	RemoveScriptEvent.prototype = Object.create(BaseEvent.prototype);
	RemoveScriptEvent.prototype.constructor = RemoveScriptEvent;

	RemoveScriptEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('removeScript.' + this.id, function (object, script) {
	        _this.onRemoveScript(object, script);
	    });
	};

	RemoveScriptEvent.prototype.stop = function () {
	    this.app.on('removeScript.' + this.id, null);
	};

	RemoveScriptEvent.prototype.onRemoveScript = function (object, script) {
	    var editor = this.app.editor;

	    if (editor.scripts[object.uuid] === undefined) return;

	    var index = editor.scripts[object.uuid].indexOf(script);

	    if (index !== -1) {

	        editor.scripts[object.uuid].splice(index, 1);

	    }

	    editor.app.call('scriptRemoved', this);
	};

	/**
	 * 选中事件
	 * @param {*} app 
	 */
	function SelectEvent(app) {
	    BaseEvent.call(this, app);
	}

	SelectEvent.prototype = Object.create(BaseEvent.prototype);
	SelectEvent.prototype.constructor = SelectEvent;

	SelectEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('select.' + this.id, function (object) {
	        _this.onSelect(object);
	    });
	};

	SelectEvent.prototype.stop = function () {
	    this.app.on('select.' + this.id, null);
	};

	SelectEvent.prototype.onSelect = function (object) {
	    var editor = this.app.editor;

	    if (editor.selected === object) return;

	    var uuid = null;

	    if (object !== null) {
	        uuid = object.uuid;
	    }

	    editor.selected = object;

	    editor.config.setKey('selected', uuid);
	    this.app.call('objectSelected', this, object);
	};

	/**
	 * 清空场景事件
	 * @param {*} app 
	 */
	function ClearEvent(app) {
	    BaseEvent.call(this, app);
	}

	ClearEvent.prototype = Object.create(BaseEvent.prototype);
	ClearEvent.prototype.constructor = ClearEvent;

	ClearEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('clear.' + this.id, function () {
	        _this.onClear();
	    });
	};

	ClearEvent.prototype.stop = function () {
	    this.app.on('clear.' + this.id, null);
	};

	ClearEvent.prototype.onClear = function () {
	    var editor = this.app.editor;

	    editor.history.clear();
	    editor.storage.clear();

	    editor.camera.copy(editor.DEFAULT_CAMERA);
	    editor.scene.background.setHex(0xaaaaaa);
	    editor.scene.fog = null;

	    var objects = editor.scene.children;

	    while (objects.length > 0) {
	        editor.removeObject(objects[0]);
	    }

	    editor.geometries = {};
	    editor.materials = {};
	    editor.textures = {};
	    editor.scripts = {};

	    editor.deselect();

	    this.app.call('editorCleared', this);
	};

	var ID$1 = -1;

	/**
	 * 所有控件基类
	 * @param {*} options 选项
	 */
	function Control(options) {
	    options = options || {};
	    this.parent = options.parent || document.body;
	    this.id = options.id || 'Control' + ID$1--;
	    this.scope = options.scope || 'global';

	    // 添加引用
	    UI.add(this.id, this, this.scope);
	}

	/**
	 * 定义控件属性
	 */
	Object.defineProperties(Control.prototype, {
	    /**
	     * 控件id（必须在options中设置，而且设置后无法改变）
	     */
	    id: {
	        get: function () {
	            return this._id;
	        },
	        set: function (id) {
	            if (this._id != null) {
	                console.warn(`Control: It is not allowed to assign new value to id.`);
	            }
	            this._id = id;
	        }
	    },

	    /**
	     * 控件id作用域（必须在options中设置，而且设置后无法改变）
	     */
	    scope: {
	        get: function () {
	            return this._scope;
	        },
	        set: function (scope) {
	            if (this._scope != null) {
	                console.warn(`Control: It is not allowed to assign new value to scope.`);
	            }
	            this._scope = scope;
	        }
	    }
	});

	/**
	 * 渲染控件
	 */
	Control.prototype.render = function () {

	};

	/**
	 * 清除该控件内部所有内容。
	 * 该控件仍然可以通过UI.get获取，可以通过render函数重写渲染该控件。
	 */
	Control.prototype.clear = function () {
	    // 移除所有子项引用
	    (function remove(items) {
	        if (items == null || items.length === 0) {
	            return;
	        }

	        items.forEach((n) => {
	            if (n.id) {
	                UI.remove(n.id, n.scope == null ? 'global' : n.scope);
	            }
	            remove(n.children);
	        });
	    })(this.children);

	    this.children.length = 0;

	    // 清空dom
	    if (this.dom) {
	        this.parent.removeChild(this.dom);
	        this.dom = null;
	    }

	    // TODO: 未清除绑定在dom上的事件
	};

	/**
	 * 彻底摧毁该控件，并删除在UI中的引用。
	 */
	Control.prototype.destroy = function () {
	    this.clear();
	    if (this.id) {
	        UI.remove(this.id, this.scope == null ? 'global' : this.scope);
	    }
	};

	/**
	 * 布尔值
	 * @param {*} options 
	 */
	function Boolean$1(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.text = options.text || 'Boolean';
	    this.value = options.value || false;
	    this.cls = options.cls || 'Checkbox';
	    this.style = options.style || null;

	    this.onChange = options.onChange || null;
	}
	Boolean$1.prototype = Object.create(Control.prototype);
	Boolean$1.prototype.constructor = Boolean$1;

	Boolean$1.prototype.render = function () {
	    this.dom = document.createElement('span');

	    if (this.cls) {
	        this.dom.className = this.cls;
	    }

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    this.parent.appendChild(this.dom);

	    this.input = document.createElement('input');
	    this.input.type = 'checkbox';
	    this.dom.appendChild(this.input);

	    this.span = document.createElement('span');
	    this.span.innerHTML = this.text;
	    this.dom.appendChild(this.span);

	    this.setValue(this.value);

	    if (this.onChange) {
	        this.input.addEventListener('change', this.onChange.bind(this), false);
	    }
	};

	Boolean$1.prototype.getValue = function () {
	    return this.input.checked;
	};

	Boolean$1.prototype.setValue = function (value) {
	    this.input.checked = value;
	};

	/**
	 * 换行符
	 * @param {*} options 
	 */
	function Break(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.cls = options.cls || null;
	}
	Break.prototype = Object.create(Control.prototype);
	Break.prototype.constructor = Break;

	Break.prototype.render = function () {
	    this.dom = document.createElement('br');

	    if (this.cls) {
	        this.dom.className = this.cls;
	    }

	    this.parent.appendChild(this.dom);
	};

	/**
	 * 按钮
	 * @param {*} options 
	 */
	function Button(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.text = options.text || 'Button';
	    this.cls = options.cls || 'Button';
	    this.style = options.style || null;
	    this.title = options.title || null;

	    this.onClick = options.onClick || null;
	}
	Button.prototype = Object.create(Control.prototype);
	Button.prototype.constructor = Button;

	Button.prototype.render = function () {
	    this.dom = document.createElement('button');

	    this.dom.innerHTML = this.text;

	    this.dom.className = this.cls;

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    if (this.title) {
	        this.dom.title = this.title;
	    }

	    this.parent.appendChild(this.dom);

	    if (this.onClick) {
	        this.dom.addEventListener('click', this.onClick.bind(this), false);
	    }
	};

	Button.prototype.select = function () {
	    this.dom.classList.add('selected');
	};

	Button.prototype.unselect = function () {
	    this.dom.classList.remove('selected');
	};

	/**
	 * 复选框
	 * @param {*} options 
	 */
	function Checkbox(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.value = options.value || false;
	    this.cls = options.cls || 'Checkbox';
	    this.style = options.style || null;

	    this.onChange = options.onChange || null;
	}
	Checkbox.prototype = Object.create(Control.prototype);
	Checkbox.prototype.constructor = Checkbox;

	Checkbox.prototype.render = function () {
	    this.dom = document.createElement('input');

	    this.dom.type = 'checkbox';

	    this.dom.className = this.cls;

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    this.parent.appendChild(this.dom);

	    if (this.onChange) {
	        this.dom.addEventListener('change', this.onChange.bind(this));
	    }

	    this.setValue(this.value);
	};

	Checkbox.prototype.getValue = function () {
	    return this.dom.checked;
	};

	Checkbox.prototype.setValue = function (value) {
	    if (value !== undefined) {
	        this.dom.checked = value;
	    }

	    return this;
	};

	/**
	 * 关闭按钮
	 * @param {*} options 
	 */
	function CloseButton(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.cls = options.cls || 'CloseButton';
	    this.style = options.style || null;

	    this.onClick = options.onClick || null;
	}

	CloseButton.prototype = Object.create(Control.prototype);
	CloseButton.prototype.constructor = CloseButton;

	CloseButton.prototype.render = function () {
	    this.dom = document.createElement('div');

	    this.dom.className = this.cls;

	    // TODO: 由于按钮默认白色，在白色背景上按钮将不可见！
	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    this.parent.appendChild(this.dom);

	    if (this.onClick) {
	        this.dom.addEventListener('click', this.onClick.bind(this));
	    }

	    this.icon = document.createElement('i');
	    this.icon.className = 'iconfont icon-close';

	    this.dom.appendChild(this.icon);
	};

	/**
	 * 颜色选择器
	 * @param {*} options 
	 */
	function Color(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.value = options.value || null;
	    this.cls = options.cls || 'Color';
	    this.style = options.style || null;

	    this.onChange = options.onChange || null;
	}
	Color.prototype = Object.create(Control.prototype);
	Color.prototype.constructor = Color;

	Color.prototype.render = function () {
	    this.dom = document.createElement('input');

	    this.dom.className = this.cls;

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    try {
	        this.dom.type = 'color';

	        if (this.value && this.value.toString().startsWith('#')) { // #ffffff
	            this.setValue(this.value);
	        } else if (this.value) { // 0xffffff
	            this.setHexValue(this.value);
	        } else {
	            this.dom.value = '#ffffff';
	        }
	    } catch (exception) {
	        console.warn(exception);
	    }

	    this.parent.appendChild(this.dom);

	    if (this.onChange) {
	        this.dom.addEventListener('change', this.onChange.bind(this));
	    }
	};

	Color.prototype.getValue = function () {
	    return this.dom.value;
	};

	Color.prototype.getHexValue = function () {
	    return parseInt(this.dom.value.substr(1), 16);
	};

	Color.prototype.setValue = function (value) {
	    this.dom.value = value;
	    return this;
	};

	Color.prototype.setHexValue = function (hex) {
	    this.dom.value = '#' + ('000000' + hex.toString(16)).slice(- 6);
	    return this;
	};

	/**
	 * 容器（外层无div等元素包裹）
	 * @param {*} options 
	 */
	function Container(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.children = options.children || [];
	}

	Container.prototype = Object.create(Control.prototype);
	Container.prototype.constructor = Container;

	Container.prototype.add = function (obj) {
	    if (!(obj instanceof Control)) {
	        throw 'Container: obj is not an instance of Control.';
	    }
	    this.children.push(obj);
	};

	Container.prototype.remove = function (obj) {
	    var index = this.children.indexOf(obj);
	    if (index > -1) {
	        this.children.splice(index, 1);
	    }
	};

	Container.prototype.render = function () {
	    var _this = this;
	    this.children.forEach(function (n) {
	        var obj = UI.create(n);
	        obj.parent = _this.parent;
	        obj.render();
	    });
	};

	/**
	 * Div元素
	 * @param {*} options 
	 */
	function Div(options) {
	    Container.call(this, options);
	    options = options || {};

	    this.html = options.html || null;
	    this.cls = options.cls || null;
	    this.style = options.style || null;

	    this.onClick = options.onClick || null;
	}
	Div.prototype = Object.create(Container.prototype);
	Div.prototype.constructor = Div;

	Div.prototype.render = function () {
	    this.dom = document.createElement('div');

	    if (this.cls) {
	        this.dom.className = this.cls;
	    }

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    this.parent.appendChild(this.dom);

	    if (this.onClick) {
	        this.dom.onclick = this.onClick.bind(this);
	    }

	    var _this = this;

	    if (this.html) {
	        this.dom.innerHTML = this.html;
	    } else {
	        this.children.forEach(function (n) {
	            var obj = UI.create(n);
	            obj.parent = _this.dom;
	            obj.render();
	        });
	    }
	};

	/**
	 * 水平线
	 * @param {*} options 
	 */
	function HorizontalRule(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.cls = options.cls || 'HorizontalRule';
	}
	HorizontalRule.prototype = Object.create(Control.prototype);
	HorizontalRule.prototype.constructor = HorizontalRule;

	HorizontalRule.prototype.render = function () {
	    this.dom = document.createElement('hr');

	    this.dom.className = this.cls;

	    this.parent.appendChild(this.dom);
	};

	/**
	 * 原生html
	 * @param {*} options 选项
	 */
	function Html(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.html = options.html || null;
	}

	Html.prototype = Object.create(Control.prototype);
	Html.prototype.constructor = Html;

	/**
	 * 渲染控件
	 */
	Html.prototype.render = function () {
	    if (this.html) {
	        this.parent.innerHTML += this.html;
	    }
	};

	/**
	 * 图标按钮
	 * @param {*} options 
	 */
	function IconButton(options) {
	    Button.call(this, options);

	    this.cls = options.cls || 'Button IconButton';
	    this.icon = options.icon || null; // 对应assets/css/icon/iconfont.css中的css
	    this.title = options.title || null;
	}

	IconButton.prototype = Object.create(Button.prototype);
	IconButton.prototype.constructor = IconButton;

	IconButton.prototype.render = function () {
	    Button.prototype.render.call(this);
	    if (this.icon) {
	        this.dom.innerHTML = `<i class="iconfont ${this.icon}"></i>`;
	    }
	};

	/**
	 * 输入框
	 * @param {*} options 
	 */
	function Input(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.value = options.value || '';
	    this.cls = options.cls || 'Input';
	    this.style = options.style || null;
	    this.disabled = options.disabled || false;

	    this.onChange = options.onChange || null;
	}
	Input.prototype = Object.create(Control.prototype);
	Input.prototype.constructor = Input;

	Input.prototype.render = function () {
	    this.dom = document.createElement('input');

	    this.dom.className = this.cls;

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    if (this.disabled) {
	        this.dom.disabled = 'disabled';
	    }

	    this.dom.addEventListener('keydown', function (event) {
	        event.stopPropagation();
	    }, false);

	    this.parent.appendChild(this.dom);

	    if (this.onChange) {
	        this.dom.addEventListener('change', this.onChange.bind(this));
	    }

	    this.setValue(this.value);
	};

	Input.prototype.getValue = function () {
	    return this.dom.value;
	};

	Input.prototype.setValue = function (value) {
	    this.dom.value = value;
	    return this;
	};

	/**
	 * 整数
	 * @param {*} options 
	 */
	function Integer(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.value = options.value || 0;
	    this.min = options.range ? options.range[0] : -Infinity;
	    this.max = options.range ? options.range[1] : Infinity;
	    this.step = options.step || 1; // TODO: step无效
	    this.cls = options.cls || 'Number';
	    this.style = options.style || null;

	    this.onChange = options.onChange || null;
	}
	Integer.prototype = Object.create(Control.prototype);
	Integer.prototype.constructor = Integer;

	Integer.prototype.render = function () {
	    this.dom = document.createElement('input');

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    this.dom.className = this.cls;
	    this.dom.value = '0';

	    this.dom.addEventListener('keydown', function (event) {
	        event.stopPropagation();
	    }, false);

	    this.setValue(this.value);

	    var changeEvent = document.createEvent('HTMLEvents');
	    changeEvent.initEvent('change', true, true);

	    var distance = 0;
	    var onMouseDownValue = 0;

	    var pointer = [0, 0];
	    var prevPointer = [0, 0];

	    var _this = this;

	    function onMouseDown(event) {
	        event.preventDefault();

	        distance = 0;
	        onMouseDownValue = _this.value;
	        prevPointer = [event.clientX, event.clientY];

	        document.addEventListener('mousemove', onMouseMove, false);
	        document.addEventListener('mouseup', onMouseUp, false);
	    }

	    function onMouseMove(event) {
	        var currentValue = _this.value;
	        pointer = [event.clientX, event.clientY];
	        distance += (pointer[0] - prevPointer[0]) - (pointer[1] - prevPointer[1]);

	        var value = onMouseDownValue + (distance / (event.shiftKey ? 5 : 50)) * _this.step;
	        value = Math.min(_this.max, Math.max(_this.min, value)) | 0;

	        if (currentValue !== value) {
	            _this.setValue(value);
	            _this.dom.dispatchEvent(changeEvent);
	        }
	        prevPointer = [event.clientX, event.clientY];
	    }

	    function onMouseUp(event) {
	        document.removeEventListener('mousemove', onMouseMove, false);
	        document.removeEventListener('mouseup', onMouseUp, false);

	        if (Math.abs(distance) < 2) {
	            _this.dom.focus();
	            _this.dom.select();
	        }
	    }

	    function onChange(event) {
	        _this.setValue(_this.dom.value);
	        if (_this.onChange) {
	            _this.onChange.call(_this, _this.dom.value);
	        }
	    }

	    function onFocus(event) {
	        _this.dom.style.backgroundColor = '';
	        _this.dom.style.cursor = '';
	    }

	    function onBlur(event) {
	        _this.dom.style.backgroundColor = 'transparent';
	        _this.dom.style.cursor = 'col-resize';
	    }

	    onBlur();

	    this.dom.addEventListener('mousedown', onMouseDown, false);
	    this.dom.addEventListener('change', onChange, false);
	    this.dom.addEventListener('focus', onFocus, false);
	    this.dom.addEventListener('blur', onBlur, false);

	    this.parent.appendChild(this.dom);
	};

	Integer.prototype.getValue = function () {
	    return this.value;
	};

	Integer.prototype.setValue = function (value) {
	    if (value !== undefined) {
	        value = parseInt(value);

	        this.value = value;
	        this.dom.value = value;
	    }

	    return this;
	};

	Integer.prototype.setStep = function (step) {
	    this.step = parseInt(step);
	    return this;
	};

	Integer.prototype.setRange = function (min, max) {
	    this.min = min;
	    this.max = max;

	    return this;
	};

	/**
	 * 标签控件
	 * @param {*} options 
	 */
	function Label(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.text = options.text || '';
	    this.cls = options.cls || null;
	    this.style = options.style || null;
	}
	Label.prototype = Object.create(Control.prototype);
	Label.prototype.constructor = Label;

	Label.prototype.render = function () {
	    this.dom = document.createElement('label');

	    if (this.text) {
	        this.setValue(this.text);
	    }

	    if (this.cls) {
	        this.dom.className = this.cls;
	    }

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    this.parent.appendChild(this.dom);
	};

	Label.prototype.getValue = function () {
	    return this.dom.textContent;
	};

	Label.prototype.setValue = function (value) {
	    if (value !== undefined) {
	        this.dom.textContent = value;
	    }
	    return this;
	};

	/**
	 * 模态框
	 * @param {*} options 
	 */
	function Modal(options) {
	    Container.call(this, options);
	    options = options || {};

	    this.cls = options.cls || 'Modal';
	    this.style = options.style || null;
	    this.width = options.width || '500px';
	    this.height = options.height || '300px';
	    this.shade = options.shade === false ? false : true;
	    this.shadeClose = options.shadeClose || false;
	}
	Modal.prototype = Object.create(Container.prototype);
	Modal.prototype.constructor = Modal;

	Modal.prototype.render = function () {
	    this.dom = document.createElement('div');

	    if (this.cls) {
	        this.dom.className = this.cls;
	    }

	    if (this.shade === false) {
	        this.dom.classList.add('NoShade');
	    }

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    this.parent.appendChild(this.dom);

	    this.container = document.createElement('div');

	    this.container.className = 'Container';

	    this.container.style.width = this.width;
	    this.container.style.height = this.height;

	    this.dom.appendChild(this.container);

	    this.container.addEventListener('mousedown', function (event) {
	        event.stopPropagation();
	    });

	    if (this.shadeClose) {
	        this.dom.addEventListener('mousedown', this.hide.bind(this));
	    }

	    var _this = this;

	    this.children.forEach(function (n) {
	        var obj = UI.create(n);
	        obj.parent = _this.container;
	        obj.render();
	    });
	};

	Modal.prototype.show = function () {
	    this.dom.style.display = 'flex';
	    return this;
	};

	Modal.prototype.hide = function () {
	    this.dom.style.display = 'none';
	    return this;
	};

	/**
	 * 数字
	 * @param {*} options 
	 */
	function Number$1(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.value = options.value === undefined ? 0 : options.value;
	    this.min = options.min === undefined ? -Infinity : options.min;
	    this.max = options.max === undefined ? Infinity : options.max;
	    this.precision = options.precision === undefined ? 2 : options.precision; // 显示时保留几位小数
	    this.step = options.step === undefined ? 1 : options.step; // 步长
	    this.unit = options.unit === undefined ? '' : options.unit; // 单位（显示时跟在数字后面）
	    this.cls = options.cls || 'Number';
	    this.style = options.style || null;

	    this.onChange = options.onChange || null;
	}
	Number$1.prototype = Object.create(Control.prototype);
	Number$1.prototype.constructor = Number$1;

	Number$1.prototype.render = function () {
	    this.dom = document.createElement('input');

	    this.dom.className = this.cls;

	    this.dom.value = '0.00';

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    var _this = this;

	    // 回车事件
	    this.dom.addEventListener('keydown', function (event) {
	        event.stopPropagation();

	        if (event.keyCode === 13) {
	            _this.dom.blur();
	        }
	    }, false);

	    this.setValue(this.value);

	    var changeEvent = document.createEvent('HTMLEvents');
	    changeEvent.initEvent('change', true, true);

	    var distance = 0;
	    var onMouseDownValue = 0;

	    var pointer = [0, 0];
	    var prevPointer = [0, 0];

	    function onMouseDown(event) {
	        event.preventDefault();
	        distance = 0;
	        onMouseDownValue = _this.value;
	        prevPointer = [event.clientX, event.clientY];
	        document.addEventListener('mousemove', onMouseMove, false);
	        document.addEventListener('mouseup', onMouseUp, false);
	    }

	    function onMouseMove(event) {
	        var currentValue = _this.value;
	        pointer = [event.clientX, event.clientY];
	        distance += (pointer[0] - prevPointer[0]) - (pointer[1] - prevPointer[1]);
	        var value = onMouseDownValue + (distance / (event.shiftKey ? 5 : 50)) * _this.step;
	        value = Math.min(_this.max, Math.max(_this.min, value));

	        if (currentValue !== value) {
	            _this.setValue(value);
	            _this.dom.dispatchEvent(changeEvent);
	        }

	        prevPointer = [event.clientX, event.clientY];
	    }

	    function onMouseUp(event) {
	        document.removeEventListener('mousemove', onMouseMove, false);
	        document.removeEventListener('mouseup', onMouseUp, false);

	        if (Math.abs(distance) < 2) {
	            _this.dom.focus();
	            _this.dom.select();
	        }
	    }

	    function onChange(event) {
	        _this.setValue(_this.dom.value);

	        if (_this.onChange) {
	            _this.onChange.call(_this, _this.dom.value);
	        }
	    }

	    function onFocus(event) {
	        _this.dom.style.backgroundColor = '';
	        _this.dom.style.cursor = '';
	    }

	    function onBlur(event) {
	        _this.dom.style.backgroundColor = 'transparent';
	        _this.dom.style.cursor = 'col-resize';
	    }

	    onBlur();

	    this.dom.addEventListener('mousedown', onMouseDown, false);
	    this.dom.addEventListener('change', onChange, false);
	    this.dom.addEventListener('focus', onFocus, false);
	    this.dom.addEventListener('blur', onBlur, false);

	    this.parent.appendChild(this.dom);
	};

	Number$1.prototype.getValue = function () {
	    return this.value;
	};

	Number$1.prototype.setValue = function (value) {
	    value = parseFloat(value);

	    if (value < this.min) {
	        value = this.min;
	    }

	    if (value > this.max) {
	        value = this.max;
	    }

	    this.value = value;
	    this.dom.value = value.toFixed(this.precision) + this.unit;
	};

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param newParent THREE.Object3D
	 * @param newBefore THREE.Object3D
	 * @constructor
	 */

	function MoveObjectCommand(object, newParent, newBefore) {

		Command.call(this);

		this.type = 'MoveObjectCommand';
		this.name = 'Move Object';

		this.object = object;
		this.oldParent = (object !== undefined) ? object.parent : undefined;
		this.oldIndex = (this.oldParent !== undefined) ? this.oldParent.children.indexOf(this.object) : undefined;
		this.newParent = newParent;

		if (newBefore !== undefined) {

			this.newIndex = (newParent !== undefined) ? newParent.children.indexOf(newBefore) : undefined;

		} else {

			this.newIndex = (newParent !== undefined) ? newParent.children.length : undefined;

		}

		if (this.oldParent === this.newParent && this.newIndex > this.oldIndex) {

			this.newIndex--;

		}

		this.newBefore = newBefore;

	}
	MoveObjectCommand.prototype = Object.create(Command.prototype);

	Object.assign(MoveObjectCommand.prototype, {

		constructor: MoveObjectCommand,

		execute: function () {

			this.oldParent.remove(this.object);

			var children = this.newParent.children;
			children.splice(this.newIndex, 0, this.object);
			this.object.parent = this.newParent;

			this.editor.app.call('sceneGraphChanged', this);

		},

		undo: function () {

			this.newParent.remove(this.object);

			var children = this.oldParent.children;
			children.splice(this.oldIndex, 0, this.object);
			this.object.parent = this.oldParent;

			this.editor.app.call('sceneGraphChanged', this);
		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.newParentUuid = this.newParent.uuid;
			output.oldParentUuid = this.oldParent.uuid;
			output.newIndex = this.newIndex;
			output.oldIndex = this.oldIndex;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.object = this.editor.objectByUuid(json.objectUuid);
			this.oldParent = this.editor.objectByUuid(json.oldParentUuid);
			if (this.oldParent === undefined) {

				this.oldParent = this.editor.scene;

			}
			this.newParent = this.editor.objectByUuid(json.newParentUuid);
			if (this.newParent === undefined) {

				this.newParent = this.editor.scene;

			}
			this.newIndex = json.newIndex;
			this.oldIndex = json.oldIndex;

		}

	});

	/**
	 * 大纲控件
	 * @param {*} options 
	 */
	function Outliner(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.editor = options.editor || null;
	    this.onChange = options.onChange || null;
	    this.onDblClick = options.onDblClick || null;
	}

	Outliner.prototype = Object.create(Control.prototype);
	Outliner.prototype.constructor = Outliner;

	Outliner.prototype.render = function () {
	    this.dom = document.createElement('div');

	    this.dom.className = 'Outliner';
	    this.dom.tabIndex = 0;	// keyup event is ignored without setting tabIndex

	    // hack
	    this.scene = this.editor.scene;

	    // Prevent native scroll behavior
	    this.dom.addEventListener('keydown', function (event) {
	        switch (event.keyCode) {
	            case 38: // up
	            case 40: // down
	                event.preventDefault();
	                event.stopPropagation();
	                break;
	        }

	    }, false);

	    var _this = this;

	    // Keybindings to support arrow navigation
	    this.dom.addEventListener('keyup', function (event) {
	        switch (event.keyCode) {
	            case 38: // up
	                _this.selectIndex(scope.selectedIndex - 1);
	                break;
	            case 40: // down
	                _this.selectIndex(scope.selectedIndex + 1);
	                break;
	        }

	    }, false);

	    this.parent.appendChild(this.dom);

	    if (this.onChange) {
	        this.dom.addEventListener('change', this.onChange.bind(this));
	    }

	    if (this.onDblClick) {
	        this.dom.addEventListener('dblclick', this.onDblClick.bind(this));
	    }

	    this.options = [];
	    this.selectedIndex = - 1;
	    this.selectedValue = null;
	};

	Outliner.prototype.selectIndex = function (index) {
	    if (index >= 0 && index < this.options.length) {
	        this.setValue(this.options[index].value);

	        var changeEvent = document.createEvent('HTMLEvents');
	        changeEvent.initEvent('change', true, true);
	        this.dom.dispatchEvent(changeEvent);
	    }
	};

	Outliner.prototype.setOptions = function (options) {
	    var _this = this;

	    while (this.dom.children.length > 0) {
	        this.dom.removeChild(this.dom.firstChild);
	    }

	    function onClick() {
	        _this.setValue(this.value);

	        var changeEvent = document.createEvent('HTMLEvents');
	        changeEvent.initEvent('change', true, true);
	        _this.dom.dispatchEvent(changeEvent);
	    }

	    // Drag
	    var currentDrag;

	    function onDrag(event) {
	        currentDrag = this;
	    }

	    function onDragStart(event) {
	        event.dataTransfer.setData('text', 'foo');
	    }

	    function onDragOver(event) {
	        if (this === currentDrag) {
	            return;
	        }

	        var area = event.offsetY / this.clientHeight;

	        if (area < 0.25) {
	            this.className = 'option dragTop';
	        } else if (area > 0.75) {
	            this.className = 'option dragBottom';
	        } else {
	            this.className = 'option drag';
	        }
	    }

	    function onDragLeave() {
	        if (this === currentDrag) {
	            return;
	        }

	        this.className = 'option';
	    }

	    function onDrop(event) {
	        if (this === currentDrag) {
	            return;
	        }

	        this.className = 'option';

	        var scene = _this.scene;
	        var object = scene.getObjectById(currentDrag.value);

	        var area = event.offsetY / this.clientHeight;

	        if (area < 0.25) {
	            var nextObject = scene.getObjectById(this.value);
	            moveObject(object, nextObject.parent, nextObject);
	        } else if (area > 0.75) {
	            var nextObject = scene.getObjectById(this.nextSibling.value);
	            moveObject(object, nextObject.parent, nextObject);
	        } else {
	            var parentObject = scene.getObjectById(this.value);
	            moveObject(object, parentObject);
	        }
	    }

	    function moveObject(object, newParent, nextObject) {
	        if (nextObject === null) nextObject = undefined;

	        var newParentIsChild = false;

	        object.traverse(function (child) {
	            if (child === newParent) newParentIsChild = true;
	        });

	        if (newParentIsChild) return;

	        _this.editor.execute(new MoveObjectCommand(object, newParent, nextObject));

	        var changeEvent = document.createEvent('HTMLEvents');
	        changeEvent.initEvent('change', true, true);
	        _this.dom.dispatchEvent(changeEvent);
	    }

	    //
	    _this.options = [];

	    for (var i = 0; i < options.length; i++) {
	        var div = options[i];
	        div.className = 'option';
	        _this.dom.appendChild(div);

	        _this.options.push(div);

	        div.addEventListener('click', onClick, false);

	        if (div.draggable === true) {
	            div.addEventListener('drag', onDrag, false);
	            div.addEventListener('dragstart', onDragStart, false); // Firefox needs this

	            div.addEventListener('dragover', onDragOver, false);
	            div.addEventListener('dragleave', onDragLeave, false);
	            div.addEventListener('drop', onDrop, false);
	        }
	    }

	    return _this;
	};

	Outliner.prototype.getValue = function () {
	    return this.selectedValue;
	};

	Outliner.prototype.setValue = function (value) {
	    for (var i = 0; i < this.options.length; i++) {
	        var element = this.options[i];

	        if (element.value === value) {
	            element.classList.add('active');

	            // scroll into view
	            var y = element.offsetTop - this.dom.offsetTop;
	            var bottomY = y + element.offsetHeight;
	            var minScroll = bottomY - this.dom.offsetHeight;

	            if (this.dom.scrollTop > y) {
	                this.dom.scrollTop = y;
	            } else if (this.dom.scrollTop < minScroll) {
	                this.dom.scrollTop = minScroll;
	            }

	            this.selectedIndex = i;
	        } else {
	            element.classList.remove('active');
	        }
	    }

	    this.selectedValue = value;

	    return this;
	};

	/**
	 * 行控件
	 * @param {*} options 
	 */
	function Row(options) {
	    Container.call(this, options);
	    options = options || {};

	    this.cls = options.cls || 'Row';
	    this.style = options.style || null;
	}
	Row.prototype = Object.create(Container.prototype);
	Row.prototype.constructor = Row;

	Row.prototype.render = function () {
	    this.dom = document.createElement('div');

	    this.dom.className = this.cls;

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    this.parent.appendChild(this.dom);

	    var _this = this;

	    this.children.forEach(function (n) {
	        var obj = UI.create(n);
	        obj.parent = _this.dom;
	        obj.render();
	    });
	};

	/**
	 * 选择列表
	 * @param {*} options 
	 */
	function Select(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.options = options.options || [];
	    this.value = options.value || '';
	    this.cls = options.cls || 'Select';
	    this.style = options.style || null;
	    this.multiple = options.multiple || false;

	    this.onChange = options.onChange || null;
	}
	Select.prototype = Object.create(Control.prototype);
	Select.prototype.constructor = Select;

	Select.prototype.render = function () {
	    this.dom = document.createElement('select');

	    this.dom.className = this.cls;

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    if (this.multiple) {
	        this.dom.multiple = this.multiple;
	    }

	    var _this = this;

	    if (this.options) {
	        Object.keys(this.options).forEach(function (n) {
	            var option = document.createElement('option');
	            option.value = n;
	            option.innerHTML = _this.options[n];

	            if (_this.value == n) {
	                option.selected = 'selected';
	            }

	            _this.dom.appendChild(option);
	        });
	    }

	    if (this.onChange) {
	        this.dom.addEventListener('change', this.onChange.bind(this));
	    }

	    this.parent.appendChild(this.dom);
	};

	Select.prototype.setMultiple = function (boolean) {
	    this.dom.multiple = boolean;
	    return this;
	};

	Select.prototype.setOptions = function (options) {
	    var selected = this.dom.value;
	    while (this.dom.children.length > 0) {
	        this.dom.removeChild(this.dom.firstChild);
	    }

	    for (var key in options) {
	        var option = document.createElement('option');
	        option.value = key;
	        option.innerHTML = options[key];
	        this.dom.appendChild(option);
	    }

	    this.dom.value = selected;

	    return this;

	};

	Select.prototype.getValue = function () {
	    return this.dom.value;
	};

	Select.prototype.setValue = function (value) {
	    value = String(value);

	    if (this.dom.value !== value) {
	        this.dom.value = value;
	    }

	    return this;
	};

	/**
	 * 文本块
	 * @param {*} options 
	 */
	function Span(options) {
	    Container.call(this, options);
	}
	Span.prototype = Object.create(Container.prototype);
	Span.prototype.constructor = Span;

	Span.prototype.render = function () {
	    this.dom = document.createElement('span');
	    this.parent.appendChild(this.dom);

	    var _this = this;
	    this.children.forEach(function (n) {
	        var obj = UI.create(n);
	        obj.parent = _this.dom;
	        obj.render();
	    });
	};

	/**
	 * 文本框
	 * @param {*} options 
	 */
	function Text(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.text = options.text || '';
	    this.cls = options.cls || 'Text';
	    this.style = options.style || null;

	    this.onClick = options.onClick || null;
	}
	Text.prototype = Object.create(Control.prototype);
	Text.prototype.constructor = Text;

	Text.prototype.render = function () {
	    this.dom = document.createElement('span');

	    this.dom.className = this.cls;

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    this.setValue(this.text);

	    if (this.onClick) {
	        this.dom.addEventListener('click', this.onClick.bind(this));
	    }

	    this.parent.appendChild(this.dom);
	};

	Text.prototype.getValue = function () {
	    return this.dom.textContent;
	};

	Text.prototype.setValue = function (value) {
	    if (value !== undefined) {
	        this.dom.textContent = value;
	    }
	    return this;
	};

	/**
	 * 文本域
	 * @param {*} options 
	 */
	function TextArea(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.value = options.value || '';
	    this.cls = options.cls || 'TextArea';
	    this.style = options.style || null;

	    this.onChange = options.onChange || null;
	    this.onKeyUp = options.onKeyUp || null;
	}
	TextArea.prototype = Object.create(Control.prototype);
	TextArea.prototype.constructor = TextArea;

	TextArea.prototype.render = function () {
	    this.dom = document.createElement('textarea');

	    this.dom.className = this.cls;

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    this.dom.spellcheck = false;

	    var _this = this;

	    this.dom.addEventListener('keydown', function (event) {
	        event.stopPropagation();

	        if (event.keyCode === 9) {
	            event.preventDefault();

	            var cursor = _this.dom.selectionStart;
	            _this.dom.value = _this.dom.value.substring(0, cursor) + '\t' + _this.dom.value.substring(cursor);
	            _this.dom.selectionStart = cursor + 1;
	            _this.dom.selectionEnd = _this.dom.selectionStart;
	        }

	    }, false);

	    this.parent.appendChild(this.dom);

	    if (this.onChange) {
	        this.dom.addEventListener('change', this.onChange.bind(this));
	    }

	    if (this.onKeyUp) {
	        this.dom.addEventListener('keyup', this.onKeyUp.bind(this));
	    }

	    this.setValue(this.value);
	};

	TextArea.prototype.getValue = function () {
	    return this.dom.value;
	};

	TextArea.prototype.setValue = function (value) {
	    this.dom.value = value;
	    return this;
	};

	/**
	 * 纹理
	 * @param {*} options 
	 */
	function Texture(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.mapping = options.mapping || THREE.UVMapping;

	    this.onChange = options.onChange || null;
	}

	Texture.prototype = Object.create(Control.prototype);
	Texture.prototype.constructor = Texture;

	Texture.prototype.render = function () {
	    this.dom = document.createElement('div');

	    this.dom.className = 'Texture';

	    this.form = document.createElement('form');

	    this.input = document.createElement('input');
	    this.input.type = 'file';

	    var _this = this;

	    this.input.addEventListener('change', function (event) {
	        _this.loadFile(event.target.files[0]);
	    });

	    this.form.appendChild(this.input);

	    this.canvas = document.createElement('canvas');
	    this.canvas.width = 32;
	    this.canvas.height = 16;

	    this.canvas.addEventListener('click', function (event) {
	        _this.input.click();
	    }, false);

	    this.canvas.addEventListener('drop', function (event) {
	        event.preventDefault();
	        event.stopPropagation();
	        _this.loadFile(event.dataTransfer.files[0]);
	    }, false);

	    this.dom.appendChild(this.canvas);

	    this.name = document.createElement('input');
	    this.name.disabled = true;
	    this.dom.appendChild(this.name);

	    this.parent.appendChild(this.dom);

	    this.texture = null;
	};

	Texture.prototype.getValue = function () {
	    return this.texture;
	};

	Texture.prototype.setValue = function (texture) {
	    var canvas = this.dom.children[0];
	    var name = this.dom.children[1];
	    var context = canvas.getContext('2d');

	    if (texture !== null) {
	        var image = texture.image;

	        if (image !== undefined && image.width > 0) {
	            if (texture.sourceFile) {
	                name.value = texture.sourceFile;
	            } else {
	                name.value = '';
	            }

	            var scale = canvas.width / image.width;
	            context.drawImage(image, 0, 0, image.width * scale, image.height * scale);
	        } else {
	            name.value = (texture.sourceFile == null ? '' : texture.sourceFile) + '错误';
	            context.clearRect(0, 0, canvas.width, canvas.height);
	        }

	    } else {
	        name.value = '';

	        if (context !== null) {
	            // Seems like context can be null if the canvas is not visible
	            context.clearRect(0, 0, canvas.width, canvas.height);
	        }
	    }

	    this.texture = texture;
	};

	Texture.prototype.loadFile = function (file) {
	    var _this = this;

	    if (file.type.match('image.*')) {
	        var reader = new FileReader();

	        if (file.type === 'image/targa') {
	            reader.addEventListener('load', function (event) {
	                var canvas = new THREE.TGALoader().parse(event.target.result);
	                var texture = new THREE.CanvasTexture(canvas, _this.mapping);

	                texture.sourceFile = file.name;

	                _this.setValue(texture);

	                if (_this.onChange) {
	                    _this.onChange();
	                }
	            }, false);

	            reader.readAsArrayBuffer(file);
	        } else {
	            reader.addEventListener('load', function (event) {
	                var image = document.createElement('img');

	                image.addEventListener('load', function (event) {
	                    var texture = new THREE.Texture(this, _this.mapping);
	                    texture.sourceFile = file.name;
	                    texture.format = file.type === 'image/jpeg' ? THREE.RGBFormat : THREE.RGBAFormat;
	                    texture.needsUpdate = true;

	                    _this.setValue(texture);

	                    if (_this.onChange) {
	                        _this.onChange();
	                    }
	                }, false);

	                image.src = event.target.result;
	            }, false);

	            reader.readAsDataURL(file);
	        }
	    }

	    this.form.reset();
	};

	/**
	 * 模态框
	 * @param {*} options 
	 */
	function Window$1(options) {
	    Modal.call(this, options);
	    options = options || {};

	    this.cls = options.cls || 'Modal Window';
	    this.style = options.style || null;
	    this.bodyStyle = options.bodyStyle || null;
	    this.title = options.title || '';
	    this.buttons = options.buttons || [];
	}
	Window$1.prototype = Object.create(Modal.prototype);
	Window$1.prototype.constructor = Window$1;

	Window$1.prototype.render = function () {
	    this.content = this.children; // 内容
	    this.children = []; // 标题栏、内容区域、按钮工具栏

	    // 标题
	    this.caption = UI.create({
	        xtype: 'container',
	        children: [{
	            xtype: 'div',
	            cls: 'caption',
	            html: this.title
	        }]
	    });

	    // 关闭按钮
	    this.closeBtn = UI.create({
	        xtype: 'closebutton',
	        onClick: () => {
	            this.hide();
	        }
	    });

	    // 标题栏
	    this.header = UI.create({
	        xtype: 'div',
	        cls: 'header',
	        children: [
	            this.caption,
	            this.closeBtn
	        ]
	    });
	    this.children.push(this.header);

	    // 内容区域
	    this.body = UI.create({
	        xtype: 'div',
	        cls: 'body',
	        style: this.bodyStyle,
	        children: this.content
	    });
	    this.children.push(this.body);

	    // 按钮区域
	    this.footer = UI.create({
	        xtype: 'div',
	        cls: 'footer',
	        children: this.buttons
	    });
	    this.children.push(this.footer);

	    Modal.prototype.render.call(this);

	    // 拖动标题栏
	    var isDown = false;
	    var offsetX = 0;
	    var offsetY = 0;

	    var _this = this;

	    function mouseDown(event) {
	        isDown = true;
	        var left = _this.container.style.left === '' ? 0 : parseInt(_this.container.style.left.replace('px', ''));
	        var top = _this.container.style.top === '' ? 0 : parseInt(_this.container.style.top.replace('px', ''));
	        offsetX = event.clientX - left;
	        offsetY = event.clientY - top;
	    }

	    function mouseMove(event) {
	        if (!isDown) {
	            return;
	        }
	        var dx = event.clientX - offsetX;
	        var dy = event.clientY - offsetY;

	        _this.container.style.left = dx + 'px';
	        _this.container.style.top = dy + 'px';
	    }

	    function mouseUp(event) {
	        isDown = false;
	        offsetX = 0;
	        offsetY = 0;
	    }

	    this.header.dom.addEventListener('mousedown', mouseDown);
	    document.body.addEventListener('mousemove', mouseMove);
	    document.body.addEventListener('mouseup', mouseUp);
	};

	/**
	 * 图片
	 * @param {*} options 选项
	 */
	function Image$1(options) {
	    Control.call(this, options);
	    options = options || {};

	    this.src = options.src || '';
	    this.title = options.title || null;
	    this.alt = options.alt || null;
	    this.cls = options.cls || 'Item';
	    this.style = options.style || null;

	    this.onClick = options.onClick || null;
	}

	Image$1.prototype = Object.create(Control.prototype);
	Image$1.prototype.constructor = Image$1;

	Image$1.prototype.render = function () {
	    this.dom = document.createElement('div');

	    if (this.cls) {
	        this.dom.className = this.cls;
	    }

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    // 图片
	    this.img = document.createElement('img');

	    this.img.src = this.src;

	    if (this.title) {
	        this.img.title = this.title;
	    }

	    if (this.alt) {
	        this.img.alt = this.alt;
	    }

	    this.dom.appendChild(this.img);

	    // 事件
	    var _this = this;

	    function onClick(event, type) {
	        event.stopPropagation();
	        event.preventDefault();
	        if (_this.onClick) {
	            _this.onClick.call(_this, event, type);
	        }
	    }
	    this.dom.addEventListener('click', (event) => onClick(event, 'default'));

	    // 操作按钮
	    this.editBtn = UI.create({
	        xtype: 'iconbutton',
	        icon: 'icon-edit',
	        cls: 'Button IconButton EditButton',
	        title: '编辑',
	        onClick: (event) => onClick(event, 'edit')
	    });
	    this.editBtn.render();

	    this.dom.appendChild(this.editBtn.dom);

	    this.deleteBtn = UI.create({
	        xtype: 'iconbutton',
	        icon: 'icon-delete',
	        cls: 'Button IconButton DeleteButton',
	        title: '删除',
	        onClick: (event) => onClick(event, 'delete')
	    });
	    this.deleteBtn.render();

	    this.dom.appendChild(this.deleteBtn.dom);

	    this.parent.appendChild(this.dom);
	};

	/**
	 * 图片列表
	 * @param {*} options 选项
	 */
	function ImageList(options = {}) {
	    Container.call(this, options);

	    this.cls = options.cls || 'ImageList';
	    this.style = options.style || {
	        width: '800px',
	        height: '500px'
	    };

	    this.onClick = options.onClick || null;
	}

	ImageList.prototype = Object.create(Container.prototype);
	ImageList.prototype.constructor = ImageList;

	ImageList.prototype.render = function () {
	    this.dom = document.createElement('div');

	    if (this.cls) {
	        this.dom.className = this.cls;
	    }

	    if (this.style) {
	        Object.assign(this.dom.style, this.style);
	    }

	    this.dom.style.width = this.width;
	    this.dom.style.height = this.height;

	    this.dom.style.flex = this.flex;

	    this.parent.appendChild(this.dom);

	    function onClick(event, type) {
	        var index = event.target.dataIndex;
	        if (this.onClick) {
	            this.onClick(event, index, type);
	        }
	    }
	    this.children.forEach((n, i) => {
	        // 容器
	        var container = document.createElement('div');
	        container.className = 'Container';
	        this.dom.appendChild(container);

	        // 图片
	        var title = n.title;
	        n.title = null;
	        var obj = UI.create(n);
	        if (!(obj instanceof Image$1)) {
	            console.warn(`ImageList: obj is not an instance of Image.`);
	        }

	        obj.parent = container;
	        obj.onClick = onClick.bind(this);
	        obj.render();
	        obj.dom.dataIndex = i; // 序号
	        obj.img.dataIndex = i;
	        obj.editBtn.dom.dataIndex = i;
	        obj.deleteBtn.dom.dataIndex = i;

	        // 说明
	        var description = document.createElement('div');
	        description.className = 'title';
	        description.innerHTML = title;
	        container.appendChild(description);
	    });
	};

	/**
	 * 消息框
	 * @param {*} options 
	 */
	function MessageBox(options) {
	    Container.call(this, options);
	    options = options || {};

	    this.time = options.time || 5000;
	}
	MessageBox.prototype = Object.create(Container.prototype);
	MessageBox.prototype.constructor = MessageBox;

	MessageBox.prototype.render = function () {
	    this.dom = document.createElement('div');
	    this.dom.className = 'MessageBox';
	    this.parent.appendChild(this.dom);
	};

	MessageBox.prototype.show = function (html) {
	    this.dom.innerHTML = html;
	    this.dom.display = 'block';

	    // 设置居中
	    this.dom.style.left = (this.parent.clientWidth - this.dom.clientWidth) / 2 + 'px';
	    this.dom.style.top = (this.parent.clientHeight - this.dom.clientHeight) / 2 + 'px';

	    if (this.time > 0) {
	        setTimeout(() => {
	            this.destroy();
	        }, this.time);
	    }
	};

	MessageBox.prototype.hide = function () {
	    this.dom.display = 'none';
	};

	/**
	 * UI类
	 */
	function UICls() {
	    this.xtypes = {};
	    this.objects = {};
	}

	/**
	 * 添加xtype
	 * @param {*} name xtype字符串
	 * @param {*} cls xtype对应类
	 */
	UICls.prototype.addXType = function (name, cls) {
	    if (this.xtypes[name] === undefined) {
	        this.xtypes[name] = cls;
	    } else {
	        console.warn(`UICls: xtype named ${name} has already been added.`);
	    }
	};

	/**
	 * 删除xtype
	 * @param {*} name xtype字符串
	 */
	UICls.prototype.removeXType = function (name) {
	    if (this.xtypes[name] !== undefined) {
	        delete this.xtypes[name];
	    } else {
	        console.warn(`UICls: xtype named ${name} is not defined.`);
	    }
	};

	/**
	 * 获取xtype
	 * @param {*} name xtype字符串
	 */
	UICls.prototype.getXType = function (name) {
	    if (this.xtypes[name] === undefined) {
	        console.warn(`UICls: xtype named ${name} is not defined.`);
	    }
	    return this.xtypes[name];
	};

	/**
	 * 添加一个对象到缓存
	 * @param {*} id 对象id
	 * @param {*} obj 对象
	 * @param {*} scope 对象作用域（默认为global）
	 */
	UICls.prototype.add = function (id, obj, scope = "global") {
	    var key = `${scope}:${id}`;
	    if (this.objects[key] !== undefined) {
	        console.warn(`UICls: object named ${id} has already been added.`);
	    }
	    this.objects[key] = obj;
	};

	/**
	 * 从缓存中移除一个对象
	 * @param {*} id 对象id
	 * @param {*} scope 对象作用域（默认为global）
	 */
	UICls.prototype.remove = function (id, scope = 'global') {
	    var key = `${scope}:${id}`;
	    if (this.objects[key] != undefined) {
	        delete this.objects[key];
	    } else {
	        console.warn(`UICls: object named ${id} is not defined.`);
	    }
	};

	/**
	 * 从缓存中获取一个对象
	 * @param {*} id 控件id
	 * @param {*} scope 对象作用域（默认为global）
	 */
	UICls.prototype.get = function (id, scope = 'global') {
	    var key = `${scope}:${id}`;
	    if (this.objects[key] === undefined) {
	        console.warn(`UICls: object named ${id} is not defined.`);
	    }
	    return this.objects[key];
	};

	/**
	 * 通过json配置创建UI实例，并自动将包含id的控件添加到缓存
	 * @param {*} config xtype配置
	 */
	UICls.prototype.create = function (config) {
	    if (config instanceof Control) { // config是Control实例
	        return config;
	    }

	    // config是json配置
	    if (config == null || config.xtype == null) {
	        throw 'UICls: config is undefined.';
	    }

	    if (config.xtype === undefined) {
	        throw 'UICls: config.xtype is undefined.';
	    }

	    var cls = this.xtypes[config.xtype];
	    if (cls == null) {
	        throw `UICls: xtype named ${config.xtype} is undefined.`;
	    }

	    return new cls(config);
	};

	/**
	 * UICls唯一一个实例
	 */
	const UI$1 = new UICls();

	// 添加所有控件
	Object.assign(UI$1, {
	    Boolean: Boolean$1,
	    Break: Break,
	    Button: Button,
	    Checkbox: Checkbox,
	    CloseButton: CloseButton,
	    Color: Color,
	    Container: Container,
	    Control: Control,
	    Div: Div,
	    HorizontalRule: HorizontalRule,
	    Html: Html,
	    IconButton: IconButton,
	    Input: Input,
	    Integer: Integer,
	    Label: Label,
	    Modal: Modal,
	    Number: Number$1,
	    Outliner: Outliner,
	    Row: Row,
	    Select: Select,
	    Span: Span,
	    Text: Text,
	    TextArea: TextArea,
	    Texture: Texture,
	    Window: Window$1,
	    Image: Image$1,
	    ImageList: ImageList,
	    MessageBox: MessageBox
	});

	// 添加所有控件的XType
	UI$1.addXType('boolean', Boolean$1);
	UI$1.addXType('br', Break);
	UI$1.addXType('button', Button);
	UI$1.addXType('checkbox', Checkbox);
	UI$1.addXType('closebutton', CloseButton);
	UI$1.addXType('color', Color);
	UI$1.addXType('container', Container);
	UI$1.addXType('control', Control);
	UI$1.addXType('div', Div);
	UI$1.addXType('hr', HorizontalRule);
	UI$1.addXType('html', Html);
	UI$1.addXType('iconbutton', IconButton);
	UI$1.addXType('input', Input);
	UI$1.addXType('int', Integer);
	UI$1.addXType('label', Label);
	UI$1.addXType('modal', Modal);
	UI$1.addXType('number', Number$1);
	UI$1.addXType('outliner', Outliner);
	UI$1.addXType('row', Row);
	UI$1.addXType('select', Select);
	UI$1.addXType('span', Span);
	UI$1.addXType('text', Text);
	UI$1.addXType('textarea', TextArea);
	UI$1.addXType('texture', Texture);
	UI$1.addXType('window', Window$1);
	UI$1.addXType('image', Image$1);
	UI$1.addXType('imagelist', ImageList);
	UI$1.addXType('msg', MessageBox);

	// 添加一些实用功能
	Object.assign(UI$1, {
	    msg: function (text) {
	        var msg = UI$1.create({ xtype: 'msg' });
	        msg.render();
	        msg.show(text);
	    }
	});

	window.UI = UI$1;

	/**
	 * 加载场景事件
	 * @param {*} app 
	 */
	function LoadEvent(app) {
	    BaseEvent.call(this, app);
	}

	LoadEvent.prototype = Object.create(BaseEvent.prototype);
	LoadEvent.prototype.constructor = LoadEvent;

	LoadEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('load.' + this.id, function () {
	        _this.onLoad();
	    });
	};

	LoadEvent.prototype.stop = function () {
	    this.app.on('load.' + this.id, null);
	};

	LoadEvent.prototype.onLoad = function () {
	    UI$1.msg('加载场景成功！');
	};

	var ID$2 = -1;

	/**
	 * 序列化器基类
	 */
	function BaseSerializer() {
	    this.id = 'BaseSerializer' + ID$2--;
	}

	/**
	 * 转为json
	 * @param {*} obj 对象
	 */
	BaseSerializer.prototype.toJSON = function (obj) {
	    return {};
	};

	/**
	 * json转对象
	 * @param {*} json json字符串
	 */
	BaseSerializer.prototype.fromJSON = function (json) {
	    return null;
	};

	var Metadata = {
	    generator: 'ShadowEditor',
	    type: 'Object',
	    version: '0.0.1'
	};

	/**
	 * Object3D序列化器
	 */
	function ConfigSerializer() {
	    BaseSerializer.call(this);
	}

	ConfigSerializer.prototype = Object.create(BaseSerializer.prototype);
	ConfigSerializer.prototype.constructor = ConfigSerializer;

	ConfigSerializer.prototype.toJSON = function (obj) {
	    var json = obj.toJSON();
	    return json;
	};

	ConfigSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * Object3D序列化器
	 */
	function ScriptSerializer() {
	    BaseSerializer.call(this);
	}

	ScriptSerializer.prototype = Object.create(BaseSerializer.prototype);
	ScriptSerializer.prototype.constructor = ScriptSerializer;

	ScriptSerializer.prototype.toJSON = function (obj) {
	    return obj;
	};

	ScriptSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * Object3D序列化器
	 */
	function Object3DSerializer() {
	    BaseSerializer.call(this);
	}

	Object3DSerializer.prototype = Object.create(BaseSerializer.prototype);
	Object3DSerializer.prototype.constructor = Object3DSerializer;

	Object3DSerializer.prototype.toJSON = function (obj) {
	    var json = BaseSerializer.prototype.toJSON(obj);

	    json.type = obj.type;
	    json.uuid = obj.uuid;
	    json.castShadow = obj.castShadow;
	    json.children = obj.children.map(function (child) {
	        return child.uuid;
	    });
	    json.frustumCulled = obj.frustumCulled;
	    json.matrix = obj.matrix;
	    json.matrixAutoUpdate = obj.matrixAutoUpdate;
	    json.name = obj.name;
	    json.parent = obj.parent == null ? null : obj.parent.uuid;
	    json.position = obj.position;
	    json.quaternion = {
	        x: obj.quaternion.x,
	        y: obj.quaternion.y,
	        z: obj.quaternion.z,
	        w: obj.quaternion.w
	    };
	    json.receiveShadow = obj.receiveShadow;
	    json.renderOrder = obj.renderOrder;
	    json.rotation = {
	        x: obj.rotation.x,
	        y: obj.rotation.y,
	        z: obj.rotation.z,
	        order: obj.rotation.order
	    };
	    json.scale = obj.scale;
	    json.up = obj.up;
	    json.userData = obj.userData;

	    return json;
	};

	Object3DSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * Scene序列化器
	 */
	function SceneSerializer() {
	    BaseSerializer.call(this);
	}

	SceneSerializer.prototype = Object.create(BaseSerializer.prototype);
	SceneSerializer.prototype.constructor = SceneSerializer;

	SceneSerializer.prototype.toJSON = function (obj) {
	    var json = Object3DSerializer.prototype.toJSON(obj);

	    json.background = obj.background;
	    json.fog = obj.fog;
	    json.overrideMaterial = obj.overrideMaterial;

	    return json;
	};

	SceneSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * Camera序列化器
	 */
	function CameraSerializer() {
	    BaseSerializer.call(this);
	}

	CameraSerializer.prototype = Object.create(BaseSerializer.prototype);
	CameraSerializer.prototype.constructor = CameraSerializer;

	CameraSerializer.prototype.toJSON = function (obj) {
	    var json = Object3DSerializer.prototype.toJSON(obj);

	    json.matrixWorldInverse = obj.matrixWorldInverse;
	    json.projectionMatrix = obj.projectionMatrix;

	    return json;
	};

	CameraSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * OrthographicCamera序列化器
	 */
	function OrthographicCameraSerializer() {
	    BaseSerializer.call(this);
	}

	OrthographicCameraSerializer.prototype = Object.create(BaseSerializer.prototype);
	OrthographicCameraSerializer.prototype.constructor = OrthographicCameraSerializer;

	OrthographicCameraSerializer.prototype.toJSON = function (obj) {
	    var json = CameraSerializer.prototype.toJSON(obj);

	    json.bottom = obj.bottom;
	    json.far = obj.far;
	    json.left = obj.left;
	    json.near = obj.near;
	    json.right = obj.right;
	    json.top = obj.top;
	    json.view = obj.view;
	    json.zoom = obj.zoom;

	    return json;
	};

	OrthographicCameraSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * PerspectiveCamera序列化器
	 */
	function PerspectiveCameraSerializer() {
	    BaseSerializer.call(this);
	}

	PerspectiveCameraSerializer.prototype = Object.create(BaseSerializer.prototype);
	PerspectiveCameraSerializer.prototype.constructor = PerspectiveCameraSerializer;

	PerspectiveCameraSerializer.prototype.toJSON = function (obj) {
	    var json = CameraSerializer.prototype.toJSON(obj);

	    json.aspect = obj.aspect;
	    json.far = obj.far;
	    json.filmGauge = obj.filmGauge;
	    json.filmOffset = obj.filmOffset;
	    json.focus = obj.focus;
	    json.fov = obj.fov;
	    json.near = obj.near;
	    json.view = obj.view;
	    json.zoom = obj.zoom;

	    return json;
	};

	PerspectiveCameraSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * Light序列化器
	 */
	function LightSerializer() {
	    BaseSerializer.call(this);
	}

	LightSerializer.prototype = Object.create(BaseSerializer.prototype);
	LightSerializer.prototype.constructor = LightSerializer;

	LightSerializer.prototype.toJSON = function (obj) {
	    var json = Object3DSerializer.prototype.toJSON(obj);

	    json.color = item.color;
	    json.intensity = item.intensity;

	    return json;
	};

	LightSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * PointLight序列化器
	 */
	function PointLightSerializer() {
	    BaseSerializer.call(this);
	}

	PointLightSerializer.prototype = Object.create(BaseSerializer.prototype);
	PointLightSerializer.prototype.constructor = PointLightSerializer;

	PointLightSerializer.prototype.toJSON = function (obj) {
	    var json = LightSerializer.prototype.toJSON(obj);

	    json.distance = item.distance;
	    json.decay = item.decay;

	    return json;
	};

	PointLightSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * SpotLight序列化器
	 */
	function SpotLightSerializer() {
	    BaseSerializer.call(this);
	}

	SpotLightSerializer.prototype = Object.create(BaseSerializer.prototype);
	SpotLightSerializer.prototype.constructor = SpotLightSerializer;

	SpotLightSerializer.prototype.toJSON = function (obj) {
	    var json = LightSerializer.prototype.toJSON(obj);

	    json.distance = item.distance;
	    json.angle = item.angle;
	    json.penumbra = item.penumbra;
	    json.decay = item.decay;

	    return json;
	};

	SpotLightSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * HemisphereLight序列化器
	 */
	function HemisphereLightSerializer() {
	    BaseSerializer.call(this);
	}

	HemisphereLightSerializer.prototype = Object.create(BaseSerializer.prototype);
	HemisphereLightSerializer.prototype.constructor = HemisphereLightSerializer;

	HemisphereLightSerializer.prototype.toJSON = function (obj) {
	    var json = LightSerializer.prototype.toJSON(obj);

	    json.skyColor = item.skyColor;
	    json.groundColor = item.groundColor;

	    return json;
	};

	HemisphereLightSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * RectAreaLight序列化器
	 */
	function RectAreaLightSerializer() {
	    BaseSerializer.call(this);
	}

	RectAreaLightSerializer.prototype = Object.create(BaseSerializer.prototype);
	RectAreaLightSerializer.prototype.constructor = RectAreaLightSerializer;

	RectAreaLightSerializer.prototype.toJSON = function (obj) {
	    var json = LightSerializer.prototype.toJSON(obj);

	    json.width = item.width;
	    json.height = item.height;

	    return json;
	};

	RectAreaLightSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * Geometry序列化器
	 */
	function GeometrySerializer() {
	    BaseSerializer.call(this);
	}

	GeometrySerializer.prototype = Object.create(BaseSerializer.prototype);
	GeometrySerializer.prototype.constructor = GeometrySerializer;

	GeometrySerializer.prototype.toJSON = function (obj) {
	    var json = BaseSerializer.prototype.toJSON(obj);

	    json.name = geometry.name;
	    json.type = geometry.type;
	    json.userData = geometry.userData;
	    json.uuid = geometry.uuid;

	    return json;
	};

	GeometrySerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * Material序列化器
	 */
	function MaterialSerializer() {
	    BaseSerializer.call(this);
	}

	MaterialSerializer.prototype = Object.create(BaseSerializer.prototype);
	MaterialSerializer.prototype.constructor = MaterialSerializer;

	MaterialSerializer.prototype.toJSON = function (obj) {
	    var json = BaseSerializer.prototype.toJSON(obj);

	    json.alphaMap = material.alphaMap;
	    json.alphaTest = material.alphaTest;
	    json.aoMap = material.aoMap;
	    json.aoMapIntensity = material.aoMapIntensity;
	    json.blendDst = material.blendDst;
	    json.blendDstAlpha = material.blendDstAlpha;
	    json.blendEquation = material.blendEquation;
	    json.blendEquationAlpha = material.blendEquationAlpha;
	    json.blendSrc = material.blendSrc;
	    json.blendSrcAlpha = material.blendSrcAlpha;
	    json.blending = material.blending;
	    json.bumpMap = material.bumpMap;
	    json.bumpScale = material.bumpScale;
	    json.clipIntersection = material.clipIntersection;
	    json.clipShadow = material.clipShadow;
	    json.clippingPlanes = material.clippingPlanes;
	    json.color = material.color;
	    json.colorWrite = material.colorWrite;
	    json.depthFunc = material.depthFunc;
	    json.depthTest = material.depthTest;
	    json.depthWrite = material.depthWrite;
	    json.displacementBias = material.displacementBias;
	    json.displacementMap = material.displacementMap;
	    json.displacementScale = material.displacementScale;
	    json.dithering = material.dithering;
	    json.emissive = material.emissive;
	    json.emissiveIntensity = material.emissiveIntensity;
	    json.emissiveMap = material.emissiveMap;
	    json.envMap = material.envMap;
	    json.envMapIntensity = material.envMapIntensity;
	    json.flatShading = material.flatShading;
	    json.fog = material.fog;
	    json.lightMap = material.lightMap;
	    json.lightMapIntensity = material.lightMapIntensity;
	    json.lights = material.lights;
	    json.linewidth = material.linewidth;
	    json.map = material.map;
	    json.metalness = material.metalness;
	    json.metalnessMap = material.metalnessMap;
	    json.morphNormals = material.morphNormals;
	    json.morphTargets = material.morphTargets;
	    json.name = material.name;
	    json.normalMap = material.normalMap;
	    json.normalScale = material.normalScale;
	    json.opacity = material.opacity;
	    json.overdraw = material.overdraw;
	    json.polygonOffset = material.polygonOffset;
	    json.polygonOffsetFactor = material.polygonOffsetFactor;
	    json.polygonOffsetUnits = material.polygonOffsetUnits;
	    json.precision = material.precision;
	    json.premultipliedAlpha = material.premultipliedAlpha;
	    json.refractionRatio = material.refractionRatio;
	    json.roughness = material.roughness;
	    json.roughnessMap = material.roughnessMap;
	    json.shadowSide = material.shadowSide;
	    json.side = material.side;
	    json.skinning = material.skinning;
	    json.transparent = material.transparent;
	    json.type = material.type;
	    json.userData = material.userData;
	    json.uuid = material.uuid;
	    json.vertexColors = material.vertexColors;
	    json.visible = material.visible;
	    json.wireframe = material.wireframe;
	    json.wireframeLinecap = material.wireframeLinecap;
	    json.wireframeLinejoin = material.wireframeLinejoin;
	    json.wireframeLinewidth = material.wireframeLinewidth;

	    return json;
	};

	MaterialSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * Mesh序列化器
	 */
	function MeshSerializer() {
	    BaseSerializer.call(this);
	}

	MeshSerializer.prototype = Object.create(BaseSerializer.prototype);
	MeshSerializer.prototype.constructor = MeshSerializer;

	MeshSerializer.prototype.toJSON = function (obj) {
	    var json = Object3DSerializer.prototype.toJSON(obj);

	    json.drawMode = obj.drawMode;
	    json.geometry = geometryToJson(obj.geometry);
	    json.material = materialToJson(obj.material);

	    return json;
	};

	MeshSerializer.prototype.fromJSON = function (json) {

	};

	/**
	 * 所有序列化器
	 */
	var Serializers = {
	    // 配置
	    Config: {
	        Serializer: new ConfigSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: ConfigSerializer.name,
	            type: 'Config'
	        })
	    },

	    Script: {
	        Serializer: new ScriptSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: ScriptSerializer.name,
	            type: 'Script'
	        })
	    },

	    // 物体
	    Object3D: {
	        Serializer: new Object3DSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: Object3DSerializer.name,
	            type: THREE.Object3D.name
	        })
	    },

	    // 场景
	    Scene: {
	        Serializer: new SceneSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: SceneSerializer.name,
	            type: THREE.Scene.name
	        })
	    },

	    // 相机
	    Camera: {
	        Serializer: new CameraSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: CameraSerializer.name,
	            type: THREE.Camera.name
	        })
	    },

	    OrthographicCamera: {
	        Serializer: new OrthographicCameraSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: OrthographicCameraSerializer.name,
	            type: THREE.OrthographicCamera.name
	        })
	    },

	    PerspectiveCamera: {
	        Serializer: new PerspectiveCameraSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: PerspectiveCameraSerializer.name,
	            type: THREE.PerspectiveCamera.name
	        })
	    },

	    // 光源
	    Light: {
	        Serializer: new LightSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: LightSerializer.name,
	            type: THREE.Light.name
	        })
	    },

	    PointLight: {
	        Serializer: new PointLightSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: PointLightSerializer.name,
	            type: THREE.PointLight.name
	        })
	    },

	    SpotLight: {
	        Serializer: new SpotLightSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: SpotLightSerializer.name,
	            type: THREE.SpotLight.name
	        })
	    },

	    HemisphereLight: {
	        Serializer: new HemisphereLightSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: HemisphereLightSerializer.name,
	            type: THREE.HemisphereLight.name
	        })
	    },

	    RectAreaLight: {
	        Serializer: new RectAreaLightSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: RectAreaLightSerializer.name,
	            type: THREE.RectAreaLight.name
	        })
	    },

	    // 集合体
	    Geometry: {
	        Serializer: new GeometrySerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: GeometrySerializer.name,
	            type: THREE.Geometry.name
	        })
	    },

	    // 材质
	    Material: {
	        Serializer: new MaterialSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: MaterialSerializer.name,
	            type: THREE.Material.name
	        })
	    },

	    // 网格
	    Mesh: {
	        Serializer: new MeshSerializer(),
	        Metadata: Object.assign({}, Metadata, {
	            generator: MeshSerializer.name,
	            type: THREE.Mesh.name
	        })
	    }
	};

	/**
	 * 应用序列化和反序列化
	 */
	const Converter = {
	    toJSON: function (app) {
	        var list = [];

	        // 配置
	        var config = {
	            Metadata: Serializers.Config.Metadata,
	            Object: Serializers.Config.Serializer.toJSON(app.editor.config),
	        };
	        list.push(config);

	        // 相机
	        var camera = {
	            Metadata: Serializers.PerspectiveCamera.Metadata,
	            Object: Serializers.PerspectiveCamera.Serializer.toJSON(app.editor.camera)
	        };
	        list.push(camera);

	        // 脚本
	        Object.keys(app.editor.scripts).forEach(function (id) {
	            var name = app.editor.scripts[id].name;
	            var source = app.editor.scripts[id].source;
	            var script = {
	                Metadata: Serializers['Script'].Metadata,
	                Object: Serializers['Script'].Serializer.toJSON({
	                    id: id,
	                    name: name,
	                    source: source
	                })
	            };
	            list.push(script);
	        });

	        // 场景
	        app.editor.scene.traverse(function (obj) {
	            if (Serializers[obj.constructor.name] != null) {
	                var json = {
	                    Metadata: Serializers[obj.constructor.name].Metadata,
	                    Object: Serializers[obj.constructor.name].Serializer.toJSON(obj)
	                };
	                list.push(json);
	            } else {
	                console.log(`There is no serializer to serialize ${obj.name}`);
	            }
	        });

	        debugger

	        return list;
	    },

	    fromJSON: function (json) {

	    }
	};

	/**
	 * ajax
	 * @param {*} params 参数
	 */
	function ajax(params) {
	    const url = params.url || '';
	    const method = params.method || 'GET';
	    const data = params.data || null;
	    const callback = params.callback || null;

	    const xhr = new XMLHttpRequest();
	    xhr.open(method, url, true);
	    xhr.onreadystatechange = function () {
	        if (xhr.readyState === 4) {
	            var data = xhr.responseText;
	            typeof (callback) === 'function' && callback(data);
	        }
	    };

	    var body;
	    if (data) {
	        var bodies = [];
	        for (var name in data) {
	            bodies.push(name + '=' + encodeURIComponent(data[name]));
	        }

	        body = bodies.join('&');
	        if (body.length) {
	            xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	        }
	    }

	    xhr.send(body);
	}

	/**
	 * get请求
	 * @param {*} url 地址
	 * @param {*} callback 回调函数
	 */
	function get$1(url, callback) {
	    ajax({
	        url: url,
	        callback: callback
	    });
	}

	/**
	 * get请求并解析json数据
	 * @param {*} url 
	 * @param {*} callback 
	 */
	function getJson(url, callback) {
	    ajax({
	        url: url,
	        callback: function (data) {
	            typeof (callback) === 'function' && callback(JSON.parse(data));
	        }
	    });
	}

	/**
	 * post请求
	 * @param {*} url 地址
	 * @param {*} data 数据
	 * @param {*} callback 回调函数
	 */
	function post(url, data, callback) {
	    const _data = typeof (data) === 'function' ? null : data;
	    const _callback = typeof (data) === 'function' ? data : callback;

	    ajax({
	        url: url,
	        method: 'POST',
	        data: _data,
	        callback: _callback
	    });
	}

	/**
	 * Ajax
	 */
	const Ajax = {
	    ajax: ajax,
	    get: get$1,
	    getJson: getJson,
	    post: post
	};

	/**
	 * 保存场景事件
	 * @param {*} app 
	 */
	function SaveEvent(app) {
	    BaseEvent.call(this, app);
	}

	SaveEvent.prototype = Object.create(BaseEvent.prototype);
	SaveEvent.prototype.constructor = SaveEvent;

	SaveEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('save.' + this.id, function () {
	        _this.onSave();
	    });
	};

	SaveEvent.prototype.stop = function () {
	    this.app.on('save.' + this.id, null);
	};

	SaveEvent.prototype.onSave = function () {
	    var obj = Converter.toJSON(this.app);

	    Ajax.post(this.app.options.server + '/api/Scene/Save', {
	        Name: 'Scene1',
	        Data: JSON.stringify(obj)
	    }, function (result) {
	        var obj = JSON.parse(result);
	        UI$1.msg(obj.Msg);
	    });
	};

	/**
	 * 旋转模式事件
	 * @param {*} app 
	 */
	function SelectModeEvent(app) {
	    BaseEvent.call(this, app);
	}

	SelectModeEvent.prototype = Object.create(BaseEvent.prototype);
	SelectModeEvent.prototype.constructor = SelectModeEvent;

	SelectModeEvent.prototype.start = function () {
	    var btn = UI$1.get('selectBtn');
	    btn.dom.addEventListener('click', this.onClick.bind(this));
	    this.app.on(`changeMode.${this.id}`, this.onChangeMode.bind(this));
	};

	SelectModeEvent.prototype.stop = function () {
	    var btn = UI$1.get('selectBtn');
	    btn.dom.removeEventListener('click', this.onClick);
	    this.app.on(`changeMode.${this.id}`, null);
	};

	SelectModeEvent.prototype.onClick = function () {
	    this.app.call('changeMode', this, 'select');
	};

	SelectModeEvent.prototype.onChangeMode = function (mode) {
	    var btn = UI$1.get('selectBtn');

	    if (mode === 'select') {
	        btn.select();
	    } else {
	        btn.unselect();
	    }
	};

	/**
	 * 平移模式事件
	 * @param {*} app 
	 */
	function TranslateModeEvent(app) {
	    BaseEvent.call(this, app);
	}

	TranslateModeEvent.prototype = Object.create(BaseEvent.prototype);
	TranslateModeEvent.prototype.constructor = TranslateModeEvent;

	TranslateModeEvent.prototype.start = function () {
	    var btn = UI$1.get('translateBtn');
	    btn.dom.addEventListener('click', this.onClick.bind(this));
	    this.app.on(`changeMode.${this.id}`, this.onChangeMode.bind(this));
	};

	TranslateModeEvent.prototype.stop = function () {
	    var btn = UI$1.get('translateBtn');
	    btn.dom.removeEventListener('click', this.onClick);
	    this.app.on(`changeMode.${this.id}`, null);
	};

	TranslateModeEvent.prototype.onClick = function () {
	    this.app.call('changeMode', this, 'translate');
	};

	TranslateModeEvent.prototype.onChangeMode = function (mode) {
	    var btn = UI$1.get('translateBtn');

	    if (mode === 'translate') {
	        btn.select();
	    } else {
	        btn.unselect();
	    }
	};

	/**
	 * 旋转模式事件
	 * @param {*} app 
	 */
	function RotateModeEvent(app) {
	    BaseEvent.call(this, app);
	}

	RotateModeEvent.prototype = Object.create(BaseEvent.prototype);
	RotateModeEvent.prototype.constructor = RotateModeEvent;

	RotateModeEvent.prototype.start = function () {
	    var btn = UI$1.get('rotateBtn');
	    btn.dom.addEventListener('click', this.onClick.bind(this));
	    this.app.on(`changeMode.${this.id}`, this.onChangeMode.bind(this));
	};

	RotateModeEvent.prototype.stop = function () {
	    var btn = UI$1.get('rotateBtn');
	    btn.dom.removeEventListener('click', this.onClick);
	    this.app.on(`changeMode.${this.id}`, null);
	};

	RotateModeEvent.prototype.onClick = function () {
	    this.app.call('changeMode', this, 'rotate');
	};

	RotateModeEvent.prototype.onChangeMode = function (mode) {
	    var btn = UI$1.get('rotateBtn');

	    if (mode === 'rotate') {
	        btn.select();
	    } else {
	        btn.unselect();
	    }
	};

	/**
	 * 缩放模式事件
	 * @param {*} app 
	 */
	function ScaleModeEvent(app) {
	    BaseEvent.call(this, app);
	}

	ScaleModeEvent.prototype = Object.create(BaseEvent.prototype);
	ScaleModeEvent.prototype.constructor = ScaleModeEvent;

	ScaleModeEvent.prototype.start = function () {
	    var btn = UI$1.get('scaleBtn');
	    btn.dom.addEventListener('click', this.onClick.bind(this));
	    this.app.on(`changeMode.${this.id}`, this.onChangeMode.bind(this));
	};

	ScaleModeEvent.prototype.stop = function () {
	    var btn = UI$1.get('scaleBtn');
	    btn.dom.removeEventListener('click', this.onClick);
	    this.app.on(`changeMode.${this.id}`, null);
	};

	ScaleModeEvent.prototype.onClick = function () {
	    this.app.call('changeMode', this, 'scale');
	};

	ScaleModeEvent.prototype.onChangeMode = function (mode) {
	    var btn = UI$1.get('scaleBtn');

	    if (mode === 'scale') {
	        btn.select();
	    } else {
	        btn.unselect();
	    }
	};

	/**
	 * 锚点事件
	 * @param {*} app 
	 */
	function AnchorPointEvent(app) {
	    BaseEvent.call(this, app);
	}

	AnchorPointEvent.prototype = Object.create(BaseEvent.prototype);
	AnchorPointEvent.prototype.constructor = AnchorPointEvent;

	AnchorPointEvent.prototype.start = function () {

	};

	AnchorPointEvent.prototype.stop = function () {

	};

	/**
	 * 手型模式事件
	 * @param {*} app 
	 */
	function HandModeEvent(app) {
	    BaseEvent.call(this, app);
	}

	HandModeEvent.prototype = Object.create(BaseEvent.prototype);
	HandModeEvent.prototype.constructor = HandModeEvent;

	HandModeEvent.prototype.start = function () {

	};

	HandModeEvent.prototype.stop = function () {

	};

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @constructor
	 */

	function AddObjectCommand(object) {

		Command.call(this);

		this.type = 'AddObjectCommand';

		this.object = object;
		if (object !== undefined) {

			this.name = 'Add Object: ' + object.name;

		}

	}
	AddObjectCommand.prototype = Object.create(Command.prototype);

	Object.assign(AddObjectCommand.prototype, {

		constructor: AddObjectCommand,

		execute: function () {

			this.editor.addObject(this.object);
			this.editor.select(this.object);

		},

		undo: function () {

			this.editor.removeObject(this.object);
			this.editor.deselect();

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);
			output.object = this.object.toJSON();

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.object = this.editor.objectByUuid(json.object.object.uuid);

			if (this.object === undefined) {

				var loader = new THREE.ObjectLoader();
				this.object = loader.parse(json.object);

			}

		}

	});

	/**
	 * 文件上传器
	 */
	function Uploader() {

	}

	/**
	 * 上传文件
	 * @param {*} input_id 文件input的id
	 * @param {*} url 后台接收上传文件url
	 * @param {*} onload 上传完成回调函数
	 * @param {*} onerror 上传出错回调函数
	 * @param {*} onprogress 上传过程回调函数
	 */
	Uploader.prototype.upload = function (input_id, url, onload, onerror, onprogress) {
	    var fileObj = document.getElementById(input_id).files[0];

	    var form = new FormData();
	    form.append("file", fileObj);

	    var xhr = new XMLHttpRequest();
	    xhr.open("post", url, true);
	    xhr.onload = onload;
	    xhr.onerror = onerror;
	    xhr.upload.onprogress = onprogress;
	    xhr.send(form);
	};

	const uploader = new Uploader();

	/**
	 * 上传工具类
	 */
	const UploadUtils = {
	    upload: uploader.upload
	};

	/**
	 * 模型窗口
	 * @param {*} options 
	 */
	function ModelWindow(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	    this.models = [];
	}

	ModelWindow.prototype = Object.create(UI$1.Control.prototype);
	ModelWindow.prototype.constructor = ModelWindow;

	ModelWindow.prototype.render = function () {
	    var container = UI$1.create({
	        xtype: 'window',
	        id: 'modelWindow',
	        parent: this.app.container,
	        title: '模型列表',
	        width: '700px',
	        height: '500px',
	        bodyStyle: {
	            paddingTop: 0
	        },
	        shade: false,
	        children: [{
	            xtype: 'row',
	            style: {
	                position: 'sticky',
	                top: '0',
	                padding: '2px',
	                backgroundColor: '#eee',
	                borderBottom: '1px solid #ddd',
	                zIndex: 100,
	                display: 'flex',
	                alignItems: 'center',
	                justifyContent: 'flex-start'
	            },
	            children: [{
	                xtype: 'button',
	                text: '上传模型',
	                onClick: this.onAddFile.bind(this)
	            }, {
	                xtype: 'button',
	                text: '编辑分组'
	            }]
	        }, {
	            xtype: 'row',
	            children: [{
	                xtype: 'imagelist',
	                id: 'modelWindowImages',
	                style: {
	                    width: '100%',
	                    height: '100%',
	                },
	                onClick: this.onClickImage.bind(this)
	            }]
	        }]
	    });
	    container.render();
	};

	ModelWindow.prototype.show = function () {
	    var app = this.app;
	    var server = app.options.server;

	    UI$1.get('modelWindow').show();

	    // 刷新模型列表
	    Ajax.getJson(`${server}/api/Model/List`, (obj) => {
	        this.models = obj.Data;

	        var images = UI$1.get('modelWindowImages');
	        images.clear();

	        images.children = obj.Data.map((n) => {
	            return {
	                xtype: 'image',
	                src: n.Image == null ? 'test/image/girl.jpg' : (server + n.Image),
	                title: n.Name
	            };
	        });
	        images.render();
	    });
	};

	ModelWindow.prototype.onAddFile = function () {
	    var input = document.getElementById('modelWindowInput');
	    if (input == null) {
	        input = document.createElement('input');
	        input.id = 'modelWindowInput';
	        input.type = 'file';
	        document.body.appendChild(input);
	        input.onchange = this.onUploadFile.bind(this);
	    }
	    input.click();
	};

	ModelWindow.prototype.onUploadFile = function (event) {
	    UploadUtils.upload('modelWindowInput', `${this.app.options.server}/api/Model/Add`, function () {
	        UI$1.msg('上传成功！');
	    });
	};

	ModelWindow.prototype.onClickImage = function (event, index, type) {
	    var model = this.models[index];

	    if (type === 'edit') { // 编辑模型
	        UI$1.msg('开始编辑模型');
	    } else if (type === 'delete') { // 删除模型
	        UI$1.msg('开始删除模型');
	    } else { // 加载模型
	        var loader = new THREE.BinaryLoader();

	        loader.load(this.app.options.server + model.Model, (geometry, materials) => {
	            var mesh = new THREE.Mesh(geometry, materials);
	            mesh.name = model.Name;
	            mesh.rotation.x = -Math.PI / 2;
	            var cmd = new AddObjectCommand(mesh);
	            cmd.execute();
	        });
	    }
	};

	/**
	 * 模型事件
	 * @param {*} app 
	 */
	function ModelEvent(app) {
	    BaseEvent.call(this, app);
	}

	ModelEvent.prototype = Object.create(BaseEvent.prototype);
	ModelEvent.prototype.constructor = ModelEvent;

	ModelEvent.prototype.start = function () {
	    var btn = UI$1.get('modelBtn');
	    btn.dom.addEventListener('click', this.onClick.bind(this));
	};

	ModelEvent.prototype.stop = function () {
	    var btn = UI$1.get('modelBtn');
	    btn.dom.removeEventListener('click', this.onClick);
	};

	ModelEvent.prototype.onClick = function () {
	    if (this.window == null) {
	        this.window = new ModelWindow({ parent: this.app.container, app: this.app });
	        this.window.render();
	    }
	    this.window.show();
	};

	/**
	 * 路径模式事件
	 * @param {*} app 
	 */
	function PathModeEvent(app) {
	    BaseEvent.call(this, app);
	}

	PathModeEvent.prototype = Object.create(BaseEvent.prototype);
	PathModeEvent.prototype.constructor = PathModeEvent;

	PathModeEvent.prototype.start = function () {

	};

	PathModeEvent.prototype.stop = function () {

	};

	/**
	 * 菜单事件
	 * @param {*} app 
	 */
	function MenuEvent(app) {
	    BaseEvent.call(this, app);
	}

	MenuEvent.prototype = Object.create(BaseEvent.prototype);
	MenuEvent.prototype.constructor = MenuEvent;

	MenuEvent.prototype.start = function () {

	};

	MenuEvent.prototype.stop = function () {

	};

	/**
	 * 新建场景
	 * @param {*} app 
	 */
	function NewSceneEvent(app) {
	    MenuEvent.call(this, app);
	}

	NewSceneEvent.prototype = Object.create(MenuEvent.prototype);
	NewSceneEvent.prototype.constructor = NewSceneEvent;

	NewSceneEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mNewScene.' + this.id, function () {
	        _this.onNewScene();
	    });
	};

	NewSceneEvent.prototype.stop = function () {
	    this.app.on('mNewScene.' + this.id, null);
	};

	NewSceneEvent.prototype.onNewScene = function () {
	    var editor = this.app.editor;

	    if (confirm('所有未保存数据将丢失，确定吗？')) {
	        editor.clear();
	    }
	};

	/**
	 * 载入场景
	 * @param {*} app 
	 */
	function LoadSceneEvent(app) {
	    MenuEvent.call(this, app);
	}

	LoadSceneEvent.prototype = Object.create(MenuEvent.prototype);
	LoadSceneEvent.prototype.constructor = LoadSceneEvent;

	LoadSceneEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mLoadScene.' + this.id, function () {
	        _this.onLoadScene();
	    });
	};

	LoadSceneEvent.prototype.stop = function () {
	    this.app.on('mLoadScene.' + this.id, null);
	};

	LoadSceneEvent.prototype.onLoadScene = function () {
	    var editor = this.app.editor;

	    if (confirm('所有未保存数据将丢失，确定吗？')) {
	        editor.load();
	    }
	};

	/**
	 * 保存场景
	 * @param {*} app 
	 */
	function SaveSceneEvent(app) {
	    MenuEvent.call(this, app);
	}

	SaveSceneEvent.prototype = Object.create(MenuEvent.prototype);
	SaveSceneEvent.prototype.constructor = SaveSceneEvent;

	SaveSceneEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mSaveScene.' + this.id, function () {
	        _this.onSaveScene();
	    });
	};

	SaveSceneEvent.prototype.stop = function () {
	    this.app.on('mSaveScene.' + this.id, null);
	};

	SaveSceneEvent.prototype.onSaveScene = function () {
	    var editor = this.app.editor;

	    editor.save();
	};

	/**
	 * 精度
	 */
	var NUMBER_PRECISION = 6;

	/**
	 * 打包数字
	 * @param {*} key 
	 * @param {*} value 
	 */
	function parseNumber$1(key, value) {
	    return typeof value === 'number' ? parseFloat(value.toFixed(NUMBER_PRECISION)) : value;
	}

	/**
	 * 数学工具
	 */
	const MathUtils = {
	    NUMBER_PRECISION: NUMBER_PRECISION,
	    parseNumber: parseNumber$1
	};

	/**
	 * 发布场景
	 * @param {*} app 
	 */
	function PublishSceneEvent(app) {
	    MenuEvent.call(this, app);
	}

	PublishSceneEvent.prototype = Object.create(MenuEvent.prototype);
	PublishSceneEvent.prototype.constructor = PublishSceneEvent;

	PublishSceneEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mPublishScene.' + this.id, function () {
	        _this.onPublishScene();
	    });

	    this.link = document.createElement('a');
	    this.link.style.display = 'none';
	    document.body.appendChild(this.link); // Firefox workaround, see #6594
	};

	PublishSceneEvent.prototype.stop = function () {
	    this.app.on('mPublishScene.' + this.id, null);
	};

	PublishSceneEvent.prototype.onPublishScene = function () {
	    var editor = this.app.editor;

	    var zip = new JSZip();
	    //

	    var output = editor.toJSON();
	    output.metadata.type = 'App';
	    delete output.history;

	    var vr = output.project.vr;

	    output = JSON.stringify(output, MathUtils.parseNumber, '\t');
	    output = output.replace(/[\n\t]+([\d\.e\-\[\]]+)/g, '$1');

	    zip.file('app.json', output);

	    //

	    var _this = this;
	    var manager = new THREE.LoadingManager(function () {
	        _this.save(zip.generate({ type: 'blob' }), 'download.zip');
	    });

	    var loader = new THREE.FileLoader(manager);
	    loader.load('third_party/app/index.html', function (content) {
	        var includes = [];

	        if (vr) {
	            includes.push('<script src="third_party/controls/VRControls.js"></script>');
	            includes.push('<script src="third_party/effects/VREffect.js"></script>');
	            includes.push('<script src="third_party/vr/WebVR.js"></script>');
	        }

	        content = content.replace('<!-- includes -->', includes.join('\n\t\t'));
	        zip.file('index.html', content);
	    });
	    loader.load('third_party/app.js', function (content) {
	        zip.file('js/app.js', content);
	    });
	    loader.load('node_modules/three/build/three.min.js', function (content) {
	        zip.file('js/three.min.js', content);
	    });

	    if (vr) {
	        loader.load('third_party/controls/VRControls.js', function (content) {
	            zip.file('js/VRControls.js', content);
	        });

	        loader.load('third_party/effects/VREffect.js', function (content) {
	            zip.file('js/VREffect.js', content);
	        });

	        loader.load('third_party/vr/WebVR.js', function (content) {
	            zip.file('js/WebVR.js', content);
	        });
	    }
	};

	PublishSceneEvent.prototype.save = function (blob, filename) {
	    this.link.href = URL.createObjectURL(blob);
	    this.link.download = filename || 'data.json';
	    this.link.click();

	    // URL.revokeObjectURL( url ); breaks Firefox...
	};

	PublishSceneEvent.prototype.saveString = function (text, filename) {
	    this.save(new Blob([text], { type: 'text/plain' }), filename);
	};

	/**
	 * 撤销事件
	 * @param {*} app 
	 */
	function UndoEvent(app) {
	    MenuEvent.call(this, app);
	}

	UndoEvent.prototype = Object.create(MenuEvent.prototype);
	UndoEvent.prototype.constructor = UndoEvent;

	UndoEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mUndo.' + this.id, function () {
	        _this.onUndo();
	    });
	    this.app.on('historyChanged.' + this.id, function () {
	        _this.onHistoryChanged();
	    });
	};

	UndoEvent.prototype.stop = function () {
	    this.app.on('mUndo.' + this.id, null);
	    this.app.on('historyChanged.' + this.id, null);
	};

	UndoEvent.prototype.onUndo = function () {
	    var editor = this.app.editor;

	    editor.undo();
	};

	UndoEvent.prototype.onHistoryChanged = function () {
	    var history = this.app.editor.history;
	    var dom = UI$1.get('mUndo').dom;

	    if (history.undos.length === 0) {
	        if (!dom.classList.contains('inactive')) {
	            dom.classList.add('inactive');
	        }
	    } else {
	        if (dom.classList.contains('inactive')) {
	            dom.classList.remove('inactive');
	        }
	    }
	};

	/**
	 * 重做事件
	 * @param {*} app 
	 */
	function RedoEvent(app) {
	    MenuEvent.call(this, app);
	}

	RedoEvent.prototype = Object.create(MenuEvent.prototype);
	RedoEvent.prototype.constructor = RedoEvent;

	RedoEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mRedo.' + this.id, function () {
	        _this.onRedo();
	    });
	    this.app.on('historyChanged.' + this.id, function () {
	        _this.onHistoryChanged();
	    });
	};

	RedoEvent.prototype.stop = function () {
	    this.app.on('mRedo.' + this.id, null);
	    this.app.on('historyChanged.' + this.id, null);
	};

	RedoEvent.prototype.onRedo = function () {
	    var editor = this.app.editor;

	    editor.redo();
	};

	RedoEvent.prototype.onHistoryChanged = function () {
	    var history = this.app.editor.history;
	    var dom = UI$1.get('mRedo').dom;

	    if (history.redos.length === 0) {
	        if (!dom.classList.contains('inactive')) {
	            dom.classList.add('inactive');
	        }
	    } else {
	        if (dom.classList.contains('inactive')) {
	            dom.classList.remove('inactive');
	        }
	    }
	};

	/**
	 * 重做事件
	 * @param {*} app 
	 */
	function ClearHistoryEvent(app) {
	    MenuEvent.call(this, app);
	}

	ClearHistoryEvent.prototype = Object.create(MenuEvent.prototype);
	ClearHistoryEvent.prototype.constructor = ClearHistoryEvent;

	ClearHistoryEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mClearHistory.' + this.id, function () {
	        _this.onClearHistory();
	    });
	};

	ClearHistoryEvent.prototype.stop = function () {
	    this.app.on('mClearHistory.' + this.id, null);
	};

	ClearHistoryEvent.prototype.onClearHistory = function () {
	    var editor = this.app.editor;

	    if (confirm('撤销/重做历史纪录将被清空。确定吗？')) {
	        editor.history.clear();
	    }
	};

	/**
	 * 拷贝事件
	 * @param {*} app 
	 */
	function CloneEvent(app) {
	    MenuEvent.call(this, app);
	}

	CloneEvent.prototype = Object.create(MenuEvent.prototype);
	CloneEvent.prototype.constructor = CloneEvent;

	CloneEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mClone.' + this.id, function () {
	        _this.onClone();
	    });
	};

	CloneEvent.prototype.stop = function () {
	    this.app.on('mClone.' + this.id, null);
	};

	CloneEvent.prototype.onClone = function () {
	    var editor = this.app.editor;

	    var object = editor.selected;

	    if (object.parent === null) return; // avoid cloning the camera or scene

	    object = object.clone();

	    editor.execute(new AddObjectCommand(object));
	};

	/**
	 * 删除事件
	 * @param {*} app 
	 */
	function DeleteEvent(app) {
	    MenuEvent.call(this, app);
	}

	DeleteEvent.prototype = Object.create(MenuEvent.prototype);
	DeleteEvent.prototype.constructor = DeleteEvent;

	DeleteEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mDelete.' + this.id, function () {
	        _this.onDelete();
	    });
	};

	DeleteEvent.prototype.stop = function () {
	    this.app.on('mDelete.' + this.id, null);
	};

	DeleteEvent.prototype.onDelete = function () {
	    var editor = this.app.editor;

	    var object = editor.selected;

	    if (confirm('删除 ' + object.name + '?') === false) return;

	    var parent = object.parent;
	    if (parent === undefined) return; // avoid deleting the camera or scene

	    editor.execute(new RemoveObjectCommand(object));
	};

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param attributeName string
	 * @param newValue number, string, boolean or object
	 * @constructor
	 */

	function SetMaterialValueCommand(object, attributeName, newValue) {

		Command.call(this);

		this.type = 'SetMaterialValueCommand';
		this.name = 'Set Material.' + attributeName;
		this.updatable = true;

		this.object = object;
		this.oldValue = (object !== undefined) ? object.material[attributeName] : undefined;
		this.newValue = newValue;
		this.attributeName = attributeName;

	}
	SetMaterialValueCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetMaterialValueCommand.prototype, {

		constructor: SetMaterialValueCommand,

		execute: function () {
			this.object.material[this.attributeName] = this.newValue;
			this.object.material.needsUpdate = true;
			this.editor.app.call('objectChanged', this, this.object);
			this.editor.app.call('materialChanged', this, this.object.material);
		},

		undo: function () {
			this.object.material[this.attributeName] = this.oldValue;
			this.object.material.needsUpdate = true;
			this.editor.app.call('objectChanged', this, this.object);
			this.editor.app.call('materialChanged', this, this.object.material);
		},

		update: function (cmd) {

			this.newValue = cmd.newValue;

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.attributeName = this.attributeName;
			output.oldValue = this.oldValue;
			output.newValue = this.newValue;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.attributeName = json.attributeName;
			this.oldValue = json.oldValue;
			this.newValue = json.newValue;
			this.object = this.editor.objectByUuid(json.objectUuid);

		}

	});

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param cmdArray array containing command objects
	 * @constructor
	 */

	function MultiCmdsCommand(cmdArray) {

		Command.call(this);

		this.type = 'MultiCmdsCommand';
		this.name = 'Multiple Changes';

		this.cmdArray = (cmdArray !== undefined) ? cmdArray : [];

	}
	MultiCmdsCommand.prototype = Object.create(Command.prototype);

	Object.assign(MultiCmdsCommand.prototype, {

		constructor: MultiCmdsCommand,

		execute: function () {
			for (var i = 0; i < this.cmdArray.length; i++) {

				this.cmdArray[i].execute();

			}
			this.editor.app.call('sceneGraphChanged', this);
		},

		undo: function () {
			for (var i = this.cmdArray.length - 1; i >= 0; i--) {

				this.cmdArray[i].undo();

			}
			this.editor.app.call('sceneGraphChanged', this);
		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			var cmds = [];
			for (var i = 0; i < this.cmdArray.length; i++) {

				cmds.push(this.cmdArray[i].toJSON());

			}
			output.cmds = cmds;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			var cmds = json.cmds;
			for (var i = 0; i < cmds.length; i++) {

				var cmd = new window[cmds[i].type]();	// creates a new object of type "json.type"
				cmd.fromJSON(cmds[i]);
				this.cmdArray.push(cmd);

			}

		}

	});

	/**
	 * 压缩着色器程序事件
	 * @param {*} app 
	 */
	function MinifyShaderEvent(app) {
	    MenuEvent.call(this, app);
	}

	MinifyShaderEvent.prototype = Object.create(MenuEvent.prototype);
	MinifyShaderEvent.prototype.constructor = MinifyShaderEvent;

	MinifyShaderEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mMinifyShader.' + this.id, function () {
	        _this.onMinifyShader();
	    });
	};

	MinifyShaderEvent.prototype.stop = function () {
	    this.app.on('mMinifyShader.' + this.id, null);
	};

	MinifyShaderEvent.prototype.onMinifyShader = function () {
	    var editor = this.app.editor;

	    var root = editor.selected || editor.scene;

	    var errors = [];
	    var nMaterialsChanged = 0;

	    var path = [];

	    function getPath(object) {
	        path.length = 0;

	        var parent = object.parent;
	        if (parent !== undefined) getPath(parent);

	        path.push(object.name || object.uuid);

	        return path;
	    }

	    var cmds = [];
	    root.traverse(function (object) {
	        var material = object.material;

	        if (material instanceof THREE.ShaderMaterial) {
	            try {
	                var shader = glslprep.minifyGlsl([
	                    material.vertexShader, material.fragmentShader]);

	                cmds.push(new SetMaterialValueCommand(object, 'vertexShader', shader[0]));
	                cmds.push(new SetMaterialValueCommand(object, 'fragmentShader', shader[1]));

	                ++nMaterialsChanged;
	            } catch (e) {

	                var path = getPath(object).join("/");

	                if (e instanceof glslprep.SyntaxError)
	                    errors.push(path + ":" +
	                        e.line + ":" + e.column + ": " + e.message);
	                else {
	                    errors.push(path +
	                        "： 未预料到的错误(详情请见控制台)。");

	                    console.error(e.stack || e);
	                }
	            }
	        }
	    });

	    if (nMaterialsChanged > 0) {
	        editor.execute(new MultiCmdsCommand(cmds), 'Minify Shaders');
	    }

	    UI$1.msg(nMaterialsChanged + "材质已经改变。\n" + errors.join("\n"));
	};

	var ID$3 = 1;

	/**
	 * 添加组事件
	 * @param {*} app 
	 */
	function AddGroupEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddGroupEvent.prototype = Object.create(MenuEvent.prototype);
	AddGroupEvent.prototype.constructor = AddGroupEvent;

	AddGroupEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddGroup.' + this.id, function () {
	        _this.onAddGroup();
	    });
	};

	AddGroupEvent.prototype.stop = function () {
	    this.app.on('mAddGroup.' + this.id, null);
	};

	AddGroupEvent.prototype.onAddGroup = function () {
	    var editor = this.app.editor;

	    var mesh = new THREE.Group();
	    mesh.name = '组' + ID$3++;

	    editor.execute(new AddObjectCommand(mesh));
	};

	var ID$4 = 1;

	/**
	 * 添加平板事件
	 * @param {*} app 
	 */
	function AddPlaneEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddPlaneEvent.prototype = Object.create(MenuEvent.prototype);
	AddPlaneEvent.prototype.constructor = AddPlaneEvent;

	AddPlaneEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddPlane.' + this.id, function () {
	        _this.onAddPlane();
	    });
	};

	AddPlaneEvent.prototype.stop = function () {
	    this.app.on('mAddPlane.' + this.id, null);
	};

	AddPlaneEvent.prototype.onAddPlane = function () {
	    var editor = this.app.editor;

	    var geometry = new THREE.PlaneBufferGeometry(2, 2);
	    var material = new THREE.MeshStandardMaterial();
	    var mesh = new THREE.Mesh(geometry, material);
	    mesh.name = '平板' + ID$4++;

	    editor.execute(new AddObjectCommand(mesh));
	};

	var ID$5 = 1;

	/**
	 * 添加正方体事件
	 * @param {*} app 
	 */
	function AddBoxEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddBoxEvent.prototype = Object.create(MenuEvent.prototype);
	AddBoxEvent.prototype.constructor = AddBoxEvent;

	AddBoxEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddBox.' + this.id, function () {
	        _this.onAddBox();
	    });
	};

	AddBoxEvent.prototype.stop = function () {
	    this.app.on('mAddBox.' + this.id, null);
	};

	AddBoxEvent.prototype.onAddBox = function () {
	    var editor = this.app.editor;

	    var geometry = new THREE.BoxBufferGeometry(1, 1, 1);
	    var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
	    mesh.name = '正方体' + ID$5++;

	    editor.execute(new AddObjectCommand(mesh));
	};

	var ID$6 = 1;

	/**
	 * 添加圆事件
	 * @param {*} app 
	 */
	function AddCircleEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddCircleEvent.prototype = Object.create(MenuEvent.prototype);
	AddCircleEvent.prototype.constructor = AddCircleEvent;

	AddCircleEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddCircle.' + this.id, function () {
	        _this.onAddCircle();
	    });
	};

	AddCircleEvent.prototype.stop = function () {
	    this.app.on('mAddCircle.' + this.id, null);
	};

	AddCircleEvent.prototype.onAddCircle = function () {
	    var editor = this.app.editor;

	    var radius = 1;
	    var segments = 32;

	    var geometry = new THREE.CircleBufferGeometry(radius, segments);
	    var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
	    mesh.name = '圆' + ID$6++;

	    editor.execute(new AddObjectCommand(mesh));
	};

	var ID$7 = 1;

	/**
	 * 添加圆柱体事件
	 * @param {*} app 
	 */
	function AddCylinderEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddCylinderEvent.prototype = Object.create(MenuEvent.prototype);
	AddCylinderEvent.prototype.constructor = AddCylinderEvent;

	AddCylinderEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddCylinder.' + this.id, function () {
	        _this.onAddCylinder();
	    });
	};

	AddCylinderEvent.prototype.stop = function () {
	    this.app.on('mAddCylinder.' + this.id, null);
	};

	AddCylinderEvent.prototype.onAddCylinder = function () {
	    var editor = this.app.editor;

	    var radiusTop = 1;
	    var radiusBottom = 1;
	    var height = 2;
	    var radiusSegments = 32;
	    var heightSegments = 1;
	    var openEnded = false;

	    var geometry = new THREE.CylinderBufferGeometry(radiusTop, radiusBottom, height, radiusSegments, heightSegments, openEnded);
	    var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
	    mesh.name = '圆柱体' + ID$7++;

	    editor.execute(new AddObjectCommand(mesh));
	};

	var ID$8 = 1;

	/**
	 * 添加球体事件
	 * @param {*} app 
	 */
	function AddSphereEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddSphereEvent.prototype = Object.create(MenuEvent.prototype);
	AddSphereEvent.prototype.constructor = AddSphereEvent;

	AddSphereEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddSphere.' + this.id, function () {
	        _this.onAddSphere();
	    });
	};

	AddSphereEvent.prototype.stop = function () {
	    this.app.on('mAddSphere.' + this.id, null);
	};

	AddSphereEvent.prototype.onAddSphere = function () {
	    var editor = this.app.editor;

	    var radius = 1;
	    var widthSegments = 32;
	    var heightSegments = 16;
	    var phiStart = 0;
	    var phiLength = Math.PI * 2;
	    var thetaStart = 0;
	    var thetaLength = Math.PI;

	    var geometry = new THREE.SphereBufferGeometry(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength);
	    var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
	    mesh.name = '球体' + ID$8++;

	    editor.execute(new AddObjectCommand(mesh));
	};

	var ID$9 = 1;

	/**
	 * 添加二十面体事件
	 * @param {*} app 
	 */
	function AddIcosahedronEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddIcosahedronEvent.prototype = Object.create(MenuEvent.prototype);
	AddIcosahedronEvent.prototype.constructor = AddIcosahedronEvent;

	AddIcosahedronEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddIcosahedron.' + this.id, function () {
	        _this.onAddIcosahedron();
	    });
	};

	AddIcosahedronEvent.prototype.stop = function () {
	    this.app.on('mAddIcosahedron.' + this.id, null);
	};

	AddIcosahedronEvent.prototype.onAddIcosahedron = function () {
	    var editor = this.app.editor;

	    var radius = 1;
	    var detail = 2;

	    var geometry = new THREE.IcosahedronGeometry(radius, detail);
	    var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
	    mesh.name = '二十面体' + ID$9++;

	    editor.execute(new AddObjectCommand(mesh));
	};

	var ID$a = 1;

	/**
	 * 添加轮胎事件
	 * @param {*} app 
	 */
	function AddTorusEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddTorusEvent.prototype = Object.create(MenuEvent.prototype);
	AddTorusEvent.prototype.constructor = AddTorusEvent;

	AddTorusEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddTorus.' + this.id, function () {
	        _this.onAddTorus();
	    });
	};

	AddTorusEvent.prototype.stop = function () {
	    this.app.on('mAddTorus.' + this.id, null);
	};

	AddTorusEvent.prototype.onAddTorus = function () {
	    var editor = this.app.editor;

	    var radius = 2;
	    var tube = 1;
	    var radialSegments = 32;
	    var tubularSegments = 12;
	    var arc = Math.PI * 2;

	    var geometry = new THREE.TorusBufferGeometry(radius, tube, radialSegments, tubularSegments, arc);
	    var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
	    mesh.name = '轮胎' + ID$a++;

	    editor.execute(new AddObjectCommand(mesh));
	};

	var ID$b = 1;

	/**
	 * 添加纽结事件
	 * @param {*} app 
	 */
	function AddTorusKnotEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddTorusKnotEvent.prototype = Object.create(MenuEvent.prototype);
	AddTorusKnotEvent.prototype.constructor = AddTorusKnotEvent;

	AddTorusKnotEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddTorusKnot.' + this.id, function () {
	        _this.onAddTorusKnot();
	    });
	};

	AddTorusKnotEvent.prototype.stop = function () {
	    this.app.on('mAddTorusKnot.' + this.id, null);
	};

	AddTorusKnotEvent.prototype.onAddTorusKnot = function () {
	    var editor = this.app.editor;

	    var radius = 2;
	    var tube = 0.8;
	    var tubularSegments = 64;
	    var radialSegments = 12;
	    var p = 2;
	    var q = 3;

	    var geometry = new THREE.TorusKnotBufferGeometry(radius, tube, tubularSegments, radialSegments, p, q);
	    var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
	    mesh.name = '纽结' + ID$b++;

	    editor.execute(new AddObjectCommand(mesh));
	};

	var ID$c = 1;

	/**
	 * 添加茶壶事件
	 * @param {*} app 
	 */
	function AddTeaportEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddTeaportEvent.prototype = Object.create(MenuEvent.prototype);
	AddTeaportEvent.prototype.constructor = AddTeaportEvent;

	AddTeaportEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddTeaport.' + this.id, function () {
	        _this.onAddTeaport();
	    });
	};

	AddTeaportEvent.prototype.stop = function () {
	    this.app.on('mAddTeaport.' + this.id, null);
	};

	AddTeaportEvent.prototype.onAddTeaport = function () {
	    var editor = this.app.editor;

	    var size = 3;
	    var segments = 10;
	    var bottom = true;
	    var lid = true;
	    var body = true;
	    var fitLid = true;
	    var blinn = true;

	    var geometry = new THREE.TeapotBufferGeometry(size, segments, bottom, lid, body, fitLid, blinn);
	    var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
	    mesh.name = '茶壶' + ID$c++;

	    editor.execute(new AddObjectCommand(mesh));
	};

	var ID$d = 1;

	/**
	 * 添加花瓶事件
	 * @param {*} app 
	 */
	function AddLatheEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddLatheEvent.prototype = Object.create(MenuEvent.prototype);
	AddLatheEvent.prototype.constructor = AddLatheEvent;

	AddLatheEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddLathe.' + this.id, function () {
	        _this.onAddLathe();
	    });
	};

	AddLatheEvent.prototype.stop = function () {
	    this.app.on('mAddLathe.' + this.id, null);
	};

	AddLatheEvent.prototype.onAddLathe = function () {
	    var editor = this.app.editor;

	    var points = [
	        new THREE.Vector2(0, 0),
	        new THREE.Vector2(4, 0),
	        new THREE.Vector2(3.5, 0.5),
	        new THREE.Vector2(1, 0.75),
	        new THREE.Vector2(0.8, 1),
	        new THREE.Vector2(0.8, 4),
	        new THREE.Vector2(1, 4.2),
	        new THREE.Vector2(1.4, 4.8),
	        new THREE.Vector2(2, 5),
	        new THREE.Vector2(2.5, 5.4),
	        new THREE.Vector2(3, 12)
	    ];
	    var segments = 20;
	    var phiStart = 0;
	    var phiLength = 2 * Math.PI;

	    var geometry = new THREE.LatheBufferGeometry(points, segments, phiStart, phiLength);
	    var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ side: THREE.DoubleSide }));
	    mesh.name = '花瓶' + ID$d++;

	    editor.execute(new AddObjectCommand(mesh));
	};

	var ID$e = 1;

	/**
	 * 添加精灵事件
	 * @param {*} app 
	 */
	function AddSpriteEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddSpriteEvent.prototype = Object.create(MenuEvent.prototype);
	AddSpriteEvent.prototype.constructor = AddSpriteEvent;

	AddSpriteEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddSprite.' + this.id, function () {
	        _this.onAddSprite();
	    });
	};

	AddSpriteEvent.prototype.stop = function () {
	    this.app.on('mAddSprite.' + this.id, null);
	};

	AddSpriteEvent.prototype.onAddSprite = function () {
	    var editor = this.app.editor;

	    var sprite = new THREE.Sprite(new THREE.SpriteMaterial());
	    sprite.name = '精灵' + ID$e++;

	    editor.execute(new AddObjectCommand(sprite));
	};

	var ID$f = 1;

	/**
	 * 添加点光源事件
	 * @param {*} app 
	 */
	function AddPointLightEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddPointLightEvent.prototype = Object.create(MenuEvent.prototype);
	AddPointLightEvent.prototype.constructor = AddPointLightEvent;

	AddPointLightEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddPointLight.' + this.id, function () {
	        _this.onAddPointLight();
	    });
	};

	AddPointLightEvent.prototype.stop = function () {
	    this.app.on('mAddPointLight.' + this.id, null);
	};

	AddPointLightEvent.prototype.onAddPointLight = function () {
	    var editor = this.app.editor;

	    var color = 0xffffff;
	    var intensity = 1;
	    var distance = 0;

	    var light = new THREE.PointLight(color, intensity, distance);
	    light.name = '点光源' + ID$f++;

	    editor.execute(new AddObjectCommand(light));
	};

	var ID$g = 1;

	/**
	 * 添加聚光灯事件
	 * @param {*} app 
	 */
	function AddSpotLightEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddSpotLightEvent.prototype = Object.create(MenuEvent.prototype);
	AddSpotLightEvent.prototype.constructor = AddSpotLightEvent;

	AddSpotLightEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddSpotLight.' + this.id, function () {
	        _this.onAddSpotLight();
	    });
	};

	AddSpotLightEvent.prototype.stop = function () {
	    this.app.on('mAddSpotLight.' + this.id, null);
	};

	AddSpotLightEvent.prototype.onAddSpotLight = function () {
	    var editor = this.app.editor;

	    var color = 0xffffff;
	    var intensity = 1;
	    var distance = 0;
	    var angle = Math.PI * 0.1;
	    var penumbra = 0;

	    var light = new THREE.SpotLight(color, intensity, distance, angle, penumbra);

	    light.name = '聚光灯' + ID$g;
	    light.target.name = 'SpotLight ' + (ID$g++) + ' Target';

	    light.position.set(5, 10, 7.5);

	    editor.execute(new AddObjectCommand(light));
	};

	var ID$h = 1;

	/**
	 * 添加平行光源事件
	 * @param {*} app 
	 */
	function AddDirectionalLightEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddDirectionalLightEvent.prototype = Object.create(MenuEvent.prototype);
	AddDirectionalLightEvent.prototype.constructor = AddDirectionalLightEvent;

	AddDirectionalLightEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddDirectionalLight.' + this.id, function () {
	        _this.onAddDirectionalLight();
	    });
	};

	AddDirectionalLightEvent.prototype.stop = function () {
	    this.app.on('mAddDirectionalLight.' + this.id, null);
	};

	AddDirectionalLightEvent.prototype.onAddDirectionalLight = function () {
	    var editor = this.app.editor;

	    var color = 0xffffff;
	    var intensity = 1;

	    var light = new THREE.DirectionalLight(color, intensity);
	    light.name = '平行光' + ID$h;
	    light.target.name = 'DirectionalLight ' + (ID$h++) + ' Target';

	    light.position.set(5, 10, 7.5);

	    editor.execute(new AddObjectCommand(light));
	};

	var ID$i = 1;

	/**
	 * 添加半球光事件
	 * @param {*} app 
	 */
	function AddHemisphereLightEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddHemisphereLightEvent.prototype = Object.create(MenuEvent.prototype);
	AddHemisphereLightEvent.prototype.constructor = AddHemisphereLightEvent;

	AddHemisphereLightEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddHemisphereLight.' + this.id, function () {
	        _this.onAddHemisphereLight();
	    });
	};

	AddHemisphereLightEvent.prototype.stop = function () {
	    this.app.on('mAddHemisphereLight.' + this.id, null);
	};

	AddHemisphereLightEvent.prototype.onAddHemisphereLight = function () {
	    var editor = this.app.editor;

	    var skyColor = 0x00aaff;
	    var groundColor = 0xffaa00;
	    var intensity = 1;

	    var light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
	    light.name = '半球光' + ID$i++;

	    light.position.set(0, 10, 0);

	    editor.execute(new AddObjectCommand(light));
	};

	var ID$j = 1;

	/**
	 * 添加环境光事件
	 * @param {*} app 
	 */
	function AddAmbientLightEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddAmbientLightEvent.prototype = Object.create(MenuEvent.prototype);
	AddAmbientLightEvent.prototype.constructor = AddAmbientLightEvent;

	AddAmbientLightEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddAmbientLight.' + this.id, function () {
	        _this.onAddAmbientLightEvent();
	    });
	};

	AddAmbientLightEvent.prototype.stop = function () {
	    this.app.on('mAddAmbientLight.' + this.id, null);
	};

	AddAmbientLightEvent.prototype.onAddAmbientLightEvent = function () {
	    var editor = this.app.editor;

	    var color = 0xaaaaaa;

	    var light = new THREE.AmbientLight(color);
	    light.name = '环境光' + ID$j++;

	    editor.execute(new AddObjectCommand(light));
	};

	var ID$k = 1;

	/**
	 * 添加透视相机事件
	 * @param {*} app 
	 */
	function AddPerspectiveCameraEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddPerspectiveCameraEvent.prototype = Object.create(MenuEvent.prototype);
	AddPerspectiveCameraEvent.prototype.constructor = AddPerspectiveCameraEvent;

	AddPerspectiveCameraEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddPerspectiveCamera.' + this.id, function () {
	        _this.onAddPerspectiveCamera();
	    });
	};

	AddPerspectiveCameraEvent.prototype.stop = function () {
	    this.app.on('mAddPerspectiveCamera.' + this.id, null);
	};

	AddPerspectiveCameraEvent.prototype.onAddPerspectiveCamera = function () {
	    var editor = this.app.editor;

	    var camera = new THREE.PerspectiveCamera(50, 1, 1, 10000);
	    camera.name = '透视相机' + ID$k++;

	    editor.execute(new AddObjectCommand(camera));
	};

	/**
	 * 添加资源事件
	 * @param {*} app 
	 */
	function AddAssetEvent(app) {
	    MenuEvent.call(this, app);
	}

	AddAssetEvent.prototype = Object.create(MenuEvent.prototype);
	AddAssetEvent.prototype.constructor = AddAssetEvent;

	AddAssetEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAddAsset.' + this.id, function () {
	        _this.onAddAsset();
	    });
	};

	AddAssetEvent.prototype.stop = function () {
	    this.app.on('mAddAsset.' + this.id, null);
	};

	AddAssetEvent.prototype.onAddAsset = function () {
	    var btn = UI.get('modelBtn');
	    if (btn) {
	        btn.dom.click();
	    }
	};

	/**
	 * 导入资源事件
	 * @param {*} app 
	 */
	function ImportAssetEvent(app) {
	    MenuEvent.call(this, app);
	}

	ImportAssetEvent.prototype = Object.create(MenuEvent.prototype);
	ImportAssetEvent.prototype.constructor = ImportAssetEvent;

	ImportAssetEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mImportAsset.' + this.id, function () {
	        _this.onImportAsset();
	    });

	    this.fileInput = document.createElement('input');
	    this.fileInput.type = 'file';

	    var _this = this;
	    this.fileInput.addEventListener('change', function (event) {
	        if (_this.fileInput.files.length > 0) {
	            _this.app.editor.loader.loadFile(_this.fileInput.files[0]);
	        }
	    });
	};

	ImportAssetEvent.prototype.stop = function () {
	    this.app.on('mImportAsset.' + this.id, null);
	};

	ImportAssetEvent.prototype.onImportAsset = function () {
	    this.fileInput.click();
	};

	var link = document.createElement('a');
	link.style.display = 'none';
	document.body.appendChild(link); // Firefox workaround, see #6594

	function save(blob, filename) {
	    link.href = URL.createObjectURL(blob);
	    link.download = filename || 'data.json';
	    link.click();
	    // URL.revokeObjectURL( url ); breaks Firefox...
	}

	function saveString(text, filename) {
	    save(new Blob([text], { type: 'text/plain' }), filename);
	}

	const StringUtils = {
	    save: save,
	    saveString: saveString
	};

	/**
	 * 导出几何体事件
	 * @param {*} app 
	 */
	function ExportGeometryEvent(app) {
	    MenuEvent.call(this, app);
	}

	ExportGeometryEvent.prototype = Object.create(MenuEvent.prototype);
	ExportGeometryEvent.prototype.constructor = ExportGeometryEvent;

	ExportGeometryEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mExportGeometry.' + this.id, function () {
	        _this.onExportGeometry();
	    });
	};

	ExportGeometryEvent.prototype.stop = function () {
	    this.app.on('mExportGeometry.' + this.id, null);
	};

	ExportGeometryEvent.prototype.onExportGeometry = function () {
	    var editor = this.app.editor;

	    var object = editor.selected;

	    if (object === null) {
	        UI$1.msg('请选择物体');
	        return;
	    }

	    var geometry = object.geometry;

	    if (geometry === undefined) {
	        UI$1.msg('选中的对象不具有Geometry属性。');
	        return;
	    }

	    var output = geometry.toJSON();

	    try {
	        output = JSON.stringify(output, parseNumber, '\t');
	        output = output.replace(/[\n\t]+([\d\.e\-\[\]]+)/g, '$1');
	    } catch (e) {
	        output = JSON.stringify(output);
	    }

	    StringUtils.saveString(output, 'geometry.json');
	};

	/**
	 * 导出物体事件
	 * @param {*} app 
	 */
	function ExportObjectEvent(app) {
	    MenuEvent.call(this, app);
	}

	ExportObjectEvent.prototype = Object.create(MenuEvent.prototype);
	ExportObjectEvent.prototype.constructor = ExportObjectEvent;

	ExportObjectEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mExportObject.' + this.id, function () {
	        _this.onExportObject();
	    });
	};

	ExportObjectEvent.prototype.stop = function () {
	    this.app.on('mExportObject.' + this.id, null);
	};

	ExportObjectEvent.prototype.onExportObject = function () {
	    var editor = this.app.editor;

	    var object = editor.selected;

	    if (object === null) {
	        UI$1.msg('请选择对象');
	        return;
	    }

	    var output = object.toJSON();

	    try {
	        output = JSON.stringify(output, parseNumber, '\t');
	        output = output.replace(/[\n\t]+([\d\.e\-\[\]]+)/g, '$1');
	    } catch (e) {
	        output = JSON.stringify(output);
	    }

	    StringUtils.saveString(output, 'model.json');
	};

	/**
	 * 导出场景事件
	 * @param {*} app 
	 */
	function ExportSceneEvent(app) {
	    MenuEvent.call(this, app);
	}

	ExportSceneEvent.prototype = Object.create(MenuEvent.prototype);
	ExportSceneEvent.prototype.constructor = ExportSceneEvent;

	ExportSceneEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mExportScene.' + this.id, function () {
	        _this.onExportScene();
	    });
	};

	ExportSceneEvent.prototype.stop = function () {
	    this.app.on('mExportScene.' + this.id, null);
	};

	ExportSceneEvent.prototype.onExportScene = function () {
	    var editor = this.app.editor;

	    var output = editor.scene.toJSON();

	    try {
	        output = JSON.stringify(output, parseNumber, '\t');
	        output = output.replace(/[\n\t]+([\d\.e\-\[\]]+)/g, '$1');
	    } catch (e) {
	        output = JSON.stringify(output);
	    }

	    StringUtils.saveString(output, 'scene.json');
	};

	/**
	 * 导出gltf文件事件
	 * @param {*} app 
	 */
	function ExportGLTFEvent(app) {
	    MenuEvent.call(this, app);
	}

	ExportGLTFEvent.prototype = Object.create(MenuEvent.prototype);
	ExportGLTFEvent.prototype.constructor = ExportGLTFEvent;

	ExportGLTFEvent.prototype.start = function () {
	    this.app.on('mExportGLTF.' + this.id, this.onExportGLTF.bind(this));
	};

	ExportGLTFEvent.prototype.stop = function () {
	    this.app.on('mExportGLTF.' + this.id, null);
	};

	ExportGLTFEvent.prototype.onExportGLTF = function () {
	    var exporter = new THREE.GLTFExporter();

	    exporter.parse(app.editor.scene, function (result) {
	        StringUtils.saveString(JSON.stringify(result), 'model.gltf');
	    });
	};

	/**
	 * 导出mmd文件事件
	 * @param {*} app 
	 */
	function ExportMMDEvent(app) {
	    MenuEvent.call(this, app);
	}

	ExportMMDEvent.prototype = Object.create(MenuEvent.prototype);
	ExportMMDEvent.prototype.constructor = ExportMMDEvent;

	ExportMMDEvent.prototype.start = function () {
	    this.app.on('mExportMMD.' + this.id, this.onExportMMD.bind(this));
	};

	ExportMMDEvent.prototype.stop = function () {
	    this.app.on('mExportMMD.' + this.id, null);
	};

	ExportMMDEvent.prototype.onExportMMD = function () {
	    // TODO:
	    // var exporter = new THREE.MMDExporter();

	    // exporter.parse(app.editor.scene, function (result) {
	    //     StringUtils.saveString(JSON.stringify(result), 'model.gltf');
	    // });
	};

	/**
	 * 导出obj文件事件
	 * @param {*} app 
	 */
	function ExportOBJEvent(app) {
	    MenuEvent.call(this, app);
	}

	ExportOBJEvent.prototype = Object.create(MenuEvent.prototype);
	ExportOBJEvent.prototype.constructor = ExportOBJEvent;

	ExportOBJEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mExportOBJ.' + this.id, function () {
	        _this.onExportOBJ();
	    });
	};

	ExportOBJEvent.prototype.stop = function () {
	    this.app.on('mExportOBJ.' + this.id, null);
	};

	ExportOBJEvent.prototype.onExportOBJ = function () {
	    var editor = this.app.editor;

	    var object = editor.selected;

	    if (object === null) {
	        UI$1.msg('请选择对象');
	        return;
	    }

	    var exporter = new THREE.OBJExporter();
	    StringUtils.saveString(exporter.parse(object), 'model.obj');
	};

	/**
	 * 导出ply文件事件
	 * @param {*} app 
	 */
	function ExportPLYEvent(app) {
	    MenuEvent.call(this, app);
	}

	ExportPLYEvent.prototype = Object.create(MenuEvent.prototype);
	ExportPLYEvent.prototype.constructor = ExportPLYEvent;

	ExportPLYEvent.prototype.start = function () {
	    this.app.on('mExportPLY.' + this.id, this.onExportPLY.bind(this));
	};

	ExportPLYEvent.prototype.stop = function () {
	    this.app.on('mExportPLY.' + this.id, null);
	};

	ExportPLYEvent.prototype.onExportPLY = function () {
	    var editor = this.app.editor;

	    var object = editor.selected;

	    if (object === null) {
	        UI$1.msg('请选择对象');
	        return;
	    }

	    var exporter = new THREE.PLYExporter();
	    StringUtils.saveString(exporter.parse(object, {
	        excludeAttributes: ['normal', 'uv', 'color', 'index']
	    }), 'model.ply');
	};

	/**
	 * 导出stl二进制文件事件
	 * @param {*} app 
	 */
	function ExportSTLBEvent(app) {
	    MenuEvent.call(this, app);
	}

	ExportSTLBEvent.prototype = Object.create(MenuEvent.prototype);
	ExportSTLBEvent.prototype.constructor = ExportSTLBEvent;

	ExportSTLBEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mExportSTLB.' + this.id, function () {
	        _this.onExportSTLB();
	    });
	};

	ExportSTLBEvent.prototype.stop = function () {
	    this.app.on('mExportSTLB.' + this.id, null);
	};

	ExportSTLBEvent.prototype.onExportSTLB = function () {
	    var editor = this.app.editor;

	    var exporter = new THREE.STLBinaryExporter();

	    StringUtils.saveString(exporter.parse(editor.scene), 'model.stl');
	};

	/**
	 * 导出stl文件事件
	 * @param {*} app 
	 */
	function ExportSTLEvent(app) {
	    MenuEvent.call(this, app);
	}

	ExportSTLEvent.prototype = Object.create(MenuEvent.prototype);
	ExportSTLEvent.prototype.constructor = ExportSTLEvent;

	ExportSTLEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mExportSTL.' + this.id, function () {
	        _this.onExportSTL();
	    });
	};

	ExportSTLEvent.prototype.stop = function () {
	    this.app.on('mExportSTL.' + this.id, null);
	};

	ExportSTLEvent.prototype.onExportSTL = function () {
	    var editor = this.app.editor;

	    var exporter = new THREE.STLExporter();

	    StringUtils.saveString(exporter.parse(editor.scene), 'model.stl');
	};

	var ID$l = 1;

	/**
	 * 添加人事件
	 * @param {*} app 
	 */
	function AddPersonEvent(app) {
	    MenuEvent.call(this, app);
	    this.persons = [];
	}

	AddPersonEvent.prototype = Object.create(MenuEvent.prototype);
	AddPersonEvent.prototype.constructor = AddPersonEvent;

	AddPersonEvent.prototype.start = function () {
	    this.app.on('mAddPerson.' + this.id, this.onAddPerson.bind(this));
	    this.app.on('objectRemoved.' + this.id, this.onObjectRemoved.bind(this));
	};

	AddPersonEvent.prototype.stop = function () {
	    this.app.on('mAddPerson.' + this.id, null);
	    this.app.on('objectRemoved.' + this.id, null);
	};

	AddPersonEvent.prototype.onAddPerson = function () {
	    var editor = this.app.editor;
	    var camera = editor.camera;

	    var _this = this;

	    new THREE.ObjectLoader().load('../assets/models/marine/marine_anims_core.json', function (loadedObject) {
	        var mesh;

	        loadedObject.traverse(function (child) {
	            if (child instanceof THREE.SkinnedMesh) {
	                mesh = child;
	            }
	        });

	        if (mesh === undefined) {
	            UI$1.msg('Unable to find a SkinnedMesh in this place:\n\n' + url + '\n\n');
	            return;
	        }

	        mesh.scale.set(1, 1, 1);
	        mesh.rotation.y = - 135 * Math.PI / 180;
	        mesh.name = 'Person ' + ID$l++;
	        editor.execute(new AddObjectCommand(mesh));

	        mesh.mixer = new THREE.AnimationMixer(mesh);
	        mesh.idleAction = mesh.mixer.clipAction('idle');
	        mesh.walkAction = mesh.mixer.clipAction('walk');
	        mesh.runAction = mesh.mixer.clipAction('run');
	        mesh.actions = [mesh.idleAction, mesh.walkAction, mesh.runAction];
	        mesh.walkAction.play();

	        _this.persons.push(mesh);

	        _this.app.on(`animate.` + _this.id, _this.onAnimate.bind(_this));
	    });
	};

	AddPersonEvent.prototype.onObjectRemoved = function (object) {
	    var index = this.persons.findIndex(function (n) {
	        return n.mesh === object;
	    });
	    if (index > -1) {
	        this.persons.splice(index, 1);
	    }

	    if (this.persons.length === 0) {
	        this.app.on(`animate.` + this.id, null);
	    }
	};

	AddPersonEvent.prototype.onAnimate = function (clock) {
	    var mixerUpdateDelta = clock.getDelta();

	    this.persons.forEach(function (person) {
	        person.mixer.update(mixerUpdateDelta);
	    });
	};

	var ID$m = 1;

	/**
	 * 添加火焰事件
	 * @param {*} app 
	 */
	function AddFireEvent(app) {
	    MenuEvent.call(this, app);
	    this.fires = [];
	}

	AddFireEvent.prototype = Object.create(MenuEvent.prototype);
	AddFireEvent.prototype.constructor = AddFireEvent;

	AddFireEvent.prototype.start = function () {
	    this.app.on('mAddFire.' + this.id, this.onAddFire.bind(this));
	    this.app.on('objectRemoved.' + this.id, this.onObjectRemoved.bind(this));
	};

	AddFireEvent.prototype.stop = function () {
	    this.app.on('mAddFire.' + this.id, null);
	    this.app.on('objectRemoved.' + this.id, null);
	};

	AddFireEvent.prototype.onAddFire = function () {
	    var editor = this.app.editor;
	    var camera = editor.camera;

	    VolumetricFire.texturePath = '../assets/textures/VolumetricFire/';

	    var fireWidth = 2;
	    var fireHeight = 4;
	    var fireDepth = 2;
	    var sliceSpacing = 0.5;

	    var fire = new VolumetricFire(
	        fireWidth,
	        fireHeight,
	        fireDepth,
	        sliceSpacing,
	        camera
	    );

	    fire.mesh.name = 'Fire ' + ID$m++;
	    fire.mesh.useSelectionBox = false;
	    fire.mesh.geometry.boundingBox = new THREE.Box3(
	        new THREE.Vector3(-fireWidth, -fireHeight, -fireDepth),
	        new THREE.Vector3(fireWidth, fireHeight, fireDepth)
	    );
	    fire.mesh.geometry.boundingSphere = new THREE.Sphere( // 没有boundingSphere则无法选中
	        new THREE.Vector3(),
	        fireHeight / 2
	    );
	    fire.mesh.position.y = 2;
	    editor.execute(new AddObjectCommand(fire.mesh));

	    this.fires.push(fire);

	    this.app.on(`animate.` + this.id, this.onAnimate.bind(this));
	};

	AddFireEvent.prototype.onObjectRemoved = function (object) {
	    var index = this.fires.findIndex(function (n) {
	        return n.mesh === object;
	    });
	    if (index > -1) {
	        this.fires.splice(index, 1);
	    }

	    if (this.fires.length === 0) {
	        this.app.on(`animate.` + this.id, null);
	    }
	};

	AddFireEvent.prototype.onAnimate = function (clock) {
	    var elapsed = clock.getElapsedTime();

	    this.fires.forEach(function (fire) {
	        fire.update(elapsed);
	    });
	};

	var ID$n = -1;

	/**
	 * 粒子基类
	 * @param {*} camera 相机
	 * @param {*} renderer 渲染器
	 * @param {*} options 选项
	 */
	function BaseParticle(camera, renderer, options) {
	    this.camera = camera;
	    this.renderer = renderer;
	    this.options = options;

	    this.id = 'BaseParticle' + ID$n--;

	    // 需要放入场景的物体
	    this.mesh = null;
	}
	BaseParticle.prototype.update = function (elapsed) {

	};

	var vertexShader = "attribute float shift;\r\nuniform float time;\r\nuniform float size;\r\nuniform float lifetime;\r\nuniform float projection;\r\nvarying float progress;\r\n\r\nfloat cubicOut( float t ) {\r\n\r\n  float f = t - 1.0;\r\n  return f * f * f + 1.0;\r\n\r\n}\r\n\r\nvoid main () {\r\n\r\n  progress = fract( time * 2. / lifetime + shift );\r\n  float eased = cubicOut( progress );\r\n  vec3 pos = vec3( position.x * eased, position.y * eased, position.z );\r\n  gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1. );\r\n  gl_PointSize = ( projection * size ) / gl_Position.w;\r\n\r\n}";

	var fragmentShader = "uniform sampler2D texture;\r\nvarying float progress;\r\n\r\nvoid main() {\r\n\r\n  vec3 color = vec3( 1. );\r\n  gl_FragColor = texture2D( texture, gl_PointCoord ) * vec4( color, .3 * ( 1. - progress ) );\r\n\r\n}";

	/**
	 * 烟
	 * @param {*} options 
	 */
	function Smoke(camera, renderer, options) {
	    BaseParticle.call(this, camera, renderer, options);

	    var smoke,
	        NUM_OF_PARTICLE = 32,
	        texture,
	        uniforms,
	        material,
	        geometry = new THREE.BufferGeometry(),
	        position = new Float32Array(NUM_OF_PARTICLE * 3),
	        shift = new Float32Array(NUM_OF_PARTICLE),
	        i;

	    var textureLoader = new THREE.TextureLoader();

	    texture = textureLoader.load('../assets/textures/VolumetricFire/smoke.png');
	    uniforms = {
	        time: { type: 'f', value: 0 },
	        size: { type: 'f', value: 3 },
	        texture: { type: 't', value: texture },
	        lifetime: { type: 'f', value: 10 },
	        projection: { type: 'f', value: Math.abs(this.renderer.domElement.height / (2 * Math.tan(THREE.Math.degToRad(this.camera.fov)))) }
	    };
	    material = new THREE.ShaderMaterial({
	        vertexShader: vertexShader,
	        fragmentShader: fragmentShader,
	        uniforms: uniforms,
	        blending: THREE.AdditiveBlending,
	        transparent: true,
	        depthWrite: false
	    });

	    for (i = 0; i < NUM_OF_PARTICLE; i++) {
	        position[i * 3 + 0] = THREE.Math.randFloat(-0.5, 0.5);
	        position[i * 3 + 1] = 2.4;
	        position[i * 3 + 3] = THREE.Math.randFloat(-0.5, 0.5);
	        shift[i] = Math.random() * 1;
	    }

	    geometry.addAttribute('position', new THREE.BufferAttribute(position, 3));
	    geometry.addAttribute('shift', new THREE.BufferAttribute(shift, 1));

	    smoke = new THREE.Points(geometry, material);
	    smoke.sortParticles = true;

	    this.mesh = smoke;
	}

	Smoke.prototype = Object.create(BaseParticle.prototype);
	Smoke.prototype.constructor = Smoke;

	Smoke.prototype.update = function (elapsed) {
	    this.mesh.material.uniforms.time.value = elapsed;
	};

	var ID$o = 1;

	/**
	 * 添加烟事件
	 * @param {*} app 
	 */
	function AddSmokeEvent(app) {
	    MenuEvent.call(this, app);
	    this.smokes = [];
	}

	AddSmokeEvent.prototype = Object.create(MenuEvent.prototype);
	AddSmokeEvent.prototype.constructor = AddSmokeEvent;

	AddSmokeEvent.prototype.start = function () {
	    this.app.on('mAddSmoke.' + this.id, this.onAddSmoke.bind(this));
	    this.app.on('objectRemoved.' + this.id, this.onObjectRemoved.bind(this));
	};

	AddSmokeEvent.prototype.stop = function () {
	    this.app.on('mAddSmoke.' + this.id, null);
	    this.app.on('objectRemoved.' + this.id, null);
	};

	AddSmokeEvent.prototype.onAddSmoke = function () {
	    var editor = this.app.editor;
	    var camera = editor.camera;
	    var renderer = editor.renderer;

	    var smoke = new Smoke(camera, renderer);

	    smoke.mesh.name = 'Smoke ' + ID$o++;
	    smoke.mesh.useSelectionBox = false;
	    smoke.mesh.position.y = 3;
	    smoke.mesh.scale;
	    editor.execute(new AddObjectCommand(smoke.mesh));

	    this.smokes.push(smoke);

	    this.app.on(`animate.` + this.id, this.onAnimate.bind(this));
	};

	AddSmokeEvent.prototype.onObjectRemoved = function (object) {
	    var index = this.smokes.findIndex(function (n) {
	        return n.mesh === object;
	    });
	    if (index > -1) {
	        this.smokes.splice(index, 1);
	    }

	    if (this.smokes.length === 0) {
	        this.app.on(`animate.` + this.id, null);
	    }
	};

	AddSmokeEvent.prototype.onAnimate = function (clock) {
	    var elapsed = clock.getElapsedTime();

	    this.smokes.forEach(function (smoke) {
	        smoke.update(elapsed);
	    });
	};

	/**
	 * 启动事件
	 * @param {*} app 
	 */
	function PlayEvent(app) {
	    MenuEvent.call(this, app);
	    this.isPlaying = false;
	}

	PlayEvent.prototype = Object.create(MenuEvent.prototype);
	PlayEvent.prototype.constructor = PlayEvent;

	PlayEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mPlay.' + this.id, function () {
	        _this.onPlay();
	    });
	};

	PlayEvent.prototype.stop = function () {
	    this.app.on('mPlay.' + this.id, null);
	};

	PlayEvent.prototype.onPlay = function () {
	    var editor = this.app.editor;

	    if (this.isPlaying === false) {
	        this.isPlaying = true;
	        UI$1.get('mPlay').dom.innerHTML = '停止';
	        this.app.call('startPlayer', this);
	    } else {
	        this.isPlaying = false;
	        UI$1.get('mPlay').dom.innerHTML = '启动';
	        this.app.call('stopPlayer', this);
	    }
	};

	/**
	 * 启动事件
	 * @param {*} app 
	 */
	function VRModeEvent(app) {
	    MenuEvent.call(this, app);
	}

	VRModeEvent.prototype = Object.create(MenuEvent.prototype);
	VRModeEvent.prototype.constructor = VRModeEvent;

	VRModeEvent.prototype.start = function () {
	    this.app.on('mVRMode.' + this.id, this.onVRMode.bind(this));
	};

	VRModeEvent.prototype.stop = function () {
	    this.app.on('mVRMode.' + this.id, null);
	};

	VRModeEvent.prototype.onVRMode = function () {
	    var editor = this.app.editor;

	    if (this.app.editor.renderer.vr.enabled) {
	        this.app.call('enterVR', this);
	    } else {
	        UI$1.msg('WebVR不可用');
	    }
	};

	/**
	 * 示例事件
	 * @param {*} app 
	 */
	function ExampleEvent(app) {
	    MenuEvent.call(this, app);
	    this.isPlaying = false;
	}

	ExampleEvent.prototype = Object.create(MenuEvent.prototype);
	ExampleEvent.prototype.constructor = ExampleEvent;

	ExampleEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mArkanoid.' + this.id, function () {
	        _this.onExample('arkanoid.app.json');
	    });
	    this.app.on('mCamera.' + this.id, function () {
	        _this.onExample('camera.app.json');
	    });
	    this.app.on('mParticles.' + this.id, function () {
	        _this.onExample('particles.app.json');
	    });
	    this.app.on('mPong.' + this.id, function () {
	        _this.onExample('pong.app.json');
	    });
	};

	ExampleEvent.prototype.stop = function () {
	    this.app.on('mArkanoid.' + this.id, null);
	    this.app.on('mCamera.' + this.id, null);
	    this.app.on('mParticles.' + this.id, null);
	    this.app.on('mPong.' + this.id, null);
	};

	ExampleEvent.prototype.onExample = function (name) {
	    var editor = this.app.editor;

	    if (confirm('任何未保存数据将丢失。确定吗？')) {
	        var loader = new THREE.FileLoader();

	        loader.load('../examples/' + name, function (text) {
	            editor.clear();
	            editor.fromJSON(JSON.parse(text));
	        });
	    }
	};

	/**
	 * 源码事件
	 * @param {*} app 
	 */
	function SourceCodeEvent(app) {
	    MenuEvent.call(this, app);
	}

	SourceCodeEvent.prototype = Object.create(MenuEvent.prototype);
	SourceCodeEvent.prototype.constructor = SourceCodeEvent;

	SourceCodeEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mSourceCode.' + this.id, function () {
	        _this.onSourceCode();
	    });
	};

	SourceCodeEvent.prototype.stop = function () {
	    this.app.on('mSourceCode.' + this.id, null);
	};

	SourceCodeEvent.prototype.onSourceCode = function () {
	    window.open('https://github.com/mrdoob/three.js/tree/master/editor', '_blank');
	};

	/**
	 * 关于事件
	 * @param {*} app 
	 */
	function AboutEvent(app) {
	    MenuEvent.call(this, app);
	}

	AboutEvent.prototype = Object.create(MenuEvent.prototype);
	AboutEvent.prototype.constructor = AboutEvent;

	AboutEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('mAbout.' + this.id, function () {
	        _this.onAbout();
	    });
	};

	AboutEvent.prototype.stop = function () {
	    this.app.on('mAbout.' + this.id, null);
	};

	AboutEvent.prototype.onAbout = function () {
	    window.open('http://threejs.org', '_blank');
	};

	/**
	 * 保存状态改变
	 * @param {*} app 
	 */
	function SavingStatusEvent(app) {
	    MenuEvent.call(this, app);
	}

	SavingStatusEvent.prototype = Object.create(MenuEvent.prototype);
	SavingStatusEvent.prototype.constructor = SavingStatusEvent;

	SavingStatusEvent.prototype.start = function () {
	    this.app.on('initApp.' + this.id, this.onInitApp.bind(this));
	    this.app.on('savingStarted.' + this.id, this.onSavingStarted.bind(this));
	    this.app.on('savingFinished.' + this.id, this.onSavingFinished.bind(this));
	};

	SavingStatusEvent.prototype.stop = function () {
	    this.app.on('initApp.' + this.id, null);
	    this.app.on('savingStarted.' + this.id, null);
	    this.app.on('savingFinished.' + this.id, null);
	};

	SavingStatusEvent.prototype.onInitApp = function () {
	    UI$1.get('bAutoSave').input.checked = this.app.editor.config.getKey('autosave');
	};

	SavingStatusEvent.prototype.onSavingStarted = function () {
	    UI$1.get('bAutoSave').input.style.textDecoration = 'underline';
	};

	SavingStatusEvent.prototype.onSavingFinished = function () {
	    UI$1.get('bAutoSave').input.style.textDecoration = 'none';
	};

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param newPosition THREE.Vector3
	 * @param optionalOldPosition THREE.Vector3
	 * @constructor
	 */

	function SetPositionCommand(object, newPosition, optionalOldPosition) {

		Command.call(this);

		this.type = 'SetPositionCommand';
		this.name = 'Set Position';
		this.updatable = true;

		this.object = object;

		if (object !== undefined && newPosition !== undefined) {
			this.oldPosition = object.position.clone();
			this.newPosition = newPosition.clone();
		}

		if (optionalOldPosition !== undefined) {
			this.oldPosition = optionalOldPosition.clone();
		}

	}
	SetPositionCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetPositionCommand.prototype, {

		constructor: SetPositionCommand,

		execute: function () {

			this.object.position.copy(this.newPosition);
			this.object.updateMatrixWorld(true);
			this.editor.app.call('objectChanged', this, this.object);

		},

		undo: function () {

			this.object.position.copy(this.oldPosition);
			this.object.updateMatrixWorld(true);
			this.editor.app.call('objectChanged', this, this.object);

		},

		update: function (command) {

			this.newPosition.copy(command.newPosition);

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.oldPosition = this.oldPosition.toArray();
			output.newPosition = this.newPosition.toArray();

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.object = this.editor.objectByUuid(json.objectUuid);
			this.oldPosition = new THREE.Vector3().fromArray(json.oldPosition);
			this.newPosition = new THREE.Vector3().fromArray(json.newPosition);

		}

	});

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param newRotation THREE.Euler
	 * @param optionalOldRotation THREE.Euler
	 * @constructor
	 */
	function SetRotationCommand(object, newRotation, optionalOldRotation) {

		Command.call(this);

		this.type = 'SetRotationCommand';
		this.name = 'Set Rotation';
		this.updatable = true;

		this.object = object;

		if (object !== undefined && newRotation !== undefined) {

			this.oldRotation = object.rotation.clone();
			this.newRotation = newRotation.clone();

		}

		if (optionalOldRotation !== undefined) {

			this.oldRotation = optionalOldRotation.clone();

		}
	}
	SetRotationCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetRotationCommand.prototype, {

		constructor: SetRotationCommand,

		execute: function () {

			this.object.rotation.copy(this.newRotation);
			this.object.updateMatrixWorld(true);
			this.editor.app.call('objectChanged', this, this.object);

		},

		undo: function () {

			this.object.rotation.copy(this.oldRotation);
			this.object.updateMatrixWorld(true);
			this.editor.app.call('objectChanged', this, this.object);

		},

		update: function (command) {

			this.newRotation.copy(command.newRotation);

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.oldRotation = this.oldRotation.toArray();
			output.newRotation = this.newRotation.toArray();

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.object = this.editor.objectByUuid(json.objectUuid);
			this.oldRotation = new THREE.Euler().fromArray(json.oldRotation);
			this.newRotation = new THREE.Euler().fromArray(json.newRotation);

		}

	});

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param newScale THREE.Vector3
	 * @param optionalOldScale THREE.Vector3
	 * @constructor
	 */
	function SetScaleCommand(object, newScale, optionalOldScale) {

		Command.call(this);

		this.type = 'SetScaleCommand';
		this.name = 'Set Scale';
		this.updatable = true;

		this.object = object;

		if (object !== undefined && newScale !== undefined) {
			this.oldScale = object.scale.clone();
			this.newScale = newScale.clone();
		}

		if (optionalOldScale !== undefined) {
			this.oldScale = optionalOldScale.clone();
		}
	}
	SetScaleCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetScaleCommand.prototype, {

		constructor: SetScaleCommand,

		execute: function () {

			this.object.scale.copy(this.newScale);
			this.object.updateMatrixWorld(true);
			this.editor.app.call('objectChanged', this, this.object);

		},

		undo: function () {

			this.object.scale.copy(this.oldScale);
			this.object.updateMatrixWorld(true);
			this.editor.app.call('objectChanged', this, this.object);

		},

		update: function (command) {

			this.newScale.copy(command.newScale);

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.oldScale = this.oldScale.toArray();
			output.newScale = this.newScale.toArray();

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.object = this.editor.objectByUuid(json.objectUuid);
			this.oldScale = new THREE.Vector3().fromArray(json.oldScale);
			this.newScale = new THREE.Vector3().fromArray(json.newScale);

		}

	});

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param newUuid string
	 * @constructor
	 */

	function SetUuidCommand(object, newUuid) {

		Command.call(this);

		this.type = 'SetUuidCommand';
		this.name = 'Update UUID';

		this.object = object;

		this.oldUuid = (object !== undefined) ? object.uuid : undefined;
		this.newUuid = newUuid;

	}
	SetUuidCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetUuidCommand.prototype, {

		constructor: SetUuidCommand,

		execute: function () {

			this.object.uuid = this.newUuid;
			this.editor.app.call('objectChanged', this, this.object);
			this.editor.app.call('sceneGraphChanged', this);

		},

		undo: function () {

			this.object.uuid = this.oldUuid;
			this.editor.app.call('objectChanged', this, this.object);
			this.editor.app.call('sceneGraphChanged', this);

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.oldUuid = this.oldUuid;
			output.newUuid = this.newUuid;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.oldUuid = json.oldUuid;
			this.newUuid = json.newUuid;
			this.object = this.editor.objectByUuid(json.oldUuid);

			if (this.object === undefined) {

				this.object = this.editor.objectByUuid(json.newUuid);

			}

		}

	});

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param attributeName string
	 * @param newValue number, string, boolean or object
	 * @constructor
	 */

	function SetValueCommand(object, attributeName, newValue) {

		Command.call(this);

		this.type = 'SetValueCommand';
		this.name = 'Set ' + attributeName;
		this.updatable = true;

		this.object = object;
		this.attributeName = attributeName;
		this.oldValue = (object !== undefined) ? object[attributeName] : undefined;
		this.newValue = newValue;

	}
	SetValueCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetValueCommand.prototype, {

		constructor: SetValueCommand,

		execute: function () {
			this.object[this.attributeName] = this.newValue;
			this.editor.app.call('objectChanged', this, this.object);
		},

		undo: function () {
			this.object[this.attributeName] = this.oldValue;
			this.editor.app.call('objectChanged', this, this.object);
		},

		update: function (cmd) {

			this.newValue = cmd.newValue;

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.attributeName = this.attributeName;
			output.oldValue = this.oldValue;
			output.newValue = this.newValue;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.attributeName = json.attributeName;
			this.oldValue = json.oldValue;
			this.newValue = json.newValue;
			this.object = this.editor.objectByUuid(json.objectUuid);

		}

	});

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param attributeName string
	 * @param newValue integer representing a hex color value
	 * @constructor
	 */

	function SetColorCommand(object, attributeName, newValue) {

		Command.call(this);

		this.type = 'SetColorCommand';
		this.name = 'Set ' + attributeName;
		this.updatable = true;

		this.object = object;
		this.attributeName = attributeName;
		this.oldValue = (object !== undefined) ? this.object[this.attributeName].getHex() : undefined;
		this.newValue = newValue;

	}
	SetColorCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetColorCommand.prototype, {

		constructor: SetColorCommand,

		execute: function () {

			this.object[this.attributeName].setHex(this.newValue);
			this.editor.app.call('objectChanged', this, this.object);

		},

		undo: function () {

			this.object[this.attributeName].setHex(this.oldValue);
			this.editor.app.call('objectChanged', this, this.object);

		},

		update: function (cmd) {

			this.newValue = cmd.newValue;

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.attributeName = this.attributeName;
			output.oldValue = this.oldValue;
			output.newValue = this.newValue;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.object = this.editor.objectByUuid(json.objectUuid);
			this.attributeName = json.attributeName;
			this.oldValue = json.oldValue;
			this.newValue = json.newValue;

		}

	});

	/**
	 * 物体面板事件
	 * @param {*} app 
	 */
	function ObjectPanelEvent(app) {
	    BaseEvent.call(this, app);
	    this.tabName = '物体';
	}

	ObjectPanelEvent.prototype = Object.create(BaseEvent.prototype);
	ObjectPanelEvent.prototype.constructor = ObjectPanelEvent;

	ObjectPanelEvent.prototype.start = function () {
	    this.app.on(`selectPropertyTab.${this.id}`, this.onSelectPropertyTab.bind(this));
	    this.app.on(`objectSelected.${this.id}`, this.onObjectSelected.bind(this));
	    this.app.on(`objectChanged.${this.id}`, this.onObjectChanged.bind(this));
	    this.app.on(`refreshSidebarObject3D.${this.id}`, this.onRefreshSidebarObject3D.bind(this));
	    this.app.on(`updateScaleX.${this.id}`, this.updateScaleX.bind(this));
	    this.app.on(`updateScaleY.${this.id}`, this.updateScaleY.bind(this));
	    this.app.on(`updateScaleZ.${this.id}`, this.updateScaleZ.bind(this));
	    this.app.on(`updateObject.${this.id}`, this.update.bind(this));
	    this.app.on(`updateObjectPanel.${this.id}`, this.updateUI.bind(this));
	};

	ObjectPanelEvent.prototype.stop = function () {
	    this.app.on(`selectPropertyTab.${this.id}`, null);
	    this.app.on(`objectSelected.${this.id}`, null);
	    this.app.on(`objectChanged.${this.id}`, null);
	    this.app.on(`refreshSidebarObject3D.${this.id}`, null);
	    this.app.on(`updateScaleX.${this.id}`, null);
	    this.app.on(`updateScaleY.${this.id}`, null);
	    this.app.on(`updateScaleZ.${this.id}`, null);
	    this.app.on(`updateObject.${this.id}`, null);
	    this.app.on(`updateObjectPanel.${this.id}`, null);
	};

	/**
	 * 选择物体选项卡
	 * @param {*} tabName 
	 */
	ObjectPanelEvent.prototype.onSelectPropertyTab = function (tabName) {
	    this.tabName = tabName;
	    if (this.app.editor.selected != null && tabName === '物体') {
	        UI$1.get('objectPanel').dom.style.display = '';
	    } else {
	        UI$1.get('objectPanel').dom.style.display = 'none';
	    }
	};

	/**
	 * 物体选中事件
	 * @param {*} object 
	 */
	ObjectPanelEvent.prototype.onObjectSelected = function (object) {
	    var container = UI$1.get('objectPanel');

	    // 设置物体面板显示隐藏
	    if (this.tabName === '物体' && object != null) {
	        container.dom.style.display = '';
	    } else {
	        container.dom.style.display = 'none';
	    }

	    if (object !== null) {
	        this.updateRows(object);
	        this.app.call('updateObjectPanel', this, object);
	    }
	};

	/**
	 * 选中物体改变
	 * @param {*} object 
	 */
	ObjectPanelEvent.prototype.onObjectChanged = function (object) {
	    var editor = this.app.editor;
	    if (object !== editor.selected) return;

	    this.app.call('updateObjectPanel', this, object);
	};

	/**
	 * 刷新物体面板
	 * @param {*} object 
	 */
	ObjectPanelEvent.prototype.onRefreshSidebarObject3D = function (object) {
	    var editor = this.app.editor;
	    if (object !== editor.selected) return;

	    this.app.call('updateObjectPanel', this, object);
	};

	/**
	 * 更新物体缩放x
	 */
	ObjectPanelEvent.prototype.updateScaleX = function () {
	    var editor = this.app.editor;
	    var object = editor.selected;
	    var objectScaleLock = UI$1.get('objectScaleLock');
	    var objectScaleX = UI$1.get('objectScaleX');
	    var objectScaleY = UI$1.get('objectScaleY');
	    var objectScaleZ = UI$1.get('objectScaleZ');

	    if (objectScaleLock.getValue() === true) {
	        var scale = objectScaleX.getValue() / object.scale.x;
	        objectScaleY.setValue(objectScaleY.getValue() * scale);
	        objectScaleZ.setValue(objectScaleZ.getValue() * scale);
	    }

	    this.app.call('updateObject', this);
	};

	/**
	 * 更新物体缩放y
	 */
	ObjectPanelEvent.prototype.updateScaleY = function () {
	    var editor = this.app.editor;
	    var object = editor.selected;
	    var objectScaleLock = UI$1.get('objectScaleLock');
	    var objectScaleX = UI$1.get('objectScaleX');
	    var objectScaleY = UI$1.get('objectScaleY');
	    var objectScaleZ = UI$1.get('objectScaleZ');

	    if (objectScaleLock.getValue() === true) {
	        var scale = objectScaleY.getValue() / object.scale.y;

	        objectScaleX.setValue(objectScaleX.getValue() * scale);
	        objectScaleZ.setValue(objectScaleZ.getValue() * scale);
	    }

	    this.app.call('updateObject', this);
	};

	/**
	 * 更新物体缩放z
	 */
	ObjectPanelEvent.prototype.updateScaleZ = function () {
	    var editor = this.app.editor;
	    var object = editor.selected;
	    var objectScaleLock = UI$1.get('objectScaleLock');
	    var objectScaleX = UI$1.get('objectScaleX');
	    var objectScaleY = UI$1.get('objectScaleY');
	    var objectScaleZ = UI$1.get('objectScaleZ');

	    if (objectScaleLock.getValue() === true) {
	        var scale = objectScaleZ.getValue() / object.scale.z;

	        objectScaleX.setValue(objectScaleX.getValue() * scale);
	        objectScaleY.setValue(objectScaleY.getValue() * scale);
	    }

	    this.app.call('updateObject', this);
	};

	/**
	 * 更新物体面板更新物体属性
	 */
	ObjectPanelEvent.prototype.update = function () {
	    var editor = this.app.editor;
	    var object = editor.selected;

	    var objectPositionX = UI$1.get('objectPositionX');
	    var objectPositionY = UI$1.get('objectPositionY');
	    var objectPositionZ = UI$1.get('objectPositionZ');
	    var objectRotationX = UI$1.get('objectRotationX');
	    var objectRotationY = UI$1.get('objectRotationY');
	    var objectRotationZ = UI$1.get('objectRotationZ');
	    var objectScaleX = UI$1.get('objectScaleX');
	    var objectScaleY = UI$1.get('objectScaleY');
	    var objectScaleZ = UI$1.get('objectScaleZ');
	    var objectFov = UI$1.get('objectFov');
	    var objectNear = UI$1.get('objectNear');
	    var objectFar = UI$1.get('objectFar');
	    var objectIntensity = UI$1.get('objectIntensity');
	    var objectColor = UI$1.get('objectColor');
	    var objectGroundColor = UI$1.get('objectGroundColor');
	    var objectDistance = UI$1.get('objectDistance');
	    var objectAngle = UI$1.get('objectAngle');
	    var objectPenumbra = UI$1.get('objectPenumbra');
	    var objectDecay = UI$1.get('objectDecay');
	    var objectVisible = UI$1.get('objectVisible');
	    var objectCastShadow = UI$1.get('objectCastShadow');
	    var objectReceiveShadow = UI$1.get('objectReceiveShadow');
	    var objectShadowRadius = UI$1.get('objectShadowRadius');
	    var objectUserData = UI$1.get('objectUserData');

	    if (object !== null) {
	        var newPosition = new THREE.Vector3(objectPositionX.getValue(), objectPositionY.getValue(), objectPositionZ.getValue());

	        if (object.position.distanceTo(newPosition) >= 0.01) {
	            editor.execute(new SetPositionCommand(object, newPosition));
	        }

	        var newRotation = new THREE.Euler(
	            objectRotationX.getValue() * THREE.Math.DEG2RAD,
	            objectRotationY.getValue() * THREE.Math.DEG2RAD,
	            objectRotationZ.getValue() * THREE.Math.DEG2RAD);
	        if (object.rotation.toVector3().distanceTo(newRotation.toVector3()) >= 0.01) {
	            editor.execute(new SetRotationCommand(object, newRotation));
	        }

	        var newScale = new THREE.Vector3(objectScaleX.getValue(), objectScaleY.getValue(), objectScaleZ.getValue());
	        if (object.scale.distanceTo(newScale) >= 0.01) {
	            editor.execute(new SetScaleCommand(object, newScale));
	        }

	        if (object.fov !== undefined && Math.abs(object.fov - objectFov.getValue()) >= 0.01) {
	            editor.execute(new SetValueCommand(object, 'fov', objectFov.getValue()));
	            object.updateProjectionMatrix();
	        }

	        if (object.near !== undefined && Math.abs(object.near - objectNear.getValue()) >= 0.01) {
	            editor.execute(new SetValueCommand(object, 'near', objectNear.getValue()));
	        }

	        if (object.far !== undefined && Math.abs(object.far - objectFar.getValue()) >= 0.01) {
	            editor.execute(new SetValueCommand(object, 'far', objectFar.getValue()));
	        }

	        if (object.intensity !== undefined && Math.abs(object.intensity - objectIntensity.getValue()) >= 0.01) {
	            editor.execute(new SetValueCommand(object, 'intensity', objectIntensity.getValue()));
	        }

	        if (object.color !== undefined && object.color.getHex() !== objectColor.getHexValue()) {
	            editor.execute(new SetColorCommand(object, 'color', objectColor.getHexValue()));
	        }

	        if (object.groundColor !== undefined && object.groundColor.getHex() !== objectGroundColor.getHexValue()) {
	            editor.execute(new SetColorCommand(object, 'groundColor', objectGroundColor.getHexValue()));
	        }

	        if (object.distance !== undefined && Math.abs(object.distance - objectDistance.getValue()) >= 0.01) {
	            editor.execute(new SetValueCommand(object, 'distance', objectDistance.getValue()));
	        }

	        if (object.angle !== undefined && Math.abs(object.angle - objectAngle.getValue()) >= 0.01) {
	            editor.execute(new SetValueCommand(object, 'angle', objectAngle.getValue()));
	        }

	        if (object.penumbra !== undefined && Math.abs(object.penumbra - objectPenumbra.getValue()) >= 0.01) {
	            editor.execute(new SetValueCommand(object, 'penumbra', objectPenumbra.getValue()));
	        }

	        if (object.decay !== undefined && Math.abs(object.decay - objectDecay.getValue()) >= 0.01) {
	            editor.execute(new SetValueCommand(object, 'decay', objectDecay.getValue()));
	        }

	        if (object.visible !== objectVisible.getValue()) {
	            editor.execute(new SetValueCommand(object, 'visible', objectVisible.getValue()));
	        }

	        if (object.castShadow !== undefined && object.castShadow !== objectCastShadow.getValue()) {
	            editor.execute(new SetValueCommand(object, 'castShadow', objectCastShadow.getValue()));
	        }

	        if (object.receiveShadow !== undefined && object.receiveShadow !== objectReceiveShadow.getValue()) {
	            editor.execute(new SetValueCommand(object, 'receiveShadow', objectReceiveShadow.getValue()));
	            object.material.needsUpdate = true;
	        }

	        if (object.shadow !== undefined) {
	            if (object.shadow.radius !== objectShadowRadius.getValue()) {
	                editor.execute(new SetValueCommand(object.shadow, 'radius', objectShadowRadius.getValue()));
	            }
	        }

	        try {
	            var userData = JSON.parse(objectUserData.getValue());

	            if (JSON.stringify(object.userData) != JSON.stringify(userData)) {
	                editor.execute(new SetValueCommand(object, 'userData', userData));
	            }
	        } catch (exception) {
	            console.warn(exception);
	        }
	    }
	};

	/**
	 * 控制每行显示隐藏
	 * @param {*} object 
	 */
	ObjectPanelEvent.prototype.updateRows = function (object) {
	    var objectFovRow = UI$1.get('objectFovRow');
	    var objectNearRow = UI$1.get('objectNearRow');
	    var objectFarRow = UI$1.get('objectFarRow');
	    var objectIntensityRow = UI$1.get('objectIntensityRow');
	    var objectColorRow = UI$1.get('objectColorRow');
	    var objectGroundColorRow = UI$1.get('objectGroundColorRow');
	    var objectDistanceRow = UI$1.get('objectDistanceRow');
	    var objectAngleRow = UI$1.get('objectAngleRow');
	    var objectPenumbraRow = UI$1.get('objectPenumbraRow');
	    var objectDecayRow = UI$1.get('objectDecayRow');
	    var objectShadowRow = UI$1.get('objectShadowRow');
	    var objectReceiveShadow = UI$1.get('objectReceiveShadow');
	    var objectShadowRadius = UI$1.get('objectShadowRadius');

	    var properties = {
	        'fov': objectFovRow,
	        'near': objectNearRow,
	        'far': objectFarRow,
	        'intensity': objectIntensityRow,
	        'color': objectColorRow,
	        'groundColor': objectGroundColorRow,
	        'distance': objectDistanceRow,
	        'angle': objectAngleRow,
	        'penumbra': objectPenumbraRow,
	        'decay': objectDecayRow,
	        'castShadow': objectShadowRow,
	        'receiveShadow': objectReceiveShadow,
	        'shadow': objectShadowRadius
	    };

	    for (var property in properties) {
	        properties[property].dom.style.display = object[property] !== undefined ? '' : 'none';
	    }
	};

	/**
	 * 更新平移旋转缩放面板
	 * @param {*} object 
	 */
	ObjectPanelEvent.prototype.updateTransformRows = function (object) {
	    var objectRotationRow = UI$1.get('objectRotationRow');
	    var objectScaleRow = UI$1.get('objectScaleRow');

	    if (object instanceof THREE.Light || (object instanceof THREE.Object3D && object.userData.targetInverse)) {
	        objectRotationRow.dom.style.display = 'none';
	        objectScaleRow.dom.style.display = 'none';
	    } else {
	        objectRotationRow.dom.style.display = '';
	        objectScaleRow.dom.style.display = '';
	    }
	};

	/**
	 * 刷新物体面板ui
	 * @param {*} object 
	 */
	ObjectPanelEvent.prototype.updateUI = function (object) {
	    var objectType = UI$1.get('objectType');
	    var objectUUID = UI$1.get('objectUUID');
	    var objectName = UI$1.get('objectName');
	    var objectPositionX = UI$1.get('objectPositionX');
	    var objectPositionY = UI$1.get('objectPositionY');
	    var objectPositionZ = UI$1.get('objectPositionZ');
	    var objectRotationX = UI$1.get('objectRotationX');
	    var objectRotationY = UI$1.get('objectRotationY');
	    var objectRotationZ = UI$1.get('objectRotationZ');
	    var objectScaleX = UI$1.get('objectScaleX');
	    var objectScaleY = UI$1.get('objectScaleY');
	    var objectScaleZ = UI$1.get('objectScaleZ');
	    var objectFov = UI$1.get('objectFov');
	    var objectNear = UI$1.get('objectNear');
	    var objectFar = UI$1.get('objectFar');
	    var objectIntensity = UI$1.get('objectIntensity');
	    var objectColor = UI$1.get('objectColor');
	    var objectGroundColor = UI$1.get('objectGroundColor');
	    var objectDistance = UI$1.get('objectDistance');
	    var objectAngle = UI$1.get('objectAngle');
	    var objectPenumbra = UI$1.get('objectPenumbra');
	    var objectDecay = UI$1.get('objectDecay');
	    var objectVisible = UI$1.get('objectVisible');
	    var objectCastShadow = UI$1.get('objectCastShadow');
	    var objectReceiveShadow = UI$1.get('objectReceiveShadow');
	    var objectShadowRadius = UI$1.get('objectShadowRadius');
	    var objectVisible = UI$1.get('objectVisible');
	    var objectUserData = UI$1.get('objectUserData');

	    objectType.setValue(object.type);

	    objectUUID.setValue(object.uuid);
	    objectName.setValue(object.name);

	    objectPositionX.setValue(object.position.x);
	    objectPositionY.setValue(object.position.y);
	    objectPositionZ.setValue(object.position.z);

	    objectRotationX.setValue(object.rotation.x * THREE.Math.RAD2DEG);
	    objectRotationY.setValue(object.rotation.y * THREE.Math.RAD2DEG);
	    objectRotationZ.setValue(object.rotation.z * THREE.Math.RAD2DEG);

	    objectScaleX.setValue(object.scale.x);
	    objectScaleY.setValue(object.scale.y);
	    objectScaleZ.setValue(object.scale.z);

	    if (object.fov !== undefined) {
	        objectFov.setValue(object.fov);
	    }

	    if (object.near !== undefined) {
	        objectNear.setValue(object.near);
	    }

	    if (object.far !== undefined) {
	        objectFar.setValue(object.far);
	    }

	    if (object.intensity !== undefined) {
	        objectIntensity.setValue(object.intensity);
	    }

	    if (object.color !== undefined) {
	        objectColor.setHexValue(object.color.getHexString());
	    }

	    if (object.groundColor !== undefined) {
	        objectGroundColor.setHexValue(object.groundColor.getHexString());
	    }

	    if (object.distance !== undefined) {
	        objectDistance.setValue(object.distance);
	    }

	    if (object.angle !== undefined) {
	        objectAngle.setValue(object.angle);
	    }

	    if (object.penumbra !== undefined) {
	        objectPenumbra.setValue(object.penumbra);
	    }

	    if (object.decay !== undefined) {
	        objectDecay.setValue(object.decay);
	    }

	    if (object.castShadow !== undefined) {
	        objectCastShadow.setValue(object.castShadow);
	    }

	    if (object.receiveShadow !== undefined) {
	        objectReceiveShadow.setValue(object.receiveShadow);
	    }

	    if (object.shadow !== undefined) {
	        objectShadowRadius.setValue(object.shadow.radius);
	    }

	    objectVisible.setValue(object.visible);

	    try {
	        objectUserData.setValue(JSON.stringify(object.userData, null, '  '));
	    } catch (error) {
	        console.log(error);
	    }

	    objectUserData.dom.style.borderColor = 'transparent';
	    objectUserData.dom.style.backgroundColor = '';

	    this.updateTransformRows(object);
	};

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param newGeometry THREE.Geometry
	 * @constructor
	 */

	function SetGeometryCommand(object, newGeometry) {

		Command.call(this);

		this.type = 'SetGeometryCommand';
		this.name = 'Set Geometry';
		this.updatable = true;

		this.object = object;
		this.oldGeometry = (object !== undefined) ? object.geometry : undefined;
		this.newGeometry = newGeometry;

	}
	SetGeometryCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetGeometryCommand.prototype, {

		constructor: SetGeometryCommand,

		execute: function () {

			this.object.geometry.dispose();
			this.object.geometry = this.newGeometry;
			this.object.geometry.computeBoundingSphere();

			this.editor.app.call('geometryChanged', this, this.object);
			this.editor.app.call('sceneGraphChanged', this);

		},

		undo: function () {

			this.object.geometry.dispose();
			this.object.geometry = this.oldGeometry;
			this.object.geometry.computeBoundingSphere();

			this.editor.app.call('geometryChanged', this, this.object);
			this.editor.app.call('sceneGraphChanged', this);

		},

		update: function (cmd) {

			this.newGeometry = cmd.newGeometry;

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.oldGeometry = this.object.geometry.toJSON();
			output.newGeometry = this.newGeometry.toJSON();

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.object = this.editor.objectByUuid(json.objectUuid);

			this.oldGeometry = parseGeometry(json.oldGeometry);
			this.newGeometry = parseGeometry(json.newGeometry);

			function parseGeometry(data) {

				var loader = new THREE.ObjectLoader();
				return loader.parseGeometries([data])[data.uuid];

			}

		}

	});

	/**
	 * 正方体几何体
	 * @author mrdoob / http://mrdoob.com/
	 */
	function BoxGeometryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	    this.object = options.object;
	}
	BoxGeometryPanel.prototype = Object.create(UI$1.Control.prototype);
	BoxGeometryPanel.prototype.constructor = BoxGeometryPanel;

	BoxGeometryPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var object = this.object;
	    var geometry = object.geometry;
	    var parameters = geometry.parameters;

	    var update = function () {
	        var boxWidth = UI$1.get('boxWidth');
	        var boxHeight = UI$1.get('boxHeight');
	        var boxDepth = UI$1.get('boxDepth');
	        var boxWidthSegments = UI$1.get('boxWidthSegments');
	        var boxHeightSegments = UI$1.get('boxHeightSegments');
	        var boxDepthSegments = UI$1.get('boxDepthSegments');

	        editor.execute(new SetGeometryCommand(object, new THREE[geometry.type](
	            boxWidth.getValue(),
	            boxHeight.getValue(),
	            boxDepth.getValue(),
	            boxWidthSegments.getValue(),
	            boxHeightSegments.getValue(),
	            boxDepthSegments.getValue()
	        )));
	    };

	    this.children = [{
	        xtype: 'row',
	        id: 'boxGeometryPanel',
	        parent: this.parent,
	        children: [{ // width
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '宽度'
	            }, {
	                xtype: 'number',
	                id: 'boxWidth',
	                value: parameters.width,
	                onChange: update
	            }]
	        }, { // height
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '高度'
	            }, {
	                xtype: 'number',
	                id: 'boxHeight',
	                value: parameters.height,
	                onChange: update
	            }]
	        }, { // depth
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '深度'
	            }, {
	                xtype: 'number',
	                id: 'boxDepth',
	                value: parameters.depth,
	                onChange: update
	            }]
	        }, { // widthSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '宽度段数'
	            }, {
	                xtype: 'int',
	                id: 'boxWidthSegments',
	                value: parameters.widthSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // heightSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '高度段数'
	            }, {
	                xtype: 'int',
	                id: 'boxHeightSegments',
	                value: parameters.heightSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // depthSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '深度段数'
	            }, {
	                xtype: 'int',
	                id: 'boxDepthSegments',
	                value: parameters.depthSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }]
	    }];

	    var control = UI$1.create(this.children[0]);
	    control.render();
	};

	/**
	 * 圆形几何体面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function CircleGeometryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	    this.object = options.object;
	}
	CircleGeometryPanel.prototype = Object.create(UI$1.Control.prototype);
	CircleGeometryPanel.prototype.constructor = CircleGeometryPanel;

	CircleGeometryPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var object = this.object;
	    var geometry = object.geometry;
	    var parameters = geometry.parameters;

	    function update() {
	        var radius = UI$1.get('circleGeometryRadius');
	        var segments = UI$1.get('circleGeometrySegments');
	        var thetaStart = UI$1.get('circleGeometryThetaStart');
	        var thetaLength = UI$1.get('circleGeometryThetaLength');

	        editor.execute(new SetGeometryCommand(object, new THREE[geometry.type](
	            radius.getValue(),
	            segments.getValue(),
	            thetaStart.getValue(),
	            thetaLength.getValue()
	        )));

	    }

	    this.children = [{
	        xtype: 'row',
	        parent: this.parent,
	        children: [{ // radius
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '半径'
	            }, {
	                xtype: 'number',
	                id: 'circleGeometryRadius',
	                value: parameters.radius,
	                onChange: update
	            }]
	        }, { // segments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '段数'
	            }, {
	                xtype: 'int',
	                id: 'circleGeometrySegments',
	                value: parameters.segments,
	                range: [3, Infinity],
	                onChange: update
	            }]
	        }, { // thetaStart
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '开始弧度'
	            }, {
	                xtype: 'number',
	                id: 'circleGeometryThetaStart',
	                value: parameters.thetaStart,
	                onChange: update
	            }]
	        }, { // thetaLength
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '结束弧度'
	            }, {
	                xtype: 'number',
	                id: 'circleGeometryThetaLength',
	                value: parameters.thetaLength,
	                onChange: update,
	                value: Math.PI * 2
	            }]
	        }]
	    }];

	    var container = UI$1.create(this.children[0]);
	    container.render();
	};

	/**
	 * 圆柱体
	 * @author mrdoob / http://mrdoob.com/
	 */
	function CylinderGeometryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	    this.object = options.object;
	}
	CylinderGeometryPanel.prototype = Object.create(UI$1.Control.prototype);
	CylinderGeometryPanel.prototype.constructor = CylinderGeometryPanel;

	CylinderGeometryPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var object = this.object;
	    var geometry = object.geometry;
	    var parameters = geometry.parameters;

	    var update = function () {
	        var radiusTop = UI$1.get('cylinderGeometryRadiusTop');
	        var radiusBottom = UI$1.get('cylinderGeometryRadiusBottom');
	        var height = UI$1.get('cylinderGeometryHeight');
	        var radialSegments = UI$1.get('cylinderGeometryRadialSegments');
	        var heightSegments = UI$1.get('cylinderGeometryHeightSegments');
	        var openEnded = UI$1.get('cylinderGeometryOpenEnded');

	        editor.execute(new SetGeometryCommand(object, new THREE[geometry.type](
	            radiusTop.getValue(),
	            radiusBottom.getValue(),
	            height.getValue(),
	            radialSegments.getValue(),
	            heightSegments.getValue(),
	            openEnded.getValue()
	        )));
	    };

	    this.children = [{ // radiusTop
	        xtype: 'row',
	        parent: this.parent,
	        children: [{ // radiusTop
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '顶部半径'
	            }, {
	                xtype: 'number',
	                id: 'cylinderGeometryRadiusTop',
	                value: parameters.radiusTop,
	                onChange: update
	            }]
	        }, { // radiusBottom
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '底部半径'
	            }, {
	                xtype: 'number',
	                id: 'cylinderGeometryRadiusBottom',
	                value: parameters.radiusBottom,
	                onChange: update
	            }]
	        }, { // height
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '高度'
	            }, {
	                xtype: 'number',
	                id: 'cylinderGeometryHeight',
	                value: parameters.height,
	                onChange: update
	            }]
	        }, { // radialSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '两端段数'
	            }, {
	                xtype: 'int',
	                id: 'cylinderGeometryRadialSegments',
	                value: parameters.radialSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // heightSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '高度段数'
	            }, {
	                xtype: 'int',
	                id: 'cylinderGeometryHeightSegments',
	                value: parameters.heightSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // openEnded
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '两端开口'
	            }, {
	                xtype: 'checkbox',
	                id: 'cylinderGeometryOpenEnded',
	                value: parameters.openEnded,
	                onChange: update
	            }]
	        }]
	    }];

	    var container = UI$1.create(this.children[0]);
	    container.render();
	};

	/**
	 * 二十面体几何体面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function IcosahedronGeometryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	    this.object = options.object;
	}
	IcosahedronGeometryPanel.prototype = Object.create(UI$1.Control.prototype);
	IcosahedronGeometryPanel.prototype.constructor = IcosahedronGeometryPanel;

	IcosahedronGeometryPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var object = this.object;
	    var geometry = object.geometry;
	    var parameters = geometry.parameters;
	    var _this = this;

	    var update = function () {
	        var radius = UI$1.get('icosahedronGeometryRadius');
	        var detail = UI$1.get('icosahedronGeometryDetail');

	        editor.execute(new SetGeometryCommand(object, new THREE[geometry.type](
	            radius.getValue(),
	            detail.getValue()
	        )));

	        _this.app.call('objectChanged', _this, object);
	    };

	    this.children = [{
	        xtype: 'row',
	        parent: this.parent,
	        children: [{ // radius
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '半径'
	            }, {
	                xtype: 'number',
	                id: 'icosahedronGeometryRadius',
	                value: parameters.radius,
	                onChange: update
	            }]
	        }, { // detail
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '面片段数'
	            }, {
	                xtype: 'int',
	                id: 'icosahedronGeometryDetail',
	                value: parameters.detail,
	                range: [0, Infinity],
	                onChange: update
	            }]
	        }]
	    }];

	    var container = UI$1.create(this.children[0]);
	    container.render();
	};

	/**
	 * 车床几何体面板
	 * @author rfm1201
	 */
	function LatheGeometryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	    this.object = options.object;
	}
	LatheGeometryPanel.prototype = Object.create(UI$1.Control.prototype);
	LatheGeometryPanel.prototype.constructor = LatheGeometryPanel;

	LatheGeometryPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var object = this.object;
	    var geometry = object.geometry;
	    var parameters = geometry.parameters;
	    var pointsUI = [];

	    this.children = [{
	        xtype: 'row',
	        parent: this.parent,
	        children: [{ // segments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '径向段数'
	            }, {
	                xtype: 'int',
	                id: 'latheGeometrySegments',
	                value: parameters.segments,
	                onChange: update
	            }]
	        }, { // phiStart
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '开始角度'
	            }, {
	                xtype: 'number',
	                id: 'latheGeometryPhiStart',
	                value: parameters.phiStart * 180 / Math.PI,
	                onChange: update
	            }]
	        }, { // phiLength
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '结束角度'
	            }, {
	                xtype: 'number',
	                id: 'latheGeometryPhiLength',
	                value: parameters.phiLength * 180 / Math.PI,
	                onChange: update
	            }]
	        }, { // points
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '点'
	            }, {
	                xtype: 'span',
	                id: 'latheGeometryPoints',
	                style: {
	                    display: 'inline-block'
	                },
	                children: [{
	                    xtype: 'div',
	                    id: 'latheGeometryPointsList',
	                }]
	            }]
	        }]
	    }];

	    var container = UI$1.create(this.children[0]);
	    container.render();

	    var points = UI$1.get('latheGeometryPoints');
	    var pointsList = UI$1.get('latheGeometryPointsList');

	    var addPointButton = UI$1.create({
	        xtype: 'button',
	        text: '+',
	        onClick: function () {
	            if (pointsUI.length === 0) {
	                pointsList.add(this.createPointRow(0, 0));
	            } else {
	                var point = pointsUI[pointsUI.length - 1];
	                pointsList.add(this.createPointRow(point.x.getValue(), point.y.getValue()));
	            }

	            this.update();
	        }
	    });

	    var update = function () {
	        var points = [];
	        var count = 0;

	        for (var i = 0; i < pointsUI.length; i++) {

	            var pointUI = pointsUI[i];

	            if (!pointUI) continue;

	            points.push(new THREE.Vector2(pointUI.x.getValue(), pointUI.y.getValue()));
	            count++;
	            pointUI.lbl.setValue(count);

	        }

	        editor.execute(new SetGeometryCommand(object, new THREE[geometry.type](
	            points,
	            segments.getValue(),
	            phiStart.getValue() / 180 * Math.PI,
	            phiLength.getValue() / 180 * Math.PI
	        )));
	    };
	};

	/**
	 * 平板几何体面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function PlaneGeometryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	    this.object = options.object;
	}
	PlaneGeometryPanel.prototype = Object.create(UI$1.Control.prototype);
	PlaneGeometryPanel.prototype.constructor = PlaneGeometryPanel;

	PlaneGeometryPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var object = this.object;
	    var geometry = object.geometry;
	    var parameters = geometry.parameters;

	    var update = function () {
	        var width = UI$1.get('planeGeometryWidth');
	        var height = UI$1.get('planeGeometryHeight');
	        var widthSegments = UI$1.get('planeGeometryWidthSegments');
	        var heightSegments = UI$1.get('planeGeometryHeightSegments');

	        editor.execute(new SetGeometryCommand(object, new THREE[geometry.type](
	            width.getValue(),
	            height.getValue(),
	            widthSegments.getValue(),
	            heightSegments.getValue()
	        )));
	    };

	    this.children = [{
	        xtype: 'row',
	        parent: this.parent,
	        children: [{ // width
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '宽度'
	            }, {
	                xtype: 'number',
	                id: 'planeGeometryWidth',
	                value: parameters.width,
	                onChange: update
	            }]
	        }, { // height
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '高度'
	            }, {
	                xtype: 'number',
	                id: 'planeGeometryHeight',
	                value: parameters.height,
	                onChange: update
	            }]
	        }, { // widthSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '宽度段数'
	            }, {
	                xtype: 'int',
	                id: 'planeGeometryWidthSegments',
	                value: parameters.widthSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // heightSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '高度段数'
	            }, {
	                xtype: 'int',
	                id: 'planeGeometryHeightSegments',
	                value: parameters.heightSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }]
	    }];

	    var container = UI$1.create(this.children[0]);
	    container.render();
	};

	/**
	 * 球形几何体面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function SphereGeometryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	    this.object = options.object;
	}
	SphereGeometryPanel.prototype = Object.create(UI$1.Control.prototype);
	SphereGeometryPanel.prototype.constructor = SphereGeometryPanel;

	SphereGeometryPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var object = this.object;
	    var geometry = object.geometry;
	    var parameters = geometry.parameters;

	    var update = function () {
	        var radius = UI$1.get('sphereGeometryRadius');
	        var widthSegments = UI$1.get('sphereGeometryWidthSegments');
	        var heightSegments = UI$1.get('sphereGeometryHeightSegments');
	        var phiStart = UI$1.get('sphereGeometryPhiStart');
	        var phiLength = UI$1.get('sphereGeometryPhiLength');
	        var thetaStart = UI$1.get('sphereGeometryThetaStart');
	        var thetaLength = UI$1.get('sphereGeometryThetaLength');

	        editor.execute(new SetGeometryCommand(object, new THREE[geometry.type](
	            radius.getValue(),
	            widthSegments.getValue(),
	            heightSegments.getValue(),
	            phiStart.getValue(),
	            phiLength.getValue(),
	            thetaStart.getValue(),
	            thetaLength.getValue()
	        )));
	    };

	    this.children = [{
	        xtype: 'row',
	        parent: this.parent,
	        children: [{ // radius
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '半径'
	            }, {
	                xtype: 'number',
	                id: 'sphereGeometryRadius',
	                value: parameters.radius,
	                onChange: update
	            }]
	        }, { // widthSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '宽度段数'
	            }, {
	                xtype: 'int',
	                id: 'sphereGeometryWidthSegments',
	                value: parameters.widthSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // heightSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '高度段数'
	            }, {
	                xtype: 'int',
	                id: 'sphereGeometryHeightSegments',
	                value: parameters.heightSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // phiStart
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '开始经度'
	            }, {
	                xtype: 'number',
	                id: 'sphereGeometryPhiStart',
	                value: parameters.phiStart,
	                onChange: update
	            }]
	        }, { // phiLength
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '结束经度'
	            }, {
	                xtype: 'number',
	                id: 'sphereGeometryPhiLength',
	                value: parameters.phiLength,
	                onChange: update
	            }]
	        }, { // thetaStart
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '开始纬度'
	            }, {
	                xtype: 'number',
	                id: 'sphereGeometryThetaStart',
	                value: parameters.thetaStart,
	                onChange: update
	            }]
	        }, { // thetaLength
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '结束纬度'
	            }, {
	                xtype: 'number',
	                id: 'sphereGeometryThetaLength',
	                value: parameters.thetaLength,
	                onChange: update
	            }]
	        }]
	    }];

	    var container = UI$1.create(this.children[0]);
	    container.render();
	};

	/**
	 * 花托几何体面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function TorusGeometryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	    this.object = options.object;
	}
	TorusGeometryPanel.prototype = Object.create(UI$1.Control.prototype);
	TorusGeometryPanel.prototype.constructor = TorusGeometryPanel;

	TorusGeometryPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var object = this.object;
	    var geometry = object.geometry;
	    var parameters = geometry.parameters;

	    var update = function () {
	        var radius = UI$1.get('torusGeometryRadius');
	        var tube = UI$1.get('torusGeometryTube');
	        var radialSegments = UI$1.get('torusGeometryRadialSegments');
	        var tubularSegments = UI$1.get('torusGeometryTubularSegments');
	        var arc = UI$1.get('torusGeometryTubularArc');

	        editor.execute(new SetGeometryCommand(object, new THREE[geometry.type](
	            radius.getValue(),
	            tube.getValue(),
	            radialSegments.getValue(),
	            tubularSegments.getValue(),
	            arc.getValue()
	        )));
	    };

	    this.children = [{
	        xtype: 'row',
	        parent: this.parent,
	        children: [{
	            xtype: 'row',
	            children: [{ // radius
	                xtype: 'label',
	                text: '半径'
	            }, {
	                xtype: 'number',
	                id: 'torusGeometryRadius',
	                value: parameters.radius,
	                onChange: update
	            }]
	        }, { // tube
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '管粗'
	            }, {
	                xtype: 'number',
	                id: 'torusGeometryTube',
	                value: parameters.tube,
	                onChange: update
	            }]
	        }, { // radialSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '管粗段数'
	            }, {
	                xtype: 'int',
	                id: 'torusGeometryRadialSegments',
	                value: parameters.radialSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // tubularSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '半径段数'
	            }, {
	                xtype: 'int',
	                id: 'torusGeometryTubularSegments',
	                value: parameters.tubularSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // radialSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '半径段数'
	            }, {
	                xtype: 'int',
	                id: 'torusGeometryRadialSegments',
	                value: parameters.tubularSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // arc
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '旋转弧度'
	            }, {
	                xtype: 'number',
	                id: 'torusGeometryTubularArc',
	                value: parameters.arc,
	                onChange: update
	            }]
	        }]
	    }];

	    var container = UI$1.create(this.children[0]);
	    container.render();
	};

	/**
	 * 环面纽结几何体面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function TorusKnotGeometryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	    this.object = options.object;
	}
	TorusKnotGeometryPanel.prototype = Object.create(UI$1.Control.prototype);
	TorusKnotGeometryPanel.prototype.constructor = TorusKnotGeometryPanel;

	TorusKnotGeometryPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var object = this.object;
	    var geometry = object.geometry;
	    var parameters = geometry.parameters;

	    var update = function () {
	        var radius = UI$1.get('torusKnotGeometryRadius');
	        var tube = UI$1.get('torusKnotGeometryTube');
	        var tubularSegments = UI$1.get('torusKnotGeometryTubularSegments');
	        var radialSegments = UI$1.get('torusKnotGeometryRadialSegments');
	        var p = UI$1.get('torusKnotGeometryP');
	        var q = UI$1.get('torusKnotGeometryQ');

	        editor.execute(new SetGeometryCommand(object, new THREE[geometry.type](
	            radius.getValue(),
	            tube.getValue(),
	            tubularSegments.getValue(),
	            radialSegments.getValue(),
	            p.getValue(),
	            q.getValue()
	        )));
	    };

	    this.children = [{
	        xtype: 'row',
	        parent: this.parent,
	        children: [{ // radius
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '半径'
	            }, {
	                xtype: 'number',
	                id: 'torusKnotGeometryRadius',
	                value: parameters.radius,
	                onChange: update
	            }]
	        }, { // tube
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '管粗'
	            }, {
	                xtype: 'number',
	                id: 'torusKnotGeometryTube',
	                value: parameters.tube,
	                onChange: update
	            }]
	        }, { // tubularSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '管长段数'
	            }, {
	                xtype: 'int',
	                id: 'torusKnotGeometryTubularSegments',
	                value: parameters.tubularSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // radialSegments
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '管粗段数'
	            }, {
	                xtype: 'int',
	                id: 'torusKnotGeometryRadialSegments',
	                value: parameters.radialSegments,
	                range: [1, Infinity],
	                onChange: update
	            }]
	        }, { // p
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '管长弧度'
	            }, {
	                xtype: 'number',
	                id: 'torusKnotGeometryP',
	                value: parameters.p,
	                onChange: update
	            }]
	        }, { // q
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '扭曲弧度'
	            }, {
	                xtype: 'number',
	                id: 'torusKnotGeometryQ',
	                value: parameters.q,
	                onChange: update
	            }]
	        }]
	    }];

	    var container = UI$1.create(this.children[0]);
	    container.render();
	};

	const GeometryPanels = {
	    'BoxGeometry': BoxGeometryPanel,
	    'BoxBufferGeometry': BoxGeometryPanel,
	    'CircleGeometry': CircleGeometryPanel,
	    'CircleBufferGeometry': CircleGeometryPanel,
	    'CylinderGeometry': CylinderGeometryPanel,
	    'CylinderBufferGeometry': CylinderGeometryPanel,
	    'IcosahedronGeometry': IcosahedronGeometryPanel,
	    'IcosahedronBufferGeometry': IcosahedronGeometryPanel,
	    'LatheGeometry': LatheGeometryPanel,
	    'LatheBufferGeometry': LatheGeometryPanel,
	    'PlaneGeometry': PlaneGeometryPanel,
	    'PlaneBufferGeometry': PlaneGeometryPanel,
	    'SphereGeometry': SphereGeometryPanel,
	    'SphereBufferGeometry': SphereGeometryPanel,
	    'TorusGeometry': TorusGeometryPanel,
	    'TorusBufferGeometry': TorusGeometryPanel,
	    'TorusKnotGeometry': TorusKnotGeometryPanel,
	    'TorusKnotBufferGeometry': TorusKnotGeometryPanel
	};

	/**
	 * 物体面板事件
	 * @param {*} app 
	 */
	function GeometryPanelEvent(app) {
	    BaseEvent.call(this, app);

	    this.tabName = '物体';
	    this.typedGeometryPanel = null;
	}

	GeometryPanelEvent.prototype = Object.create(BaseEvent.prototype);
	GeometryPanelEvent.prototype.constructor = GeometryPanelEvent;

	GeometryPanelEvent.prototype.start = function () {
	    this.app.on(`selectPropertyTab.${this.id}`, this.onSelectPropertyTab.bind(this));
	    this.app.on(`objectSelected.${this.id}`, this.update.bind(this));
	    this.app.on(`geometryChanged.${this.id}`, this.update.bind(this));
	};

	GeometryPanelEvent.prototype.stop = function () {
	    this.app.on(`selectPropertyTab.${this.id}`, null);
	    this.app.on(`objectSelected.${this.id}`, null);
	    this.app.on(`geometryChanged.${this.id}`, null);
	};

	/**
	 * 选择几何体选项卡
	 * @param {*} tabName 
	 */
	GeometryPanelEvent.prototype.onSelectPropertyTab = function (tabName) {
	    this.tabName = tabName;
	    if (this.app.editor.selected != null && tabName === '几何') {
	        UI$1.get('geometryPanel').dom.style.display = '';
	    } else {
	        UI$1.get('geometryPanel').dom.style.display = 'none';
	    }
	};

	GeometryPanelEvent.prototype.update = function () {
	    if (this.app.editor.selected != null && this.tabName === '几何') {
	        UI$1.get('geometryPanel').dom.style.display = '';
	    } else {
	        UI$1.get('geometryPanel').dom.style.display = 'none';
	    }

	    var editor = this.app.editor;
	    var geometryType = UI$1.get('geometryType');
	    var geometryUUID = UI$1.get('geometryUUID');
	    var geometryName = UI$1.get('geometryName');
	    var parameters = UI$1.get('geometryParameters');

	    var object = editor.selected;

	    if (object == null || object.geometry == null) {
	        return;
	    }

	    var geometry = object.geometry;

	    geometryType.setValue(geometry.type);
	    geometryUUID.setValue(geometry.uuid);
	    geometryName.setValue(geometry.name);

	    parameters.clear();
	    parameters.render();

	    if (GeometryPanels[geometry.type] !== undefined) {
	        if (this.typedGeometryPanel) {
	            this.typedGeometryPanel.destroy();
	        }
	        this.typedGeometryPanel = new GeometryPanels[geometry.type]({ app: this.app, object: object, parent: parameters.dom });
	        this.typedGeometryPanel.render();
	    }
	};

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param newMaterial THREE.Material
	 * @constructor
	 */

	function SetMaterialCommand(object, newMaterial) {

		Command.call(this);

		this.type = 'SetMaterialCommand';
		this.name = 'New Material';

		this.object = object;
		this.oldMaterial = (object !== undefined) ? object.material : undefined;
		this.newMaterial = newMaterial;

	}
	SetMaterialCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetMaterialCommand.prototype, {

		constructor: SetMaterialCommand,

		execute: function () {
			this.object.material = this.newMaterial;
			this.editor.app.call('materialChanged', this, this.newMaterial);
		},

		undo: function () {
			this.object.material = this.oldMaterial;
			this.editor.app.call('materialChanged', this, this.oldMaterial);
		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.oldMaterial = this.oldMaterial.toJSON();
			output.newMaterial = this.newMaterial.toJSON();

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.object = this.editor.objectByUuid(json.objectUuid);
			this.oldMaterial = parseMaterial(json.oldMaterial);
			this.newMaterial = parseMaterial(json.newMaterial);


			function parseMaterial(json) {

				var loader = new THREE.ObjectLoader();
				var images = loader.parseImages(json.images);
				var textures = loader.parseTextures(json.textures, images);
				var materials = loader.parseMaterials([json], textures);
				return materials[json.uuid];

			}

		}

	});

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param attributeName string
	 * @param newValue integer representing a hex color value
	 * @constructor
	 */

	function SetMaterialColorCommand(object, attributeName, newValue) {

		Command.call(this);

		this.type = 'SetMaterialColorCommand';
		this.name = 'Set Material.' + attributeName;
		this.updatable = true;

		this.object = object;
		this.attributeName = attributeName;
		this.oldValue = (object !== undefined) ? this.object.material[this.attributeName].getHex() : undefined;
		this.newValue = newValue;

	}
	SetMaterialColorCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetMaterialColorCommand.prototype, {

		constructor: SetMaterialColorCommand,

		execute: function () {

			this.object.material[this.attributeName].setHex(this.newValue);
			this.editor.app.call('materialChanged', this, this.object.material);

		},

		undo: function () {

			this.object.material[this.attributeName].setHex(this.oldValue);
			this.editor.app.call('materialChanged', this, this.object.material);

		},

		update: function (cmd) {

			this.newValue = cmd.newValue;

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.attributeName = this.attributeName;
			output.oldValue = this.oldValue;
			output.newValue = this.newValue;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.object = this.editor.objectByUuid(json.objectUuid);
			this.attributeName = json.attributeName;
			this.oldValue = json.oldValue;
			this.newValue = json.newValue;

		}

	});

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param mapName string
	 * @param newMap THREE.Texture
	 * @constructor
	 */

	function SetMaterialMapCommand(object, mapName, newMap) {

		Command.call(this);
		this.type = 'SetMaterialMapCommand';
		this.name = 'Set Material.' + mapName;

		this.object = object;
		this.mapName = mapName;
		this.oldMap = (object !== undefined) ? object.material[mapName] : undefined;
		this.newMap = newMap;

	}
	SetMaterialMapCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetMaterialMapCommand.prototype, {

		constructor: SetMaterialMapCommand,

		execute: function () {

			this.object.material[this.mapName] = this.newMap;
			this.object.material.needsUpdate = true;
			this.editor.app.call('materialChanged', this, this.object.material);

		},

		undo: function () {

			this.object.material[this.mapName] = this.oldMap;
			this.object.material.needsUpdate = true;
			this.editor.app.call('materialChanged', this, this.object.material);

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.mapName = this.mapName;
			output.newMap = serializeMap(this.newMap);
			output.oldMap = serializeMap(this.oldMap);

			return output;

			// serializes a map (THREE.Texture)

			function serializeMap(map) {

				if (map === null || map === undefined) return null;

				var meta = {
					geometries: {},
					materials: {},
					textures: {},
					images: {}
				};

				var json = map.toJSON(meta);
				var images = extractFromCache(meta.images);
				if (images.length > 0) json.images = images;
				json.sourceFile = map.sourceFile;

				return json;

			}

			// Note: The function 'extractFromCache' is copied from Object3D.toJSON()

			// extract data from the cache hash
			// remove metadata on each item
			// and return as array
			function extractFromCache(cache) {

				var values = [];
				for (var key in cache) {

					var data = cache[key];
					delete data.metadata;
					values.push(data);

				}
				return values;

			}

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.object = this.editor.objectByUuid(json.objectUuid);
			this.mapName = json.mapName;
			this.oldMap = parseTexture(json.oldMap);
			this.newMap = parseTexture(json.newMap);

			function parseTexture(json) {

				var map = null;
				if (json !== null) {

					var loader = new THREE.ObjectLoader();
					var images = loader.parseImages(json.images);
					var textures = loader.parseTextures([json], images);
					map = textures[json.uuid];
					map.sourceFile = json.sourceFile;

				}
				return map;

			}

		}

	});

	/**
	 * 材质改变事件
	 * @param {*} app 
	 */
	function MaterialPanelEvent(app) {
	    BaseEvent.call(this, app);

	    this.currentObject = null;
	    this.copiedMaterial = null;
	    this.tabName = '物体';
	}

	MaterialPanelEvent.prototype = Object.create(BaseEvent.prototype);
	MaterialPanelEvent.prototype.constructor = MaterialPanelEvent;

	MaterialPanelEvent.prototype.start = function () {
	    this.app.on(`selectPropertyTab.${this.id}`, this.onSelectPropertyTab.bind(this));
	    this.app.on(`objectSelected.${this.id}`, this.onObjectSelected.bind(this));
	    this.app.on(`materialChanged.${this.id}`, this.onMaterialChanged.bind(this));
	    this.app.on(`newMaterial.${this.id}`, this.onNewMaterial.bind(this));
	    this.app.on(`copyMaterial.${this.id}`, this.onCopyMaterial.bind(this));
	    this.app.on(`pasteMaterial.${this.id}`, this.onPasteMaterial.bind(this));
	    this.app.on(`updateMaterial.${this.id}`, this.update.bind(this));
	    this.app.on(`updateMaterialPanel.${this.id}`, this.refreshUI.bind(this));
	};

	MaterialPanelEvent.prototype.stop = function () {
	    this.app.on(`selectPropertyTab.${this.id}`, null);
	    this.app.on(`objectSelected.${this.id}`, null);
	    this.app.on(`materialChanged.${this.id}`, null);
	    this.app.on(`newMaterial.${this.id}`, null);
	    this.app.on(`copyMaterial.${this.id}`, null);
	    this.app.on(`pasteMaterial.${this.id}`, null);
	    this.app.on(`updateMaterial.${this.id}`, null);
	    this.app.on(`updateMaterialPanel.${this.id}`, null);
	};

	/**
	 * 选择材质选项卡
	 * @param {*} tabName 
	 */
	MaterialPanelEvent.prototype.onSelectPropertyTab = function (tabName) {
	    this.tabName = tabName;
	    if (this.app.editor.selected != null && tabName === '材质') {
	        UI$1.get('materialPanel').dom.style.display = '';
	    } else {
	        UI$1.get('materialPanel').dom.style.display = 'none';
	    }
	};

	/**
	 * 新建材质
	 */
	MaterialPanelEvent.prototype.onNewMaterial = function () {
	    var materialClass = UI$1.get('materialClass');
	    var editor = this.app.editor;

	    var material = new THREE[materialClass.getValue()]();
	    editor.execute(new SetMaterialCommand(this.currentObject, material), '新材质：' + materialClass.getValue());
	    this.app.call('updateMaterialPanelUI', this);
	};

	/**
	 * 复制材质
	 */
	MaterialPanelEvent.prototype.onCopyMaterial = function () {
	    this.copiedMaterial = this.currentObject.material;
	};

	/**
	 * 粘贴材质
	 */
	MaterialPanelEvent.prototype.onPasteMaterial = function () {
	    var materialClass = UI$1.get('materialClass');
	    var copiedMaterial = this.copiedMaterial;

	    if (copiedMaterial === undefined) return;
	    editor.execute(new SetMaterialCommand(this.currentObject, copiedMaterial), '粘贴材质：' + materialClass.getValue());
	    this.app.call('updateMaterialPanel', this);
	    this.app.call('updateMaterial', this);
	};

	/**
	 * 选中物体
	 * @param {*} object 
	 */
	MaterialPanelEvent.prototype.onObjectSelected = function (object) {
	    if (this.tabName === '材质' && object != null) {
	        UI$1.get('materialPanel').dom.style.display = '';
	    } else {
	        UI$1.get('materialPanel').dom.style.display = 'none';
	    }

	    if (object && object.material) {
	        var objectChanged = object !== this.currentObject;
	        this.currentObject = object;
	        this.refreshUI(objectChanged);
	    } else {
	        this.currentObject = null;
	    }
	};

	/**
	 * 材质改变
	 */
	MaterialPanelEvent.prototype.onMaterialChanged = function () {
	    this.refreshUI();
	};

	/**
	 * 判断材质面板哪些行应该显示
	 */
	MaterialPanelEvent.prototype.setRowVisibility = function () {
	    var materialNameRow = UI$1.get('materialNameRow');
	    var materialColorRow = UI$1.get('materialColorRow');
	    var materialRoughnessRow = UI$1.get('materialRoughnessRow');
	    var materialMetalnessRow = UI$1.get('materialMetalnessRow');
	    var materialEmissiveRow = UI$1.get('materialEmissiveRow');
	    var materialSpecularRow = UI$1.get('materialSpecularRow');
	    var materialShininessRow = UI$1.get('materialShininessRow');
	    var materialClearCoatRow = UI$1.get('materialClearCoatRow');
	    var materialClearCoatRoughnessRow = UI$1.get('materialClearCoatRoughnessRow');
	    var materialProgramRow = UI$1.get('materialProgramRow');
	    var materialVertexColorsRow = UI$1.get('materialVertexColorsRow');
	    var materialSkinningRow = UI$1.get('materialSkinningRow');
	    var materialMapRow = UI$1.get('materialMapRow');
	    var materialAlphaMapRow = UI$1.get('materialAlphaMapRow');
	    var materialBumpMapRow = UI$1.get('materialBumpMapRow');
	    var materialNormalMapRow = UI$1.get('materialNormalMapRow');
	    var materialDisplacementMapRow = UI$1.get('materialDisplacementMapRow');
	    var materialRoughnessMapRow = UI$1.get('materialRoughnessMapRow');
	    var materialMetalnessMapRow = UI$1.get('materialMetalnessMapRow');
	    var materialSpecularMapRow = UI$1.get('materialSpecularMapRow');
	    var materialEnvMapRow = UI$1.get('materialEnvMapRow');
	    var materialLightMapRow = UI$1.get('materialLightMapRow');
	    var materialAOMapRow = UI$1.get('materialAOMapRow');
	    var materialEmissiveMapRow = UI$1.get('materialEmissiveMapRow');
	    var materialSideRow = UI$1.get('materialSideRow');
	    var materialShadingRow = UI$1.get('materialShadingRow');
	    var materialBlendingRow = UI$1.get('materialBlendingRow');
	    var materialOpacityRow = UI$1.get('materialOpacityRow');
	    var materialTransparentRow = UI$1.get('materialTransparentRow');
	    var materialAlphaTestRow = UI$1.get('materialAlphaTestRow');
	    var materialWireframeRow = UI$1.get('materialWireframeRow');

	    var properties = {
	        'name': materialNameRow,
	        'color': materialColorRow,
	        'roughness': materialRoughnessRow,
	        'metalness': materialMetalnessRow,
	        'emissive': materialEmissiveRow,
	        'specular': materialSpecularRow,
	        'shininess': materialShininessRow,
	        'clearCoat': materialClearCoatRow,
	        'clearCoatRoughness': materialClearCoatRoughnessRow,
	        'vertexShader': materialProgramRow,
	        'vertexColors': materialVertexColorsRow,
	        'skinning': materialSkinningRow,
	        'map': materialMapRow,
	        'alphaMap': materialAlphaMapRow,
	        'bumpMap': materialBumpMapRow,
	        'normalMap': materialNormalMapRow,
	        'displacementMap': materialDisplacementMapRow,
	        'roughnessMap': materialRoughnessMapRow,
	        'metalnessMap': materialMetalnessMapRow,
	        'specularMap': materialSpecularMapRow,
	        'envMap': materialEnvMapRow,
	        'lightMap': materialLightMapRow,
	        'aoMap': materialAOMapRow,
	        'emissiveMap': materialEmissiveMapRow,
	        'side': materialSideRow,
	        'flatShading': materialShadingRow,
	        'blending': materialBlendingRow,
	        'opacity': materialOpacityRow,
	        'transparent': materialTransparentRow,
	        'alphaTest': materialAlphaTestRow,
	        'wireframe': materialWireframeRow
	    };

	    var material = this.currentObject.material;
	    for (var property in properties) {
	        properties[property].dom.style.display = material[property] !== undefined ? '' : 'none';
	    }
	};

	/**
	 * 根据材质变化更新ui
	 */
	MaterialPanelEvent.prototype.refreshUI = function (resetTextureSelectors) {
	    var currentObject = this.currentObject;
	    if (!currentObject) return;

	    var materialUUID = UI$1.get('materialUUID');
	    var materialName = UI$1.get('materialName');
	    var materialClass = UI$1.get('materialClass');
	    var materialColor = UI$1.get('materialColor');
	    var materialRoughness = UI$1.get('materialRoughness');
	    var materialMetalness = UI$1.get('materialMetalness');
	    var materialEmissive = UI$1.get('materialEmissive');
	    var materialSpecular = UI$1.get('materialSpecular');
	    var materialShininess = UI$1.get('materialShininess');
	    var materialClearCoat = UI$1.get('materialClearCoat');
	    var materialClearCoatRoughness = UI$1.get('materialClearCoatRoughness');
	    var materialVertexColors = UI$1.get('materialVertexColors');
	    var materialSkinning = UI$1.get('materialSkinning');
	    var materialMapEnabled = UI$1.get('materialMapEnabled');
	    var materialMap = UI$1.get('materialMap');
	    var materialAlphaMapEnabled = UI$1.get('materialAlphaMapEnabled');
	    var materialAlphaMap = UI$1.get('materialAlphaMap');
	    var materialBumpMapEnabled = UI$1.get('materialBumpMapEnabled');
	    var materialBumpMap = UI$1.get('materialBumpMap');
	    var materialBumpScale = UI$1.get('materialBumpScale');
	    var materialNormalMapEnabled = UI$1.get('materialNormalMapEnabled');
	    var materialNormalMap = UI$1.get('materialNormalMap');
	    var materialDisplacementMapEnabled = UI$1.get('materialDisplacementMapEnabled');
	    var materialDisplacementMap = UI$1.get('materialDisplacementMap');
	    var materialDisplacementScale = UI$1.get('materialDisplacementScale');
	    var materialRoughnessMapEnabled = UI$1.get('materialRoughnessMapEnabled');
	    var materialRoughnessMap = UI$1.get('materialRoughnessMap');
	    var materialMetalnessMapEnabled = UI$1.get('materialMetalnessMapEnabled');
	    var materialMetalnessMap = UI$1.get('materialMetalnessMap');
	    var materialSpecularMapEnabled = UI$1.get('materialSpecularMapEnabled');
	    var materialSpecularMap = UI$1.get('materialSpecularMap');
	    var materialEnvMapEnabled = UI$1.get('materialEnvMapEnabled');
	    var materialEnvMap = UI$1.get('materialEnvMap');
	    var materialReflectivity = UI$1.get('materialReflectivity');
	    var materialLightMapEnabled = UI$1.get('materialLightMapEnabled');
	    var materialLightMap = UI$1.get('materialLightMap');
	    var materialAOMapEnabled = UI$1.get('materialAOMapEnabled');
	    var materialAOMap = UI$1.get('materialAOMap');
	    var materialAOScale = UI$1.get('materialAOScale');
	    var materialEmissiveMapEnabled = UI$1.get('materialEmissiveMapEnabled');
	    var materialEmissiveMap = UI$1.get('materialEmissiveMap');
	    var materialSide = UI$1.get('materialSide');
	    var materialShading = UI$1.get('materialShading');
	    var materialBlending = UI$1.get('materialBlending');
	    var materialOpacity = UI$1.get('materialOpacity');
	    var materialTransparent = UI$1.get('materialTransparent');
	    var materialAlphaTest = UI$1.get('materialAlphaTest');
	    var materialWireframe = UI$1.get('materialWireframe');
	    var materialWireframeLinewidth = UI$1.get('materialWireframeLinewidth');

	    var material = currentObject.material;

	    if (material.uuid !== undefined) {
	        materialUUID.setValue(material.uuid);
	    }

	    if (material.name !== undefined) {
	        materialName.setValue(material.name);
	    }

	    materialClass.setValue(material.type);

	    if (material.color !== undefined) {
	        materialColor.setHexValue(material.color.getHexString());
	    }

	    if (material.roughness !== undefined) {
	        materialRoughness.setValue(material.roughness);
	    }

	    if (material.metalness !== undefined) {
	        materialMetalness.setValue(material.metalness);
	    }

	    if (material.emissive !== undefined) {
	        materialEmissive.setHexValue(material.emissive.getHexString());
	    }

	    if (material.specular !== undefined) {
	        materialSpecular.setHexValue(material.specular.getHexString());
	    }

	    if (material.shininess !== undefined) {
	        materialShininess.setValue(material.shininess);
	    }

	    if (material.clearCoat !== undefined) {
	        materialClearCoat.setValue(material.clearCoat);
	    }

	    if (material.clearCoatRoughness !== undefined) {
	        materialClearCoatRoughness.setValue(material.clearCoatRoughness);
	    }

	    if (material.vertexColors !== undefined) {
	        materialVertexColors.setValue(material.vertexColors);
	    }

	    if (material.skinning !== undefined) {
	        materialSkinning.setValue(material.skinning);
	    }

	    if (material.map !== undefined) {
	        materialMapEnabled.setValue(material.map !== null);

	        if (material.map !== null || resetTextureSelectors) {
	            materialMap.setValue(material.map);
	        }
	    }

	    if (material.alphaMap !== undefined) {
	        materialAlphaMapEnabled.setValue(material.alphaMap !== null);

	        if (material.alphaMap !== null || resetTextureSelectors) {
	            materialAlphaMap.setValue(material.alphaMap);
	        }
	    }

	    if (material.bumpMap !== undefined) {
	        materialBumpMapEnabled.setValue(material.bumpMap !== null);

	        if (material.bumpMap !== null || resetTextureSelectors) {
	            materialBumpMap.setValue(material.bumpMap);
	        }

	        materialBumpScale.setValue(material.bumpScale);
	    }

	    if (material.normalMap !== undefined) {
	        materialNormalMapEnabled.setValue(material.normalMap !== null);

	        if (material.normalMap !== null || resetTextureSelectors) {
	            materialNormalMap.setValue(material.normalMap);
	        }
	    }

	    if (material.displacementMap !== undefined) {
	        materialDisplacementMapEnabled.setValue(material.displacementMap !== null);

	        if (material.displacementMap !== null || resetTextureSelectors) {
	            materialDisplacementMap.setValue(material.displacementMap);
	        }

	        materialDisplacementScale.setValue(material.displacementScale);
	    }

	    if (material.roughnessMap !== undefined) {
	        materialRoughnessMapEnabled.setValue(material.roughnessMap !== null);

	        if (material.roughnessMap !== null || resetTextureSelectors) {
	            materialRoughnessMap.setValue(material.roughnessMap);
	        }
	    }

	    if (material.metalnessMap !== undefined) {
	        materialMetalnessMapEnabled.setValue(material.metalnessMap !== null);

	        if (material.metalnessMap !== null || resetTextureSelectors) {
	            materialMetalnessMap.setValue(material.metalnessMap);
	        }
	    }

	    if (material.specularMap !== undefined) {
	        materialSpecularMapEnabled.setValue(material.specularMap !== null);

	        if (material.specularMap !== null || resetTextureSelectors) {
	            materialSpecularMap.setValue(material.specularMap);
	        }
	    }

	    if (material.envMap !== undefined) {
	        materialEnvMapEnabled.setValue(material.envMap !== null);

	        if (material.envMap !== null || resetTextureSelectors) {
	            materialEnvMap.setValue(material.envMap);
	        }
	    }

	    if (material.reflectivity !== undefined) {
	        materialReflectivity.setValue(material.reflectivity);
	    }

	    if (material.lightMap !== undefined) {
	        materialLightMapEnabled.setValue(material.lightMap !== null);

	        if (material.lightMap !== null || resetTextureSelectors) {
	            materialLightMap.setValue(material.lightMap);
	        }
	    }

	    if (material.aoMap !== undefined) {
	        materialAOMapEnabled.setValue(material.aoMap !== null);

	        if (material.aoMap !== null || resetTextureSelectors) {
	            materialAOMap.setValue(material.aoMap);
	        }

	        materialAOScale.setValue(material.aoMapIntensity);
	    }

	    if (material.emissiveMap !== undefined) {
	        materialEmissiveMapEnabled.setValue(material.emissiveMap !== null);

	        if (material.emissiveMap !== null || resetTextureSelectors) {
	            materialEmissiveMap.setValue(material.emissiveMap);
	        }
	    }

	    if (material.side !== undefined) {
	        materialSide.setValue(material.side);
	    }

	    if (material.flatShading !== undefined) {
	        materialShading.setValue(material.flatShading);
	    }

	    if (material.blending !== undefined) {
	        materialBlending.setValue(material.blending);
	    }

	    if (material.opacity !== undefined) {
	        materialOpacity.setValue(material.opacity);
	    }

	    if (material.transparent !== undefined) {
	        materialTransparent.setValue(material.transparent);
	    }

	    if (material.alphaTest !== undefined) {
	        materialAlphaTest.setValue(material.alphaTest);
	    }

	    if (material.wireframe !== undefined) {
	        materialWireframe.setValue(material.wireframe);
	    }

	    if (material.wireframeLinewidth !== undefined) {
	        materialWireframeLinewidth.setValue(material.wireframeLinewidth);
	    }

	    this.setRowVisibility();
	};

	/**
	 * 根据ui变化更新材质
	 */
	MaterialPanelEvent.prototype.update = function () {
	    var editor = this.app.editor;

	    var currentObject = this.currentObject;

	    var materialUUID = UI$1.get('materialUUID');
	    var materialName = UI$1.get('materialName');
	    var materialClass = UI$1.get('materialClass');
	    var materialColor = UI$1.get('materialColor');
	    var materialRoughness = UI$1.get('materialRoughness');
	    var materialMetalness = UI$1.get('materialMetalness');
	    var materialEmissive = UI$1.get('materialEmissive');
	    var materialSpecular = UI$1.get('materialSpecular');
	    var materialShininess = UI$1.get('materialShininess');
	    var materialClearCoat = UI$1.get('materialClearCoat');
	    var materialClearCoatRoughness = UI$1.get('materialClearCoatRoughness');
	    var materialVertexColors = UI$1.get('materialVertexColors');
	    var materialSkinning = UI$1.get('materialSkinning');
	    var materialMapEnabled = UI$1.get('materialMapEnabled');
	    var materialMap = UI$1.get('materialMap');
	    var materialAlphaMapEnabled = UI$1.get('materialAlphaMapEnabled');
	    var materialAlphaMap = UI$1.get('materialAlphaMap');
	    var materialBumpMapEnabled = UI$1.get('materialBumpMapEnabled');
	    var materialBumpMap = UI$1.get('materialBumpMap');
	    var materialBumpScale = UI$1.get('materialBumpScale');
	    var materialNormalMapEnabled = UI$1.get('materialNormalMapEnabled');
	    var materialNormalMap = UI$1.get('materialNormalMap');
	    var materialDisplacementMapEnabled = UI$1.get('materialDisplacementMapEnabled');
	    var materialDisplacementMap = UI$1.get('materialDisplacementMap');
	    var materialDisplacementScale = UI$1.get('materialDisplacementScale');
	    var materialRoughnessMapEnabled = UI$1.get('materialRoughnessMapEnabled');
	    var materialRoughnessMap = UI$1.get('materialRoughnessMap');
	    var materialMetalnessMapEnabled = UI$1.get('materialMetalnessMapEnabled');
	    var materialMetalnessMap = UI$1.get('materialMetalnessMap');
	    var materialSpecularMapEnabled = UI$1.get('materialSpecularMapEnabled');
	    var materialSpecularMap = UI$1.get('materialSpecularMap');
	    var materialEnvMapEnabled = UI$1.get('materialEnvMapEnabled');
	    var materialEnvMap = UI$1.get('materialEnvMap');
	    var materialReflectivity = UI$1.get('materialReflectivity');
	    var materialLightMapEnabled = UI$1.get('materialLightMapEnabled');
	    var materialLightMap = UI$1.get('materialLightMap');
	    var materialAOMapEnabled = UI$1.get('materialAOMapEnabled');
	    var materialAOMap = UI$1.get('materialAOMap');
	    var materialAOScale = UI$1.get('materialAOScale');
	    var materialEmissiveMapEnabled = UI$1.get('materialEmissiveMapEnabled');
	    var materialEmissiveMap = UI$1.get('materialEmissiveMap');
	    var materialSide = UI$1.get('materialSide');
	    var materialShading = UI$1.get('materialShading');
	    var materialBlending = UI$1.get('materialBlending');
	    var materialOpacity = UI$1.get('materialOpacity');
	    var materialTransparent = UI$1.get('materialTransparent');
	    var materialAlphaTest = UI$1.get('materialAlphaTest');
	    var materialWireframe = UI$1.get('materialWireframe');
	    var materialWireframeLinewidth = UI$1.get('materialWireframeLinewidth');

	    var object = currentObject;

	    var geometry = object.geometry;
	    var material = object.material;

	    var textureWarning = false;
	    var objectHasUvs = false;

	    if (object instanceof THREE.Sprite) objectHasUvs = true;
	    if (geometry instanceof THREE.Geometry && geometry.faceVertexUvs[0].length > 0) objectHasUvs = true;
	    if (geometry instanceof THREE.BufferGeometry && geometry.attributes.uv !== undefined) objectHasUvs = true;

	    if (material) {
	        if (material.uuid !== undefined && material.uuid !== materialUUID.getValue()) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'uuid', materialUUID.getValue()));
	        }

	        if (material instanceof THREE[materialClass.getValue()] === false) {
	            material = new THREE[materialClass.getValue()]();

	            editor.execute(new SetMaterialCommand(currentObject, material), '新材质：' + materialClass.getValue());
	            // TODO Copy other references in the scene graph
	            // keeping name and UUID then.
	            // Also there should be means to create a unique
	            // copy for the current object explicitly and to
	            // attach the current material to other objects.
	        }

	        if (material.color !== undefined && material.color.getHex() !== materialColor.getHexValue()) {
	            editor.execute(new SetMaterialColorCommand(currentObject, 'color', materialColor.getHexValue()));
	        }

	        if (material.roughness !== undefined && Math.abs(material.roughness - materialRoughness.getValue()) >= 0.01) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'roughness', materialRoughness.getValue()));
	        }

	        if (material.metalness !== undefined && Math.abs(material.metalness - materialMetalness.getValue()) >= 0.01) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'metalness', materialMetalness.getValue()));
	        }

	        if (material.emissive !== undefined && material.emissive.getHex() !== materialEmissive.getHexValue()) {
	            editor.execute(new SetMaterialColorCommand(currentObject, 'emissive', materialEmissive.getHexValue()));
	        }

	        if (material.specular !== undefined && material.specular.getHex() !== materialSpecular.getHexValue()) {
	            editor.execute(new SetMaterialColorCommand(currentObject, 'specular', materialSpecular.getHexValue()));
	        }

	        if (material.shininess !== undefined && Math.abs(material.shininess - materialShininess.getValue()) >= 0.01) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'shininess', materialShininess.getValue()));
	        }

	        if (material.clearCoat !== undefined && Math.abs(material.clearCoat - materialClearCoat.getValue()) >= 0.01) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'clearCoat', materialClearCoat.getValue()));
	        }

	        if (material.clearCoatRoughness !== undefined && Math.abs(material.clearCoatRoughness - materialClearCoatRoughness.getValue()) >= 0.01) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'clearCoatRoughness', materialClearCoatRoughness.getValue()));
	        }

	        if (material.vertexColors !== undefined) {
	            var vertexColors = parseInt(materialVertexColors.getValue());

	            if (material.vertexColors !== vertexColors) {
	                editor.execute(new SetMaterialValueCommand(currentObject, 'vertexColors', vertexColors));
	            }
	        }

	        if (material.skinning !== undefined && material.skinning !== materialSkinning.getValue()) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'skinning', materialSkinning.getValue()));
	        }

	        if (material.map !== undefined) {
	            var mapEnabled = materialMapEnabled.getValue() === true;

	            if (objectHasUvs) {
	                var map = mapEnabled ? materialMap.getValue() : null;
	                if (material.map !== map) {
	                    editor.execute(new SetMaterialMapCommand(currentObject, 'map', map));
	                }
	            } else {
	                if (mapEnabled) textureWarning = true;
	            }
	        }

	        if (material.alphaMap !== undefined) {
	            var mapEnabled = materialAlphaMapEnabled.getValue() === true;

	            if (objectHasUvs) {
	                var alphaMap = mapEnabled ? materialAlphaMap.getValue() : null;

	                if (material.alphaMap !== alphaMap) {
	                    editor.execute(new SetMaterialMapCommand(currentObject, 'alphaMap', alphaMap));
	                }
	            } else {
	                if (mapEnabled) textureWarning = true;
	            }
	        }

	        if (material.bumpMap !== undefined) {
	            var bumpMapEnabled = materialBumpMapEnabled.getValue() === true;

	            if (objectHasUvs) {
	                var bumpMap = bumpMapEnabled ? materialBumpMap.getValue() : null;

	                if (material.bumpMap !== bumpMap) {
	                    editor.execute(new SetMaterialMapCommand(currentObject, 'bumpMap', bumpMap));
	                }

	                if (material.bumpScale !== materialBumpScale.getValue()) {
	                    editor.execute(new SetMaterialValueCommand(currentObject, 'bumpScale', materialBumpScale.getValue()));
	                }
	            } else {
	                if (bumpMapEnabled) textureWarning = true;
	            }
	        }

	        if (material.normalMap !== undefined) {
	            var normalMapEnabled = materialNormalMapEnabled.getValue() === true;

	            if (objectHasUvs) {
	                var normalMap = normalMapEnabled ? materialNormalMap.getValue() : null;

	                if (material.normalMap !== normalMap) {
	                    editor.execute(new SetMaterialMapCommand(currentObject, 'normalMap', normalMap));
	                }
	            } else {
	                if (normalMapEnabled) textureWarning = true;
	            }
	        }

	        if (material.displacementMap !== undefined) {
	            var displacementMapEnabled = materialDisplacementMapEnabled.getValue() === true;

	            if (objectHasUvs) {
	                var displacementMap = displacementMapEnabled ? materialDisplacementMap.getValue() : null;

	                if (material.displacementMap !== displacementMap) {
	                    editor.execute(new SetMaterialMapCommand(currentObject, 'displacementMap', displacementMap));
	                }

	                if (material.displacementScale !== materialDisplacementScale.getValue()) {
	                    editor.execute(new SetMaterialValueCommand(currentObject, 'displacementScale', materialDisplacementScale.getValue()));
	                }
	            } else {
	                if (displacementMapEnabled) textureWarning = true;
	            }

	        }

	        if (material.roughnessMap !== undefined) {
	            var roughnessMapEnabled = materialRoughnessMapEnabled.getValue() === true;

	            if (objectHasUvs) {
	                var roughnessMap = roughnessMapEnabled ? materialRoughnessMap.getValue() : null;

	                if (material.roughnessMap !== roughnessMap) {
	                    editor.execute(new SetMaterialMapCommand(currentObject, 'roughnessMap', roughnessMap));
	                }
	            } else {
	                if (roughnessMapEnabled) textureWarning = true;
	            }
	        }

	        if (material.metalnessMap !== undefined) {
	            var metalnessMapEnabled = materialMetalnessMapEnabled.getValue() === true;

	            if (objectHasUvs) {
	                var metalnessMap = metalnessMapEnabled ? materialMetalnessMap.getValue() : null;

	                if (material.metalnessMap !== metalnessMap) {
	                    editor.execute(new SetMaterialMapCommand(currentObject, 'metalnessMap', metalnessMap));
	                }
	            } else {
	                if (metalnessMapEnabled) textureWarning = true;
	            }
	        }

	        if (material.specularMap !== undefined) {
	            var specularMapEnabled = materialSpecularMapEnabled.getValue() === true;

	            if (objectHasUvs) {
	                var specularMap = specularMapEnabled ? materialSpecularMap.getValue() : null;

	                if (material.specularMap !== specularMap) {
	                    editor.execute(new SetMaterialMapCommand(currentObject, 'specularMap', specularMap));
	                }
	            } else {
	                if (specularMapEnabled) textureWarning = true;
	            }
	        }

	        if (material.envMap !== undefined) {
	            var envMapEnabled = materialEnvMapEnabled.getValue() === true;
	            var envMap = envMapEnabled ? materialEnvMap.getValue() : null;

	            if (material.envMap !== envMap) {
	                editor.execute(new SetMaterialMapCommand(currentObject, 'envMap', envMap));
	            }
	        }

	        if (material.reflectivity !== undefined) {
	            var reflectivity = materialReflectivity.getValue();

	            if (material.reflectivity !== reflectivity) {
	                editor.execute(new SetMaterialValueCommand(currentObject, 'reflectivity', reflectivity));
	            }
	        }

	        if (material.lightMap !== undefined) {
	            var lightMapEnabled = materialLightMapEnabled.getValue() === true;

	            if (objectHasUvs) {
	                var lightMap = lightMapEnabled ? materialLightMap.getValue() : null;

	                if (material.lightMap !== lightMap) {
	                    editor.execute(new SetMaterialMapCommand(currentObject, 'lightMap', lightMap));
	                }
	            } else {
	                if (lightMapEnabled) textureWarning = true;
	            }
	        }

	        if (material.aoMap !== undefined) {
	            var aoMapEnabled = materialAOMapEnabled.getValue() === true;

	            if (objectHasUvs) {
	                var aoMap = aoMapEnabled ? materialAOMap.getValue() : null;

	                if (material.aoMap !== aoMap) {
	                    editor.execute(new SetMaterialMapCommand(currentObject, 'aoMap', aoMap));
	                }

	                if (material.aoMapIntensity !== materialAOScale.getValue()) {
	                    editor.execute(new SetMaterialValueCommand(currentObject, 'aoMapIntensity', materialAOScale.getValue()));
	                }
	            } else {
	                if (aoMapEnabled) textureWarning = true;
	            }
	        }

	        if (material.emissiveMap !== undefined) {
	            var emissiveMapEnabled = materialEmissiveMapEnabled.getValue() === true;

	            if (objectHasUvs) {
	                var emissiveMap = emissiveMapEnabled ? materialEmissiveMap.getValue() : null;

	                if (material.emissiveMap !== emissiveMap) {
	                    editor.execute(new SetMaterialMapCommand(currentObject, 'emissiveMap', emissiveMap));
	                }
	            } else {
	                if (emissiveMapEnabled) textureWarning = true;
	            }
	        }

	        if (material.side !== undefined) {
	            var side = parseInt(materialSide.getValue());

	            if (material.side !== side) {
	                editor.execute(new SetMaterialValueCommand(currentObject, 'side', side));
	            }
	        }

	        if (material.flatShading !== undefined) {
	            var flatShading = materialShading.getValue();

	            if (material.flatShading != flatShading) {
	                editor.execute(new SetMaterialValueCommand(currentObject, 'flatShading', flatShading, currentMaterialSlot));
	            }
	        }

	        if (material.blending !== undefined) {
	            var blending = parseInt(materialBlending.getValue());

	            if (material.blending !== blending) {
	                editor.execute(new SetMaterialValueCommand(currentObject, 'blending', blending));
	            }
	        }

	        if (material.opacity !== undefined && Math.abs(material.opacity - materialOpacity.getValue()) >= 0.01) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'opacity', materialOpacity.getValue()));
	        }

	        if (material.transparent !== undefined && material.transparent !== materialTransparent.getValue()) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'transparent', materialTransparent.getValue()));
	        }

	        if (material.alphaTest !== undefined && Math.abs(material.alphaTest - materialAlphaTest.getValue()) >= 0.01) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'alphaTest', materialAlphaTest.getValue()));
	        }

	        if (material.wireframe !== undefined && material.wireframe !== materialWireframe.getValue()) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'wireframe', materialWireframe.getValue()));
	        }

	        if (material.wireframeLinewidth !== undefined && Math.abs(material.wireframeLinewidth - materialWireframeLinewidth.getValue()) >= 0.01) {
	            editor.execute(new SetMaterialValueCommand(currentObject, 'wireframeLinewidth', materialWireframeLinewidth.getValue()));
	        }

	        this.refreshUI();
	    }

	    if (textureWarning) {
	        console.warn("Can't set texture, model doesn't have texture coordinates");
	    }
	};

	/**
	 * 历史面板事件
	 * @param {*} app 
	 */
	function HistoryPanelEvent(app) {
	    BaseEvent.call(this, app);

	    this.currentObject = null;
	    this.copiedMaterial = null;
	}

	HistoryPanelEvent.prototype = Object.create(BaseEvent.prototype);
	HistoryPanelEvent.prototype.constructor = HistoryPanelEvent;

	HistoryPanelEvent.prototype.start = function () {
	    this.app.on(`editorCleared.${this.id}`, this.refreshUI.bind(this));
	    this.app.on(`historyChanged.${this.id}`, this.refreshUI.bind(this));
	    this.refreshUI();
	};

	HistoryPanelEvent.prototype.stop = function () {
	    this.app.on(`editorCleared.${this.id}`, null);
	    this.app.on(`historyChanged.${this.id}`, null);
	};

	HistoryPanelEvent.prototype.refreshUI = function () {
	    var editor = this.app.editor;
	    var history = editor.history;
	    var outliner = UI$1.get('historyOutlinear');

	    var options = [];

	    function buildOption(object) {
	        var option = document.createElement('div');
	        option.value = object.id;
	        return option;
	    }

	    (function addObjects(objects) {
	        for (var i = 0, l = objects.length; i < l; i++) {
	            var object = objects[i];
	            var option = buildOption(object);
	            option.innerHTML = '&nbsp;' + object.name;
	            options.push(option);
	        }
	    })(history.undos);


	    (function addObjects(objects, pad) {
	        for (var i = objects.length - 1; i >= 0; i--) {
	            var object = objects[i];
	            var option = buildOption(object);
	            option.innerHTML = '&nbsp;' + object.name;
	            option.style.opacity = 0.3;
	            options.push(option);
	        }
	    })(history.redos, '&nbsp;');

	    outliner.setOptions(options);
	};

	/**
	 * 工程面板事件
	 * @param {*} app 
	 */
	function ProjectPanelEvent(app) {
	    BaseEvent.call(this, app);
	}

	ProjectPanelEvent.prototype = Object.create(BaseEvent.prototype);
	ProjectPanelEvent.prototype.constructor = ProjectPanelEvent;

	ProjectPanelEvent.prototype.start = function () {
	    this.app.on(`updateRenderer.${this.id}`, this.updateRenderer.bind(this));

	    var config = this.app.editor.config;

	    this.createRenderer(
	        config.getKey('project/renderer'),
	        config.getKey('project/renderer/antialias'),
	        config.getKey('project/renderer/shadows'),
	        config.getKey('project/renderer/gammaInput'),
	        config.getKey('project/renderer/gammaOutput')
	    );
	};

	ProjectPanelEvent.prototype.stop = function () {
	    this.app.on(`updateRenderer.${this.id}`, null);
	};

	ProjectPanelEvent.prototype.updateRenderer = function () {
	    var rendererType = UI$1.get('rendererType');
	    var rendererAntialias = UI$1.get('rendererAntialias');
	    var rendererShadows = UI$1.get('rendererShadows');
	    var rendererGammaInput = UI$1.get('rendererGammaInput');
	    var rendererGammaOutput = UI$1.get('rendererGammaOutput');

	    this.createRenderer(
	        rendererType.getValue(),
	        rendererAntialias.getValue(),
	        rendererShadows.getValue(),
	        rendererGammaInput.getValue(),
	        rendererGammaOutput.getValue()
	    );
	};

	ProjectPanelEvent.prototype.createRenderer = function (type, antialias, shadows, gammaIn, gammaOut) {
	    var rendererPropertiesRow = UI$1.get('rendererPropertiesRow');

	    var rendererTypes = {
	        'WebGLRenderer': THREE.WebGLRenderer,
	        'CanvasRenderer': THREE.CanvasRenderer,
	        'SVGRenderer': THREE.SVGRenderer,
	        'SoftwareRenderer': THREE.SoftwareRenderer,
	        'RaytracingRenderer': THREE.RaytracingRenderer
	    };

	    if (type === 'WebGLRenderer' && System.support.webgl === false) {
	        type = 'CanvasRenderer';
	    }

	    rendererPropertiesRow.dom.style.display = type === 'WebGLRenderer' ? '' : 'none';

	    var renderer = new rendererTypes[type]({ antialias: antialias });
	    renderer.gammaInput = gammaIn;
	    renderer.gammaOutput = gammaOut;
	    if (shadows && renderer.shadowMap) {
	        renderer.shadowMap.enabled = true;
	        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
	    }

	    this.app.call('rendererChanged', this, renderer);
	};

	/**
	 * 属性面板事件
	 * @param {*} app 
	 */
	function PropertyPanelEvent(app) {
	    BaseEvent.call(this, app);
	}

	PropertyPanelEvent.prototype = Object.create(BaseEvent.prototype);
	PropertyPanelEvent.prototype.constructor = PropertyPanelEvent;

	PropertyPanelEvent.prototype.start = function () {
	    this.app.on(`appStarted.${this.id}`, this.onAppStarted.bind(this));
	    this.app.on(`selectPropertyTab.${this.id}`, this.onSelectPropertyTab.bind(this));
	};

	PropertyPanelEvent.prototype.stop = function () {
	    this.app.on(`appStarted.${this.id}`, null);
	    this.app.on(`selectPropertyTab.${this.id}`, null);
	};

	PropertyPanelEvent.prototype.onAppStarted = function () {
	    this.app.call('selectPropertyTab', this, '物体');
	};

	PropertyPanelEvent.prototype.onSelectPropertyTab = function (tabName) {
	    var objectTab = UI$1.get('objectTab');
	    var geometryTab = UI$1.get('geometryTab');
	    var materialTab = UI$1.get('materialTab');

	    objectTab.dom.className = '';
	    geometryTab.dom.className = '';
	    materialTab.dom.className = '';

	    switch (tabName) {
	        case '物体':
	            objectTab.dom.className = 'selected';
	            break;
	        case '几何':
	            geometryTab.dom.className = 'selected';
	            break;
	        case '材质':
	            materialTab.dom.className = 'selected';
	            break;
	    }
	};

	/**
	 * 场景面板事件
	 * @param {*} app 
	 */
	function ScenePanelEvent(app) {
	    BaseEvent.call(this, app);
	    this.ignoreObjectSelectedSignal = false;
	}

	ScenePanelEvent.prototype = Object.create(BaseEvent.prototype);
	ScenePanelEvent.prototype.constructor = ScenePanelEvent;

	ScenePanelEvent.prototype.start = function () {
	    this.app.on(`updateScenePanelFog.${this.id}`, this.refreshFogUI.bind(this));
	    this.app.on(`editorCleared.${this.id}`, this.refreshUI.bind(this));
	    this.app.on(`sceneGraphChanged.${this.id}`, this.refreshUI.bind(this));
	    this.app.on(`objectChanged.${this.id}`, this.onObjectChanged.bind(this));
	    this.app.on(`objectSelected.${this.id}`, this.onObjectSelected.bind(this));
	    this.app.on(`outlinerChange.${this.id}`, this.onOutlinerChange.bind(this));
	    this.refreshUI();
	};

	ScenePanelEvent.prototype.stop = function () {
	    this.app.on(`updateScenePanelFog.${this.id}`, null);
	    this.app.on(`editorCleared.${this.id}`, null);
	    this.app.on(`sceneGraphChanged.${this.id}`, null);
	    this.app.on(`objectChanged.${this.id}`, null);
	    this.app.on(`objectSelected.${this.id}`, null);
	    this.app.on(`outlinerChange.${this.id}`, null);
	};

	ScenePanelEvent.prototype.onOutlinerChange = function (control) {
	    var editor = this.app.editor;

	    this.ignoreObjectSelectedSignal = true;
	    editor.selectById(parseInt(control.getValue()));
	    this.ignoreObjectSelectedSignal = false;
	};

	/**
	 * 场景物体改变
	 * @param {*} object 
	 */
	ScenePanelEvent.prototype.onObjectChanged = function (object) {
	    var outliner = UI$1.get('outliner');

	    var options = outliner.options;

	    for (var i = 0; i < options.length; i++) {
	        var option = options[i];

	        if (option.value === object.id) {
	            option.innerHTML = this.buildHTML(object);
	            return;
	        }
	    }
	};

	/**
	 * 选中物体改变
	 * @param {*} object 
	 */
	ScenePanelEvent.prototype.onObjectSelected = function (object) {
	    var outliner = UI$1.get('outliner');

	    if (this.ignoreObjectSelectedSignal === true) {
	        return;
	    }

	    outliner.setValue(object !== null ? object.id : null);
	};

	// outliner
	ScenePanelEvent.prototype.buildOption = function (object, draggable) {
	    var option = document.createElement('div');
	    option.draggable = draggable;
	    option.innerHTML = this.buildHTML(object);
	    option.value = object.id;
	    return option;
	};

	ScenePanelEvent.prototype.buildHTML = function (object) {
	    var html = '<span class="type ' + object.type + '"></span> ' + object.name;

	    if (object instanceof THREE.Mesh) {
	        var geometry = object.geometry;
	        var material = object.material;

	        html += ' <span class="type ' + geometry.type + '"></span> ' + geometry.name;
	        html += ' <span class="type ' + material.type + '"></span> ' + (material.name == null ? '' : material.name);
	    }

	    html += this.getScript(object.uuid);
	    return html;
	};

	ScenePanelEvent.prototype.getScript = function (uuid) {
	    var editor = this.app.editor;

	    if (editor.scripts[uuid] !== undefined) {
	        return ' <span class="type Script"></span>';
	    }

	    return '';
	};

	ScenePanelEvent.prototype.refreshUI = function () {
	    var editor = this.app.editor;
	    var camera = editor.camera;
	    var scene = editor.scene;
	    var outliner = UI$1.get('outliner');
	    var backgroundColor = UI$1.get('backgroundColor');
	    var fogColor = UI$1.get('fogColor');
	    var fogType = UI$1.get('fogType');
	    var fogNear = UI$1.get('fogNear');
	    var fogFar = UI$1.get('fogFar');
	    var fogDensity = UI$1.get('fogDensity');

	    var options = [];

	    options.push(this.buildOption(camera, false));
	    options.push(this.buildOption(scene, false));

	    var _this = this;

	    (function addObjects(objects, pad) {
	        for (var i = 0, l = objects.length; i < l; i++) {
	            var object = objects[i];

	            var option = _this.buildOption(object, true);
	            option.style.paddingLeft = (pad * 10) + 'px';
	            options.push(option);

	            addObjects(object.children, pad + 1);
	        }
	    })(scene.children, 1);

	    outliner.setOptions(options);

	    if (editor.selected !== null) {
	        outliner.setValue(editor.selected.id);
	    }

	    if (scene.background) {
	        backgroundColor.setHexValue(scene.background.getHex());
	    }

	    if (scene.fog) {
	        fogColor.setHexValue(scene.fog.color.getHex());

	        if (scene.fog instanceof THREE.Fog) {
	            fogType.setValue("Fog");
	            fogNear.setValue(scene.fog.near);
	            fogFar.setValue(scene.fog.far);
	        } else if (scene.fog instanceof THREE.FogExp2) {
	            fogType.setValue("FogExp2");
	            fogDensity.setValue(scene.fog.density);
	        }
	    } else {
	        fogType.setValue("None");
	    }

	    this.refreshFogUI();
	};

	ScenePanelEvent.prototype.refreshFogUI = function () {
	    var fogType = UI$1.get('fogType');
	    var fogPropertiesRow = UI$1.get('fogPropertiesRow');
	    var fogNear = UI$1.get('fogNear');
	    var fogFar = UI$1.get('fogFar');
	    var fogDensity = UI$1.get('fogDensity');

	    var type = fogType.getValue();

	    fogPropertiesRow.dom.style.display = type === 'None' ? 'none' : '';
	    fogNear.dom.style.display = type === 'Fog' ? '' : 'none';
	    fogFar.dom.style.display = type === 'Fog' ? '' : 'none';
	    fogDensity.dom.style.display = type === 'FogExp2' ? '' : 'none';
	};

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param script javascript object
	 * @param attributeName string
	 * @param newValue string, object
	 * @param cursorPosition javascript object with format {line: 2, ch: 3}
	 * @param scrollInfo javascript object with values {left, top, width, height, clientWidth, clientHeight}
	 * @constructor
	 */

	function SetScriptValueCommand(object, script, attributeName, newValue, cursorPosition, scrollInfo) {

		Command.call(this);

		this.type = 'SetScriptValueCommand';
		this.name = 'Set Script.' + attributeName;
		this.updatable = true;

		this.object = object;
		this.script = script;

		this.attributeName = attributeName;
		this.oldValue = (script !== undefined) ? script[this.attributeName] : undefined;
		this.newValue = newValue;
		this.cursorPosition = cursorPosition;
		this.scrollInfo = scrollInfo;

	}
	SetScriptValueCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetScriptValueCommand.prototype, {

		constructor: SetScriptValueCommand,

		execute: function () {

			this.script[this.attributeName] = this.newValue;

			this.editor.app.call('scriptChanged', this);
			this.editor.app.call('refreshScriptEditor', this, this.object, this.script, this.cursorPosition, this.scrollInfo);

		},

		undo: function () {

			this.script[this.attributeName] = this.oldValue;

			this.editor.app.call('scriptChanged', this);
			this.editor.app.call('refreshScriptEditor', this, this.object, this.script, this.cursorPosition, this.scrollInfo);

		},

		update: function (cmd) {

			this.cursorPosition = cmd.cursorPosition;
			this.scrollInfo = cmd.scrollInfo;
			this.newValue = cmd.newValue;

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.index = this.editor.scripts[this.object.uuid].indexOf(this.script);
			output.attributeName = this.attributeName;
			output.oldValue = this.oldValue;
			output.newValue = this.newValue;
			output.cursorPosition = this.cursorPosition;
			output.scrollInfo = this.scrollInfo;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.oldValue = json.oldValue;
			this.newValue = json.newValue;
			this.attributeName = json.attributeName;
			this.object = this.editor.objectByUuid(json.objectUuid);
			this.script = this.editor.scripts[json.objectUuid][json.index];
			this.cursorPosition = json.cursorPosition;
			this.scrollInfo = json.scrollInfo;

		}

	});

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param script javascript object
	 * @constructor
	 */

	function RemoveScriptCommand(object, script) {

		Command.call(this);

		this.type = 'RemoveScriptCommand';
		this.name = 'Remove Script';

		this.object = object;
		this.script = script;
		if (this.object && this.script) {

			this.index = this.editor.scripts[this.object.uuid].indexOf(this.script);

		}

	}
	RemoveScriptCommand.prototype = Object.create(Command.prototype);

	Object.assign(RemoveScriptCommand.prototype, {

		constructor: RemoveScriptCommand,

		execute: function () {

			if (this.editor.scripts[this.object.uuid] === undefined) return;

			if (this.index !== - 1) {

				this.editor.scripts[this.object.uuid].splice(this.index, 1);

			}

			this.editor.app.call('scriptRemoved', this, this.script);
		},

		undo: function () {

			if (this.editor.scripts[this.object.uuid] === undefined) {

				this.editor.scripts[this.object.uuid] = [];

			}

			this.editor.scripts[this.object.uuid].splice(this.index, 0, this.script);

			this.editor.app.call('scriptAdded', this, this.script);

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.script = this.script;
			output.index = this.index;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.script = json.script;
			this.index = json.index;
			this.object = this.editor.objectByUuid(json.objectUuid);

		}

	});

	/**
	 * 脚本面板事件
	 * @param {*} app 
	 */
	function ScriptPanelEvent(app) {
	    BaseEvent.call(this, app);
	}

	ScriptPanelEvent.prototype = Object.create(BaseEvent.prototype);
	ScriptPanelEvent.prototype.constructor = ScriptPanelEvent;

	ScriptPanelEvent.prototype.start = function () {
	    this.app.on(`objectSelected.${this.id}`, this.onObjectSelected.bind(this));
	    this.app.on(`scriptAdded.${this.id}`, this.update.bind(this));
	    this.app.on(`scriptRemoved.${this.id}`, this.update.bind(this));
	    this.app.on(`scriptChanged.${this.id}`, this.update.bind(this));
	};

	ScriptPanelEvent.prototype.stop = function () {
	    this.app.on(`objectSelected.${this.id}`, null);
	    this.app.on(`scriptAdded.${this.id}`, null);
	    this.app.on(`scriptRemoved.${this.id}`, null);
	    this.app.on(`scriptChanged.${this.id}`, null);
	};

	ScriptPanelEvent.prototype.onObjectSelected = function (object) {
	    var container = UI$1.get('scriptPanel');

	    if (object !== null) {
	        container.dom.style.display = 'block';
	        this.update();
	    } else {
	        container.dom.style.display = 'none';
	    }
	};

	ScriptPanelEvent.prototype.update = function () {
	    var scriptsContainer = UI$1.get('scriptsContainer');
	    var editor = this.app.editor;
	    var _this = this;

	    scriptsContainer.dom.innerHTML = '';
	    scriptsContainer.dom.style.display = 'none';

	    var object = editor.selected;
	    if (object === null) {
	        return;
	    }

	    var scripts = editor.scripts[object.uuid];

	    if (scripts !== undefined) {
	        scriptsContainer.dom.style.display = 'block';

	        for (var i = 0; i < scripts.length; i++) {
	            (function (object, script) {
	                var data = {
	                    xtype: 'container',
	                    parent: scriptsContainer.dom,
	                    children: [{
	                        xtype: 'input',
	                        value: script.name,
	                        style: {
	                            width: '130px',
	                            fontSize: '12px'
	                        },
	                        onChange: function () {
	                            editor.execute(new SetScriptValueCommand(editor.selected, script, 'name', this.getValue()));
	                        }
	                    }, {
	                        xtype: 'button',
	                        text: '编辑',
	                        style: {
	                            marginLeft: '4px'
	                        },
	                        onClick: function () {
	                            _this.app.call('editScript', _this, object, script);
	                        }
	                    }, {
	                        xtype: 'button',
	                        text: '删除',
	                        style: {
	                            marginLeft: '4px'
	                        },
	                        onClick: function () {
	                            if (confirm('确定吗？')) {
	                                editor.execute(new RemoveScriptCommand(editor.selected, script));
	                            }
	                        }
	                    }, {
	                        xtype: 'br'
	                    }]
	                };

	                UI$1.create(data).render();
	            })(object, scripts[i]);
	        }
	    }
	};

	/**
	 * 侧边栏事件
	 * @param {*} app 
	 */
	function SidebarEvent(app) {
	    BaseEvent.call(this, app);
	}

	SidebarEvent.prototype = Object.create(BaseEvent.prototype);
	SidebarEvent.prototype.constructor = SidebarEvent;

	SidebarEvent.prototype.start = function () {
	    this.app.on(`selectTab.${this.id}`, this.onSelectTab.bind(this));
	    this.onSelectTab('场景');
	};

	SidebarEvent.prototype.stop = function () {
	    this.app.on(`selectTab.${this.id}`, null);
	};

	SidebarEvent.prototype.onSelectTab = function (section) {
	    var sceneTab = UI$1.get('sceneTab');
	    var projectTab = UI$1.get('projectTab');
	    var settingsTab = UI$1.get('settingsTab');
	    var scene = UI$1.get('scene');
	    var project = UI$1.get('project');
	    var settings = UI$1.get('settings');

	    sceneTab.dom.className = '';
	    projectTab.dom.className = '';
	    settingsTab.dom.className = '';

	    scene.dom.style.display = 'none';
	    project.dom.style.display = 'none';
	    settings.dom.style.display = 'none';

	    switch (section) {
	        case '场景':
	            sceneTab.dom.className = 'selected';
	            scene.dom.style.display = '';
	            break;
	        case '工程':
	            projectTab.dom.className = 'selected';
	            project.dom.style.display = '';
	            break;
	        case '设置':
	            settingsTab.dom.className = 'selected';
	            settings.dom.style.display = '';
	            break;
	    }
	};

	/**
	 * 平移旋转缩放控件事件
	 * @param {*} app 
	 */
	function TransformControlsEvent(app) {
	    BaseEvent.call(this, app);

	    this.mode = 'select';

	    this.objectPosition = null;
	    this.objectRotation = null;
	    this.objectScale = null;
	}

	TransformControlsEvent.prototype = Object.create(BaseEvent.prototype);
	TransformControlsEvent.prototype.constructor = TransformControlsEvent;

	TransformControlsEvent.prototype.start = function () {
	    var transformControls = this.app.editor.transformControls;

	    transformControls.addEventListener('change', this.onChange.bind(this));
	    transformControls.addEventListener('mouseDown', this.onMouseDown.bind(this));
	    transformControls.addEventListener('mouseUp', this.onMouseUp.bind(this));

	    this.app.on('objectSelected.' + this.id, this.onObjectSelected.bind(this));
	    this.app.on('changeMode.' + this.id, this.onChangeMode.bind(this));
	    this.app.on('snapChanged.' + this.id, this.onSnapChanged.bind(this));
	    this.app.on('spaceChanged.' + this.id, this.onSpaceChanged.bind(this));
	};

	TransformControlsEvent.prototype.stop = function () {
	    var transformControls = this.app.editor.transformControls;

	    transformControls.removeEventListener('change', this.onChange);
	    transformControls.removeEventListener('mouseDown', this.onMouseDown);
	    transformControls.removeEventListener('mouseUp', this.onMouseUp);

	    this.app.on('changeMode.' + this.id, null);
	    this.app.on('snapChanged.' + this.id, null);
	    this.app.on('spaceChanged.' + this.id, null);
	};

	/**
	 * 控件发生改变，需要更新包围盒位置，重绘场景
	 */
	TransformControlsEvent.prototype.onChange = function () {
	    var editor = this.app.editor;
	    var object = editor.transformControls.object;

	    if (object == null) {
	        this.app.call('render', this);
	        return;
	    }

	    // 重新设置包围盒位置
	    editor.selectionBox.setFromObject(object);

	    if (editor.helpers[object.id] !== undefined && !(editor.helpers[object.id] instanceof THREE.SkeletonHelper)) {
	        editor.helpers[object.id].update();
	    }

	    this.app.call('refreshSidebarObject3D', this, object);
	    this.app.call('render');
	};

	/**
	 * 点击鼠标，记录选中物体当前平移、旋转和缩放值
	 */
	TransformControlsEvent.prototype.onMouseDown = function () {
	    if (['translate', 'rotate', 'scale'].indexOf(this.mode) === -1) {
	        return;
	    }

	    var object = this.app.editor.transformControls.object;

	    this.objectPosition = object.position.clone();
	    this.objectRotation = object.rotation.clone();
	    this.objectScale = object.scale.clone();

	    this.app.editor.controls.enabled = false; // EditorControls
	};

	/**
	 * 抬起鼠标，更新选中物体的平移、旋转和缩放值
	 */
	TransformControlsEvent.prototype.onMouseUp = function () {
	    if (['translate', 'rotate', 'scale'].indexOf(this.mode) === -1) {
	        return;
	    }

	    var editor = this.app.editor;
	    var transformControls = editor.transformControls;
	    var object = transformControls.object;

	    if (object == null) {
	        return;
	    }

	    switch (transformControls.getMode()) {
	        case 'translate':
	            if (!this.objectPosition.equals(object.position)) {
	                editor.execute(new SetPositionCommand(object, object.position, this.objectPosition));
	            }
	            break;
	        case 'rotate':
	            if (!this.objectRotation.equals(object.rotation)) {
	                editor.execute(new SetRotationCommand(object, object.rotation, this.objectRotation));
	            }
	            break;
	        case 'scale':
	            if (!this.objectScale.equals(object.scale)) {
	                editor.execute(new SetScaleCommand(object, object.scale, this.objectScale));
	            }
	            break;
	    }

	    this.app.editor.controls.enabled = true; // EditorControls
	};

	/**
	 * 物体已经选中
	 * @param {*} object 选中的物体
	 */
	TransformControlsEvent.prototype.onObjectSelected = function (object) {
	    this.app.editor.transformControls.detach();

	    if (object && ['translate', 'rotate', 'scale'].indexOf(this.mode) > -1) {
	        this.app.editor.transformControls.attach(object);
	    }
	};

	/**
	 * 切换平移、旋转、缩放模式
	 * @param {*} mode 模式
	 */
	TransformControlsEvent.prototype.onChangeMode = function (mode) {
	    this.mode = mode;
	    var transformControls = this.app.editor.transformControls;

	    if (mode === 'translate' || mode === 'rotate' || mode === 'scale') { // 设置模式在选中物体上
	        transformControls.setMode(mode);
	        var object = this.app.editor.selected;
	        if (object != null) {
	            transformControls.attach(object);
	        }
	    } else { // 取消对选中物体平移、旋转、缩放
	        transformControls.detach();
	    }
	};

	/**
	 * 设置平移移动的大小
	 * @param {*} dist 
	 */
	TransformControlsEvent.prototype.onSnapChanged = function (dist) {
	    this.app.editor.transformControls.setTranslationSnap(dist);
	};

	/**
	 * 设置世界坐标系还是物体坐标系
	 * @param {*} space 
	 */
	TransformControlsEvent.prototype.onSpaceChanged = function (space) {
	    this.app.editor.transformControls.setSpace(space);
	};

	/**
	 * 更新场景编辑区信息事件
	 * @param {*} app 
	 */
	function UpdateSceneStatusEvent(app) {
	    BaseEvent.call(this, app);
	}

	UpdateSceneStatusEvent.prototype = Object.create(BaseEvent.prototype);
	UpdateSceneStatusEvent.prototype.constructor = UpdateSceneStatusEvent;

	UpdateSceneStatusEvent.prototype.start = function () {
	    this.app.on('objectAdded.' + this.id, this.onUpdateInfo.bind(this));
	    this.app.on('objectRemoved.' + this.id, this.onUpdateInfo.bind(this));
	    this.app.on('geometryChanged.' + this.id, this.onUpdateInfo.bind(this));
	};

	UpdateSceneStatusEvent.prototype.stop = function () {
	    this.app.on('objectAdded.' + this.id, null);
	    this.app.on('objectRemoved.' + this.id, null);
	    this.app.on('geometryChanged.' + this.id, null);
	};

	UpdateSceneStatusEvent.prototype.onUpdateInfo = function () {
	    var editor = this.app.editor;

	    var scene = editor.scene;

	    var objects = 0, vertices = 0, triangles = 0;

	    for (var i = 0, l = scene.children.length; i < l; i++) {
	        var object = scene.children[i];

	        object.traverseVisible(function (object) {
	            objects++;

	            if (object instanceof THREE.Mesh) {
	                var geometry = object.geometry;

	                if (geometry instanceof THREE.Geometry) {
	                    vertices += geometry.vertices.length;
	                    triangles += geometry.faces.length;
	                } else if (geometry instanceof THREE.BufferGeometry) {
	                    if (geometry.index !== null) {
	                        vertices += geometry.index.count * 3;
	                        triangles += geometry.index.count;
	                    } else {
	                        vertices += geometry.attributes.position.count;
	                        triangles += geometry.attributes.position.count / 3;
	                    }
	                }
	            }
	        });
	    }

	    var objectsText = UI$1.get('objectsText');
	    var verticesText = UI$1.get('verticesText');
	    var trianglesText = UI$1.get('trianglesText');

	    objectsText.setValue(objects.format());
	    verticesText.setValue(vertices.format());
	    trianglesText.setValue(triangles.format());
	};

	/**
	 * 渲染事件
	 * @param {*} app 
	 */
	function RenderEvent(app) {
	    BaseEvent.call(this, app);
	}

	RenderEvent.prototype = Object.create(BaseEvent.prototype);
	RenderEvent.prototype.constructor = RenderEvent;

	RenderEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('render.' + this.id, function () {
	        _this.onRender();
	    });
	    this.app.on('materialChanged.' + this.id, function (material) {
	        _this.onRender();
	    });
	    this.app.on('sceneGraphChanged.' + this.id, function () {
	        _this.onRender();
	    });
	    this.app.on('cameraChanged.' + this.id, function () {
	        _this.onRender();
	    });
	};

	RenderEvent.prototype.stop = function () {
	    this.app.on('render.' + this.id, null);
	    this.app.on('materialChanged.' + this.id, null);
	    this.app.on('sceneGraphChanged.' + this.id, null);
	    this.app.on('cameraChanged.' + this.id, null);
	};

	RenderEvent.prototype.onRender = function () {
	    var editor = this.app.editor;
	    var sceneHelpers = editor.sceneHelpers;
	    var scene = editor.scene;
	    var vrEffect = editor.vrEffect;
	    var vrControls = editor.vrControls;
	    var camera = editor.camera;
	    var renderer = editor.renderer;

	    sceneHelpers.updateMatrixWorld();
	    scene.updateMatrixWorld();

	    if (vrEffect && vrEffect.isPresenting) {
	        vrControls.update();

	        camera.updateMatrixWorld();

	        vrEffect.render(scene, vrCamera);
	        vrEffect.render(sceneHelpers, vrCamera);
	    } else {
	        renderer.render(scene, camera);

	        if (renderer instanceof THREE.RaytracingRenderer === false) {
	            renderer.render(sceneHelpers, camera);
	        }
	    }
	};

	/**
	 * 显示隐藏网格事件
	 * @param {*} app 
	 */
	function ShowGridChangedEvent(app) {
	    BaseEvent.call(this, app);
	}

	ShowGridChangedEvent.prototype = Object.create(BaseEvent.prototype);
	ShowGridChangedEvent.prototype.constructor = ShowGridChangedEvent;

	ShowGridChangedEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('showGridChanged.' + this.id, function (showGrid) {
	        _this.onShowGridChanged(showGrid);
	    });
	};

	ShowGridChangedEvent.prototype.stop = function () {
	    this.app.on('showGridChanged.' + this.id, null);
	};

	ShowGridChangedEvent.prototype.onShowGridChanged = function (showGrid) {
	    var grid = this.app.editor.grid;

	    grid.visible = showGrid;

	    this.app.call('render');
	};

	/**
	 * 雾效改变事件
	 * @param {*} app 
	 */
	function SceneFogChangedEvent(app) {
	    BaseEvent.call(this, app);
	    this.currentFogType = null;
	}

	SceneFogChangedEvent.prototype = Object.create(BaseEvent.prototype);
	SceneFogChangedEvent.prototype.constructor = SceneFogChangedEvent;

	SceneFogChangedEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('sceneFogChanged.' + this.id, function (fogType, fogColor, fogNear, fogFar, fogDensity) {
	        _this.onSceneFogChanged(fogType, fogColor, fogNear, fogFar, fogDensity);
	    });
	};

	SceneFogChangedEvent.prototype.stop = function () {
	    this.app.on('sceneFogChanged.' + this.id, null);
	};

	SceneFogChangedEvent.prototype.onSceneFogChanged = function (fogType, fogColor, fogNear, fogFar, fogDensity) {
	    var scene = this.app.editor.scene;

	    if (this.currentFogType !== fogType) {
	        switch (fogType) {
	            case 'None':
	                scene.fog = null;
	                break;
	            case 'Fog':
	                scene.fog = new THREE.Fog();
	                break;
	            case 'FogExp2':
	                scene.fog = new THREE.FogExp2();
	                break;
	        }

	        this.currentFogType = fogType;
	    }

	    if (scene.fog instanceof THREE.Fog) {
	        scene.fog.color.setHex(fogColor);
	        scene.fog.near = fogNear;
	        scene.fog.far = fogFar;
	    } else if (scene.fog instanceof THREE.FogExp2) {
	        scene.fog.color.setHex(fogColor);
	        scene.fog.density = fogDensity;
	    }

	    this.app.call('render');
	};

	/**
	 * 场景背景改变改变事件
	 * @param {*} app 
	 */
	function SceneBackgroundChangedEvent(app) {
	    BaseEvent.call(this, app);
	}

	SceneBackgroundChangedEvent.prototype = Object.create(BaseEvent.prototype);
	SceneBackgroundChangedEvent.prototype.constructor = SceneBackgroundChangedEvent;

	SceneBackgroundChangedEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('sceneBackgroundChanged.' + this.id, function (backgroundColor) {
	        _this.onSceneBackgroundChanged(backgroundColor);
	    });
	};

	SceneBackgroundChangedEvent.prototype.stop = function () {
	    this.app.on('sceneBackgroundChanged.' + this.id, null);
	};

	SceneBackgroundChangedEvent.prototype.onSceneBackgroundChanged = function (backgroundColor) {
	    var scene = this.app.editor.scene;

	    scene.background.setHex(backgroundColor);
	    this.app.call('render');
	};

	/**
	 * 帮助工具事件
	 * @param {*} app 
	 */
	function HelperEvent(app) {
	    BaseEvent.call(this, app);
	}

	HelperEvent.prototype = Object.create(BaseEvent.prototype);
	HelperEvent.prototype.constructor = HelperEvent;

	HelperEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('helperAdded.' + this.id, function (object) {
	        _this.onHelperAdded(object);
	    });
	    this.app.on('helperRemoved.' + this.id, function (object) {
	        _this.onHelperRemoved(object);
	    });
	};

	HelperEvent.prototype.stop = function () {
	    this.app.on('helperAdded.' + this.id, null);
	    this.app.on('helperRemoved.' + this.id, null);
	};

	HelperEvent.prototype.onHelperAdded = function (object) {
	    var objects = this.app.editor.objects;

	    objects.push(object.getObjectByName('picker'));
	};

	HelperEvent.prototype.onHelperRemoved = function (object) {
	    var objects = this.app.editor.objects;

	    objects.splice(objects.indexOf(object.getObjectByName('picker')), 1);
	};

	/**
	 * 物体事件
	 * @param {*} app 
	 */
	function ObjectEvent(app) {
	    BaseEvent.call(this, app);
	    this.box = new THREE.Box3();
	}

	ObjectEvent.prototype = Object.create(BaseEvent.prototype);
	ObjectEvent.prototype.constructor = ObjectEvent;

	ObjectEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('objectAdded.' + this.id, function (object) {
	        _this.onObjectAdded(object);
	    });
	    this.app.on('objectChanged.' + this.id, function (object) {
	        _this.onObjectChanged(object);
	    });
	    this.app.on('objectRemoved.' + this.id, function (object) {
	        _this.onObjectRemoved(object);
	    });
	    this.app.on('objectSelected.' + this.id, function (object) {
	        _this.onObjectSelected(object);
	    });
	    this.app.on('objectFocused.' + this.id, function (object) {
	        _this.onObjectFocused(object);
	    });
	};

	ObjectEvent.prototype.stop = function () {
	    this.app.on('objectAdded.' + this.id, null);
	    this.app.on('objectChanged.' + this.id, null);
	    this.app.on('objectRemoved.' + this.id, null);
	    this.app.on('objectSelected.' + this.id, null);
	    this.app.on('objectFocused.' + this.id, null);
	};

	ObjectEvent.prototype.onObjectAdded = function (object) {
	    var objects = this.app.editor.objects;

	    object.traverse(function (child) {
	        objects.push(child);
	    });
	};

	ObjectEvent.prototype.onObjectChanged = function (object) {
	    var editor = this.app.editor;
	    var selectionBox = editor.selectionBox;
	    var transformControls = editor.transformControls;

	    if (editor.selected === object && object.useSelectionBox !== false) {
	        selectionBox.setFromObject(object);
	        transformControls.update();
	    }

	    if (object instanceof THREE.PerspectiveCamera) {
	        object.updateProjectionMatrix();
	    }

	    if (editor.helpers[object.id] !== undefined && !(editor.helpers[object.id] instanceof THREE.SkeletonHelper)) {
	        editor.helpers[object.id].update();
	    }

	    this.app.call('render');
	};

	ObjectEvent.prototype.onObjectRemoved = function (object) {
	    var objects = this.app.editor.objects;

	    object.traverse(function (child) {
	        objects.splice(objects.indexOf(child), 1);
	    });
	};

	ObjectEvent.prototype.onObjectSelected = function (object) {
	    var editor = this.app.editor;
	    var selectionBox = editor.selectionBox;
	    var scene = editor.scene;
	    var box = this.box;

	    selectionBox.visible = false;

	    if (object !== null && object !== scene) {
	        box.setFromObject(object);
	        if (box.isEmpty() === false && object.useSelectionBox !== false) {
	            selectionBox.setFromObject(object);
	            selectionBox.visible = true;
	        }
	    }

	    this.app.call('render');
	};

	ObjectEvent.prototype.onObjectFocused = function (object) {
	    var controls = this.app.editor.controls;

	    controls.focus(object);
	};

	/**
	 * 几何体改变事件
	 * @param {*} app 
	 */
	function GeometryEvent(app) {
	    BaseEvent.call(this, app);
	}

	GeometryEvent.prototype = Object.create(BaseEvent.prototype);
	GeometryEvent.prototype.constructor = GeometryEvent;

	GeometryEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('geometryChanged.' + this.id, function (object) {
	        _this.onGeometryChanged(object);
	    });
	};

	GeometryEvent.prototype.stop = function () {
	    this.app.on('geometryChanged.' + this.id, null);
	};

	GeometryEvent.prototype.onGeometryChanged = function (object) {
	    var selectionBox = this.app.editor.selectionBox;

	    if (object !== undefined && object.useSelectionBox !== false) {
	        selectionBox.setFromObject(object);
	    }

	    this.app.call('render');
	};

	/**
	 * 选取事件
	 * @param {*} app 
	 */
	function PickEvent(app) {
	    BaseEvent.call(this, app);

	    this.raycaster = new THREE.Raycaster();
	    this.mouse = new THREE.Vector2();

	    this.onDownPosition = new THREE.Vector2();
	    this.onUpPosition = new THREE.Vector2();
	    this.onDoubleClickPosition = new THREE.Vector2();
	}

	PickEvent.prototype = Object.create(BaseEvent.prototype);
	PickEvent.prototype.constructor = PickEvent;

	PickEvent.prototype.start = function () {
	    var container = this.app.viewport.container;

	    container.dom.addEventListener('mousedown', this.onMouseDown.bind(this), false);
	    container.dom.addEventListener('touchstart', this.onTouchStart.bind(this), false);
	    container.dom.addEventListener('dblclick', this.onDoubleClick.bind(this), false);
	};

	PickEvent.prototype.stop = function () {
	    var container = this.app.viewport.container;

	    container.dom.removeEventListener('mousedown', this.onMouseDown, false);
	    container.dom.removeEventListener('touchstart', this.onTouchStart, false);
	    container.dom.removeEventListener('dblclick', this.onDoubleClick, false);
	};

	PickEvent.prototype.onMouseDown = function (event) {
	    var container = this.app.viewport.container;

	    event.preventDefault();

	    var array = this.getMousePosition(container.dom, event.clientX, event.clientY);
	    this.onDownPosition.fromArray(array);

	    document.addEventListener('mouseup', this.onMouseUp.bind(this), false);
	};

	PickEvent.prototype.onMouseUp = function (event) {
	    var container = this.app.viewport.container;

	    var array = this.getMousePosition(container.dom, event.clientX, event.clientY);
	    this.onUpPosition.fromArray(array);

	    this.handleClick();

	    document.removeEventListener('mouseup', this.onMouseUp, false);
	};

	PickEvent.prototype.onTouchStart = function (event) {
	    var container = this.app.viewport.container;

	    var touch = event.changedTouches[0];

	    var array = this.getMousePosition(container.dom, touch.clientX, touch.clientY);
	    this.onDownPosition.fromArray(array);

	    document.addEventListener('touchend', this.onTouchEnd.bind(this), false);
	};

	PickEvent.prototype.onTouchEnd = function (event) {
	    var container = this.app.viewport.container;

	    var touch = event.changedTouches[0];

	    var array = this.getMousePosition(container.dom, touch.clientX, touch.clientY);
	    this.onUpPosition.fromArray(array);

	    this.handleClick();

	    document.removeEventListener('touchend', this.onTouchEnd, false);
	};

	PickEvent.prototype.onDoubleClick = function (event) {
	    var container = this.app.viewport.container;
	    var objects = this.app.editor.objects;

	    var array = this.getMousePosition(container.dom, event.clientX, event.clientY);
	    this.onDoubleClickPosition.fromArray(array);

	    var intersects = this.getIntersects(this.onDoubleClickPosition, objects);

	    if (intersects.length > 0) {
	        var intersect = intersects[0];
	        this.app.call('objectFocused', this, intersect.object);
	    }
	};

	PickEvent.prototype.getIntersects = function (point, objects) {
	    this.mouse.set((point.x * 2) - 1, -(point.y * 2) + 1);
	    this.raycaster.setFromCamera(this.mouse, this.app.editor.camera);
	    return this.raycaster.intersectObjects(objects);
	};

	PickEvent.prototype.getMousePosition = function (dom, x, y) {
	    var rect = dom.getBoundingClientRect();
	    return [(x - rect.left) / rect.width, (y - rect.top) / rect.height];
	};

	PickEvent.prototype.handleClick = function () {
	    var container = this.app.viewport.container;
	    var editor = this.app.editor;
	    var objects = editor.objects;

	    if (this.onDownPosition.distanceTo(this.onUpPosition) === 0) {
	        var intersects = this.getIntersects(this.onUpPosition, objects);

	        if (intersects.length > 0) {
	            var object = intersects[0].object;

	            if (object.userData.object !== undefined) {
	                // helper
	                editor.select(object.userData.object);
	            } else {
	                editor.select(object);
	            }
	        } else {
	            editor.select(null);
	        }

	        this.app.call('render');
	    }
	};

	/**
	 * 渲染器改变事件
	 * @param {*} app 
	 */
	function RendererChangedEvent(app) {
	    BaseEvent.call(this, app);

	    this.vrControls = null;
	    this.vrCamera = null;
	    this.vrEffect = null;
	}

	RendererChangedEvent.prototype = Object.create(BaseEvent.prototype);
	RendererChangedEvent.prototype.constructor = RendererChangedEvent;

	RendererChangedEvent.prototype.start = function () {
	    this.app.on('rendererChanged.' + this.id, this.onRendererChanged.bind(this));
	};

	RendererChangedEvent.prototype.stop = function () {
	    this.app.on('rendererChanged.' + this.id, null);
	};

	RendererChangedEvent.prototype.onRendererChanged = function (newRenderer) {
	    var editor = this.app.editor;
	    var renderer = this.app.editor.renderer;
	    var container = this.app.viewport.container;

	    if (renderer != null) {
	        container.dom.removeChild(renderer.domElement);
	    }

	    renderer = newRenderer;
	    this.app.editor.renderer = renderer;

	    renderer.autoClear = false;
	    renderer.autoUpdateScene = false;
	    renderer.setPixelRatio(window.devicePixelRatio);
	    renderer.setSize(container.dom.offsetWidth, container.dom.offsetHeight);

	    container.dom.appendChild(renderer.domElement);

	    if (renderer.vr && renderer.vr.enabled) {
	        this.vrCamera = new THREE.PerspectiveCamera();
	        this.vrCamera.projectionMatrix = editor.camera.projectionMatrix;
	        editor.camera.add(this.vrCamera);
	        editor.vrCamera = this.vrCamera;

	        this.vrControls = new THREE.VRControls(this.vrCamera);
	        editor.vrControls = this.vrControls;

	        this.vrEffect = new THREE.VREffect(renderer);
	        editor.vrEffect = this.vrEffect;

	        var _this = this;

	        window.addEventListener('vrdisplaypresentchange', function (event) {
	            _this.vrEffect.isPresenting ? _this.app.call('enteredVR', _this) : _this.app.call('exitedVR', _this);
	        }, false);
	    }

	    this.app.call('render');
	};

	/**
	 * 渲染事件
	 * @param {*} app 
	 */
	function WindowResizeEvent(app) {
	    BaseEvent.call(this, app);
	}

	WindowResizeEvent.prototype = Object.create(BaseEvent.prototype);
	WindowResizeEvent.prototype.constructor = WindowResizeEvent;

	WindowResizeEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('windowResize.' + this.id, function () {
	        _this.onWindowResize();
	    });
	};

	WindowResizeEvent.prototype.stop = function () {
	    this.app.on('windowResize.' + this.id, null);
	};

	WindowResizeEvent.prototype.onWindowResize = function () {
	    var editor = this.app.editor;
	    var container = this.app.viewport.container;
	    var camera = editor.camera;
	    var renderer = editor.renderer;

	    editor.DEFAULT_CAMERA.aspect = container.dom.offsetWidth / container.dom.offsetHeight;
	    editor.DEFAULT_CAMERA.updateProjectionMatrix();

	    camera.aspect = container.dom.offsetWidth / container.dom.offsetHeight;
	    camera.updateProjectionMatrix();

	    renderer.setSize(container.dom.offsetWidth, container.dom.offsetHeight);

	    this.app.call('render', this);
	};

	/**
	 * 主题改变事件
	 * @param {*} app 
	 */
	function ThemeChangedEvent(app) {
	    BaseEvent.call(this, app);
	}

	ThemeChangedEvent.prototype = Object.create(BaseEvent.prototype);
	ThemeChangedEvent.prototype.constructor = ThemeChangedEvent;

	ThemeChangedEvent.prototype.start = function () {
	    this.app.on('themeChanged.' + this.id, this.onThemeChanged.bind(this));
	};

	ThemeChangedEvent.prototype.stop = function () {
	    this.app.on('themeChanged.' + this.id, null);
	};

	ThemeChangedEvent.prototype.onThemeChanged = function (value) {
	    var editor = this.app.editor;
	    var sceneHelpers = editor.sceneHelpers;
	    var grid = editor.grid;

	    switch (value) {
	        case 'assets/css/light.css':
	            sceneHelpers.remove(grid);
	            grid = new THREE.GridHelper(60, 60, 0x444444, 0x888888);
	            sceneHelpers.add(grid);
	            editor.grid = grid;
	            break;
	        case 'assets/css/dark.css':
	            sceneHelpers.remove(grid);
	            grid = new THREE.GridHelper(60, 60, 0xbbbbbb, 0x888888);
	            sceneHelpers.add(grid);
	            editor.grid = grid;
	            break;
	    }

	    this.app.call('render');
	};

	/**
	 * 编辑器控件事件
	 * @param {*} app 
	 */
	function EditorControlsEvent(app) {
	    BaseEvent.call(this, app);
	}

	EditorControlsEvent.prototype = Object.create(BaseEvent.prototype);
	EditorControlsEvent.prototype.constructor = EditorControlsEvent;

	EditorControlsEvent.prototype.start = function () {
	    var controls = this.app.editor.controls;

	    controls.addEventListener('change', this.onChange.bind(this));

	    this.app.on('editorCleared.' + this.id, this.onEditorCleared.bind(this));
	};

	EditorControlsEvent.prototype.stop = function () {
	    var controls = this.app.editor.controls;

	    controls.removeEventListener('change', this.onChange);

	    this.app.on('editorCleared.' + this.id, null);
	};

	EditorControlsEvent.prototype.onChange = function () {
	    var editor = this.app.editor;
	    var transformControls = editor.transformControls;
	    var camera = editor.camera;

	    transformControls.update();

	    this.app.call('cameraChanged', this, camera);
	};

	EditorControlsEvent.prototype.onEditorCleared = function () {
	    var controls = this.app.editor.controls;

	    controls.center.set(0, 0, 0);

	    this.app.call('render');
	};

	/**
	 * 网格设置改变事件
	 * @param {*} app 
	 */
	function GridChangeEvent(app) {
	    BaseEvent.call(this, app);
	}

	GridChangeEvent.prototype = Object.create(BaseEvent.prototype);
	GridChangeEvent.prototype.constructor = GridChangeEvent;

	GridChangeEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('gridChange.' + this.id, function () {
	        _this.onGridChange(this);
	    });
	};

	GridChangeEvent.prototype.stop = function () {
	    this.app.on('gridChange.' + this.id, null);
	};

	GridChangeEvent.prototype.onGridChange = function (statusBar) {
	    var grid = statusBar.grid;
	    var snap = statusBar.snap;
	    var local = statusBar.local;
	    var showGrid = statusBar.showGrid;

	    this.app.call('snapChanged', this, snap.getValue() === true ? grid.getValue() : null);
	    this.app.call('spaceChanged', this, local.getValue() === true ? 'local' : 'world');
	    this.app.call('showGridChanged', this, showGrid.getValue());
	};

	/**
	 * CodeMirror改变事件
	 * @param {*} app 
	 */
	function CodeMirrorChangeEvent(app) {
	    BaseEvent.call(this, app);
	    this.delay = null;
	    this.errorLines = [];
	    this.widgets = [];
	}

	CodeMirrorChangeEvent.prototype = Object.create(BaseEvent.prototype);
	CodeMirrorChangeEvent.prototype.constructor = CodeMirrorChangeEvent;

	CodeMirrorChangeEvent.prototype.start = function () {
	    var _this = this;
	    this.app.on('codeMirrorChange.' + this.id, function (codemirror, currentMode, currentScript, currentObject) {
	        _this.onCodeMirrorChange(codemirror, currentMode, currentScript, currentObject);
	    });
	};

	CodeMirrorChangeEvent.prototype.stop = function () {
	    this.app.on('codeMirrorChange.' + this.id, null);
	};

	CodeMirrorChangeEvent.prototype.onCodeMirrorChange = function (codemirror, currentMode, currentScript, currentObject) {
	    if (codemirror.state.focused === false) {
	        return;
	    }

	    clearTimeout(this.delay);

	    var _this = this;

	    this.delay = setTimeout(function () {
	        var value = codemirror.getValue();

	        if (!_this.validate(codemirror, currentMode, currentObject, value)) {
	            return;
	        }

	        if (typeof (currentScript) === 'object') {
	            if (value !== currentScript.source) {
	                _this.app.editor.execute(new SetScriptValueCommand(currentObject, currentScript, 'source', value, codemirror.getCursor(), codemirror.getScrollInfo()));
	            }
	            return;
	        }

	        if (currentScript !== 'programInfo') {
	            return;
	        }

	        var json = JSON.parse(value);

	        if (JSON.stringify(currentObject.material.defines) !== JSON.stringify(json.defines)) {
	            var cmd = new SetMaterialValueCommand(currentObject, 'defines', json.defines);
	            cmd.updatable = false;
	            editor.execute(cmd);
	        }
	        if (JSON.stringify(currentObject.material.uniforms) !== JSON.stringify(json.uniforms)) {
	            var cmd = new SetMaterialValueCommand(currentObject, 'uniforms', json.uniforms);
	            cmd.updatable = false;
	            editor.execute(cmd);
	        }
	        if (JSON.stringify(currentObject.material.attributes) !== JSON.stringify(json.attributes)) {
	            var cmd = new SetMaterialValueCommand(currentObject, 'attributes', json.attributes);
	            cmd.updatable = false;
	            editor.execute(cmd);
	        }
	    }, 1000);
	};

	CodeMirrorChangeEvent.prototype.validate = function (codemirror, currentMode, currentObject, string) {
	    var errorLines = this.errorLines;
	    var widgets = this.widgets;

	    var valid;
	    var errors = [];

	    var _this = this;

	    return codemirror.operation(function () {

	        while (errorLines.length > 0) {
	            codemirror.removeLineClass(errorLines.shift(), 'background', 'errorLine');
	        }

	        while (widgets.length > 0) {
	            codemirror.removeLineWidget(widgets.shift());
	        }

	        //
	        switch (currentMode) {
	            case 'javascript':
	                try {
	                    var syntax = esprima.parse(string, { tolerant: true });
	                    errors = syntax.errors;
	                } catch (error) {
	                    errors.push({
	                        lineNumber: error.lineNumber - 1,
	                        message: error.message
	                    });
	                }

	                for (var i = 0; i < errors.length; i++) {
	                    var error = errors[i];
	                    error.message = error.message.replace(/Line [0-9]+: /, '');
	                }
	                break;
	            case 'json':
	                errors = [];

	                jsonlint.parseError = function (message, info) {
	                    message = message.split('\n')[3];
	                    errors.push({
	                        lineNumber: info.loc.first_line - 1,
	                        message: message
	                    });
	                };

	                try {
	                    jsonlint.parse(string);
	                } catch (error) {
	                    // ignore failed error recovery
	                }
	                break;
	            case 'glsl':
	                try {
	                    var shaderType = currentScript === 'vertexShader' ?
	                        glslprep.Shader.VERTEX : glslprep.Shader.FRAGMENT;
	                    glslprep.parseGlsl(string, shaderType);
	                } catch (error) {
	                    if (error instanceof glslprep.SyntaxError) {
	                        errors.push({
	                            lineNumber: error.line,
	                            message: "Syntax Error: " + error.message
	                        });
	                    } else {
	                        console.error(error.stack || error);
	                    }
	                }

	                if (errors.length !== 0) {
	                    break;
	                }
	                if (_this.app.editor.renderer instanceof THREE.WebGLRenderer === false) {
	                    break;
	                }

	                currentObject.material[currentScript] = string;
	                currentObject.material.needsUpdate = true;

	                _this.app.call('materialChanged', _this, currentObject.material);

	                var programs = _this.app.editor.renderer.info.programs;

	                valid = true;
	                var parseMessage = /^(?:ERROR|WARNING): \d+:(\d+): (.*)/g;

	                for (var i = 0, n = programs.length; i !== n; ++i) {
	                    var diagnostics = programs[i].diagnostics;

	                    if (diagnostics === undefined || diagnostics.material !== currentObject.material) {
	                        continue;
	                    }

	                    if (!diagnostics.runnable) {
	                        valid = false;
	                    }

	                    var shaderInfo = diagnostics[currentScript];
	                    var lineOffset = shaderInfo.prefix.split(/\r\n|\r|\n/).length;

	                    while (true) {
	                        var parseResult = parseMessage.exec(shaderInfo.log);
	                        if (parseResult === null) break;

	                        errors.push({
	                            lineNumber: parseResult[1] - lineOffset,
	                            message: parseResult[2]
	                        });
	                    } // messages
	                    break;
	                } // programs

	        } // mode switch

	        for (var i = 0; i < errors.length; i++) {
	            var error = errors[i];

	            var message = document.createElement('div');
	            message.className = 'esprima-error';
	            message.textContent = error.message;

	            var lineNumber = Math.max(error.lineNumber, 0);
	            errorLines.push(lineNumber);

	            codemirror.addLineClass(lineNumber, 'background', 'errorLine');

	            var widget = codemirror.addLineWidget(lineNumber, message);
	            widgets.push(widget);
	        }

	        return valid !== undefined ? valid : errors.length === 0;
	    });
	};

	/**
	 * 播放器事件
	 * @param {*} app 
	 */
	function PlayerEvent(app) {
	    BaseEvent.call(this, app);
	}

	PlayerEvent.prototype = Object.create(BaseEvent.prototype);
	PlayerEvent.prototype.constructor = PlayerEvent;

	PlayerEvent.prototype.start = function () {
	    window.addEventListener('resize', this.onResize.bind(this));

	    this.app.on('startPlayer.' + this.id, this.onStartPlayer.bind(this));
	    this.app.on('stopPlayer.' + this.id, this.onStopPlayer.bind(this));
	};

	PlayerEvent.prototype.stop = function () {
	    window.removeEventListener('resize', this.onResize);

	    this.app.on('startPlayer.' + this.id, null);
	    this.app.on('stopPlayer.' + this.id, null);
	};

	PlayerEvent.prototype.onResize = function () {
	    var player = this.app.player.player;
	    var container = this.app.player.container;

	    player.setSize(container.dom.clientWidth, container.dom.clientHeight);
	};

	PlayerEvent.prototype.onStartPlayer = function () {
	    var editor = this.app.editor;
	    var player = this.app.player.player;
	    var container = this.app.player.container;

	    container.dom.style.display = '';

	    player.load(editor.toJSON());
	    player.setSize(container.dom.clientWidth, container.dom.clientHeight);
	    player.play();
	};

	PlayerEvent.prototype.onStopPlayer = function () {
	    var player = this.app.player.player;
	    var container = this.app.player.container;

	    container.dom.style.display = 'none';

	    player.stop();
	    player.dispose();
	};

	/**
	 * 事件执行器
	 */
	function EventDispatcher(app) {
	    this.app = app;
	    this.dispatch = dispatch.apply(dispatch, EventList);
	    this.addDomEventListener();

	    this.events = [
	        new AnimateEvent(this.app),

	        // Application中的事件
	        new DragOverEvent(this.app),
	        new DropEvent(this.app),
	        new KeyDownEvent(this.app),
	        new ResizeEvent(this.app),
	        new MessageEvent(this.app),
	        new LoadFromHashEvent(this.app),
	        new AutoSaveEvent(this.app),
	        new VREvent(this.app),
	        new InitAppEvent(this.app),

	        // Editor中的事件
	        new SetThemeEvent(this.app),
	        new SetSceneEvent(this.app),
	        new AddObjectEvent(this.app),
	        new MoveObjectEvent(this.app),
	        new NameObjectEvent(this.app),
	        new RemoveObjectEvent(this.app),
	        new AddGeometryEvent(this.app),
	        new SetGeometryNameEvent(this.app),
	        new AddMaterialEvent(this.app),
	        new SetMaterialNameEvent(this.app),
	        new AddTextureEvent(this.app),
	        new AddHelperEvent(this.app),
	        new RemoveHelperEvent(this.app),
	        new AddScriptEvent(this.app),
	        new RemoveScriptEvent(this.app),
	        new SelectEvent(this.app),
	        new ClearEvent(this.app),
	        new LoadEvent(this.app),
	        new SaveEvent(this.app),

	        // 工具栏
	        new SelectModeEvent(this.app),
	        new TranslateModeEvent(this.app),
	        new RotateModeEvent(this.app),
	        new ScaleModeEvent(this.app),

	        new AnchorPointEvent(this.app),
	        new HandModeEvent(this.app),
	        new ModelEvent(this.app),
	        new PathModeEvent(this.app),

	        // menubar中的事件
	        new NewSceneEvent(this.app),
	        new LoadSceneEvent(this.app),
	        new SaveSceneEvent(this.app),
	        new PublishSceneEvent(this.app),

	        new UndoEvent(this.app),
	        new RedoEvent(this.app),
	        new ClearHistoryEvent(this.app),
	        new CloneEvent(this.app),
	        new DeleteEvent(this.app),
	        new MinifyShaderEvent(this.app),

	        new AddGroupEvent(this.app),
	        new AddPlaneEvent(this.app),
	        new AddBoxEvent(this.app),
	        new AddCircleEvent(this.app),
	        new AddCylinderEvent(this.app),
	        new AddSphereEvent(this.app),
	        new AddIcosahedronEvent(this.app),
	        new AddTorusEvent(this.app),
	        new AddTorusKnotEvent(this.app),
	        new AddTeaportEvent(this.app),
	        new AddLatheEvent(this.app),
	        new AddSpriteEvent(this.app),
	        new AddPointLightEvent(this.app),
	        new AddSpotLightEvent(this.app),
	        new AddDirectionalLightEvent(this.app),
	        new AddHemisphereLightEvent(this.app),
	        new AddAmbientLightEvent(this.app),
	        new AddPerspectiveCameraEvent(this.app),

	        new AddAssetEvent(this.app),
	        new ImportAssetEvent(this.app),
	        new ExportGeometryEvent(this.app),
	        new ExportObjectEvent(this.app),
	        new ExportSceneEvent(this.app),
	        new ExportGLTFEvent(this.app),
	        new ExportMMDEvent(this.app),
	        new ExportOBJEvent(this.app),
	        new ExportPLYEvent(this.app),
	        new ExportSTLBEvent(this.app),
	        new ExportSTLEvent(this.app),

	        new AddPersonEvent(this.app),
	        new AddFireEvent(this.app),
	        new AddSmokeEvent(this.app),

	        new PlayEvent(this.app),

	        new VRModeEvent(this.app),

	        new ExampleEvent(this.app),

	        new SourceCodeEvent(this.app),
	        new AboutEvent(this.app),

	        new SavingStatusEvent(this.app),

	        // 侧边栏
	        new ObjectPanelEvent(this.app),
	        new GeometryPanelEvent(this.app),
	        new MaterialPanelEvent(this.app),
	        new HistoryPanelEvent(this.app),
	        new ProjectPanelEvent(this.app),
	        new PropertyPanelEvent(this.app),
	        new ScenePanelEvent(this.app),
	        new ScriptPanelEvent(this.app),
	        new SidebarEvent(this.app),

	        // viewport中的事件
	        new TransformControlsEvent(this.app),
	        new UpdateSceneStatusEvent(this.app),
	        new RenderEvent(this.app),
	        new ShowGridChangedEvent(this.app),
	        new SceneFogChangedEvent(this.app),
	        new SceneBackgroundChangedEvent(this.app),
	        new HelperEvent(this.app),
	        new ObjectEvent(this.app),
	        new GeometryEvent(this.app),
	        new PickEvent(this.app),
	        new RendererChangedEvent(this.app),
	        new WindowResizeEvent(this.app),
	        new ThemeChangedEvent(this.app),
	        new EditorControlsEvent(this.app),

	        // 状态栏事件
	        new GridChangeEvent(this.app),

	        // 代码编辑器中的事件
	        new CodeMirrorChangeEvent(this.app),

	        // 播放器中的事件
	        new PlayerEvent(this.app),
	    ];
	}

	EventDispatcher.prototype = Object.create(BaseEvent.prototype);
	EventDispatcher.prototype.constructor = EventDispatcher;

	/**
	 * 启动
	 */
	EventDispatcher.prototype.start = function () {
	    this.events.forEach(function (n) {
	        n.start();
	    });
	};

	/**
	 * 停止
	 */
	EventDispatcher.prototype.stop = function () {
	    this.events.forEach(function (n) {
	        n.stop();
	    });
	};

	/**
	 * 执行事件
	 * @param {*} eventName 
	 * @param {*} _this 
	 * @param {*} others 
	 */
	EventDispatcher.prototype.call = function (eventName, _this, ...others) {
	    this.dispatch.call(eventName, _this, ...others);
	};

	/**
	 * 监听事件
	 * @param {*} eventName 
	 * @param {*} callback 
	 */
	EventDispatcher.prototype.on = function (eventName, callback) {
	    this.dispatch.on(eventName, callback);
	};

	/**
	 * 监听dom事件
	 */
	EventDispatcher.prototype.addDomEventListener = function () {
	    var container = this.app.container;
	    container.addEventListener('click', (event) => {
	        this.dispatch.call('click', this, event);
	    });
	    container.addEventListener('contextmenu', (event) => {
	        this.dispatch.call('contextmenu', this, event);
	        event.preventDefault();
	        return false;
	    });
	    container.addEventListener('dblclick', (event) => {
	        this.dispatch.call('dblclick', this, event);
	    });
	    document.addEventListener('keydown', (event) => {
	        this.dispatch.call('keydown', this, event);
	    });
	    document.addEventListener('keyup', (event) => {
	        this.dispatch.call('keyup', this, event);
	    });
	    container.addEventListener('mousedown', (event) => {
	        this.dispatch.call('mousedown', this, event);
	    });
	    container.addEventListener('mousemove', (event) => {
	        this.dispatch.call('mousemove', this, event);
	    });
	    container.addEventListener('mouseup', (event) => {
	        this.dispatch.call('mouseup', this, event);
	    });
	    container.addEventListener('mousewheel', (event) => {
	        this.dispatch.call('mousewheel', this, event);
	    });
	    window.addEventListener('resize', (event) => {
	        this.dispatch.call('resize', this, event);
	    }, false);
	    document.addEventListener('dragover', (event) => {
	        this.dispatch.call('dragover', this, event);
	    }, false);
	    document.addEventListener('drop', (event) => {
	        this.dispatch.call('drop', this, event);
	    }, false);
	    window.addEventListener('message', (event) => {
	        this.dispatch.call('message', this, event);
	    });
	};

	/**
	 * 系统配置
	 * @author mrdoob / http://mrdoob.com/
	 */
	function Config(name) {
	    var storage = {
	        'autosave': true,
	        'theme': 'assets/css/light.css',

	        'project/renderer': 'WebGLRenderer',
	        'project/renderer/antialias': true,
	        'project/renderer/gammaInput': false,
	        'project/renderer/gammaOutput': false,
	        'project/renderer/shadows': true,
	        'project/vr': false,

	        'settings/history': false
	    };

	    if (window.localStorage[name] === undefined) {
	        window.localStorage[name] = JSON.stringify(storage);
	    } else {
	        var data = JSON.parse(window.localStorage[name]);

	        for (var key in data) {
	            storage[key] = data[key];
	        }
	    }

	    return {
	        getKey: function (key) {
	            return storage[key];
	        },

	        setKey: function () { // key, value, key, value ...
	            for (var i = 0, l = arguments.length; i < l; i += 2) {
	                storage[arguments[i]] = arguments[i + 1];
	            }

	            window.localStorage[name] = JSON.stringify(storage);

	            console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', '保存配置到LocalStorage。');
	        },

	        clear: function () {
	            delete window.localStorage[name];
	        },

	        toJSON: function () {
	            return storage;
	        }
	    };
	}

	/**
	 * 历史记录
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */
	function History(editor) {
	    this.app = editor.app;

	    this.editor = editor;
	    this.undos = [];
	    this.redos = [];
	    this.lastCmdTime = new Date();
	    this.idCounter = 0;

	    this.historyDisabled = false;
	    this.config = editor.config;

	    //Set editor-reference in Command

	    Command.call(this, editor);

	    var scope = this;

	    this.app.on('startPlayer.History', function () {
	        scope.historyDisabled = true;
	    });

	    this.app.on('stopPlayer.History', function () {
	        scope.historyDisabled = false;
	    });
	}
	History.prototype = Object.create(Command.prototype);

	Object.assign(History.prototype, {

	    constructor: History,

	    execute: function (cmd, optionalName) {

	        var lastCmd = this.undos[this.undos.length - 1];
	        var timeDifference = new Date().getTime() - this.lastCmdTime.getTime();

	        var isUpdatableCmd = lastCmd &&
	            lastCmd.updatable &&
	            cmd.updatable &&
	            lastCmd.object === cmd.object &&
	            lastCmd.type === cmd.type &&
	            lastCmd.script === cmd.script &&
	            lastCmd.attributeName === cmd.attributeName;

	        if (isUpdatableCmd && cmd.type === "SetScriptValueCommand") {

	            // When the cmd.type is "SetScriptValueCommand" the timeDifference is ignored

	            lastCmd.update(cmd);
	            cmd = lastCmd;

	        } else if (isUpdatableCmd && timeDifference < 500) {

	            lastCmd.update(cmd);
	            cmd = lastCmd;

	        } else {

	            // the command is not updatable and is added as a new part of the history

	            this.undos.push(cmd);
	            cmd.id = ++this.idCounter;

	        }
	        cmd.name = (optionalName !== undefined) ? optionalName : cmd.name;
	        cmd.execute();
	        cmd.inMemory = true;

	        if (this.config.getKey('settings/history')) {

	            cmd.json = cmd.toJSON();	// serialize the cmd immediately after execution and append the json to the cmd

	        }
	        this.lastCmdTime = new Date();

	        // clearing all the redo-commands

	        this.redos = [];
	        this.app.call('historyChanged', this, cmd);

	    },

	    undo: function () {
	        if (this.historyDisabled) {
	            UI$1.msg("场景启动时撤销/重做将被禁用。");
	            return;
	        }

	        var cmd = undefined;

	        if (this.undos.length > 0) {
	            cmd = this.undos.pop();

	            if (cmd.inMemory === false) {
	                cmd.fromJSON(cmd.json);
	            }
	        }

	        if (cmd !== undefined) {
	            cmd.undo();
	            this.redos.push(cmd);
	            this.app.call('historyChanged', this, cmd);
	        }

	        return cmd;
	    },

	    redo: function () {
	        if (this.historyDisabled) {
	            UI$1.msg("场景启动时撤销/重做将被禁用。");
	            return;
	        }

	        var cmd = undefined;

	        if (this.redos.length > 0) {
	            cmd = this.redos.pop();

	            if (cmd.inMemory === false) {
	                cmd.fromJSON(cmd.json);
	            }
	        }

	        if (cmd !== undefined) {
	            cmd.execute();
	            this.undos.push(cmd);
	            this.app.call('historyChanged', this, cmd);
	        }

	        return cmd;
	    },

	    toJSON: function () {
	        var history = {};
	        history.undos = [];
	        history.redos = [];

	        if (!this.config.getKey('settings/history')) {
	            return history;
	        }

	        // Append Undos to History
	        for (var i = 0; i < this.undos.length; i++) {
	            if (this.undos[i].hasOwnProperty("json")) {
	                history.undos.push(this.undos[i].json);
	            }
	        }

	        // Append Redos to History
	        for (var i = 0; i < this.redos.length; i++) {
	            if (this.redos[i].hasOwnProperty("json")) {
	                history.redos.push(this.redos[i].json);
	            }
	        }

	        return history;
	    },

	    fromJSON: function (json) {
	        if (json === undefined) return;

	        for (var i = 0; i < json.undos.length; i++) {
	            var cmdJSON = json.undos[i];
	            var cmd = new window[cmdJSON.type]();	// creates a new object of type "json.type"
	            cmd.json = cmdJSON;
	            cmd.id = cmdJSON.id;
	            cmd.name = cmdJSON.name;
	            this.undos.push(cmd);
	            this.idCounter = (cmdJSON.id > this.idCounter) ? cmdJSON.id : this.idCounter; // set last used idCounter
	        }

	        for (var i = 0; i < json.redos.length; i++) {
	            var cmdJSON = json.redos[i];
	            var cmd = new window[cmdJSON.type]();	// creates a new object of type "json.type"
	            cmd.json = cmdJSON;
	            cmd.id = cmdJSON.id;
	            cmd.name = cmdJSON.name;
	            this.redos.push(cmd);
	            this.idCounter = (cmdJSON.id > this.idCounter) ? cmdJSON.id : this.idCounter; // set last used idCounter
	        }

	        // Select the last executed undo-command
	        this.app.call('historyChanged', this, this.undos[this.undos.length - 1]);
	    },

	    clear: function () {
	        this.undos = [];
	        this.redos = [];
	        this.idCounter = 0;

	        this.app.call('historyChanged', this);
	    },

	    goToState: function (id) {
	        if (this.historyDisabled) {
	            UI$1.msg("场景启动时撤销/重做将被禁用。");
	            return;
	        }

	        var cmd = this.undos.length > 0 ? this.undos[this.undos.length - 1] : undefined;	// next cmd to pop

	        if (cmd === undefined || id > cmd.id) {
	            cmd = this.redo();
	            while (cmd !== undefined && id > cmd.id) {
	                cmd = this.redo();
	            }
	        } else {
	            while (true) {
	                cmd = this.undos[this.undos.length - 1];	// next cmd to pop
	                if (cmd === undefined || id === cmd.id) break;
	                cmd = this.undo();
	            }
	        }

	        this.editor.app.call('sceneGraphChanged', this);
	        this.editor.app.call('historyChanged', this, cmd);
	    },

	    enableSerialization: function (id) {

	        /**
			 * because there might be commands in this.undos and this.redos
			 * which have not been serialized with .toJSON() we go back
			 * to the oldest command and redo one command after the other
			 * while also calling .toJSON() on them.
			 */

	        this.goToState(-1);

	        var cmd = this.redo();
	        while (cmd !== undefined) {
	            if (!cmd.hasOwnProperty("json")) {
	                cmd.json = cmd.toJSON();
	            }
	            cmd = this.redo();
	        }

	        this.goToState(id);
	    }
	});

	/**
	 * 本地存储
	 * @author mrdoob / http://mrdoob.com/
	 */
	function Storage() {
	    var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;

	    if (indexedDB === undefined) {
	        console.warn('Storage: IndexedDB不可用。');
	        return { init: function () { }, get: function () { }, set: function () { }, clear: function () { } };
	    }

	    var name = 'threejs-editor';
	    var version = 1;

	    var database;

	    return {
	        init: function (callback) {
	            var request = indexedDB.open(name, version);
	            request.onupgradeneeded = function (event) {
	                var db = event.target.result;

	                if (db.objectStoreNames.contains('states') === false) {

	                    db.createObjectStore('states');

	                }
	            };
	            request.onsuccess = function (event) {
	                database = event.target.result;

	                callback();
	            };
	            request.onerror = function (event) {
	                console.error('IndexedDB', event);
	            };
	        },

	        get: function (callback) {
	            var transaction = database.transaction(['states'], 'readwrite');
	            var objectStore = transaction.objectStore('states');
	            var request = objectStore.get(0);
	            request.onsuccess = function (event) {
	                callback(event.target.result);
	            };

	        },

	        set: function (data, callback) {
	            var start = performance.now();

	            var transaction = database.transaction(['states'], 'readwrite');
	            var objectStore = transaction.objectStore('states');
	            var request = objectStore.put(data, 0);
	            request.onsuccess = function (event) {
	                console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', '保存到IndexedDB中。 ' + (performance.now() - start).toFixed(2) + 'ms');
	            };

	        },

	        clear: function () {
	            if (database === undefined) return;

	            var transaction = database.transaction(['states'], 'readwrite');
	            var objectStore = transaction.objectStore('states');
	            var request = objectStore.clear();
	            request.onsuccess = function (event) {
	                console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', '清空IndexedDB。');
	            };
	        }
	    };
	}

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param scene containing children to import
	 * @constructor
	 */

	function SetSceneCommand(scene) {

		Command.call(this);

		this.type = 'SetSceneCommand';
		this.name = 'Set Scene';

		this.cmdArray = [];

		if (scene !== undefined) {

			this.cmdArray.push(new SetUuidCommand(this.editor.scene, scene.uuid));
			this.cmdArray.push(new SetValueCommand(this.editor.scene, 'name', scene.name));
			this.cmdArray.push(new SetValueCommand(this.editor.scene, 'userData', JSON.parse(JSON.stringify(scene.userData))));

			while (scene.children.length > 0) {

				var child = scene.children.pop();
				this.cmdArray.push(new AddObjectCommand(child));

			}

		}

	}
	SetSceneCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetSceneCommand.prototype, {

		constructor: SetSceneCommand,

		execute: function () {
			for (var i = 0; i < this.cmdArray.length; i++) {

				this.cmdArray[i].execute();

			}
			this.editor.app.call('sceneGraphChanged', this);
		},

		undo: function () {
			for (var i = this.cmdArray.length - 1; i >= 0; i--) {

				this.cmdArray[i].undo();

			}
			this.editor.app.call('sceneGraphChanged', this);
		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			var cmds = [];
			for (var i = 0; i < this.cmdArray.length; i++) {

				cmds.push(this.cmdArray[i].toJSON());

			}
			output.cmds = cmds;

			return output;

		},

		fromJSON: function (json) {
			Command.prototype.fromJSON.call(this, json);

			var cmds = json.cmds;
			for (var i = 0; i < cmds.length; i++) {
				var cmd = new window[cmds[i].type]();	// creates a new object of type "json.type"
				cmd.fromJSON(cmds[i]);
				this.cmdArray.push(cmd);
			}
		}
	});

	/**
	 * 模型加载
	 * @author mrdoob / http://mrdoob.com/
	 */
	function Loader(editor) {

	    var scope = this;

	    this.texturePath = '';

	    this.loadFile = function (file) {

	        var filename = file.name;
	        var extension = filename.split('.').pop().toLowerCase();

	        var reader = new FileReader();
	        reader.addEventListener('progress', function (event) {

	            var size = '(' + Math.floor(event.total / 1000).format() + ' KB)';
	            var progress = Math.floor((event.loaded / event.total) * 100) + '%';
	            console.log('加载中', filename, size, progress);

	        });

	        switch (extension) {

	            case 'amf':

	                reader.addEventListener('load', function (event) {

	                    var loader = new THREE.AMFLoader();
	                    var amfobject = loader.parse(event.target.result);

	                    editor.execute(new AddObjectCommand(amfobject));

	                }, false);
	                reader.readAsArrayBuffer(file);

	                break;

	            case 'awd':

	                reader.addEventListener('load', function (event) {

	                    var loader = new THREE.AWDLoader();
	                    var scene = loader.parse(event.target.result);

	                    editor.execute(new SetSceneCommand(scene));

	                }, false);
	                reader.readAsArrayBuffer(file);

	                break;

	            case 'babylon':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;
	                    var json = JSON.parse(contents);

	                    var loader = new THREE.BabylonLoader();
	                    var scene = loader.parse(json);

	                    editor.execute(new SetSceneCommand(scene));

	                }, false);
	                reader.readAsText(file);

	                break;

	            case 'babylonmeshdata':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;
	                    var json = JSON.parse(contents);

	                    var loader = new THREE.BabylonLoader();

	                    var geometry = loader.parseGeometry(json);
	                    var material = new THREE.MeshStandardMaterial();

	                    var mesh = new THREE.Mesh(geometry, material);
	                    mesh.name = filename;

	                    editor.execute(new AddObjectCommand(mesh));

	                }, false);
	                reader.readAsText(file);

	                break;

	            case 'ctm':

	                reader.addEventListener('load', function (event) {

	                    var data = new Uint8Array(event.target.result);

	                    var stream = new CTM.Stream(data);
	                    stream.offset = 0;

	                    var loader = new THREE.CTMLoader();
	                    loader.createModel(new CTM.File(stream), function (geometry) {

	                        geometry.sourceType = "ctm";
	                        geometry.sourceFile = file.name;

	                        var material = new THREE.MeshStandardMaterial();

	                        var mesh = new THREE.Mesh(geometry, material);
	                        mesh.name = filename;

	                        editor.execute(new AddObjectCommand(mesh));

	                    });

	                }, false);
	                reader.readAsArrayBuffer(file);

	                break;

	            case 'dae':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;

	                    var loader = new THREE.ColladaLoader();
	                    var collada = loader.parse(contents);

	                    collada.scene.name = filename;

	                    editor.execute(new AddObjectCommand(collada.scene));

	                }, false);
	                reader.readAsText(file);

	                break;

	            case 'fbx':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;

	                    var loader = new THREE.FBXLoader();
	                    var object = loader.parse(contents);

	                    editor.execute(new AddObjectCommand(object));

	                }, false);
	                reader.readAsText(file);

	                break;

	            case 'glb':
	            case 'gltf':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;

	                    var loader = new THREE.GLTFLoader();
	                    loader.parse(contents, function (result) {

	                        result.scene.name = filename;
	                        editor.execute(new AddObjectCommand(result.scene));

	                    });

	                }, false);
	                reader.readAsArrayBuffer(file);

	                break;

	            case 'js':
	            case 'json':

	            case '3geo':
	            case '3mat':
	            case '3obj':
	            case '3scn':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;

	                    // 2.0

	                    if (contents.indexOf('postMessage') !== -1) {

	                        var blob = new Blob([contents], { type: 'text/javascript' });
	                        var url = URL.createObjectURL(blob);

	                        var worker = new Worker(url);

	                        worker.onmessage = function (event) {

	                            event.data.metadata = { version: 2 };
	                            handleJSON(event.data, file, filename);

	                        };

	                        worker.postMessage(Date.now());

	                        return;

	                    }

	                    // >= 3.0

	                    var data;

	                    try {

	                        data = JSON.parse(contents);

	                    } catch (error) {

	                        UI$1.msg(error);
	                        return;

	                    }

	                    handleJSON(data, file, filename);

	                }, false);
	                reader.readAsText(file);

	                break;


	            case 'kmz':

	                reader.addEventListener('load', function (event) {

	                    var loader = new THREE.KMZLoader();
	                    var collada = loader.parse(event.target.result);

	                    collada.scene.name = filename;

	                    editor.execute(new AddObjectCommand(collada.scene));

	                }, false);
	                reader.readAsArrayBuffer(file);

	                break;

	            case 'md2':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;

	                    var geometry = new THREE.MD2Loader().parse(contents);
	                    var material = new THREE.MeshStandardMaterial({
	                        morphTargets: true,
	                        morphNormals: true
	                    });

	                    var mesh = new THREE.Mesh(geometry, material);
	                    mesh.mixer = new THREE.AnimationMixer(mesh);
	                    mesh.name = filename;

	                    editor.execute(new AddObjectCommand(mesh));

	                }, false);
	                reader.readAsArrayBuffer(file);

	                break;

	            case 'obj':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;

	                    var object = new THREE.OBJLoader().parse(contents);
	                    object.name = filename;

	                    editor.execute(new AddObjectCommand(object));

	                }, false);
	                reader.readAsText(file);

	                break;

	            case 'playcanvas':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;
	                    var json = JSON.parse(contents);

	                    var loader = new THREE.PlayCanvasLoader();
	                    var object = loader.parse(json);

	                    editor.execute(new AddObjectCommand(object));

	                }, false);
	                reader.readAsText(file);

	                break;

	            case 'ply':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;

	                    var geometry = new THREE.PLYLoader().parse(contents);
	                    geometry.sourceType = "ply";
	                    geometry.sourceFile = file.name;

	                    var material = new THREE.MeshStandardMaterial();

	                    var mesh = new THREE.Mesh(geometry, material);
	                    mesh.name = filename;

	                    editor.execute(new AddObjectCommand(mesh));

	                }, false);
	                reader.readAsArrayBuffer(file);

	                break;

	            case 'stl':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;

	                    var geometry = new THREE.STLLoader().parse(contents);
	                    geometry.sourceType = "stl";
	                    geometry.sourceFile = file.name;

	                    var material = new THREE.MeshStandardMaterial();

	                    var mesh = new THREE.Mesh(geometry, material);
	                    mesh.name = filename;

	                    editor.execute(new AddObjectCommand(mesh));

	                }, false);

	                if (reader.readAsBinaryString !== undefined) {

	                    reader.readAsBinaryString(file);

	                } else {

	                    reader.readAsArrayBuffer(file);

	                }

	                break;

	            /*
	            case 'utf8':
	 
	                reader.addEventListener( 'load', function ( event ) {
	 
	                    var contents = event.target.result;
	 
	                    var geometry = new THREE.UTF8Loader().parse( contents );
	                    var material = new THREE.MeshLambertMaterial();
	 
	                    var mesh = new THREE.Mesh( geometry, material );
	 
	                    editor.execute( new AddObjectCommand( mesh ) );
	 
	                }, false );
	                reader.readAsBinaryString( file );
	 
	                break;
	            */

	            case 'vtk':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;

	                    var geometry = new THREE.VTKLoader().parse(contents);
	                    geometry.sourceType = "vtk";
	                    geometry.sourceFile = file.name;

	                    var material = new THREE.MeshStandardMaterial();

	                    var mesh = new THREE.Mesh(geometry, material);
	                    mesh.name = filename;

	                    editor.execute(new AddObjectCommand(mesh));

	                }, false);
	                reader.readAsText(file);

	                break;

	            case 'wrl':

	                reader.addEventListener('load', function (event) {

	                    var contents = event.target.result;

	                    var result = new THREE.VRMLLoader().parse(contents);

	                    editor.execute(new SetSceneCommand(result));

	                }, false);
	                reader.readAsText(file);

	                break;

	            default:

	                UI$1.msg('不支持的文件类型(' + extension + ').');

	                break;

	        }

	    };

	    function handleJSON(data, file, filename) {

	        if (data.metadata === undefined) { // 2.0

	            data.metadata = { type: 'Geometry' };

	        }

	        if (data.metadata.type === undefined) { // 3.0

	            data.metadata.type = 'Geometry';

	        }

	        if (data.metadata.formatVersion !== undefined) {

	            data.metadata.version = data.metadata.formatVersion;

	        }

	        switch (data.metadata.type.toLowerCase()) {

	            case 'buffergeometry':

	                var loader = new THREE.BufferGeometryLoader();
	                var result = loader.parse(data);

	                var mesh = new THREE.Mesh(result);

	                editor.execute(new AddObjectCommand(mesh));

	                break;

	            case 'geometry':

	                var loader = new THREE.JSONLoader();
	                loader.setTexturePath(scope.texturePath);

	                var result = loader.parse(data);

	                var geometry = result.geometry;
	                var material;

	                if (result.materials !== undefined) {

	                    if (result.materials.length > 1) {

	                        material = new THREE.MultiMaterial(result.materials);

	                    } else {

	                        material = result.materials[0];

	                    }

	                } else {

	                    material = new THREE.MeshStandardMaterial();

	                }

	                geometry.sourceType = "ascii";
	                geometry.sourceFile = file.name;

	                var mesh;

	                if (geometry.animation && geometry.animation.hierarchy) {

	                    mesh = new THREE.SkinnedMesh(geometry, material);

	                } else {

	                    mesh = new THREE.Mesh(geometry, material);

	                }

	                mesh.name = filename;

	                editor.execute(new AddObjectCommand(mesh));

	                break;

	            case 'object':

	                var loader = new THREE.ObjectLoader();
	                loader.setTexturePath(scope.texturePath);

	                var result = loader.parse(data);

	                if (result instanceof THREE.Scene) {

	                    editor.execute(new SetSceneCommand(result));

	                } else {

	                    editor.execute(new AddObjectCommand(result));

	                }

	                break;

	            case 'scene':

	                // DEPRECATED

	                var loader = new THREE.SceneLoader();
	                loader.parse(data, function (result) {

	                    editor.execute(new SetSceneCommand(result.scene));

	                }, '');

	                break;

	            case 'app':

	                editor.fromJSON(data);

	                break;

	        }

	    }

	}

	/**
	 * 播放器
	 * @author mrdoob / http://mrdoob.com/
	 */
	function AppPlayer() {
	    var loader = new THREE.ObjectLoader();
	    var camera, scene, renderer;

	    var events = {};

	    var dom = document.createElement('div');

	    this.dom = dom;

	    this.width = 500;
	    this.height = 500;

	    this.load = function (json) {
	        renderer = new THREE.WebGLRenderer({ antialias: true });
	        renderer.setClearColor(0x000000);
	        renderer.setPixelRatio(window.devicePixelRatio);

	        var project = json.project;

	        if (project.gammaInput) renderer.gammaInput = true;
	        if (project.gammaOutput) renderer.gammaOutput = true;
	        if (project.shadows) renderer.shadowMap.enabled = true;
	        if (project.vr) renderer.vr.enabled = true;

	        dom.appendChild(renderer.domElement);

	        this.setScene(loader.parse(json.scene));
	        this.setCamera(loader.parse(json.camera));

	        events = {
	            init: [],
	            start: [],
	            stop: [],
	            keydown: [],
	            keyup: [],
	            mousedown: [],
	            mouseup: [],
	            mousemove: [],
	            touchstart: [],
	            touchend: [],
	            touchmove: [],
	            update: []
	        };

	        var scriptWrapParams = 'player,renderer,scene,camera';
	        var scriptWrapResultObj = {};

	        for (var eventKey in events) {
	            scriptWrapParams += ',' + eventKey;
	            scriptWrapResultObj[eventKey] = eventKey;
	        }

	        var scriptWrapResult = JSON.stringify(scriptWrapResultObj).replace(/\"/g, '');

	        for (var uuid in json.scripts) {
	            var object = scene.getObjectByProperty('uuid', uuid, true);

	            if (object === undefined) {
	                console.warn('APP.Player: Script without object.', uuid);
	                continue;
	            }

	            var scripts = json.scripts[uuid];

	            for (var i = 0; i < scripts.length; i++) {
	                var script = scripts[i];

	                var functions = (new Function(scriptWrapParams, script.source + '\nreturn ' + scriptWrapResult + ';').bind(object))(this, renderer, scene, camera);

	                for (var name in functions) {

	                    if (functions[name] === undefined) continue;

	                    if (events[name] === undefined) {
	                        console.warn('APP.Player: Event type not supported (', name, ')');
	                        continue;
	                    }

	                    events[name].push(functions[name].bind(object));
	                }

	            }

	        }

	        dispatch$$1(events.init, arguments);

	    };

	    this.setCamera = function (value) {
	        camera = value;
	        camera.aspect = this.width / this.height;
	        camera.updateProjectionMatrix();

	        if (renderer.vr.enabled) {
	            dom.appendChild(WEBVR.createButton(renderer));
	        }
	    };

	    this.setScene = function (value) {
	        scene = value;
	    };

	    this.setSize = function (width, height) {
	        this.width = width;
	        this.height = height;

	        if (camera) {
	            camera.aspect = this.width / this.height;
	            camera.updateProjectionMatrix();
	        }

	        if (renderer) {
	            renderer.setSize(width, height);
	        }
	    };

	    function dispatch$$1(array, event) {
	        for (var i = 0, l = array.length; i < l; i++) {
	            array[i](event);
	        }
	    }

	    var prevTime;

	    function animate() {
	        var time = performance.now();

	        try {
	            dispatch$$1(events.update, { time: time, delta: time - prevTime });
	        } catch (e) {
	            console.error((e.message || e), (e.stack || ""));
	        }

	        renderer.render(scene, camera);
	        prevTime = time;
	    }

	    this.play = function () {
	        prevTime = performance.now();

	        document.addEventListener('keydown', onDocumentKeyDown);
	        document.addEventListener('keyup', onDocumentKeyUp);
	        document.addEventListener('mousedown', onDocumentMouseDown);
	        document.addEventListener('mouseup', onDocumentMouseUp);
	        document.addEventListener('mousemove', onDocumentMouseMove);
	        document.addEventListener('touchstart', onDocumentTouchStart);
	        document.addEventListener('touchend', onDocumentTouchEnd);
	        document.addEventListener('touchmove', onDocumentTouchMove);

	        dispatch$$1(events.start, arguments);

	        renderer.setAnimationLoop(animate);
	    };

	    this.stop = function () {
	        document.removeEventListener('keydown', onDocumentKeyDown);
	        document.removeEventListener('keyup', onDocumentKeyUp);
	        document.removeEventListener('mousedown', onDocumentMouseDown);
	        document.removeEventListener('mouseup', onDocumentMouseUp);
	        document.removeEventListener('mousemove', onDocumentMouseMove);
	        document.removeEventListener('touchstart', onDocumentTouchStart);
	        document.removeEventListener('touchend', onDocumentTouchEnd);
	        document.removeEventListener('touchmove', onDocumentTouchMove);

	        dispatch$$1(events.stop, arguments);

	        renderer.setAnimationLoop(null);
	    };

	    this.dispose = function () {
	        while (dom.children.length) {
	            dom.removeChild(dom.firstChild);
	        }

	        renderer.dispose();

	        camera = undefined;
	        scene = undefined;
	        renderer = undefined;
	    };

	    //

	    function onDocumentKeyDown(event) {
	        dispatch$$1(events.keydown, event);
	    }

	    function onDocumentKeyUp(event) {
	        dispatch$$1(events.keyup, event);
	    }

	    function onDocumentMouseDown(event) {
	        dispatch$$1(events.mousedown, event);
	    }

	    function onDocumentMouseUp(event) {
	        dispatch$$1(events.mouseup, event);
	    }

	    function onDocumentMouseMove(event) {
	        dispatch$$1(events.mousemove, event);
	    }

	    function onDocumentTouchStart(event) {
	        dispatch$$1(events.touchstart, event);
	    }

	    function onDocumentTouchEnd(event) {
	        dispatch$$1(events.touchend, event);
	    }

	    function onDocumentTouchMove(event) {
	        dispatch$$1(events.touchmove, event);
	    }
	}

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param script javascript object
	 * @constructor
	 */

	var AddScriptCommand = function (object, script) {

		Command.call(this);

		this.type = 'AddScriptCommand';
		this.name = 'Add Script';

		this.object = object;
		this.script = script;

	};

	AddScriptCommand.prototype = Object.create(Command.prototype);

	Object.assign(AddScriptCommand.prototype, {

		constructor: AddScriptCommand,

		execute: function () {
			if (this.editor.scripts[this.object.uuid] === undefined) {
				this.editor.scripts[this.object.uuid] = [];
			}

			this.editor.scripts[this.object.uuid].push(this.script);
			this.editor.app.call('scriptAdded', this, this.script);
		},

		undo: function () {

			if (this.editor.scripts[this.object.uuid] === undefined) return;

			var index = this.editor.scripts[this.object.uuid].indexOf(this.script);

			if (index !== - 1) {

				this.editor.scripts[this.object.uuid].splice(index, 1);

			}

			this.editor.app.call('scriptRemoved', this, this.script);
		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.script = this.script;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.script = json.script;
			this.object = this.editor.objectByUuid(json.objectUuid);

		}

	});

	/**
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */

	/**
	 * @param object THREE.Object3D
	 * @param attributeName string
	 * @param newValue number, string, boolean or object
	 * @constructor
	 */

	function SetGeometryValueCommand(object, attributeName, newValue) {

		Command.call(this);

		this.type = 'SetGeometryValueCommand';
		this.name = 'Set Geometry.' + attributeName;

		this.object = object;
		this.attributeName = attributeName;
		this.oldValue = (object !== undefined) ? object.geometry[attributeName] : undefined;
		this.newValue = newValue;

	}
	SetGeometryValueCommand.prototype = Object.create(Command.prototype);

	Object.assign(SetGeometryValueCommand.prototype, {

		constructor: SetGeometryValueCommand,

		execute: function () {

			this.object.geometry[this.attributeName] = this.newValue;
			this.editor.app.call('objectChanged', this, this.object);
			this.editor.app.call('geometryChanged', this);
			this.editor.app.call('sceneGraphChanged', this);

		},

		undo: function () {

			this.object.geometry[this.attributeName] = this.oldValue;
			this.editor.app.call('objectChanged', this, this.object);
			this.editor.app.call('geometryChanged', this);
			this.editor.app.call('sceneGraphChanged', this);

		},

		toJSON: function () {

			var output = Command.prototype.toJSON.call(this);

			output.objectUuid = this.object.uuid;
			output.attributeName = this.attributeName;
			output.oldValue = this.oldValue;
			output.newValue = this.newValue;

			return output;

		},

		fromJSON: function (json) {

			Command.prototype.fromJSON.call(this, json);

			this.object = this.editor.objectByUuid(json.objectUuid);
			this.attributeName = json.attributeName;
			this.oldValue = json.oldValue;
			this.newValue = json.newValue;

		}

	});

	/**
	 * 场景编辑区
	 * @author mrdoob / http://mrdoob.com/
	 */
	function Viewport(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	Viewport.prototype = Object.create(UI$1.Control.prototype);
	Viewport.prototype.constructor = Viewport;

	Viewport.prototype.render = function () {
	    this.container = UI$1.create({
	        xtype: 'div',
	        id: 'viewport',
	        parent: this.app.container,
	        cls: 'viewport'
	    });
	    this.container.render();
	};

	/**
	 * Logo标志
	 * @param {*} options 
	 */
	function Logo(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}

	Logo.prototype = Object.create(UI$1.Control.prototype);
	Logo.prototype.constructor = Logo;

	Logo.prototype.render = function () {

	    var container = UI$1.create({
	        xtype: 'div',
	        parent: this.parent,
	        cls: 'logo',
	        html: '<i class="iconfont icon-shadow"></i>'
	    });

	    container.render();
	};

	/**
	 * 场景菜单
	 * @param {*} options 
	 */
	function SceneMenu(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}

	SceneMenu.prototype = Object.create(UI$1.Control.prototype);
	SceneMenu.prototype.constructor = SceneMenu;

	SceneMenu.prototype.render = function () {
	    var _this = this;

	    var container = UI$1.create({
	        xtype: 'div',
	        parent: this.parent,
	        cls: 'menu',
	        children: [{
	            xtype: 'div',
	            cls: 'title',
	            html: '场景'
	        }, {
	            xtype: 'div',
	            cls: 'options',
	            children: [{
	                xtype: 'div',
	                id: 'mNewScene',
	                html: '新建',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mNewScene');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mLoadScene',
	                html: '载入',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mLoadScene');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mSaveScene',
	                html: '保存',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mSaveScene');
	                }
	            }, {
	                xtype: 'hr'
	            }, {
	                xtype: 'div',
	                id: 'mPublishScene',
	                html: '发布',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mPublishScene');
	                }
	            }]
	        }]
	    });

	    container.render();
	};

	/**
	 * 编辑菜单
	 * @param {*} options 
	 */
	function EditMenu(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}

	EditMenu.prototype = Object.create(UI$1.Control.prototype);
	EditMenu.prototype.constructor = EditMenu;

	EditMenu.prototype.render = function () {
	    var _this = this;

	    var container = UI$1.create({
	        xtype: 'div',
	        parent: this.parent,
	        cls: 'menu',
	        children: [{
	            xtype: 'div',
	            cls: 'title',
	            html: '编辑'
	        }, {
	            xtype: 'div',
	            cls: 'options',
	            children: [{
	                xtype: 'div',
	                id: 'mUndo',
	                html: '撤销(Ctrl+Z)',
	                cls: 'option inactive',
	                onClick: function () {
	                    _this.app.call('mUndo');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mRedo',
	                html: '重做(Ctrl+Shift+Z)',
	                cls: 'option inactive',
	                onClick: function () {
	                    _this.app.call('mRedo');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mClearHistory',
	                html: '清空历史记录',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mClearHistory');
	                }
	            }, {
	                xtype: 'hr'
	            }, {
	                xtype: 'div',
	                id: 'mClone',
	                html: '复制',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mClone');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mDelete',
	                html: '删除(Del)',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mDelete');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mMinifyShader',
	                html: '压缩着色器程序',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mMinifyShader');
	                }
	            }]
	        }]
	    });

	    container.render();
	};

	/**
	 * 添加菜单
	 * @param {*} options 
	 */
	function AddMenu(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}

	AddMenu.prototype = Object.create(UI$1.Control.prototype);
	AddMenu.prototype.constructor = AddMenu;

	AddMenu.prototype.render = function () {
	    var _this = this;

	    var container = UI$1.create({
	        xtype: 'div',
	        parent: this.parent,
	        cls: 'menu',
	        children: [{
	            xtype: 'div',
	            cls: 'title',
	            html: '添加'
	        }, {
	            xtype: 'div',
	            cls: 'options',
	            children: [{
	                xtype: 'div',
	                id: 'mAddGroup',
	                html: '组',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddGroup');
	                }
	            }, {
	                xtype: 'hr'
	            }, {
	                xtype: 'div',
	                id: 'mAddPlane',
	                html: '平板',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddPlane');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddBox',
	                html: '正方体',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddBox');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddCircle',
	                html: '圆',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddCircle');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddCylinder',
	                html: '圆柱体',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddCylinder');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddSphere',
	                html: '球体',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddSphere');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddIcosahedron',
	                html: '二十面体',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddIcosahedron');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddTorus',
	                html: '轮胎',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddTorus');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddTorusKnot',
	                html: '扭结',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddTorusKnot');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddTeaport',
	                html: '茶壶',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddTeaport');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddLathe',
	                html: '花瓶',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddLathe');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddSprite',
	                html: '精灵',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddSprite');
	                }
	            }, {
	                xtype: 'hr'
	            }, {
	                xtype: 'div',
	                id: 'mAddPointLight',
	                html: '点光源',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddPointLight');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddSpotLight',
	                html: '聚光灯',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddSpotLight');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddDirectionalLight',
	                html: '平行光源',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddDirectionalLight');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddHemisphereLight',
	                html: '半球光',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddHemisphereLight');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mAddAmbientLight',
	                html: '环境光',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddAmbientLight');
	                }
	            }, {
	                xtype: 'hr'
	            }, {
	                xtype: 'div',
	                id: 'mAddPerspectiveCamera',
	                html: '透视相机',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddPerspectiveCamera');
	                }
	            }]
	        }]
	    });

	    container.render();
	};

	/**
	 * 资源菜单
	 * @param {*} options 
	 */
	function AssetMenu(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}

	AssetMenu.prototype = Object.create(UI$1.Control.prototype);
	AssetMenu.prototype.constructor = AssetMenu;

	AssetMenu.prototype.render = function () {
	    var _this = this;

	    var container = UI$1.create({
	        xtype: 'div',
	        parent: this.parent,
	        cls: 'menu',
	        children: [{
	            xtype: 'div',
	            cls: 'title',
	            html: '资源'
	        }, {
	            xtype: 'div',
	            cls: 'options',
	            children: [{
	                xtype: 'div',
	                id: 'mAddAsset',
	                html: '添加模型',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mAddAsset');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mImportAsset',
	                html: '导入模型',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mImportAsset');
	                }
	            }, {
	                xtype: 'hr'
	            }, {
	                xtype: 'div',
	                id: 'mExportGeometry',
	                html: '导出几何体',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mExportGeometry');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mExportObject',
	                html: '导出物体',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mExportObject');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mExportScene',
	                html: '导出场景',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mExportScene');
	                }
	            }, {
	                xtype: 'hr'
	            }, {
	                xtype: 'div',
	                id: 'mExportGLTF',
	                html: '导出gltf文件',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mExportGLTF');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mExportMMD',
	                html: '导出mmd文件',
	                cls: 'option inactive',
	                onClick: function () {
	                    _this.app.call('mExportMMD');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mExportOBJ',
	                html: '导出obj文件',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mExportOBJ');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mExportPLY',
	                html: '导出ply文件',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mExportPLY');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mExportSTLB',
	                html: '导出stl二进制文件',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mExportSTLB');
	                }
	            }, {
	                xtype: 'div',
	                id: 'mExportSTL',
	                html: '导出stl文件',
	                cls: 'option',
	                onClick: function () {
	                    _this.app.call('mExportSTL');
	                }
	            }]
	        }]
	    });

	    container.render();
	};

	/**
	 * 动画菜单
	 * @param {*} options 
	 */
	function AnimationMenu(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}

	AnimationMenu.prototype = Object.create(UI$1.Control.prototype);
	AnimationMenu.prototype.constructor = AnimationMenu;

	AnimationMenu.prototype.render = function () {
	    var _this = this;

	    var container = UI$1.create({
	        xtype: 'div',
	        parent: this.parent,
	        cls: 'menu',
	        children: [{
	            xtype: 'div',
	            cls: 'title',
	            html: '动画'
	        }, {
	            xtype: 'div',
	            cls: 'options',
	            children: [{
	                id: 'mPerson',
	                xtype: 'div',
	                cls: 'option',
	                html: '人',
	                onClick: function () {
	                    _this.app.call('mAddPerson', _this);
	                }
	            }, {
	                id: 'mFire',
	                xtype: 'div',
	                cls: 'option',
	                html: '火焰',
	                onClick: function () {
	                    _this.app.call('mAddFire', _this);
	                }
	            }, {
	                id: 'mSmoke',
	                xtype: 'div',
	                cls: 'option',
	                html: '烟',
	                onClick: function () {
	                    _this.app.call('mAddSmoke', _this);
	                }
	            }]
	        }]
	    });

	    container.render();
	};

	/**
	 * 启动菜单
	 * @param {*} options 
	 */
	function PlayMenu(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}

	PlayMenu.prototype = Object.create(UI$1.Control.prototype);
	PlayMenu.prototype.constructor = PlayMenu;

	PlayMenu.prototype.render = function () {
	    var _this = this;

	    var container = UI$1.create({
	        xtype: 'div',
	        parent: this.parent,
	        cls: 'menu',
	        children: [{
	            id: 'mPlay',
	            xtype: 'div',
	            cls: 'title',
	            html: '启动',
	            onClick: function () {
	                _this.app.call('mPlay');
	            }
	        }]
	    });

	    container.render();
	};

	/**
	 * 视图菜单
	 * @param {*} options 
	 */
	function ViewMenu(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}

	ViewMenu.prototype = Object.create(UI$1.Control.prototype);
	ViewMenu.prototype.constructor = ViewMenu;

	ViewMenu.prototype.render = function () {
	    var _this = this;

	    var container = UI$1.create({
	        xtype: 'div',
	        parent: this.parent,
	        cls: 'menu',
	        children: [{
	            xtype: 'div',
	            cls: 'title',
	            html: '视图'
	        }, {
	            xtype: 'div',
	            cls: 'options',
	            children: [{
	                id: 'mVRMode',
	                xtype: 'div',
	                cls: 'option',
	                html: 'VR模式',
	                onClick: function () {
	                    _this.app.call('mVRMode');
	                }
	            }]
	        }]
	    });

	    container.render();
	};

	/**
	 * 示例菜单
	 * @param {*} options 
	 */
	function ExampleMenu(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}

	ExampleMenu.prototype = Object.create(UI$1.Control.prototype);
	ExampleMenu.prototype.constructor = ExampleMenu;

	ExampleMenu.prototype.render = function () {
	    var _this = this;

	    var container = UI$1.create({
	        xtype: 'div',
	        parent: this.parent,
	        cls: 'menu',
	        children: [{
	            xtype: 'div',
	            cls: 'title',
	            html: '示例'
	        }, {
	            xtype: 'div',
	            cls: 'options',
	            children: [{
	                id: 'mArkanoid',
	                xtype: 'div',
	                cls: 'option',
	                html: '打砖块',
	                onClick: function () {
	                    _this.app.call('mArkanoid');
	                }
	            }, {
	                id: 'mCamera',
	                xtype: 'div',
	                cls: 'option',
	                html: '相机',
	                onClick: function () {
	                    _this.app.call('mCamera');
	                }
	            }, {
	                id: 'mParticles',
	                xtype: 'div',
	                cls: 'option',
	                html: '粒子',
	                onClick: function () {
	                    _this.app.call('mParticles');
	                }
	            }, {
	                id: 'mPong',
	                xtype: 'div',
	                cls: 'option',
	                html: '乒乓球',
	                onClick: function () {
	                    _this.app.call('mPong');
	                }
	            }]
	        }]
	    });

	    container.render();
	};

	/**
	 * 帮助菜单
	 * @param {*} options 
	 */
	function HelpMenu(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}

	HelpMenu.prototype = Object.create(UI$1.Control.prototype);
	HelpMenu.prototype.constructor = HelpMenu;

	HelpMenu.prototype.render = function () {
	    var _this = this;

	    var container = UI$1.create({
	        xtype: 'div',
	        parent: this.parent,
	        cls: 'menu',
	        children: [{
	            xtype: 'div',
	            cls: 'title',
	            html: '帮助'
	        }, {
	            xtype: 'div',
	            cls: 'options',
	            children: [{
	                id: 'mSourceCode',
	                xtype: 'div',
	                cls: 'option',
	                html: '源码',
	                onClick: function () {
	                    _this.app.call('mSourceCode');
	                }
	            }, {
	                id: 'mAbout',
	                xtype: 'div',
	                cls: 'option',
	                html: '关于',
	                onClick: function () {
	                    _this.app.call('mAbout');
	                }
	            }]
	        }]
	    });

	    container.render();
	};

	/**
	 * 状态菜单（菜单栏右侧）
	 * @param {*} options 
	 */
	function StatusMenu(options) {
	    UI$1.Control.call(this, options);
	    options = options || {};

	    this.app = options.app;
	}

	StatusMenu.prototype = Object.create(UI$1.Control.prototype);
	StatusMenu.prototype.constructor = StatusMenu;

	StatusMenu.prototype.render = function () {
	    var _this = this;

	    var container = UI$1.create({
	        xtype: 'div',
	        id: 'mStatus',
	        parent: this.parent,
	        cls: 'menu right',
	        children: [{
	            id: 'bAutoSave',
	            xtype: 'boolean',
	            text: '自动保存',
	            value: true,
	            style: {
	                color: '#888 !important;'
	            },
	            onChange: function (e) {
	                _this.app.editor.config.setKey('autosave', e.target.checked);
	                _this.app.call('sceneGraphChanged', _this);
	            }
	        }, {
	            xtype: 'text',
	            text: 'r' + THREE.REVISION,
	            cls: 'title version'
	        }]
	    });

	    container.render();
	};

	/**
	 * 菜单栏
	 * @author mrdoob / http://mrdoob.com/
	 */
	function Menubar(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	Menubar.prototype = Object.create(UI$1.Control.prototype);
	Menubar.prototype.constructor = Menubar;

	Menubar.prototype.render = function () {
	    var params = { app: this.app };

	    var container = UI$1.create({
	        xtype: 'div',
	        id: 'menubar',
	        cls: 'menubar',
	        parent: this.parent,
	        children: [
	            // Logo
	            new Logo(params),

	            // 左侧
	            new SceneMenu(params),
	            new EditMenu(params),
	            new AddMenu(params),
	            new AssetMenu(params),
	            new AnimationMenu(params),
	            new PlayMenu(params),
	            new ViewMenu(params),
	            new ExampleMenu(params),
	            new HelpMenu(params),

	            // 右侧
	            new StatusMenu(params)
	        ]
	    });

	    container.render();
	};

	/**
	 * 状态栏
	 * @author mrdoob / http://mrdoob.com/
	 */
	function StatusBar(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	StatusBar.prototype = Object.create(UI$1.Control.prototype);
	StatusBar.prototype.constructor = StatusBar;

	StatusBar.prototype.render = function () {

	    var data = {
	        xtype: 'div',
	        id: 'statusBar',
	        parent: this.app.container,
	        cls: 'statusBar',
	        children: [{
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '物体'
	            }, {
	                xtype: 'text',
	                id: 'objectsText',
	                text: '0' // 物体数
	            }, {
	                xtype: 'label',
	                text: '顶点'
	            }, {
	                xtype: 'text',
	                id: 'verticesText',
	                text: '0' // 顶点数
	            }, {
	                xtype: 'label',
	                text: '三角形'
	            }, {
	                xtype: 'text',
	                id: 'trianglesText',
	                text: '0' // 三角形数
	            }]
	        }]
	    };

	    var control = UI$1.create(data);
	    control.render();
	};

	/**
	 * 编辑器
	 * @author mrdoob / http://mrdoob.com/
	 */
	function Editor(app) {
	    this.app = app;
	    this.app.editor = this;

	    // 基础
	    this.config = new Config('threejs-editor');
	    this.history = new History(this);
	    this.storage = new Storage();
	    this.loader = new Loader(this);

	    // 场景
	    this.scene = new THREE.Scene();
	    this.scene.name = '场景';
	    this.scene.background = new THREE.Color(0xaaaaaa);

	    this.sceneHelpers = new THREE.Scene();

	    // 相机
	    this.DEFAULT_CAMERA = new THREE.PerspectiveCamera(50, 1, 0.1, 10000);
	    this.DEFAULT_CAMERA.name = '默认相机';
	    this.DEFAULT_CAMERA.position.set(20, 10, 20);
	    this.DEFAULT_CAMERA.lookAt(new THREE.Vector3());

	    this.camera = this.DEFAULT_CAMERA.clone();

	    // 渲染器
	    this.rendererTypes = {
	        'WebGLRenderer': THREE.WebGLRenderer,
	        'CanvasRenderer': THREE.CanvasRenderer,
	        'SVGRenderer': THREE.SVGRenderer,
	        'SoftwareRenderer': THREE.SoftwareRenderer,
	        'RaytracingRenderer': THREE.RaytracingRenderer
	    };

	    this.renderer = this.createRendererFromConfig();
	    this.app.viewport.container.dom.appendChild(this.renderer.domElement);
	    (new RendererChangedEvent(this.app)).onRendererChanged(this.renderer);

	    // 缓存
	    this.object = {};
	    this.objects = [];
	    this.geometries = {};
	    this.materials = {};
	    this.textures = {};
	    this.scripts = {};
	    this.helpers = {};

	    // 当前选中物体
	    this.selected = null;

	    // 网格
	    this.grid = new THREE.GridHelper(30, 30, 0x444444, 0x888888);
	    this.sceneHelpers.add(this.grid);

	    // 选中包围盒（当mesh.useSelectionBox === false时，不使用包围盒）
	    this.selectionBox = new THREE.BoxHelper();
	    this.selectionBox.material.depthTest = false;
	    this.selectionBox.material.transparent = true;
	    this.selectionBox.visible = false;
	    this.sceneHelpers.add(this.selectionBox);

	    // 平移旋转缩放控件
	    this.transformControls = new THREE.TransformControls(this.camera, this.app.viewport.container.dom);
	    this.sceneHelpers.add(this.transformControls);

	    // 编辑器控件
	    this.controls = new THREE.EditorControls(this.camera, this.app.viewport.container.dom);

	    // 性能控件
	    this.stats = new Stats();
	    this.stats.dom.style.position = 'absolute';
	    this.stats.dom.style.left = '8px';
	    this.stats.dom.style.top = '8px';
	    this.stats.dom.style.zIndex = 'initial';
	    this.app.viewport.container.dom.appendChild(this.stats.dom);
	}
	// ---------------------- 渲染器 ---------------------------

	Editor.prototype.createRenderer = function (options) { // 创建渲染器
	    var rendererType = options.rendererType === undefined ? 'WebGLRenderer' : options.rendererType;
	    var antialias = options.antialias === undefined ? true : options.antialias;
	    var shadows = options.shadows === undefined ? true : options.shadows;
	    var gammaIn = options.gammaIn === undefined ? false : options.gammaIn;
	    var gammaOut = options.gammaOut === undefined ? false : options.gammaOut;
	    var rendererTypes = this.rendererTypes;

	    var renderer = new rendererTypes[rendererType]({ antialias: antialias });
	    renderer.gammaInput = gammaIn;
	    renderer.gammaOutput = gammaOut;
	    if (shadows && renderer.shadowMap) {
	        renderer.shadowMap.enabled = true;
	        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
	    }
	    return renderer;
	};

	Editor.prototype.createRendererFromConfig = function () { // 从配置创建渲染器
	    var rendererType = this.config.getKey('project/renderer');
	    var antialias = this.config.getKey('project/renderer/antialias');
	    var shadows = this.config.getKey('project/renderer/shadows');
	    var gammaIn = this.config.getKey('project/renderer/gammaInput');
	    var gammaOut = this.config.getKey('project/renderer/gammaOutput');

	    return this.createRenderer({
	        rendererType: rendererType,
	        antialias: antialias,
	        shadows: shadows,
	        gammaIn: gammaIn,
	        gammaOut: gammaOut
	    });
	};

	// -------------------- 编辑器 --------------------------

	Editor.prototype.setTheme = function (value) { // 设置主题
	    this.app.call('setTheme', this, value);
	};

	Editor.prototype.setScene = function (scene) { // 设置场景
	    this.app.call('setScene', this, scene);
	};

	// ---------------------- 物体 ---------------------------

	Editor.prototype.objectByUuid = function (uuid) { // 根据uuid获取物体
	    return this.scene.getObjectByProperty('uuid', uuid, true);
	};

	Editor.prototype.addObject = function (object) { // 添加物体
	    this.app.call('addObject', this, object);
	};

	Editor.prototype.moveObject = function (object, parent, before) { // 移动物体
	    this.app.call('moveObject', this, object, parent, before);
	};

	Editor.prototype.nameObject = function (object, name) { // 重命名物体
	    this.app.call('nameObject', this, object, name);
	};

	Editor.prototype.removeObject = function (object) { // 移除物体
	    this.app.call('removeObject', this, object);
	};

	Editor.prototype.addGeometry = function (geometry) { // 添加几何体
	    this.app.call('addGeometry', this, geometry);
	};

	Editor.prototype.setGeometryName = function (geometry, name) { // 设置几何体名称
	    this.app.call('setGeometryName', this, geometry, name);
	};

	Editor.prototype.addMaterial = function (material) { // 添加材质
	    this.app.call('addMaterial', this, material);
	};

	Editor.prototype.setMaterialName = function (material, name) { // 设置材质名称事件
	    this.app.call('setMaterialName', this, material, name);
	};

	Editor.prototype.addTexture = function (texture) { // 添加纹理事件
	    this.app.call('addTexture', this, texture);
	};

	// ------------------------- 帮助 ------------------------------

	Editor.prototype.addHelper = function (object) { // 添加物体帮助
	    this.app.call('addHelper', this, object);
	};

	Editor.prototype.removeHelper = function (object) { // 移除物体帮助
	    this.app.call('removeHelper', this, object);
	};

	// ------------------------ 脚本 ----------------------------

	Editor.prototype.addScript = function (object, script) { // 添加脚本
	    this.app.call('addScript', this, object, script);
	};

	Editor.prototype.removeScript = function (object, script) { // 移除脚本
	    this.app.call('removeScript', this, object, script);
	};

	// ------------------------ 选中事件 --------------------------------

	Editor.prototype.select = function (object) { // 选中物体
	    this.app.call('select', this, object);
	};

	Editor.prototype.selectById = function (id) { // 根据id选中物体
	    if (id === this.camera.id) {
	        this.select(this.camera);
	        return;
	    }

	    this.select(this.scene.getObjectById(id, true));
	};

	Editor.prototype.selectByUuid = function (uuid) { // 根据uuid选中物体
	    var _this = this;
	    this.scene.traverse(function (child) {
	        if (child.uuid === uuid) {
	            _this.select(child);
	        }
	    });
	};

	Editor.prototype.deselect = function () { // 取消选中物体
	    this.select(null);
	};

	// ---------------------- 焦点事件 --------------------------

	Editor.prototype.focus = function (object) { // 设置焦点
	    this.app.call('objectFocused', this, object);
	};

	Editor.prototype.focusById = function (id) { // 根据id设置交点
	    this.focus(this.scene.getObjectById(id, true));
	};

	// ----------------------- 场景事件 ----------------------------

	Editor.prototype.clear = function () { // 清空场景
	    this.app.call('clear', this);
	};

	Editor.prototype.load = function () { // 加载场景
	    this.app.call('load', this);
	};

	Editor.prototype.save = function () { // 保存场景
	    this.app.call('save', this);
	};

	// --------------------- 命令事件 ------------------------

	Editor.prototype.execute = function (cmd, optionalName) { // 执行事件
	    this.history.execute(cmd, optionalName);
	};

	Editor.prototype.undo = function () { // 撤销事件
	    this.history.undo();
	};

	Editor.prototype.redo = function () { // 重做事件
	    this.history.redo();
	};

	// ------------------------- 序列化 ----------------------------

	Editor.prototype.fromJSON = function (json) { // 根据json创建场景
	    var loader = new THREE.ObjectLoader();

	    // backwards

	    if (json.scene === undefined) {
	        this.setScene(loader.parse(json));
	        return;
	    }

	    var camera = loader.parse(json.camera);

	    this.camera.copy(camera);
	    this.camera.aspect = this.DEFAULT_CAMERA.aspect;
	    this.camera.updateProjectionMatrix();

	    this.history.fromJSON(json.history);
	    this.scripts = json.scripts;

	    this.setScene(loader.parse(json.scene));
	};

	Editor.prototype.toJSON = function () { // 将场景转换为json
	    // scripts clean up
	    var scene = this.scene;
	    var scripts = this.scripts;

	    for (var key in scripts) {
	        var script = scripts[key];

	        if (script.length === 0 || scene.getObjectByProperty('uuid', key) === undefined) {
	            delete scripts[key];
	        }
	    }

	    return {
	        metadata: {},
	        project: {
	            gammaInput: this.config.getKey('project/renderer/gammaInput'),
	            gammaOutput: this.config.getKey('project/renderer/gammaOutput'),
	            shadows: this.config.getKey('project/renderer/shadows'),
	            vr: this.config.getKey('project/vr')
	        },
	        camera: this.camera.toJSON(),
	        scene: this.scene.toJSON(),
	        scripts: this.scripts,
	        history: this.history.toJSON()
	    };
	};

	/**
	 * 脚本编辑面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function Script(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	Script.prototype = Object.create(UI$1.Control.prototype);
	Script.prototype.constructor = Script;

	Script.prototype.render = function () {
	    var container;

	    var data = {
	        xtype: 'div',
	        parent: this.app.container,
	        id: 'script',
	        cls: 'script',
	        style: {
	            backgroundColor: '#272822',
	            display: 'none'
	        },
	        children: [{
	            xtype: 'div',
	            style: {
	                padding: '10px'
	            },
	            children: [{
	                id: 'scriptTitle',
	                xtype: 'text',
	                style: {
	                    color: '#fff'
	                }
	            }, {
	                xtype: 'closebutton',
	                style: {
	                    position: 'absolute',
	                    top: '3px',
	                    right: '1px',
	                    cursor: 'pointer'
	                },
	                onClick: function () {
	                    if (container) {
	                        container.dom.style.display = 'none';
	                    }
	                }
	            }]
	        }]
	    };

	    container = UI$1.create(data);
	    container.render();

	    var title = UI$1.get('scriptTitle');

	    // 业务逻辑
	    var currentMode;
	    var currentScript;
	    var currentObject;

	    var _this = this;

	    var codemirror = CodeMirror(container.dom, {
	        value: '',
	        lineNumbers: true,
	        matchBrackets: true,
	        indentWithTabs: true,
	        tabSize: 4,
	        indentUnit: 4,
	        hintOptions: {
	            completeSingle: false
	        }
	    });
	    codemirror.setOption('theme', 'monokai');
	    codemirror.on('change', function () {
	        _this.app.call('codeMirrorChange', _this, codemirror, currentMode, currentScript, currentObject);
	    });

	    // 防止回退键删除物体
	    var wrapper = codemirror.getWrapperElement();
	    wrapper.addEventListener('keydown', function (event) {
	        event.stopPropagation();
	    });

	    // tern js 自动完成
	    var server = new CodeMirror.TernServer({
	        caseInsensitive: true,
	        plugins: { threejs: null }
	    });

	    codemirror.setOption('extraKeys', {
	        'Ctrl-Space': function (cm) { server.complete(cm); },
	        'Ctrl-I': function (cm) { server.showType(cm); },
	        'Ctrl-O': function (cm) { server.showDocs(cm); },
	        'Alt-.': function (cm) { server.jumpToDef(cm); },
	        'Alt-,': function (cm) { server.jumpBack(cm); },
	        'Ctrl-Q': function (cm) { server.rename(cm); },
	        'Ctrl-.': function (cm) { server.selectName(cm); }
	    });

	    codemirror.on('cursorActivity', function (cm) {
	        if (currentMode !== 'javascript') {
	            return;
	        }
	        server.updateArgHints(cm);
	    });

	    codemirror.on('keypress', function (cm, kb) {
	        if (currentMode !== 'javascript') {
	            return;
	        }
	        var typed = String.fromCharCode(kb.which || kb.keyCode);
	        if (/[\w\.]/.exec(typed)) {
	            server.complete(cm);
	        }
	    });

	    //
	    this.app.on('editorCleared.Script', function () {
	        container.dom.style.display = 'none';
	    });

	    this.app.on('editScript.Script', function (object, script) {
	        var mode, name, source;

	        if (typeof (script) === 'object') {
	            mode = 'javascript';
	            name = script.name;
	            source = script.source;
	            title.setValue(object.name + ' / ' + name);
	        } else {
	            switch (script) {
	                case 'vertexShader':
	                    mode = 'glsl';
	                    name = 'Vertex Shader';
	                    source = object.material.vertexShader || "";
	                    break;
	                case 'fragmentShader':
	                    mode = 'glsl';
	                    name = 'Fragment Shader';
	                    source = object.material.fragmentShader || "";
	                    break;
	                case 'programInfo':
	                    mode = 'json';
	                    name = 'Program Properties';
	                    var json = {
	                        defines: object.material.defines,
	                        uniforms: object.material.uniforms,
	                        attributes: object.material.attributes
	                    };
	                    source = JSON.stringify(json, null, '\t');
	            }
	            title.setValue(object.material.name + ' / ' + name);
	        }

	        currentMode = mode;
	        currentScript = script;
	        currentObject = object;

	        container.dom.style.display = 'block';
	        codemirror.setValue(source);
	        if (mode === 'json') mode = { name: 'javascript', json: true };
	        codemirror.setOption('mode', mode);
	    });

	    this.app.on('scriptRemoved.Script', function (script) {
	        if (currentScript === script) {
	            container.dom.style.display = 'none';
	        }
	    });

	    this.app.on('refreshScriptEditor.Script', function (object, script, cursorPosition, scrollInfo) {
	        if (currentScript !== script) return;

	        // copying the codemirror history because "codemirror.setValue(...)" alters its history

	        var history = codemirror.getHistory();
	        title.setValue(object.name + ' / ' + script.name);
	        codemirror.setValue(script.source);

	        if (cursorPosition !== undefined) {

	            codemirror.setCursor(cursorPosition);
	            codemirror.scrollTo(scrollInfo.left, scrollInfo.top);

	        }
	        codemirror.setHistory(history); // setting the history to previous state
	    });
	};

	/**
	 * 播放器面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function Player(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	Player.prototype = Object.create(UI$1.Control.prototype);
	Player.prototype.constructor = Player;

	Player.prototype.render = function () {
	    this.container = UI$1.create({
	        xtype: 'div',
	        parent: this.parent,
	        id: 'player',
	        cls: 'Panel player',
	        style: {
	            position: 'absolute',
	            display: 'none'
	        }
	    });

	    this.container.render();

	    this.player = new AppPlayer();
	    this.container.dom.appendChild(this.player.dom);
	};

	/**
	 * 历史记录面板
	 * @author dforrer / https://github.com/dforrer
	 * Developed as part of a project at University of Applied Sciences and Arts Northwestern Switzerland (www.fhnw.ch)
	 */
	function HistoryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	HistoryPanel.prototype = Object.create(UI$1.Control.prototype);
	HistoryPanel.prototype.constructor = HistoryPanel;

	HistoryPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var config = editor.config;
	    var history = editor.history;

	    var _this = this;

	    var data = {
	        xtype: 'div',
	        parent: this.parent,
	        cls: 'Panel',
	        children: [{
	            xtype: 'label',
	            text: '历史记录'
	        }, {
	            xtype: 'boolean',
	            text: '永久',
	            style: {
	                position: 'absolute',
	                right: '8px'
	            },
	            onChange: function () {
	                var value = this.getValue();
	                config.setKey('settings/history', value);
	                if (value) {
	                    UI$1.msg('历史记录将被保存在会话中。\n这会对使用材质的性能产生影响。');
	                    var lastUndoCmd = history.undos[history.undos.length - 1];
	                    var lastUndoId = (lastUndoCmd !== undefined) ? lastUndoCmd.id : 0;
	                    editor.history.enableSerialization(lastUndoId);
	                } else {
	                    _this.app.call('historyChanged');
	                }
	            }
	        }, {
	            xtype: 'br'
	        }, {
	            xtype: 'br'
	        }, {
	            xtype: 'outliner',
	            id: 'historyOutlinear',
	            editor: editor,
	            onChange: function () {
	                history.goToState(parseInt(this.getValue()));
	            }
	        }]
	    };

	    var control = UI$1.create(data);
	    control.render();
	};

	/**
	 * 材质面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function MaterialPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	MaterialPanel.prototype = Object.create(UI$1.Control.prototype);
	MaterialPanel.prototype.constructor = MaterialPanel;

	MaterialPanel.prototype.render = function () {
	    var _this = this;
	    var editor = this.app.editor;

	    var update = function () {
	        _this.app.call('updateMaterial', _this);
	    };

	    var data = {
	        xtype: 'div',
	        parent: this.parent,
	        id: 'materialPanel',
	        cls: 'Panel',
	        style: {
	            borderTop: 0,
	            paddingTop: '20px',
	            display: 'none'
	        },
	        children: [{ // New Copy Paste
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: ''
	            }, {
	                xtype: 'button',
	                id: 'btnNewMaterial',
	                text: '新建',
	                onClick: function () {
	                    this.app.call('newMaterial', this);
	                }
	            }, {
	                xtype: 'button',
	                id: 'btnCopyMaterial',
	                text: '复制',
	                style: {
	                    marginLeft: '4px'
	                },
	                onClick: function () {
	                    this.app.call('copyMaterial', this);
	                }
	            }, {
	                xtype: 'button',
	                text: '粘贴',
	                id: 'btnPasteMaterial',
	                style: {
	                    marginLeft: '4px'
	                },
	                onClick: function () {
	                    this.app.call('pasteMaterial', this);
	                }
	            }]
	        }, { // type
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '类型'
	            }, {
	                xtype: 'select',
	                id: 'materialClass',
	                options: {
	                    'LineBasicMaterial': '线条材质',
	                    'LineDashedMaterial': '虚线材质',
	                    'MeshBasicMaterial': '基本材质',
	                    'MeshDepthMaterial': '深度材质',
	                    'MeshNormalMaterial': '法向量材质',
	                    'MeshLambertMaterial': '兰伯特材质',
	                    'MeshPhongMaterial': '冯氏材质',
	                    'PointCloudMaterial': '点云材质',
	                    'MeshStandardMaterial': '标准材质',
	                    'MeshPhysicalMaterial': '物理材质',
	                    'ShaderMaterial': '着色器材质',
	                    'SpriteMaterial': '精灵材质',
	                    'RawShaderMaterial': '原始着色器材质'
	                },
	                style: {
	                    width: '150px',
	                    fontSize: '12px'
	                },
	                onChange: function () {
	                    _this.app.call('updateMaterial');
	                }
	            }]
	        }, { // uuid
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: 'UUID'
	            }, {
	                xtype: 'input',
	                id: 'materialUUID',
	                style: {
	                    width: '102px',
	                    fontSize: '12px'
	                },
	                disabled: true
	            }, {
	                xtype: 'button',
	                text: '新建',
	                style: {
	                    marginLeft: '7px'
	                },
	                onClick: function () {
	                    var materialUUID = UI$1.get('materialUUID');
	                    materialUUID.setValue(THREE.Math.generateUUID());
	                    _this.app.call('updateMaterial');
	                }
	            }]
	        }, { // name
	            xtype: 'row',
	            id: 'materialNameRow',
	            children: [{
	                xtype: 'label',
	                text: '名称'
	            }, {
	                xtype: 'input',
	                id: 'materialName',
	                style: {
	                    width: '150px',
	                    fontSize: '12px'
	                },
	                onChange: function () {
	                    editor.execute(new SetMaterialValueCommand(editor.selected, 'name', this.getValue()));
	                }
	            }]
	        }, { // program
	            xtype: 'row',
	            id: 'materialProgramRow',
	            children: [{
	                xtype: 'label',
	                text: '着色器程序'
	            }, {
	                xtype: 'button',
	                text: '信息',
	                style: {
	                    marginLeft: '4px'
	                },
	                onClick: function () {
	                    _this.app.call('editScript', _this, currentObject, 'programInfo');
	                }
	            }, {
	                xtype: 'button',
	                text: '顶点着色器',
	                style: {
	                    marginLeft: '4px'
	                },
	                onClick: function () {
	                    _this.app.call('editScript', _this, currentObject, 'vertexShader');
	                }
	            }, {
	                xtype: 'button',
	                text: '片源着色器',
	                style: {
	                    marginLeft: '4px'
	                },
	                onClick: function () {
	                    _this.app.call('editScript', _this, currentObject, 'fragmentShader');
	                }
	            }]
	        }, { // color
	            xtype: 'row',
	            id: 'materialColorRow',
	            children: [{
	                xtype: 'label',
	                text: '颜色'
	            }, {
	                xtype: 'color',
	                id: 'materialColor',
	                onChange: update
	            }]
	        }, { // roughness
	            xtype: 'row',
	            id: 'materialRoughnessRow',
	            children: [{
	                xtype: 'label',
	                text: '粗糙度'
	            }, {
	                xtype: 'number',
	                id: 'materialRoughness',
	                value: 0.5,
	                style: {
	                    width: '60px'
	                },
	                range: [0, 1],
	                onChange: update
	            }]
	        }, { // metalness
	            xtype: 'row',
	            id: 'materialMetalnessRow',
	            children: [{
	                xtype: 'label',
	                text: '金属度'
	            }, {
	                xtype: 'number',
	                id: 'materialMetalness',
	                value: 0.5,
	                style: {
	                    width: '60px'
	                },
	                range: [0, 1],
	                onChange: update
	            }]
	        }, { // emissive
	            xtype: 'row',
	            id: 'materialEmissiveRow',
	            children: [{
	                xtype: 'label',
	                text: '发光'
	            }, {
	                xtype: 'color',
	                id: 'materialEmissive',
	                value: 0x000000,
	                onChange: update
	            }]
	        }, { // specular
	            xtype: 'row',
	            id: 'materialSpecularRow',
	            children: [{
	                xtype: 'label',
	                text: '镜面度'
	            }, {
	                xtype: 'color',
	                id: 'materialSpecular',
	                value: 0x111111,
	                onChange: update
	            }]
	        }, { // shininess
	            xtype: 'row',
	            id: 'materialShininessRow',
	            children: [{
	                xtype: 'label',
	                text: '光亮度'
	            }, {
	                xtype: 'number',
	                id: 'materialShininess',
	                value: 30,
	                onChange: update
	            }]
	        }, { // clearCoat
	            xtype: 'row',
	            id: 'materialClearCoatRow',
	            children: [{
	                xtype: 'label',
	                text: '透明度'
	            }, {
	                xtype: 'number',
	                id: 'materialClearCoat',
	                value: 1,
	                style: {
	                    width: '60px'
	                },
	                range: [0, 1],
	                onChange: update
	            }]
	        }, { // clearCoatRoughness
	            xtype: 'row',
	            id: 'materialClearCoatRoughnessRow',
	            children: [{
	                xtype: 'label',
	                text: '透明粗糙度'
	            }, {
	                xtype: 'number',
	                id: 'materialClearCoatRoughness',
	                value: 1,
	                style: {
	                    width: '60px'
	                },
	                range: [0, 1],
	                onChange: update
	            }]
	        }, { // vertex colors
	            xtype: 'row',
	            id: 'materialVertexColorsRow',
	            children: [{
	                xtype: 'label',
	                text: '顶点颜色'
	            }, {
	                xtype: 'select',
	                id: 'materialVertexColors',
	                options: {
	                    0: '无',
	                    1: '面',
	                    2: '顶点'
	                },
	                onChange: update
	            }]
	        }, { // skinning
	            xtype: 'row',
	            id: 'materialSkinningRow',
	            children: [{
	                xtype: 'label',
	                text: '皮肤'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialSkinning',
	                value: false,
	                onChange: update
	            }]
	        }, { // map
	            xtype: 'row',
	            id: 'materialMapRow',
	            children: [{
	                xtype: 'label',
	                text: '纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialMap',
	                onChange: update
	            }]
	        }, { // alpha map
	            xtype: 'row',
	            id: 'materialAlphaMapRow',
	            children: [{
	                xtype: 'label',
	                text: '透明纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialAlphaMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialAlphaMap',
	                onChange: update
	            }]
	        }, { // bump map
	            xtype: 'row',
	            id: 'materialBumpMapRow',
	            children: [{
	                xtype: 'label',
	                text: '凹凸纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialBumpMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialBumpMap',
	                value: 1,
	                style: {
	                    width: '30px'
	                },
	                onChange: update
	            }, {
	                xtype: 'number',
	                id: 'materialBumpScale',
	                value: 1,
	                style: {
	                    width: '30px'
	                },
	                onChange: update
	            }]
	        }, { // normal map
	            xtype: 'row',
	            id: 'materialNormalMapRow',
	            children: [{
	                xtype: 'label',
	                text: '法线纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialNormalMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialNormalMap',
	                onChange: update
	            }]
	        }, { // displacement map
	            xtype: 'row',
	            id: 'materialDisplacementMapRow',
	            children: [{
	                xtype: 'label',
	                text: '位移纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialDisplacementMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialDisplacementMap',
	                onChange: update
	            }, {
	                xtype: 'number',
	                id: 'materialDisplacementScale',
	                value: 1,
	                style: {
	                    width: '30px'
	                },
	                onChange: update
	            }]
	        }, { // roughness map
	            xtype: 'row',
	            id: 'materialRoughnessMapRow',
	            children: [{
	                xtype: 'label',
	                text: '粗糙纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialRoughnessMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialRoughnessMap',
	                onChange: update
	            }]
	        }, { // metalness map
	            xtype: 'row',
	            id: 'materialMetalnessMapRow',
	            children: [{
	                xtype: 'label',
	                text: '金属纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialMetalnessMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialMetalnessMap',
	                onChange: update
	            }]
	        }, { // specular map
	            xtype: 'row',
	            id: 'materialSpecularMapRow',
	            children: [{
	                xtype: 'label',
	                text: '镜面纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialSpecularMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialSpecularMap',
	                onChange: update
	            }]
	        }, { // env map
	            xtype: 'row',
	            id: 'materialEnvMapRow',
	            children: [{
	                xtype: 'label',
	                text: '环境纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialEnvMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialEnvMap',
	                mapping: THREE.SphericalReflectionMapping,
	                onChange: update
	            }, {
	                xtype: 'number',
	                id: 'materialReflectivity',
	                value: 1,
	                style: {
	                    width: '30px'
	                },
	                onChange: update
	            }]
	        }, { // light map
	            xtype: 'row',
	            id: 'materialLightMapRow',
	            children: [{
	                xtype: 'label',
	                text: '光照纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialLightMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialLightMap',
	                onChange: update
	            }]
	        }, { // ambient occlusion map
	            xtype: 'row',
	            id: 'materialAOMapRow',
	            children: [{
	                xtype: 'label',
	                text: '遮挡纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialAOMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialAOMap',
	                onChange: update
	            }, {
	                xtype: 'number',
	                id: 'materialAOScale',
	                value: 1,
	                range: [0, 1],
	                style: {
	                    width: '30px'
	                },
	                onChange: update
	            }]
	        }, { // emissive map
	            xtype: 'row',
	            id: 'materialEmissiveMapRow',
	            children: [{
	                xtype: 'label',
	                text: '放射纹理'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialEmissiveMapEnabled',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'texture',
	                id: 'materialEmissiveMap',
	                onChange: update
	            }]
	        }, { // side
	            xtype: 'row',
	            id: 'materialSideRow',
	            children: [{
	                xtype: 'label',
	                text: '剔除'
	            }, {
	                xtype: 'select',
	                id: 'materialSide',
	                options: {
	                    0: '正面',
	                    1: '反面',
	                    2: '双面'
	                },
	                style: {
	                    width: '150px',
	                    fontSize: '12px'
	                },
	                onChange: update
	            }]
	        }, { // shading
	            xtype: 'row',
	            id: 'materialShadingRow',
	            children: [{
	                xtype: 'label',
	                text: '着色'
	            }, {
	                xtype: 'select',
	                id: 'materialShading',
	                options: {
	                    0: '无',
	                    1: '平坦',
	                    2: '光滑'
	                },
	                style: {
	                    width: '150px',
	                    fontSize: '12px'
	                },
	                onChange: update
	            }]
	        }, { // blending
	            xtype: 'row',
	            id: 'materialBlendingRow',
	            children: [{
	                xtype: 'label',
	                text: '混合'
	            }, {
	                xtype: 'select',
	                id: 'materialBlending',
	                options: {
	                    0: '不混合',
	                    1: '一般混合',
	                    2: '和混合',
	                    3: '差混合',
	                    4: '积混合',
	                    5: '自定义混合'
	                },
	                style: {
	                    width: '150px',
	                    fontSize: '12px'
	                },
	                onChange: update
	            }]
	        }, { // opacity
	            xtype: 'row',
	            id: 'materialOpacityRow',
	            children: [{
	                xtype: 'label',
	                text: '不透明度'
	            }, {
	                xtype: 'number',
	                id: 'materialOpacity',
	                value: 1,
	                style: {
	                    width: '60px'
	                },
	                range: [0, 1],
	                onChange: update
	            }]
	        }, { // transparent
	            xtype: 'row',
	            id: 'materialTransparentRow',
	            children: [{
	                xtype: 'label',
	                text: '透明'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialTransparent',
	                style: {
	                    left: '100px'
	                },
	                onChange: update
	            }]
	        }, { // alpha test
	            xtype: 'row',
	            id: 'materialAlphaTestRow',
	            children: [{
	                xtype: 'label',
	                text: 'α测试'
	            }, {
	                xtype: 'number',
	                id: 'materialAlphaTest',
	                style: {
	                    width: '60px'
	                },
	                range: [0, 1],
	                onChange: update
	            }]
	        }, { // wireframe
	            xtype: 'row',
	            id: 'materialWireframeRow',
	            children: [{
	                xtype: 'label',
	                text: '线框'
	            }, {
	                xtype: 'checkbox',
	                id: 'materialWireframe',
	                value: false,
	                onChange: update
	            }, {
	                xtype: 'number',
	                id: 'materialWireframeLinewidth',
	                value: 1,
	                style: {
	                    width: '60px'
	                },
	                range: [0, 100],
	                onChange: update
	            }]
	        }]
	    };

	    var control = UI$1.create(data);
	    control.render();
	};

	/**
	 * 物体面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function ObjectPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	ObjectPanel.prototype = Object.create(UI$1.Control.prototype);
	ObjectPanel.prototype.constructor = ObjectPanel;

	ObjectPanel.prototype.render = function () {
	    var editor = this.app.editor;

	    var _this = this;

	    var update = function () {
	        _this.app.call('updateObject', _this);
	    };

	    var data = {
	        xtype: 'div',
	        id: 'objectPanel',
	        parent: this.parent,
	        cls: 'Panel',
	        style: {
	            borderTop: 0,
	            paddingTop: '20px',
	            display: 'none'
	        },
	        children: [{ // type
	            xtype: 'row',
	            id: 'objectTypeRow',
	            children: [{
	                xtype: 'label',
	                text: '类型'
	            }, {
	                xtype: 'text',
	                id: 'objectType'
	            }]
	        }, { // uuid
	            xtype: 'row',
	            id: 'objectUUIDRow',
	            children: [{
	                xtype: 'label',
	                text: 'UUID'
	            }, {
	                xtype: 'input',
	                id: 'objectUUID',
	                style: {
	                    width: '102px',
	                    fontSize: '12px'
	                },
	                disabled: true
	            }, {
	                xtype: 'button',
	                id: 'objectUUIDRenew',
	                text: '新建',
	                style: {
	                    marginLeft: '7px'
	                },
	                onClick: function () {
	                    var objectUUID = UI$1.get('objectUUID');
	                    objectUUID.setValue(THREE.Math.generateUUID());
	                    editor.execute(new SetUuidCommand(editor.selected, objectUUID.getValue()));
	                }
	            }]
	        }, { // name
	            xtype: 'row',
	            id: 'objectNameRow',
	            children: [{
	                xtype: 'label',
	                text: '名称'
	            }, {
	                xtype: 'input',
	                id: 'objectName',
	                style: {
	                    width: '150px',
	                    fontSize: '12px'
	                },
	                onChange: function () {
	                    editor.execute(new SetValueCommand(editor.selected, 'name', this.getValue()));
	                }
	            }]
	        }, { // position
	            xtype: 'row',
	            id: 'objectPositionRow',
	            children: [{
	                xtype: 'label',
	                text: '位置'
	            }, {
	                xtype: 'number',
	                id: 'objectPositionX',
	                style: {
	                    width: '50px'
	                },
	                onChange: update
	            }, {
	                xtype: 'number',
	                id: 'objectPositionY',
	                style: {
	                    width: '50px'
	                },
	                onChange: update
	            }, {
	                xtype: 'number',
	                id: 'objectPositionZ',
	                style: {
	                    width: '50px'
	                },
	                onChange: update
	            }]
	        }, { // rotation
	            xtype: 'row',
	            id: 'objectRotationRow',
	            children: [{
	                xtype: 'label',
	                text: '旋转'
	            }, {
	                xtype: 'number',
	                id: 'objectRotationX',
	                step: 10,
	                unit: '°',
	                style: {
	                    width: '50px'
	                },
	                onChange: update
	            }, {
	                xtype: 'number',
	                id: 'objectRotationY',
	                step: 10,
	                unit: '°',
	                style: {
	                    width: '50px'
	                },
	                onChange: update
	            }, {
	                xtype: 'number',
	                id: 'objectRotationZ',
	                step: 10,
	                unit: '°',
	                style: {
	                    width: '50px'
	                },
	                onChange: update
	            }]
	        }, { // scale
	            xtype: 'row',
	            id: 'objectScaleRow',
	            children: [{
	                xtype: 'label',
	                text: '尺寸'
	            }, {
	                xtype: 'checkbox',
	                id: 'objectScaleLock',
	                value: true,
	                style: {
	                    position: 'absolute',
	                    left: '75px'
	                }
	            }, {
	                xtype: 'number',
	                id: 'objectScaleX',
	                value: 1,
	                range: [0.01, Infinity],
	                style: {
	                    width: '50px'
	                },
	                onChange: function () {
	                    _this.app.call('updateScaleX', _this);
	                }
	            }, {
	                xtype: 'number',
	                id: 'objectScaleY',
	                value: 1,
	                range: [0.01, Infinity],
	                style: {
	                    width: '50px'
	                },
	                onChange: function () {
	                    _this.app.call('updateScaleY', _this);
	                }
	            }, {
	                xtype: 'number',
	                id: 'objectScaleZ',
	                value: 1,
	                range: [0.01, Infinity],
	                style: {
	                    width: '50px'
	                },
	                onChange: function () {
	                    _this.app.call('updateScaleZ', _this);
	                }
	            }]
	        }, { // fov
	            xtype: 'row',
	            id: 'objectFovRow',
	            children: [{
	                xtype: 'label',
	                text: '视场'
	            }, {
	                xtype: 'number',
	                id: 'objectFov',
	                onChange: update
	            }]
	        }, { // near
	            xtype: 'row',
	            id: 'objectNearRow',
	            children: [{
	                xtype: 'label',
	                text: '近点'
	            }, {
	                xtype: 'number',
	                id: 'objectNear',
	                onChange: update
	            }]
	        }, { // far
	            xtype: 'row',
	            id: 'objectFarRow',
	            children: [{
	                xtype: 'label',
	                text: '远点'
	            }, {
	                xtype: 'number',
	                id: 'objectFar',
	                onChange: update
	            }]
	        }, { // intensity
	            xtype: 'row',
	            id: 'objectIntensityRow',
	            children: [{
	                xtype: 'label',
	                text: '强度'
	            }, {
	                xtype: 'number',
	                id: 'objectIntensity',
	                range: [0, Infinity],
	                onChange: update
	            }]
	        }, { // color
	            xtype: 'row',
	            id: 'objectColorRow',
	            children: [{
	                xtype: 'label',
	                text: '颜色'
	            }, {
	                xtype: 'color',
	                id: 'objectColor',
	                onChange: update
	            }]
	        }, { // ground color
	            xtype: 'row',
	            id: 'objectGroundColorRow',
	            children: [{
	                xtype: 'label',
	                text: '地面颜色'
	            }, {
	                xtype: 'color',
	                id: 'objectGroundColor',
	                onChange: update
	            }]
	        }, { // distance
	            xtype: 'row',
	            id: 'objectDistanceRow',
	            children: [{
	                xtype: 'label',
	                text: '距离'
	            }, {
	                xtype: 'number',
	                id: 'objectDistance',
	                range: [0, Infinity],
	                onChange: update
	            }]
	        }, { // angle
	            xtype: 'row',
	            id: 'objectAngleRow',
	            children: [{
	                xtype: 'label',
	                text: '角度'
	            }, {
	                xtype: 'number',
	                id: 'objectAngle',
	                precision: 3,
	                range: [0, Math.PI / 2],
	                onChange: update
	            }]
	        }, { // penumbra
	            xtype: 'row',
	            id: 'objectPenumbraRow',
	            children: [{
	                xtype: 'label',
	                text: '边缘'
	            }, {
	                xtype: 'number',
	                id: 'objectPenumbra',
	                range: [0, 1],
	                onChange: update
	            }]
	        }, { // decay
	            xtype: 'row',
	            id: 'objectDecayRow',
	            children: [{
	                xtype: 'label',
	                text: '衰变'
	            }, {
	                xtype: 'number',
	                id: 'objectDecay',
	                range: [0, Infinity],
	                onChange: update
	            }]
	        }, { // shadow
	            xtype: 'row',
	            id: 'objectShadowRow',
	            children: [{
	                xtype: 'label',
	                text: '阴影'
	            }, {
	                xtype: 'boolean',
	                id: 'objectCastShadow',
	                value: false,
	                text: '产生',
	                onChange: update
	            }, {
	                xtype: 'boolean',
	                id: 'objectReceiveShadow',
	                value: false,
	                text: '接收',
	                onChange: update
	            }, {
	                xtype: 'number',
	                id: 'objectShadowRadius',
	                value: 1,
	                onChange: update
	            }]
	        }, { // visible
	            xtype: 'row',
	            id: 'objectVisibleRow',
	            children: [{
	                xtype: 'label',
	                text: '可见性'
	            }, {
	                xtype: 'checkbox',
	                id: 'objectVisible',
	                onChange: update
	            }]
	        }, { // user data
	            xtype: 'row',
	            id: 'objectUserDataRow',
	            children: [{
	                xtype: 'label',
	                text: '用户数据'
	            }, {
	                xtype: 'textarea',
	                id: 'objectUserData',
	                style: {
	                    width: '150px',
	                    height: '40px',
	                    fontSize: '12px'
	                },
	                onChange: update,
	                onKeyUp: function () {
	                    try {
	                        JSON.parse(this.getValue());
	                        this.dom.classList.add('success');
	                        this.dom.classList.remove('fail');
	                    } catch (error) {
	                        this.dom.classList.remove('success');
	                        this.dom.classList.add('fail');
	                    }
	                }
	            }]
	        }]
	    };

	    var control = UI$1.create(data);
	    control.render();
	};

	/**
	 * 工程面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function ProjectPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	ProjectPanel.prototype = Object.create(UI$1.Control.prototype);
	ProjectPanel.prototype.constructor = ProjectPanel;

	ProjectPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var config = editor.config;

	    var rendererTypes = {
	        'WebGLRenderer': THREE.WebGLRenderer,
	        'CanvasRenderer': THREE.CanvasRenderer,
	        'SVGRenderer': THREE.SVGRenderer,
	        'SoftwareRenderer': THREE.SoftwareRenderer,
	        'RaytracingRenderer': THREE.RaytracingRenderer
	    };

	    var options = {};

	    for (var key in rendererTypes) {
	        if (key.indexOf('WebGL') >= 0 && System.support.webgl === false) continue;
	        options[key] = key;
	    }

	    var _this = this;

	    var updateRenderer = function () {
	        _this.app.call('updateRenderer', _this);
	    };

	    var data = {
	        xtype: 'div',
	        id: 'projectPanel',
	        parent: this.parent,
	        style: {
	            borderTop: 0,
	            paddingTop: '20px'
	        },
	        cls: 'Panel',
	        children: [{ // class
	            xtype: 'row',
	            id: 'rendererTypeRow',
	            children: [{
	                xtype: 'label',
	                text: '渲染器',
	                style: {
	                    width: '90px'
	                }
	            }, {
	                xtype: 'select',
	                id: 'rendererType',
	                options: options,
	                value: config.getKey('project/renderer'),
	                style: {
	                    width: '150px'
	                },
	                onChange: function () {
	                    var value = this.getValue();
	                    config.setKey('project/renderer', value);
	                    updateRenderer();
	                }
	            }]
	        }, {
	            xtype: 'row',
	            id: 'rendererPropertiesRow',
	            style: {
	                marginLeft: '90px'
	            },
	            children: [{ // antialiasing
	                xtype: 'boolean',
	                id: 'rendererAntialias',
	                value: config.getKey('project/renderer/antialias'),
	                text: '抗锯齿',
	                onChange: function () {
	                    config.setKey('project/renderer/antialias', this.getValue());
	                    updateRenderer();
	                }
	            }, { // shadow
	                xtype: 'boolean',
	                id: 'rendererShadows',
	                value: config.getKey('project/renderer/shadows'),
	                text: '阴影',
	                onChange: function () {
	                    config.setKey('project/renderer/shadows', this.getValue());
	                    updateRenderer();
	                }
	            }, {
	                xtype: 'br'
	            }, { // gamma input
	                xtype: 'boolean',
	                id: 'rendererGammaInput',
	                value: config.getKey('project/renderer/gammaInput'),
	                text: 'γ输入',
	                onChange: function () {
	                    config.setKey('project/renderer/gammaInput', this.getValue());
	                    updateRenderer();
	                }
	            }, { // gamma output
	                xtype: 'boolean',
	                id: 'rendererGammaOutput',
	                value: config.getKey('project/renderer/gammaOutput'),
	                text: 'γ输出',
	                onChange: function () {
	                    config.setKey('project/renderer/gammaOutput', this.getValue());
	                    updateRenderer();
	                }
	            }]
	        }, { // VR
	            xtype: 'row',
	            id: 'vrRow',
	            children: [{
	                xtype: 'label',
	                text: '虚拟现实',
	                style: {
	                    width: '90px'
	                }
	            }, {
	                xtype: 'checkbox',
	                id: 'vr',
	                value: config.getKey('project/vr'),
	                style: {
	                    left: '100px'
	                },
	                onChange: function () {
	                    config.setKey('project/vr', this.getValue());
	                    // updateRenderer();
	                }
	            }]
	        }]
	    };

	    var control = UI$1.create(data);
	    control.render();
	};

	/**
	 * 几何体信息面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function GeometryInfoPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	GeometryInfoPanel.prototype = Object.create(UI$1.Control.prototype);
	GeometryInfoPanel.prototype.constructor = GeometryInfoPanel;

	GeometryInfoPanel.prototype.render = function () {
	    var editor = this.app.editor;

	    this.children = {
	        'xtype': 'row',
	        parent: this.parent,
	        children: [{ // vertices
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '顶点'
	            }, {
	                xtype: 'text',
	                id: 'geometryInfoVertices'
	            }]
	        }, { // faces
	            xtype: 'row',
	            children: [{
	                xtype: 'label',
	                text: '面'
	            }, {
	                xtype: 'text',
	                id: 'geometryInfoFaces',
	            }]
	        }]
	    };

	    var container = UI$1.create(this.children);
	    container.render();

	    function update(object) {
	        var vertices = UI$1.get('geometryInfoVertices');
	        var faces = UI$1.get('geometryInfoFaces');

	        if (object === null) return; // objectSelected.dispatch( null )
	        if (object === undefined) return;

	        var geometry = object.geometry;

	        if (geometry instanceof THREE.Geometry) {
	            container.dom.style.display = 'block';

	            vertices.setValue((geometry.vertices.length).format());
	            faces.setValue((geometry.faces.length).format());
	        } else {
	            container.dom.style.display = 'none';
	        }
	    }

	    this.app.on('objectSelected.GeometryInfoPanel', function (mesh) {
	        update(mesh);
	    });

	    this.app.on('geometryChanged.GeometryInfoPanel', function (mesh) {
	        update(mesh);
	    });
	};

	/**
	 * 缓冲几何体面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function BufferGeometryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	BufferGeometryPanel.prototype = Object.create(UI$1.Control.prototype);
	BufferGeometryPanel.prototype.constructor = BufferGeometryPanel;

	BufferGeometryPanel.prototype.render = function () {
	    var editor = this.app.editor;

	    var data = {
	        xtype: 'row',
	        id: 'bufferGeometryPanel',
	        parent: this.parent
	    };

	    var container = UI$1.create(data);
	    container.render();

	    function update(object) {
	        if (object === null) return; // objectSelected.dispatch( null )
	        if (object === undefined) return;

	        var geometry = object.geometry;

	        if (geometry instanceof THREE.BufferGeometry) {
	            container.dom.innerHTML = '';
	            container.dom.style.display = 'block';

	            var index = geometry.index;

	            if (index !== null) {
	                var panel = UI$1.create({
	                    xtype: 'row',
	                    parent: container.dom,
	                    children: [{
	                        xtype: 'label',
	                        text: '索引数'
	                    }, {
	                        xtype: 'text',
	                        text: (index.count).format(),
	                        style: {
	                            fontSize: '12px'
	                        }
	                    }]
	                });

	                panel.render();
	            }

	            var attributes = geometry.attributes;

	            for (var name in attributes) {

	                var attribute = attributes[name];

	                var panel = UI$1.create({
	                    xtype: 'row',
	                    parent: container.dom,
	                    children: [{
	                        xtype: 'label',
	                        text: name
	                    }, {
	                        xtype: 'text',
	                        text: (attribute.count).format() + ' (' + attribute.itemSize + ')',
	                        style: {
	                            fontSize: '12px'
	                        }
	                    }]
	                });

	                panel.render();
	            }
	        } else {
	            container.dom.style.display = 'none';
	        }
	    }

	    this.app.on('objectSelected.BufferGeometryPanel', function (mesh) {
	        update(mesh);
	    });

	    this.app.on('geometryChanged.BufferGeometryPanel', function (mesh) {
	        update(mesh);
	    });
	};

	/**
	 * 几何体面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function GeometryPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	GeometryPanel.prototype = Object.create(UI$1.Control.prototype);
	GeometryPanel.prototype.constructor = GeometryPanel;

	GeometryPanel.prototype.render = function () {
	    var editor = this.app.editor;

	    this.children = [{
	        xtype: 'div',
	        id: 'geometryPanel',
	        parent: this.parent,
	        cls: 'Panel',
	        style: {
	            borderTop: 0,
	            paddingTop: '20px',
	            display: 'none'
	        },
	        children: [{ // type
	            xtype: 'row',
	            id: 'geometryTypeRow',
	            children: [{
	                xtype: 'label',
	                text: '类型'
	            }, {
	                xtype: 'text',
	                id: 'geometryType'
	            }]
	        }, { // uuid
	            xtype: 'row',
	            id: 'geometryUUIDRow',
	            children: [{
	                xtype: 'label',
	                text: 'UUID'
	            }, {
	                xtype: 'input',
	                id: 'geometryUUID',
	                style: {
	                    width: '102px',
	                    fontSize: '12px'
	                },
	                disabled: true
	            }, {
	                xtype: 'button',
	                id: 'geometryUUIDRenew',
	                text: '新建',
	                style: {
	                    marginLeft: '7px'
	                },
	                onClick: function () {
	                    geometryUUID.setValue(THREE.Math.generateUUID());
	                    editor.execute(new SetGeometryValueCommand(editor.selected, 'uuid', geometryUUID.getValue()));
	                }
	            }]
	        }, { // name
	            xtype: 'row',
	            id: 'geometryNameRow',
	            children: [{
	                xtype: 'label',
	                text: '名称'
	            }, {
	                xtype: 'input',
	                id: 'geometryName',
	                style: {
	                    width: '150px',
	                    fontSize: '12px'
	                },
	                onChange: function () {
	                    editor.execute(new SetGeometryValueCommand(editor.selected, 'name', this.getValue()));
	                }
	            }]
	        }, {
	            xtype: 'row',
	            id: 'geometryParameters',
	            children: [
	                new BufferGeometryPanel({ app: this.app })
	            ]
	        },
	        new GeometryInfoPanel({ app: this.app, id: 'geometryInfoPanel' })
	        ]
	    }];

	    var container = UI$1.create(this.children[0]);
	    container.render();
	};

	/**
	 * 属性面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function PropertyPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	PropertyPanel.prototype = Object.create(UI$1.Control.prototype);
	PropertyPanel.prototype.constructor = PropertyPanel;

	PropertyPanel.prototype.render = function () {
	    var editor = this.app.editor;

	    var _this = this;

	    var onClick = function (event) {
	        _this.app.call('selectPropertyTab', _this, event.target.textContent);
	    };

	    var data = {
	        xtype: 'div',
	        id: 'propertyPanel',
	        parent: this.parent,
	        children: [{
	            xtype: 'div',
	            cls: 'tabs',
	            children: [{
	                xtype: 'text',
	                id: 'objectTab',
	                text: '物体',
	                onClick: onClick
	            }, {
	                xtype: 'text',
	                id: 'geometryTab',
	                text: '几何',
	                onClick: onClick
	            }, {
	                xtype: 'text',
	                id: 'materialTab',
	                text: '材质',
	                onClick: onClick
	            }]
	        }, {
	            xtype: 'div',
	            children: [
	                new ObjectPanel({ app: this.app, id: 'object' })
	            ]
	        }, {
	            xtype: 'div',
	            children: [
	                new GeometryPanel({ app: this.app, id: 'geometry' })
	            ]
	        }, {
	            xtype: 'div',
	            children: [
	                new MaterialPanel({ app: this.app, id: 'material' })
	            ]
	        }]
	    };

	    var control = UI$1.create(data);
	    control.render();
	};

	/**
	 * 场景面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function ScenePanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	ScenePanel.prototype = Object.create(UI$1.Control.prototype);
	ScenePanel.prototype.constructor = ScenePanel;

	ScenePanel.prototype.render = function () {
	    var editor = this.app.editor;

	    var _this = this;

	    var onFogChanged = function () {
	        var fogType = UI$1.get('fogType');
	        var fogColor = UI$1.get('fogColor');
	        var fogNear = UI$1.get('fogNear');
	        var fogFar = UI$1.get('fogFar');
	        var fogDensity = UI$1.get('fogDensity');

	        _this.app.call('sceneFogChanged',
	            _this,
	            fogType.getValue(),
	            fogColor.getHexValue(),
	            fogNear.getValue(),
	            fogFar.getValue(),
	            fogDensity.getValue()
	        );
	    };

	    var refreshFogUI = function () {
	        _this.app.call('updateScenePanelFog', _this);
	    };

	    var data = {
	        xtype: 'div',
	        id: 'scenePanel',
	        parent: this.parent,
	        cls: 'Panel',
	        children: [{ // outliner
	            xtype: 'outliner',
	            id: 'outliner',
	            editor: editor,
	            onChange: function () {
	                _this.app.call('outlinerChange', _this, this);
	            },
	            onDblClick: function () {
	                editor.focusById(parseInt(this.getValue()));
	            }
	        }, {
	            xtype: 'br'
	        }, { // background
	            xtype: 'row',
	            id: 'backgroundRow',
	            children: [{
	                xtype: 'label',
	                text: '背景',
	                style: {
	                    width: '90px'
	                }
	            }, {
	                xtype: 'color',
	                id: 'backgroundColor',
	                value: '#aaaaaa',
	                onChange: function () {
	                    _this.app.call('sceneBackgroundChanged', _this, this.getHexValue());
	                }
	            }]
	        }, { // fog
	            xtype: 'row',
	            id: 'fogTypeRow',
	            children: [{
	                xtype: 'label',
	                text: '雾',
	                style: {
	                    width: '90px'
	                }
	            }, {
	                xtype: 'select',
	                id: 'fogType',
	                options: {
	                    'None': '无',
	                    'Fog': '线性',
	                    'FogExp2': '指数型'
	                },
	                style: {
	                    width: '150px'
	                },
	                onChange: function () {
	                    onFogChanged();
	                    refreshFogUI();
	                }
	            }]
	        }, {
	            xtype: 'row',
	            id: 'fogPropertiesRow',
	            children: [{ // fog color
	                xtype: 'color',
	                id: 'fogColor',
	                value: '#aaaaaa',
	                onChange: onFogChanged
	            }, { // fog near
	                xtype: 'number',
	                id: 'fogNear',
	                value: 0.1,
	                style: {
	                    width: '40px'
	                },
	                range: [0, Infinity],
	                onChange: onFogChanged
	            }, { // fog far
	                xtype: 'number',
	                id: 'fogFar',
	                value: 50,
	                style: {
	                    width: '40px'
	                },
	                range: [0, Infinity],
	                onChange: onFogChanged
	            }, { // fog density
	                xtype: 'number',
	                id: 'fogDensity',
	                value: 0.05,
	                style: {
	                    width: '40px'
	                },
	                range: [0, 0.1],
	                precision: 3,
	                onChange: onFogChanged
	            }]
	        }]
	    };

	    var control = UI$1.create(data);
	    control.render();
	};

	/**
	 * 脚本面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function ScriptPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	ScriptPanel.prototype = Object.create(UI$1.Control.prototype);
	ScriptPanel.prototype.constructor = ScriptPanel;

	ScriptPanel.prototype.render = function () {
	    var editor = this.app.editor;

	    var data = {
	        xtype: 'div',
	        id: 'scriptPanel',
	        parent: this.parent,
	        cls: 'Panel scriptPanel',
	        style: {
	            display: 'none'
	        },
	        children: [{
	            xtype: 'label',
	            text: '脚本'
	        }, {
	            xtype: 'br'
	        }, {
	            xtype: 'br'
	        }, {
	            xtype: 'row',
	            id: 'scriptsContainer'
	        }, {
	            xtype: 'button',
	            id: 'newScript',
	            text: '新建',
	            onClick: function () {
	                var script = { name: '', source: 'function update( event ) {}' };
	                editor.execute(new AddScriptCommand(editor.selected, script));
	            }
	        }]
	    };

	    var control = UI$1.create(data);
	    control.render();
	};

	/**
	 * 设置面板
	 * @author mrdoob / http://mrdoob.com/
	 */
	function SettingPanel(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	SettingPanel.prototype = Object.create(UI$1.Control.prototype);
	SettingPanel.prototype.constructor = SettingPanel;

	SettingPanel.prototype.render = function () {
	    var editor = this.app.editor;
	    var config = editor.config;

	    var data = {
	        xtype: 'div',
	        id: 'settingPanel',
	        parent: this.parent,
	        cls: 'Panel',
	        style: {
	            borderTop: 0,
	            paddingTop: '20px'
	        },
	        children: [{
	            xtype: 'row',
	            id: 'themeRow',
	            children: [{
	                xtype: 'label',
	                text: '主题'
	            }, { // class
	                xtype: 'select',
	                options: {
	                    'assets/css/light.css': '浅色',
	                    'assets/css/dark.css': '深色'
	                },
	                value: config.getKey('theme'),
	                style: {
	                    width: '150px'
	                },
	                onChange: function () {
	                    var value = this.getValue();
	                    editor.setTheme(value);
	                    editor.config.setKey('theme', value);
	                }
	            }]
	        }]
	    };

	    var control = UI$1.create(data);
	    control.render();
	};

	/**
	 * 侧边栏
	 * @author mrdoob / http://mrdoob.com/
	 */
	function Sidebar(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	Sidebar.prototype = Object.create(UI$1.Control.prototype);
	Sidebar.prototype.constructor = Sidebar;

	Sidebar.prototype.render = function () {
	    var editor = this.app.editor;
	    var _this = this;

	    function onClick(event) {
	        _this.app.call('selectTab', _this, event.target.textContent);
	    }

	    var data = {
	        xtype: 'div',
	        id: 'sidebar',
	        cls: 'sidebar',
	        parent: this.app.container,
	        children: [{
	            xtype: 'div',
	            cls: 'tabs',
	            children: [{
	                xtype: 'text',
	                id: 'sceneTab',
	                text: '场景',
	                onClick: onClick
	            }, {
	                xtype: 'text',
	                id: 'projectTab',
	                text: '工程',
	                onClick: onClick
	            }, {
	                xtype: 'text',
	                id: 'settingsTab',
	                text: '设置',
	                onClick: onClick
	            }]
	        }, { // scene
	            xtype: 'div',
	            id: 'scene',
	            children: [
	                new ScenePanel({ app: this.app }),
	                new PropertyPanel({ app: this.app }),
	                new ScriptPanel({ app: this.app })
	            ]
	        }, { // project
	            xtype: 'div',
	            id: 'project',
	            children: [
	                new ProjectPanel({ app: this.app })
	            ]
	        }, {
	            xtype: 'div',
	            id: 'settings',
	            children: [
	                new SettingPanel({ app: this.app }),
	                new HistoryPanel({ app: this.app })
	            ]
	        }]
	    };

	    var control = UI$1.create(data);
	    control.render();
	};

	/**
	 * 异步加载css文件
	 * @param {*} url css文件url
	 */
	function loadCss(url) {
	    var head = document.getElementsByTagName('head')[0];
	    var link = document.createElement('link');
	    link.type = 'text/css';
	    link.rel = 'stylesheet';
	    link.href = url;
	    head.appendChild(link);
	}
	/**
	 * css工具类
	 */
	const CssUtils = {
	    load: loadCss
	};

	/**
	 * 异步加载js文件
	 * @param {*} url js文件url
	 * @param {*} callback 回调函数
	 */
	function loadJs(url, callback) {
	    var head = document.getElementsByTagName('head')[0];
	    var script = document.createElement('script');
	    script.type = 'text/javascript';
	    script.src = url;
	    head.appendChild(script);
	    if (typeof (callback) === 'function') {
	        script.onload = script.onreadystatechange = function () {
	            if (!this.readyState || this.readyState === "loaded" || this.readyState === "complete") {
	                callback();
	                script.onload = script.onreadystatechange = null;
	            }
	        };
	    }
	}
	/**
	 * js工具类
	 */
	const JsUtils = {
	    load: loadJs
	};

	/**
	 * 判断是否是three.js内置类
	 * @param {*} geometry 几何体
	 */
	function isBuildInGeometry(geometry) {
	    if (geometry instanceof THREE.BoxBufferGeometry ||
	        geometry instanceof THREE.BoxGeometry ||
	        geometry instanceof THREE.CircleBufferGeometry ||
	        geometry instanceof THREE.CircleGeometry ||
	        geometry instanceof THREE.ConeBufferGeometry ||
	        geometry instanceof THREE.ConeGeometry ||
	        geometry instanceof THREE.CylinderBufferGeometry ||
	        geometry instanceof THREE.CylinderGeometry ||
	        geometry instanceof THREE.DodecahedronBufferGeometry ||
	        geometry instanceof THREE.DodecahedronGeometry ||
	        geometry instanceof THREE.ExtrudeBufferGeometry ||
	        geometry instanceof THREE.ExtrudeGeometry ||
	        geometry instanceof THREE.IcosahedronBufferGeometry ||
	        geometry instanceof THREE.IcosahedronGeometry ||
	        geometry instanceof THREE.LatheBufferGeometry ||
	        geometry instanceof THREE.LatheGeometry ||
	        geometry instanceof THREE.OctahedronBufferGeometry ||
	        geometry instanceof THREE.OctahedronGeometry ||
	        geometry instanceof THREE.ParametricBufferGeometry ||
	        geometry instanceof THREE.ParametricGeometry ||
	        geometry instanceof THREE.PlaneBufferGeometry ||
	        geometry instanceof THREE.PlaneGeometry ||
	        geometry instanceof THREE.PolyhedronBufferGeometry ||
	        geometry instanceof THREE.PolyhedronGeometry ||
	        geometry instanceof THREE.RingBufferGeometry ||
	        geometry instanceof THREE.RingGeometry ||
	        geometry instanceof THREE.ShapeBufferGeometry ||
	        geometry instanceof THREE.ShapeGeometry ||
	        geometry instanceof THREE.SphereBufferGeometry ||
	        geometry instanceof THREE.SphereGeometry ||
	        geometry instanceof THREE.TetrahedronBufferGeometry ||
	        geometry instanceof THREE.TetrahedronGeometry ||
	        geometry instanceof THREE.TextBufferGeometry ||
	        geometry instanceof THREE.TextGeometry ||
	        geometry instanceof THREE.TorusBufferGeometry ||
	        geometry instanceof THREE.TorusGeometry ||
	        geometry instanceof THREE.TorusKnotBufferGeometry ||
	        geometry instanceof THREE.TorusKnotGeometry ||
	        geometry instanceof THREE.TubeBufferGeometry ||
	        geometry instanceof THREE.TubeGeometry
	    ) {
	        return true;
	    }

	    return false;
	}

	/**
	 * 几何体工具类
	 */
	const GeometryUtils = {
	    isBuildInGeometry: isBuildInGeometry
	};

	/**
	 * Socket工具类
	 * @param {*} url Socket服务器地址 
	 */
	function Socket(url) {
	    this.url = url;
	    this.reconnectTime = 5000; // 重新连接时间

	    this.socket = new WebSocket(this.url);

	    this.dispatch = dispatch('open', 'message', 'error', 'close');

	    var _this = this;
	    this.socket.onopen = function (evt) {
	        _this.dispatch.call.apply(_this.dispatch, arguments);
	    };

	    this.socket.onmessage = function (evt) {
	        _this.dispatch.call.apply(_this.dispatch, arguments);
	    };

	    this.socket.onerror = function (evt) {
	        _this.dispatch.call.apply(_this.dispatch, arguments);
	    };

	    this.socket.onclose = function (evt) {
	        _this.dispatch.call.apply(_this.dispatch, arguments);
	        if (this.reconnectTime != null) {
	            setTimeout(function () {
	                _this.socket = new WebSocket(this.url);
	            }, this.reconnectTime);
	        }
	    };
	}

	Socket.prototype.on = function (eventName, callback) {
	    this.dispatch.on(eventName, callback);
	};

	/**
	 * 配置选项
	 * @param {*} options 配置选项
	 */
	function Options(options) {
	    options = options || {};
	    this.server = options.server || 'http://127.0.0.1:1050';
	}

	/**
	 * 工具栏
	 */
	function Toolbar(options) {
	    UI$1.Control.call(this, options);
	    this.app = options.app;
	}
	Toolbar.prototype = Object.create(UI$1.Control.prototype);
	Toolbar.prototype.constructor = Toolbar;

	Toolbar.prototype.render = function () {

	    var data = {
	        xtype: 'div',
	        id: 'toolbar',
	        parent: this.app.container,
	        cls: 'toolbar',
	        children: [{
	            xtype: 'iconbutton',
	            id: 'selectBtn',
	            icon: 'icon-select',
	            cls: 'Button IconButton selected',
	            title: '选择'
	        }, {
	            xtype: 'iconbutton',
	            id: 'translateBtn',
	            icon: 'icon-translate',
	            title: '平移(W)'
	        }, {
	            xtype: 'iconbutton',
	            id: 'rotateBtn',
	            icon: 'icon-rotate',
	            title: '旋转(E)'
	        }, {
	            xtype: 'iconbutton',
	            id: 'scaleBtn',
	            icon: 'icon-scale',
	            title: '缩放(R)'
	        }, {
	            xtype: 'hr'
	        }, {
	            xtype: 'iconbutton',
	            id: 'modelBtn',
	            icon: 'icon-model-view',
	            title: '模型'
	        }
	            // , {
	            //     xtype: 'iconbutton',
	            //     id: 'handBtn',
	            //     icon: 'icon-hand',
	            //     title: '抓手'
	            // }, {
	            //     xtype: 'iconbutton',
	            //     id: 'anchorPointBtn',
	            //     icon: 'icon-anchor-point',
	            //     title: '添加锚点'
	            // }, {
	            //     xtype: 'iconbutton',
	            //     id: 'pathBtn',
	            //     icon: 'icon-path',
	            //     title: '绘制路径'
	            // }
	        ]
	    };

	    var control = UI$1.create(data);
	    control.render();
	};

	/**
	 * 应用程序
	 */
	function Application(container, options) {

	    // 容器
	    this.container = container;
	    this.width = this.container.clientWidth;
	    this.height = this.container.clientHeight;

	    // 配置
	    this.options = new Options(options);

	    // 事件
	    this.event = new EventDispatcher(this);
	    this.call = this.event.call.bind(this.event);
	    this.on = this.event.on.bind(this.event);

	    var params = { app: this, parent: this.container };

	    // 用户界面
	    this.ui = UI$1;

	    this.menubar = new Menubar(params); // 菜单栏
	    this.menubar.render();

	    this.toolbar = new Toolbar(params); // 工具栏
	    this.toolbar.render();

	    this.viewport = new Viewport(params); // 场景编辑区
	    this.viewport.render();

	    this.editor = new Editor(this); // 编辑器

	    this.sidebar = new Sidebar(params); // 侧边栏
	    this.sidebar.render();

	    this.statusBar = new StatusBar(params); // 状态栏
	    this.statusBar.render();

	    this.script = new Script(params); // 脚本编辑面板
	    this.script.render();

	    this.player = new Player(params); // 播放器面板
	    this.player.render();

	    this.running = false;

	    // 是否从文件中加载场景，从文件中加载场景的url格式是index.html#file=xxx
	    this.isLoadingFromHash = false;
	}

	Application.prototype.start = function () {
	    this.running = true;

	    // 启动事件 - 事件要在ui创建完成后启动
	    this.event.start();

	    this.call('appStart', this);
	    this.call('resize', this);
	    this.call('initApp', this);
	    this.call('appStarted', this);
	};

	Application.prototype.stop = function () {
	    this.running = false;

	    this.call('appStop', this);
	    this.call('appStoped', this);

	    this.event.stop();
	};

	exports.Options = Options;
	exports.Application = Application;
	exports.System = System;
	exports.WEBVR = WEBVR;
	exports.html2canvas = html2canvas;
	exports.dispatch = dispatch;
	exports.EventList = EventList;
	exports.BaseEvent = BaseEvent;
	exports.EventDispatcher = EventDispatcher;
	exports.InitAppEvent = InitAppEvent;
	exports.DragOverEvent = DragOverEvent;
	exports.DropEvent = DropEvent;
	exports.KeyDownEvent = KeyDownEvent;
	exports.MessageEvent = MessageEvent;
	exports.ResizeEvent = ResizeEvent;
	exports.AddGeometryEvent = AddGeometryEvent;
	exports.AddHelperEvent = AddHelperEvent;
	exports.AddMaterialEvent = AddMaterialEvent;
	exports.AddObjectEvent = AddObjectEvent;
	exports.AddScriptEvent = AddScriptEvent;
	exports.AddTextureEvent = AddTextureEvent;
	exports.AutoSaveEvent = AutoSaveEvent;
	exports.ClearEvent = ClearEvent;
	exports.LoadEvent = LoadEvent;
	exports.LoadFromHashEvent = LoadFromHashEvent;
	exports.MoveObjectEvent = MoveObjectEvent;
	exports.NameObjectEvent = NameObjectEvent;
	exports.RemoveHelperEvent = RemoveHelperEvent;
	exports.RemoveObjectEvent = RemoveObjectEvent;
	exports.RemoveScriptEvent = RemoveScriptEvent;
	exports.SaveEvent = SaveEvent;
	exports.SelectEvent = SelectEvent;
	exports.SetGeometryNameEvent = SetGeometryNameEvent;
	exports.SetMaterialNameEvent = SetMaterialNameEvent;
	exports.SetSceneEvent = SetSceneEvent;
	exports.SetThemeEvent = SetThemeEvent;
	exports.VREvent = VREvent;
	exports.Config = Config;
	exports.History = History;
	exports.Storage = Storage;
	exports.Loader = Loader;
	exports.AppPlayer = AppPlayer;
	exports.Command = Command;
	exports.AddObjectCommand = AddObjectCommand;
	exports.AddScriptCommand = AddScriptCommand;
	exports.MoveObjectCommand = MoveObjectCommand;
	exports.MultiCmdsCommand = MultiCmdsCommand;
	exports.RemoveObjectCommand = RemoveObjectCommand;
	exports.RemoveScriptCommand = RemoveScriptCommand;
	exports.SetColorCommand = SetColorCommand;
	exports.SetGeometryCommand = SetGeometryCommand;
	exports.SetGeometryValueCommand = SetGeometryValueCommand;
	exports.SetMaterialColorCommand = SetMaterialColorCommand;
	exports.SetMaterialCommand = SetMaterialCommand;
	exports.SetMaterialMapCommand = SetMaterialMapCommand;
	exports.SetMaterialValueCommand = SetMaterialValueCommand;
	exports.SetPositionCommand = SetPositionCommand;
	exports.SetRotationCommand = SetRotationCommand;
	exports.SetScaleCommand = SetScaleCommand;
	exports.SetSceneCommand = SetSceneCommand;
	exports.SetScriptValueCommand = SetScriptValueCommand;
	exports.SetUuidCommand = SetUuidCommand;
	exports.SetValueCommand = SetValueCommand;
	exports.UI = UI$1;
	exports.Viewport = Viewport;
	exports.Menubar = Menubar;
	exports.StatusBar = StatusBar;
	exports.Editor = Editor;
	exports.Script = Script;
	exports.Player = Player;
	exports.HistoryPanel = HistoryPanel;
	exports.MaterialPanel = MaterialPanel;
	exports.ObjectPanel = ObjectPanel;
	exports.ProjectPanel = ProjectPanel;
	exports.PropertyPanel = PropertyPanel;
	exports.ScenePanel = ScenePanel;
	exports.ScriptPanel = ScriptPanel;
	exports.SettingPanel = SettingPanel;
	exports.Sidebar = Sidebar;
	exports.GeometryInfoPanel = GeometryInfoPanel;
	exports.BoxGeometryPanel = BoxGeometryPanel;
	exports.BufferGeometryPanel = BufferGeometryPanel;
	exports.CircleGeometryPanel = CircleGeometryPanel;
	exports.CylinderGeometryPanel = CylinderGeometryPanel;
	exports.IcosahedronGeometryPanel = IcosahedronGeometryPanel;
	exports.LatheGeometryPanel = LatheGeometryPanel;
	exports.PlaneGeometryPanel = PlaneGeometryPanel;
	exports.SphereGeometryPanel = SphereGeometryPanel;
	exports.TorusGeometryPanel = TorusGeometryPanel;
	exports.TorusKnotGeometryPanel = TorusKnotGeometryPanel;
	exports.GeometryPanel = GeometryPanel;
	exports.BaseSerializer = BaseSerializer;
	exports.GeometrySerializer = GeometrySerializer;
	exports.HemisphereLightSerializer = HemisphereLightSerializer;
	exports.LightSerializer = LightSerializer;
	exports.MaterialSerializer = MaterialSerializer;
	exports.MeshSerializer = MeshSerializer;
	exports.Object3DSerializer = Object3DSerializer;
	exports.PointLightSerializer = PointLightSerializer;
	exports.RectAreaLightSerializer = RectAreaLightSerializer;
	exports.SceneSerializer = SceneSerializer;
	exports.SpotLightSerializer = SpotLightSerializer;
	exports.Serializers = Serializers;
	exports.Converter = Converter;
	exports.Ajax = Ajax;
	exports.CssUtils = CssUtils;
	exports.JsUtils = JsUtils;
	exports.GeometryUtils = GeometryUtils;
	exports.Socket = Socket;

	Object.defineProperty(exports, '__esModule', { value: true });

})));
