Browse Source

图标更新

huangyan 7 months ago
parent
commit
f5bb1dbd0a
100 changed files with 54946 additions and 14 deletions
  1. 30 14
      index.html
  2. 47 0
      public/archive/LoadLocal.html
  3. BIN
      public/archive/static/LocalProject/17249825054165763d-c176-48a7-b398-48f70cfcf72f.zip
  4. 6 0
      public/archive/static/LocalProject/main.json
  5. 1534 0
      public/archive/static/build/IOT3D.js
  6. 102 0
      public/archive/static/build/aaa.js
  7. 915 0
      public/archive/static/build/ccc.js
  8. 305 0
      public/archive/static/build/es-module-shims.js
  9. 9773 0
      public/archive/static/build/three.cjs
  10. 13350 0
      public/archive/static/build/three.module.js
  11. 1529 0
      public/archive/static/js/IOT3D-.js
  12. 1526 0
      public/archive/static/js/IOT3D.js
  13. 201 0
      public/archive/static/js/VRButton.js
  14. 305 0
      public/archive/static/js/es-module-shims.js
  15. BIN
      public/archive/static/js/fonts/172397064437d19568-688d-4032-a8e8-cf561dae4f3a.7z
  16. 88 0
      public/archive/static/js/fonts/172397064437d19568-688d-4032-a8e8-cf561dae4f3a.json
  17. 0 0
      public/archive/static/js/fonts/FZYaoTi_Regular.json
  18. 0 0
      public/archive/static/js/fonts/STKaiti_Regular.json
  19. 0 0
      public/archive/static/js/fonts/helvetiker_regular.typeface.json
  20. BIN
      public/archive/static/js/img/bk/Dusk/nx.png
  21. BIN
      public/archive/static/js/img/bk/Dusk/ny.png
  22. BIN
      public/archive/static/js/img/bk/Dusk/nz.png
  23. BIN
      public/archive/static/js/img/bk/Dusk/px.png
  24. BIN
      public/archive/static/js/img/bk/Dusk/py.png
  25. BIN
      public/archive/static/js/img/bk/Dusk/pz.png
  26. BIN
      public/archive/static/js/img/bk/Sky/negx.jpg
  27. BIN
      public/archive/static/js/img/bk/Sky/negy.jpg
  28. BIN
      public/archive/static/js/img/bk/Sky/negz.jpg
  29. BIN
      public/archive/static/js/img/bk/Sky/posx.jpg
  30. BIN
      public/archive/static/js/img/bk/Sky/posy.jpg
  31. BIN
      public/archive/static/js/img/bk/Sky/posz.jpg
  32. BIN
      public/archive/static/js/img/bk/Starry/nx.jpg
  33. BIN
      public/archive/static/js/img/bk/Starry/ny.jpg
  34. BIN
      public/archive/static/js/img/bk/Starry/nz.jpg
  35. BIN
      public/archive/static/js/img/bk/Starry/px.jpg
  36. BIN
      public/archive/static/js/img/bk/Starry/py.jpg
  37. BIN
      public/archive/static/js/img/bk/Starry/pz.jpg
  38. BIN
      public/archive/static/js/img/bk/f557c3a477552a1ba6268aaffa1cb18c.png
  39. BIN
      public/archive/static/js/img/brick-wall.jpg
  40. BIN
      public/archive/static/js/img/cd.jpg
  41. BIN
      public/archive/static/js/img/grasslight-big-nm.jpg
  42. 1 0
      public/archive/static/js/jquery.min.js
  43. 114 0
      public/archive/static/js/jsm/animation/AnimationClipCreator.js
  44. 458 0
      public/archive/static/js/jsm/animation/CCDIKSolver.js
  45. 1207 0
      public/archive/static/js/jsm/animation/MMDAnimationHelper.js
  46. 1381 0
      public/archive/static/js/jsm/animation/MMDPhysics.js
  47. 209 0
      public/archive/static/js/jsm/cameras/CinematicCamera.js
  48. 91 0
      public/archive/static/js/jsm/capabilities/WebGL.js
  49. 39 0
      public/archive/static/js/jsm/capabilities/WebGPU.js
  50. 3216 0
      public/archive/static/js/jsm/controls/ArcballControls.js
  51. 220 0
      public/archive/static/js/jsm/controls/DragControls.js
  52. 325 0
      public/archive/static/js/jsm/controls/FirstPersonControls.js
  53. 285 0
      public/archive/static/js/jsm/controls/FlyControls.js
  54. 1247 0
      public/archive/static/js/jsm/controls/OrbitControls.js
  55. 157 0
      public/archive/static/js/jsm/controls/PointerLockControls.js
  56. 802 0
      public/archive/static/js/jsm/controls/TrackballControls.js
  57. 1558 0
      public/archive/static/js/jsm/controls/TransformControls.js
  58. 377 0
      public/archive/static/js/jsm/csm/CSM.js
  59. 152 0
      public/archive/static/js/jsm/csm/CSMFrustum.js
  60. 163 0
      public/archive/static/js/jsm/csm/CSMHelper.js
  61. 251 0
      public/archive/static/js/jsm/csm/CSMShader.js
  62. 422 0
      public/archive/static/js/jsm/curves/CurveExtras.js
  63. 80 0
      public/archive/static/js/jsm/curves/NURBSCurve.js
  64. 52 0
      public/archive/static/js/jsm/curves/NURBSSurface.js
  65. 487 0
      public/archive/static/js/jsm/curves/NURBSUtils.js
  66. 168 0
      public/archive/static/js/jsm/effects/AnaglyphEffect.js
  67. 266 0
      public/archive/static/js/jsm/effects/AsciiEffect.js
  68. 553 0
      public/archive/static/js/jsm/effects/OutlineEffect.js
  69. 116 0
      public/archive/static/js/jsm/effects/ParallaxBarrierEffect.js
  70. 153 0
      public/archive/static/js/jsm/effects/PeppersGhostEffect.js
  71. 55 0
      public/archive/static/js/jsm/effects/StereoEffect.js
  72. 52 0
      public/archive/static/js/jsm/environments/DebugEnvironment.js
  73. 121 0
      public/archive/static/js/jsm/environments/RoomEnvironment.js
  74. 713 0
      public/archive/static/js/jsm/exporters/ColladaExporter.js
  75. 225 0
      public/archive/static/js/jsm/exporters/DRACOExporter.js
  76. 507 0
      public/archive/static/js/jsm/exporters/EXRExporter.js
  77. 2755 0
      public/archive/static/js/jsm/exporters/GLTFExporter.js
  78. 281 0
      public/archive/static/js/jsm/exporters/KTX2Exporter.js
  79. 217 0
      public/archive/static/js/jsm/exporters/MMDExporter.js
  80. 284 0
      public/archive/static/js/jsm/exporters/OBJExporter.js
  81. 521 0
      public/archive/static/js/jsm/exporters/PLYExporter.js
  82. 195 0
      public/archive/static/js/jsm/exporters/STLExporter.js
  83. 558 0
      public/archive/static/js/jsm/exporters/USDZExporter.js
  84. 69 0
      public/archive/static/js/jsm/geometries/BoxLineGeometry.js
  85. 59 0
      public/archive/static/js/jsm/geometries/ConvexGeometry.js
  86. 356 0
      public/archive/static/js/jsm/geometries/DecalGeometry.js
  87. 1017 0
      public/archive/static/js/jsm/geometries/LightningStrike.js
  88. 254 0
      public/archive/static/js/jsm/geometries/ParametricGeometries.js
  89. 129 0
      public/archive/static/js/jsm/geometries/ParametricGeometry.js
  90. 155 0
      public/archive/static/js/jsm/geometries/RoundedBoxGeometry.js
  91. 704 0
      public/archive/static/js/jsm/geometries/TeapotGeometry.js
  92. 57 0
      public/archive/static/js/jsm/geometries/TextGeometry.js
  93. 130 0
      public/archive/static/js/jsm/helpers/LightProbeHelper.js
  94. 58 0
      public/archive/static/js/jsm/helpers/OctreeHelper.js
  95. 109 0
      public/archive/static/js/jsm/helpers/PositionalAudioHelper.js
  96. 85 0
      public/archive/static/js/jsm/helpers/RectAreaLightHelper.js
  97. 90 0
      public/archive/static/js/jsm/helpers/VertexNormalsHelper.js
  98. 81 0
      public/archive/static/js/jsm/helpers/VertexTangentsHelper.js
  99. 295 0
      public/archive/static/js/jsm/helpers/ViewHelper.js
  100. 553 0
      public/archive/static/js/jsm/interactive/HTMLMesh.js

+ 30 - 14
index.html

@@ -5,27 +5,43 @@
   <meta charset="UTF-8" />
   <link rel="icon" href="/favicon.ico" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-  <script type="importmap">
-    {
-      "imports": {
-        "three": "https://vdata.baozhida.cn/3d1v/static/js/three.module.js",
-        "three/addons/": "https://vdata.baozhida.cn/3d1v/static/js/jsm/"
-      }
+    <script type="importmap">
+        {
+    "imports": {
+      "three": "./public/archive/static/build/three.module.js",
+      "three/addons/": "./public/archive/static/js/jsm/",
+      "three/libs/": "./public/archive/static/js/libs/"
     }
-   </script>
-  <title>IofTV-Screen-Vue3</title>
+  }
+    </script>
+    <style>
+        body {
+            font-family: sans-serif;
+            font-size: 11px;
+            background-color: #000;
+            margin: 0px;
+        }
+
+        canvas {
+            display: block;
+        }
+    </style>
+    <script src="./public/archive/static/js/jquery.min.js"></script>
+
+    <title>IofTV-Screen-Vue3</title>
+
 </head>
 
 <body>
-  <div id="app">
-  </div>
+  <div id="app"></div>
   <script type="module" src="/src/main.ts"></script>
   <!-- 3D 1v -->
-  <script async src="https://vdata.baozhida.cn/3d1v/static/js/es-module-shims.js"></script>
-  <script src="https://vdata.baozhida.cn/3d1v/static/js/jquery.min.js"></script>
+  <script type="module" src="./public/archive/static/build/IOT3D.js"></script>
+
+  <link rel="stylesheet" href="./public/archive/static/js/libs/loading/load.css" media="all">
+  <script async src="./public/archive/static/js/libs/loading/load.js" charset="utf-8"></script>
+<!--  <script src="./public/archive/static/editor/lib/lib.js"></script>   &lt;!&ndash;  物联智控核心  &ndash;&gt;-->
 
-  <link rel="stylesheet" href="https://vdata.baozhida.cn/3d1v/static/js/libs/loading/load.css" media="all">
-  <script async src="https://vdata.baozhida.cn/3d1v/static/js/libs/loading/load.js" charset="utf-8"></script>
   <!-- 3D 1v -->
 </body>
 <script type="text/javascript" src="/src/webrtc/webrtcstreamer.js"></script>

+ 47 - 0
public/archive/LoadLocal.html

@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<title>宝智达 • 3D数字孪生平台</title>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+	<link rel="shortcut icon" href="./favicon.ico">
+	<style>
+		body {
+			font-family: sans-serif;
+			font-size: 11px;
+			background-color: #000;
+			margin: 0px;
+		}
+
+		canvas {
+			display: block;
+		}
+	</style>
+	<script src="./static/js/jquery.min.js"></script>
+</head>
+
+<body>
+<div id="iot3d" style="height: 80%; width: 80%;"></div>
+<!-- Import maps polyfill -->
+<!-- Remove this when import maps will be widely supported "three": "./build/three.module.js",  -->
+<script async src="./static/build/es-module-shims.js"></script>
+
+<script type="importmap">
+			{
+				"imports": {
+					"three": "./static/build/three.module.js",
+					"three/addons/": "./static/js/jsm/",
+					"three/libs/": "./static/js/libs/"
+				}
+			}
+</script>
+
+<script type="module" src="./static/build/IOT3D.js"></script>
+
+<link rel="stylesheet" href="./static/js/libs/loading/load.css" media="all">
+<script async src="./static/js/libs/loading/load.js" charset="utf-8"></script>
+<!--<script src="./static/editor/lib/lib.js"></script>   &lt;!&ndash;  物联智控核心  &ndash;&gt;-->
+
+
+</body>
+</html>

BIN
public/archive/static/LocalProject/17249825054165763d-c176-48a7-b398-48f70cfcf72f.zip


+ 6 - 0
public/archive/static/LocalProject/main.json

@@ -0,0 +1,6 @@
+[
+  {
+    "T_name": "园区0",
+    "T_url": "./static/LocalProject/17249825054165763d-c176-48a7-b398-48f70cfcf72f.zip"
+  }
+]

+ 1534 - 0
public/archive/static/build/IOT3D.js

@@ -0,0 +1,1534 @@
+import * as THREE from 'three';
+
+import {FontLoader} from 'three/addons/loaders/FontLoader.js';
+import {CinematicCamera} from 'three/addons/cameras/CinematicCamera.js';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
+import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js';
+import { CSS3DRenderer  } from 'three/addons/renderers/CSS3DRenderer.js';
+import { CSS2DRenderer } from 'three/addons/renderers/CSS2DRenderer.js';
+import { Storage as _Storage } from 'three/libs/Storage.js';
+import {unzipSync} from 'three/libs/fflate.module.js';
+
+console.log('Three.js version:', THREE.REVISION);
+
+// --------------------- 折叠 -----------------------
+//region Description
+
+//endregion
+
+function IOT3D() {
+	console.log("-------------- IOT3D --------------")
+	window.iot3d = this
+	window.THREE = THREE; // Used by APP Scripts.
+	window.FontLoader = FontLoader; // Used by APP Scripts.
+	// window.VRButton = VRButton; // Used by APP Scripts.
+	window.OrbitControls = OrbitControls; // Used by APP Scripts.
+	window.CinematicCamera = CinematicCamera; // Used by APP Scripts.
+	window.EffectComposer = EffectComposer; // Used by APP Scripts.
+	window.RenderPass = RenderPass; // Used by APP Scripts.
+	window.ShaderPass = ShaderPass; // Used by APP Scripts.
+	window.OutlinePass = OutlinePass; // Used by APP Scripts.
+
+
+	this.loading_open = function (time){
+		//1000为遮罩层显示时长,若不传则一直显示,须调用关闭方法
+		$.mask_fullscreen(time);
+	}
+	this.loading_close = function (){
+		//关闭遮罩层
+		$.mask_close_all();
+	}
+	this.loading_text = function (text){
+		//关闭遮罩层
+		$.mask_text(text);
+	}
+	this.loading_html = function (html){
+		//关闭遮罩层
+		$.mask_html(html);
+	}
+	//
+	var self;
+
+	THREE.Cache.enabled = true; //这是一个全局属性,只需要设置一次,供内部使用FileLoader的所有加载器使用。
+	// 项目
+	// var IOT3D_Url = "https://iot3d.baozhida.cn"
+	// var IOT3D_Url = "https://iot3d.baozhida.cn"
+	// if(url.indexOf("127.0.0.1") != -1){
+	// 	IOT3D_Url = "http://127.0.0.1:6210"
+	// }else {
+	// 	IOT3D_Url = "https://iot3d.baozhida.cn"
+	// }
+	var clock = new THREE.Clock();
+	// 公共
+	var dom_width = 500, dom_height = 500;
+	var camera, scene, dom, renderer, rendererCss2, rendererCss3,controls;
+	var events = {};
+	window.parkId = 0;   // 园区ID
+	var project;  // 项目配置
+	/// =-  选取
+	var raycaster, mouse, INTERSECTED = null;
+
+	// 变量初始化
+	self = this;
+	dom_width = window.innerWidth;
+	dom_height = window.innerHeight;
+	self.width = dom_width;
+	self.height = dom_width;
+	// var vrButton = VRButton.createButton( renderer ); // eslint-disable-line no-undef
+
+
+	// --------------------- 初始化 -----------------------
+	//region Description
+
+	// 舞台
+	scene = new THREE.Scene();
+	// 相机
+	camera = new THREE.PerspectiveCamera(45, dom_width / dom_height, 0.1, 3000);
+	camera.position.set(-10, 10, 30);
+	camera.lookAt(scene.position);
+
+	window.mixer = new THREE.AnimationMixer( scene ); // 动画
+
+
+	// HTML dom
+	dom = document.getElementById("iot3d");
+
+	//渲染器
+	renderer = new THREE.WebGLRenderer({antialias: true});
+	renderer.setPixelRatio(window.devicePixelRatio); // TODO: Use setPixelRatio()
+	// renderer.outputEncoding = THREE.sRGBEncoding;
+	renderer.shadowMap.enabled = true;// 阴影
+	renderer.setSize( dom_width, dom_height );
+	renderer.setAnimationLoop( animate );
+	dom.appendChild(renderer.domElement);
+
+
+	// rendererCss3
+	rendererCss3 = new CSS3DRenderer();
+	rendererCss3.setSize( dom.innerWidth, dom.innerHeight );
+	rendererCss3.domElement.style.position = 'absolute';
+	rendererCss3.domElement.style.top = 0;
+	dom.appendChild( rendererCss3.domElement );
+
+	// rendererCss2
+	rendererCss2 = new CSS2DRenderer();
+	rendererCss2.setSize( dom.innerWidth, dom.innerHeight );
+	rendererCss2.domElement.style.position = 'absolute';
+	rendererCss2.domElement.style.top = '0px';
+	dom.appendChild( rendererCss2.domElement );
+
+	// 加载到 网页
+	document.body.appendChild(dom);
+	window.addEventListener('resize', function () {
+		self.setSize(window.innerWidth, window.innerHeight);
+	});
+
+	/// =-  选取
+	raycaster = new THREE.Raycaster();
+	mouse = new THREE.Vector2();
+
+
+	var selectedObjects = [],compose,renderPass,outlinePass;
+
+	// 鼠标移动 -轨道控制
+	// var AutomaticRotationPerspective = [1,2,20]  //自动旋转视角  0 【关闭】  1【360度旋转[1,旋转速度{1~9,2},俯视角度{0~100,20}]】
+	var AutomaticRotationPerspectiveInterval = undefined  // 定时任务
+	var AutomaticRotationPerspectiveTally = 0 // 计数
+	// - 鼠标移动视角
+	controls = new OrbitControls(camera, renderer.domElement);
+	// controls.addEventListener('change', animate); // use if there is no animation loop
+	controls.dampingFactor = 0.25;
+	controls.minDistance = 5;  // 最小距离
+	controls.maxDistance = 1000;
+	controls.screenSpacePanning = false; // 允许相机平移
+	// 设置最大和最小角度
+	controls.maxPolarAngle = Math.PI / 2; // 最大角度 (90度) - 可视化平面
+	controls.minPolarAngle = 0;             // 最小角度 (0度) - 直接向下
+	controls.target.set(0, 0, -2.2);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// controls.position0.set(200, 200, 500 )
+	controls.update();
+	// 检查 鼠标是否有操作
+	renderer.domElement.addEventListener( 'pointerdown', function () {
+		if(AutomaticRotationPerspectiveInterval !== undefined){
+			clearInterval(AutomaticRotationPerspectiveInterval); // 停止定时任务的执行
+		}
+		AutomaticRotationPerspectiveTally = 0
+		AutomaticRotationPerspectiveInterval = undefined
+	} );
+	// 定时 开始触发 自动旋转视角
+	setInterval(() => {
+		if(project === undefined || project.autoangle === undefined || project.autoangle === "None") return; // 直接跳过
+
+		if(AutomaticRotationPerspectiveInterval === undefined){
+			AutomaticRotationPerspectiveTally += 1
+			if(AutomaticRotationPerspectiveTally === 3){
+				console.log("project.autoangle:",project.autoangle)
+				switch (project.autoangle) {
+					case "Angle360":  // 360度旋转
+						self.AroundRotation(scene,project.autoangle_speed,project.autoangle_angle)
+						break;
+					case "Regainstate":  // 回到原始视角
+						self.Focus(self.GetScene())
+						break;
+				}
+
+			}
+		}
+	}, 1000); // 每秒
+
+	// 数据订阅
+	window.pubSub = {
+		list: {},
+		// 订阅
+		subscribe: function(key, fn) {
+			if (!this.list[key]) this.list[key] = [];
+			this.list[key].push(fn);
+		},
+		//取消订阅
+		unsubscribe: function(key, fn) {
+			let fnList = this.list[key];
+			if (!fnList) return false;
+			if (!fn) { // 不传入指定的方法,清空所用 key 下的订阅
+				fnList && (fnList.length = 0);
+			} else {
+				fnList.forEach((item, index) => {
+					item === fn && fnList.splice(index, 1);
+				});
+			}
+		},
+		// 发布
+		publish: function(key, ...args) {
+			if(this.list[key] === undefined) return;
+			for (let fn of this.list[key]) fn.call(this, ...args);
+		}
+	}
+
+	// this.storage.get(fxx)
+	//
+	// function fxx(xxx) {
+	// 	console.log("fxx-------------------------",xxx)
+	// }
+
+	// 测试
+	this.Test = function (rotationSpeed = 0.001) {
+		console.log("Test-------------------------")
+
+
+
+		return
+	}
+
+
+// 测试
+
+	function cubeDr(a, x, y, z) {
+		var cubeGeo = new THREE.BoxGeometry(a, a, a);
+		var cubeMat = new THREE.MeshPhongMaterial({
+			color: 0xfff000 * Math.random()
+		});
+		var cube = new THREE.Mesh(cubeGeo, cubeMat);
+		cube.position.set(x, y, z);
+		cube.castShadow = true;
+		scene.add(cube);
+		return cube;
+	}
+
+
+
+
+//endregion
+
+// --------------------- 核心 -----------------------
+//region Description
+
+
+
+	// 核心方法
+	//region Description
+	// 舞台
+	this.GetScene = function () {
+		return scene
+	}
+	// 相机
+	this.GetCamera = function () {
+		return camera
+	}
+	//渲染器
+	this.GetRenderer = function () {
+		return renderer
+	}
+	// 获取 UUID 模型
+	this.GetModelByUuid = function (uuid) {
+		return scene.getObjectByProperty('uuid', uuid, true);
+	}
+	// 获取 UUID 模型 内部函数
+	this.Model = function (uuid,fun) {
+		fun(scene.getObjectByProperty('uuid', uuid, true));
+	}
+	// 设置显示比例
+	this.setPixelRatio = function (pixelRatio) {
+		renderer.setPixelRatio(pixelRatio);
+	};
+	// 设置大小
+	this.setSize = function (width, height) {
+		dom_width = width;
+		dom_height = height;
+
+		if (camera) {
+			camera.aspect = dom_width / dom_height;
+			camera.updateProjectionMatrix();
+		}
+		renderer.setSize(width, height);
+
+		if ( rendererCss3 !== null ) {
+			rendererCss3.setSize( dom.offsetWidth, dom.offsetHeight );
+		}
+
+		if ( rendererCss2 !== null ) {
+			rendererCss2.setSize( dom.offsetWidth, dom.offsetHeight );
+		}
+		self.width = dom_width;
+		self.height = dom_width;
+	};
+	//endregion
+
+	// 鼠标
+	//region Description
+
+	//更新视角中心点
+	this.orbitControls_target = function (position) {
+		console.log("更新视角中心点:", position)
+		controls.target.set(position.x, position.y, position.z);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+		controls.update();
+	}
+	// 计算场景最远距离,并控制参数
+	function orbitControls_maxDistance() {
+		// 计算场景中的包围盒
+		function calculateSceneBoundingBox(scene) {
+			const box = new THREE.Box3();
+
+			scene.traverse((object) => {
+				if (object.isMesh) {
+					// 更新包围盒以包含当前对象的包围盒
+					const objectBox = new THREE.Box3().setFromObject(object);
+					box.union(objectBox);
+				}
+			});
+
+			return box;
+		}
+		// 计算场景的包围盒
+		const boundingBox = calculateSceneBoundingBox(scene);
+		// 获取包围盒的尺寸
+		const size = new THREE.Vector3();
+		boundingBox.getSize(size);
+		// 获取包围盒的中心
+		const center = new THREE.Vector3();
+		boundingBox.getCenter(center);
+		// 计算最大尺寸距离 (从中心到某个角的距离)
+		const maxDistance = center.distanceTo(boundingBox.max);
+		// console.log("从中心到最远角的距离:", maxDistance);
+		controls.maxDistance = maxDistance * 10;
+	}
+
+
+	// 鼠标选择初始化
+	var OutlinePass_selectedObjects_Map = new Map();
+	function OutlinePass_inte(){
+		// console.log("OutlinePass_inte")
+		// 清空所有选项
+		selectedObjects = []
+		self.Model_Selected_Clear()
+
+		camera.lookAt(scene.position);
+
+		compose = new EffectComposer(renderer);
+		renderPass = new RenderPass(scene, camera);
+
+		outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth,window.innerHeight),scene,camera);
+		outlinePass.renderToScreen = true;
+		outlinePass.selectedObjects = selectedObjects;
+
+		compose.addPass(renderPass);
+		compose.addPass(outlinePass);
+
+		// https://threejs.org/examples/?q=webgl_postprocessing_outline#webgl_postprocessing_outline
+		outlinePass.renderToScreen = true;
+		outlinePass.edgeStrength = 3 //粗   0.01, 10
+		outlinePass.edgeGlow = 1 //发光  0.0, 1
+		outlinePass.edgeThickness = 2 //光晕粗   1, 4
+		outlinePass.pulsePeriod = 0 //闪烁  0.0, 5
+		outlinePass.usePatternTexture = false //是否使用贴图
+		let visibleEdgeColor = '#00a1fd';  // 选择颜色
+		let hiddenEdgeColor = '#00a1fd';  //遮挡部分颜色
+		outlinePass.visibleEdgeColor.set(visibleEdgeColor);
+		outlinePass.hiddenEdgeColor.set(hiddenEdgeColor);
+
+		// let light = new THREE.AmbientLight(0x333333);
+		// scene.add(light);
+		//
+		// 这里没有 渲染会报错
+		let light = new THREE.SpotLight(0xFFFFFF);
+		light.position.set(0, 40, 30);
+		light.castShadow = true;
+		light.shadow.mapSize.height = 1;
+		light.shadow.mapSize.width = 1;
+		light.angle = 0;
+		scene.add(light);
+
+		// light = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
+		// light.position.set(0, 200, 0);
+		// scene.add(light);
+
+
+
+		// const light = new THREE.DirectionalLight( 0xffffff, 0.6 );
+		// light.position.set( 1, 1, 1 );
+		// light.castShadow = true;
+		// light.shadow.mapSize.width = 1024;
+		// light.shadow.mapSize.height = 1024;
+		//
+		// const d = 10;
+		//
+		// light.shadow.camera.left = - d;
+		// light.shadow.camera.right = d;
+		// light.shadow.camera.top = d;
+		// light.shadow.camera.bottom = - d;
+		// light.shadow.camera.far = 1000;
+		//
+		// scene.add( light );
+	}
+	// 刷新
+	function OutlinePass_selectedObjects_Refresh() {
+		if (outlinePass == undefined) return;
+		selectedObjects = [];
+		OutlinePass_selectedObjects_Map.forEach(function(value, key) {
+			// console.log(key, value);
+			selectedObjects.push( value );
+		})
+
+		if (INTERSECTED != null){
+			if(!OutlinePass_selectedObjects_Map.has(INTERSECTED.uuid)){
+				selectedObjects.push( INTERSECTED );
+			}
+		}
+
+		// console.log("selectedObjects:",selectedObjects)
+		outlinePass.selectedObjects = selectedObjects;
+
+		// render()
+	}
+	// 选中配置   选择颜色 ,遮挡部分颜色
+	this.Model_Selected_Config = function(visibleEdgeColor="#00ff18",hiddenEdgeColor="#ff0000") {
+		outlinePass.visibleEdgeColor.set(visibleEdgeColor);
+		outlinePass.hiddenEdgeColor.set(hiddenEdgeColor);
+	}
+	// 添加
+	this.Model_Selected_Add = function(Model) {
+		OutlinePass_selectedObjects_Map.set(Model.uuid, Model)
+		OutlinePass_selectedObjects_Refresh()
+	}
+	// 删除
+	this.Model_Selected_Del = function(Model) {
+		OutlinePass_selectedObjects_Map.delete(Model.uuid)
+		OutlinePass_selectedObjects_Refresh()
+	}
+	// 清空
+	this.Model_Selected_Clear = function() {
+		OutlinePass_selectedObjects_Map.clear()
+		OutlinePass_selectedObjects_Refresh()
+	}
+	//endregion
+
+
+	//  ------------------------------ 运动  ---------------------------------------------
+
+
+	// 聚焦物体 -
+	var startMove_is = false
+
+	// 聚焦物体 - V1
+	// this.startFocus = function (ob,MoveTime=1) {
+	//
+	// 	if(startMove_is) {
+	// 		console.log("任务还没结束,不能开始新任务!")
+	// 		return;
+	// 	}
+	// 	startMove_is = true // 开始
+	// 	// if(ob.type != "Group") {
+	// 	// 	console.log("Group != ")
+	// 	// 	startMove_is = false
+	// 	// 	return;
+	// 	// }
+	// 	if(ob.children.length == 0) {
+	// 		console.log("children.length == 0!")
+	// 		startMove_is = false
+	// 		return;
+	// 	}
+	// 	if(ob.children[0].type != "PerspectiveCamera") {
+	// 		console.log("children[0].type != PerspectiveCamera")
+	// 		startMove_is = false
+	// 		return;
+	// 	}
+	// 	let MoveList = []
+	// 	MoveList.push([camera.position.x,camera.position.y,camera.position.z])
+	// 	// MoveList.push([ob.position.x,ob.position.y,ob.position.z])
+	// 	// MoveList.push([ob.children[0].position.x + ob.position.x, ob.children[0].position.y + ob.position.y, ob.children[0].position.z + ob.position.z])
+	// 	MoveList.push([ob.children[0].matrixWorld.elements[12], ob.children[0].matrixWorld.elements[13], ob.children[0].matrixWorld.elements[14]])
+	// 	// MoveList.push([ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14]])
+	// 	console.log("MoveList:",MoveList)
+	//
+	// 	let MoveListCurve = []
+	// 	for(var item of MoveList) {
+	// 		MoveListCurve.push(new THREE.Vector3(item[0], item[1], item[2]))
+	// 	}
+	// 	let curve = new THREE.CatmullRomCurve3(MoveListCurve)
+	//
+	// 	var curveList = curve.getPoints(20 * MoveTime)
+	// 	// console.log("curveList:",curveList)
+	//
+	// 	var testIndex = 0
+	// 	var t = setInterval(function () {
+	// 		if(!startMove_is) {
+	// 			curveList = []
+	// 			testIndex = 0
+	//
+	// 			clearTimeout(t) //停止 t 定时器
+	// 			return
+	// 		}
+	// 		// 模仿管道的镜头推进
+	// 		if (curveList.length !== 0) {
+	// 			if (testIndex < curveList.length  ) {
+	//
+	// 				const point = curveList[testIndex] //获取样条曲线指定点坐标,作为相机的位置
+	// 				const pointBox = curveList[testIndex+2] //获取样条曲线指定点坐标
+	//
+	// 				camera.position.set(point.x, point.y , point.z)
+	// 				camera.lookAt(ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14])
+	//
+	// 				controls.target.set(ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	// 				// camera.lookAt(ob.position.x,ob.position.y,ob.position.z)
+	// 				testIndex += 1
+	// 			} else {
+	// 				curveList = []
+	// 				testIndex = 0
+	//
+	// 				clearTimeout(t) //停止 t 定时器
+	// 				startMove_is = false
+	//
+	//
+	// 				// 更新视角中心点
+	// 				controls.target.set(ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	//
+	// 			}
+	// 		}
+	//
+	// 		render()
+	// 	}, 50)
+	//
+	// 	return
+	// }
+	// 围绕旋转  ,围绕对象 , 旋转速度{1~9,2} , 俯视角度{0~100,20}
+	this.AroundRotation = function (ob,anglespeed_ = 2,targetHeight = 20) {
+		// 自动计算包围盒
+		const box = new THREE.Box3().setFromObject(ob);
+		const size = box.getSize(new THREE.Vector3());
+		const center = box.getCenter(new THREE.Vector3());
+
+		// const targetHeight = 20; // 俯视角度的高度
+		const radius = Math.max(size.x, size.z) *1.2; // 基于包围盒计算半径
+		let anglespeed = 0.001 * anglespeed_; // 旋转速度
+		let angle = Math.atan2(camera.position.z - center.z, camera.position.x - center.x); // 当前相机角度
+
+		// 设置相机位置的函数
+		function updateCameraPosition() {
+			camera.position.x = radius * Math.cos(angle);
+			camera.position.z = radius * Math.sin(angle);
+			camera.position.y = targetHeight; // 固定Y坐标高度
+			camera.lookAt(center); // 始终朝向场景中心
+		}
+
+		// 定时循环
+		AutomaticRotationPerspectiveInterval = setInterval(() => {
+			angle += anglespeed; // 旋转速度
+			updateCameraPosition();
+			renderer.render(scene, camera);
+		}, 1000 / 60); // 每秒60帧
+
+	}
+	// 聚焦物体 - V2
+	this.Focus = function (ob) {
+		// 计算物体的边界盒
+		const box = new THREE.Box3().setFromObject(ob);
+		const size = box.getSize(new THREE.Vector3());
+		const distance = size.length() * 2.0; // 增加一些距离,以便能看得到
+
+		// 按照目标物体的大小和 20 度俯视角度计算相机位置
+		const pitchAngle = THREE.MathUtils.degToRad(20); // 转换为弧度 20
+		// const yawAngle = THREE.MathUtils.degToRad(angle); // 旋转 90 度
+		const yawAngle = ob.rotation._y
+
+		const targetEnd = box.getCenter(new THREE.Vector3());
+		const targetPosition = new THREE.Vector3(
+			targetEnd.x + distance * Math.sin(yawAngle),
+			targetEnd.y + distance * Math.sin(pitchAngle),
+			targetEnd.z + distance * Math.cos(yawAngle)
+		);
+
+		let initialPosition = new THREE.Vector3();
+		initialPosition.copy(camera.position); // 记录初始位置
+		// 在动画开始前,确保相机在正确的初始位置
+		// 		camera.position.copy(targetPosition);
+		// 		controls.target.copy(target);
+		// 		controls.update(); // 更新控件
+
+
+		const targetStart = new THREE.Vector3(0, 0, 0); // 初始中心点
+		// const targetEnd = new THREE.Vector3(1, 1, 1); // 目标中心点
+		targetStart.x = controls.target.x
+		targetStart.y = controls.target.y
+		targetStart.z = controls.target.z
+
+		// console.log("targetStart:",targetStart)
+		// console.log("targetEnd:",targetEnd)
+		// console.log("initialPosition:",initialPosition)
+		// console.log("targetPosition:",targetPosition)
+		const distancex = initialPosition.distanceTo(targetPosition);
+		// console.log("运动距离:", distancex);
+
+		if(startMove_is) {
+			console.log("任务还没结束,不能开始新任务!")
+			return;
+		}
+		startMove_is = true // 开始
+
+		// 动画属性
+		const animationDuration = distancex * 2; // 动画持续 2 秒
+		let animationDurationTime = 0; // 动画开始时间
+
+		var It = setInterval(function () {
+
+			animationDurationTime += 1;
+			const t = Math.min(animationDurationTime / animationDuration, 1); // 归一化
+			// console.log("t:",animationDurationTime)
+			// 插值计算新的相机位置
+			camera.position.x = THREE.MathUtils.lerp(initialPosition.x, targetPosition.x, t);
+			camera.position.y = THREE.MathUtils.lerp(initialPosition.y, targetPosition.y, t);
+			camera.position.z = THREE.MathUtils.lerp(initialPosition.z, targetPosition.z, t);
+			// 插值控制目标位置
+			controls.target.x = THREE.MathUtils.lerp(targetStart.x, targetEnd.x, t);
+			controls.target.y = THREE.MathUtils.lerp(targetStart.y, targetEnd.y, t);
+			controls.target.z = THREE.MathUtils.lerp(targetStart.z, targetEnd.z, t);
+
+
+			// 更新控件目标
+			camera.lookAt(controls.target); // 始终看向目标
+			controls.update(); // 更新控件以应用新位置和目标
+
+			// render()
+
+			if(animationDuration <= animationDurationTime || !startMove_is){
+				startMove_is = false
+				clearTimeout(It) //停止 t 定时器
+			}
+		}, 10)
+
+	}
+
+	// 聚焦物体运动 -
+	// this.startFocusMotion = function (ob,MoveList,MoveTime=1) {
+	// 	if(startMove_is) {
+	// 		console.log("任务还没结束,不能开始新任务!")
+	// 		return;
+	// 	}
+	// 	startMove_is = true // 开始
+	//
+	// 	console.log("MoveList:",MoveList)
+	//
+	// 	let MoveListCurve = []
+	// 	for(var item of MoveList) {
+	// 		MoveListCurve.push(new THREE.Vector3(item[0], item[1], item[2]))
+	// 	}
+	// 	let curve = new THREE.CatmullRomCurve3(MoveListCurve)
+	//
+	// 	var curveList = curve.getPoints(20 * MoveTime)
+	// 	// console.log("curveList:",curveList)
+	//
+	// 	var testIndex = 0
+	// 	var t = setInterval(function () {
+	// 		if(!startMove_is) {
+	// 			curveList = []
+	// 			testIndex = 0
+	//
+	// 			clearTimeout(t) //停止 t 定时器
+	// 		}
+	// 		// 模仿管道的镜头推进
+	// 		if (curveList.length !== 0) {
+	// 			if (testIndex < curveList.length  ) {
+	//
+	// 				const point = curveList[testIndex] //获取样条曲线指定点坐标,作为相机的位置
+	//
+	// 				camera.position.set(point.x, point.y , point.z)
+	//
+	// 				camera.lookAt(ob[0], ob[1], ob[2])
+	// 				controls.target.set(ob[0], ob[1], ob[2]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	// 				// camera.lookAt(ob.position.x,ob.position.y,ob.position.z)
+	// 				testIndex += 1
+	// 			} else {
+	// 				curveList = []
+	// 				testIndex = 0
+	//
+	// 				clearTimeout(t) //停止 t 定时器
+	// 				startMove_is = false
+	//
+	//
+	// 				// 更新视角中心点
+	// 				controls.target.set(ob[0], ob[1], ob[2]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	//
+	// 			}
+	// 		}
+	//
+	// 		render()
+	// 	}, 50)
+	//
+	// 	return
+	// }
+
+	// 路径移动
+	this.startMove = function (MoveList,MoveTime=1,orbitControls_target=[]) {
+
+		if(startMove_is) {
+			console.log("任务还没结束,不能开始新任务!")
+			return;
+		}
+		startMove_is = true // 开始
+		if(MoveList.length == 0) {
+			console.log("数据异常!")
+			startMove_is = false
+			return;
+		}
+		if(MoveList[0].length != 3) {
+			console.log("数据异常!")
+			startMove_is = false
+			return;
+		}
+
+
+		let MoveListCurve = []
+		for(var item of MoveList) {
+			MoveListCurve.push(new THREE.Vector3(item[0], item[1], item[2]))
+		}
+		let curve = new THREE.CatmullRomCurve3(MoveListCurve)
+
+		var curveList = curve.getPoints(20 * MoveTime)
+		// console.log("curveList:",curveList)
+
+		var testIndex = 0
+		var t = setInterval(function () {
+			if(!startMove_is) {
+				curveList = []
+				testIndex = 0
+
+				clearTimeout(t) //停止 t 定时器
+			}
+			// 模仿管道的镜头推进
+			if (curveList.length !== 0) {
+				if (testIndex < curveList.length - 2) {
+
+					const point = curveList[testIndex] //获取样条曲线指定点坐标,作为相机的位置
+					const pointBox = curveList[testIndex+2] //获取样条曲线指定点坐标
+
+					camera.position.set(point.x, point.y , point.z)
+					camera.lookAt(pointBox.x, pointBox.y , pointBox.z)
+
+					testIndex += 1
+				} else {
+					curveList = []
+					testIndex = 0
+
+					clearTimeout(t) //停止 t 定时器
+					startMove_is = false
+
+					// 更新视角中心点
+					if(orbitControls_target.length == 3){
+						controls.target.set(orbitControls_target[0], orbitControls_target[1], orbitControls_target[2]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+						controls.update();
+					}
+				}
+			}
+
+			// render()
+		}, 50)
+
+		return
+	}
+	// 停止移动
+	this.stopMove = function () {
+		startMove_is = false
+	}
+
+	// 渲染
+	//region Description
+
+	// ---------- 渲染 -----------
+	// renderer.setAnimationLoop( render );
+
+	var clock = new THREE.Clock(); // only used for animations
+
+	// function render() {
+	// 	// console.log("render")
+	//
+	// 	// 渲染方式
+	// 	if(compose != undefined) {
+	// 		if(selectedObjects.length > 0){
+	// 			compose.render()
+	// 		}else {
+	// 			renderer.render(scene, camera);
+	// 		}
+	// 	}else {
+	// 		renderer.render(scene, camera);
+	// 	}
+	//
+	// 	if ( rendererCss2 !== null ) {
+	// 		rendererCss2.render( scene, camera );
+	// 	}
+	// 	if ( rendererCss3 !== null ) {
+	// 		rendererCss3.render( scene, camera );
+	// 	}
+	//
+	// }
+	// this.Render = function () {
+	// 	render()
+	// }
+
+	// 渲染
+	// let lastRender = performance.now();
+	function animate() {
+		// let timestamp = timestamp.now();
+		// if (timestamp - lastRender < 1000 / 60) return; // 限制为 60fps
+		// lastRender = timestamp;
+
+
+		if(window.ScenePlane !== undefined){
+			scene.add( window.ScenePlane );
+		}
+		// renderer.render(scene, camera);
+		if(compose != undefined) {
+			if(selectedObjects.length > 0){
+				compose.render()
+			}else {
+				renderer.render(scene, camera);
+			}
+		}else {
+			renderer.render(scene, camera);
+		}
+
+		if ( rendererCss2 !== null ) {
+			rendererCss2.render( scene, camera );
+		}
+		if ( rendererCss3 !== null ) {
+			rendererCss3.render( scene, camera );
+		}
+		// 更新控制器
+		// controls.update(); // 仅在需要时调用,例如当你使相机移动时
+
+		if(window.ScenePlane !== undefined){
+			scene.remove( window.ScenePlane );
+		}
+
+		// Animations  动画
+		if(window.mixer != null){
+			window.mixer.update( clock.getDelta() );
+		}
+		// requestAnimationFrame(animate);
+	}
+	//endregion
+
+//endregion
+
+
+// --------------------- 鼠标事件 -----------------------
+//region Description
+	var Model_onEvents = {
+		mousemove:undefined,
+		click:undefined,
+		dblclick:undefined,
+		mousedown:undefined,
+	}
+	// 递归寻找上级可触发
+	function finde_parent_choice(Ob) {
+		if (Ob.choice) {
+			return {Ob:Ob,is:true}
+		}
+		if(Ob.parent.isScene) return null,false
+		var Obf = finde_parent_choice(Ob.parent)
+		if(Obf.is){
+			return Obf
+		}
+		return {Ob:null,is:false}
+	}
+	// 移动
+	dom.addEventListener('mousemove', onMouseMove, false);
+	function onMouseMove(event) {
+
+		// console.log("onMouseMove:",event)
+		if (event.isPrimary === false) return;
+
+		// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
+		mouse.x = (event.clientX / dom_width) * 2 - 1;
+		mouse.y = -(event.clientY / dom_height) * 2 + 1;
+
+		raycaster.setFromCamera(mouse, camera);
+		// 计算物体和射线的焦点
+		var intersects = raycaster.intersectObjects(scene.children);
+
+		// console.log("intersects:",intersects)
+		if (intersects.length > 0){
+			//
+			for (let n = 0;intersects.length > n;n++) {
+				let objectP = intersects[n].object
+				// console.log("objectP:",objectP)
+				if(objectP === undefined) break;
+				let objectF = finde_parent_choice(objectP)
+				// console.log("finde_parent_choice:",objectF)
+				if (objectF.is) {
+					if(INTERSECTED !== null && INTERSECTED.uuid === objectF.Ob.uuid) break;
+					INTERSECTED = objectF.Ob;
+					// console.log("移入:",INTERSECTED)
+					if (Model_onEvents.mousemove !== undefined) Model_onEvents.mousemove(INTERSECTED)
+					if(INTERSECTED.scriptsf !== undefined ){
+						INTERSECTED.scriptsf.forEach(scriptf => {
+							if(scriptf["onMouseMoveIn"] !== undefined){
+								scriptf.onMouseMoveIn()
+							}
+						})
+
+					}
+					break;
+				}
+
+				// for (let xn = 0; 10 > xn; xn++) {
+				// 	console.log(xn,"-objectP:",objectP)
+				// 	if(objectP == null) break;
+				// 	// if (objectP.type == "Object3D") {
+				// 	if (INTERSECTED !== objectP) {
+				// 		console.log("intersects[0].object:", intersects[0].object.parent)
+				// 		if (objectP.choice === true) {
+				// 			INTERSECTED = objectP;
+				// 			if (Model_onEvents.mousemove !== undefined) Model_onEvents.mousemove(INTERSECTED)
+				// 			break;
+				// 		}
+				// 	}
+				// 	// }
+				// 	objectP = objectP.parent
+				// }
+
+			}
+			// console.log("onMouseMove:",INTERSECTED)
+		}else {
+			// console.log("移出:",INTERSECTED)
+			if(INTERSECTED !== null && INTERSECTED.scriptsf !== undefined ){
+				INTERSECTED.scriptsf.forEach(scriptf => {
+					if(scriptf["onMouseMoveOut"] !== undefined){
+						scriptf.onMouseMoveOut()
+					}
+				})
+
+			}
+
+			INTERSECTED = null;
+
+		}
+
+
+		OutlinePass_selectedObjects_Refresh()
+		// render()  // 可优化空间
+	}
+	this.Model_onMouseMove = function (fun) {
+		Model_onEvents.mousemove = fun
+	}
+
+	//单击延时触发
+	var  clickTimeId,clickTimeIs = false;
+	// 单击
+	dom.addEventListener('click', onClick, false);
+	function onClick(event) {
+		// console.log("onClick:",INTERSECTED)
+		if(INTERSECTED === null){return}
+		// 取消上次延时未执行的方法
+		clearTimeout(clickTimeId);
+		const INTERSECTED_ = INTERSECTED
+		//执行延时
+		clickTimeId = setTimeout( function () {
+			if (INTERSECTED_) {
+				if(Model_onEvents.click != undefined) Model_onEvents.click(INTERSECTED)
+			}
+			// console.log("onClick:",INTERSECTED)
+
+			if(INTERSECTED_.scriptsf !== undefined ){
+				INTERSECTED_.scriptsf.forEach(scriptf => {
+					if(scriptf["onClick"] !== undefined){
+						scriptf.onClick()
+					}
+				})
+
+			}
+
+
+			// render()
+		}, 250);
+
+	}
+	this.Model_onClick = function (fun) {
+		Model_onEvents.click = fun
+	}
+
+	// 双击
+	dom.addEventListener('dblclick', onDblclick, false);
+	function onDblclick(event) {
+		if(INTERSECTED === null){return}
+		clearTimeout(clickTimeId); // 取消上次延时未执行的方法
+		if (INTERSECTED) {
+			if(Model_onEvents.dblclick != undefined) Model_onEvents.dblclick(INTERSECTED)
+		}
+
+		if(INTERSECTED.scriptsf !== undefined ){
+			INTERSECTED.scriptsf.forEach(scriptf => {
+				if(scriptf["onDblclick"] !== undefined){
+					scriptf.onDblclick()
+				}
+			})
+
+		}
+
+
+		// render()
+	}
+	this.Model_onDblclick = function (fun) {
+		Model_onEvents.dblclick = fun
+	}
+
+	// 右击---
+	dom.addEventListener('contextmenu', onMousedown, false);
+	function onMousedown(event) {
+		if(clickTimeIs) return;
+		clickTimeIs = true;
+		setTimeout( function () {
+			clickTimeIs = false;
+		}, 2000);  // 防止 反复触发退出
+		if(Model_onEvents.mousedown != undefined) Model_onEvents.mousedown(INTERSECTED,event)
+		onBackclick()
+		// render()
+	}
+	this.Model_onMousedown = function (fun) {
+		Model_onEvents.mousedown = fun
+	}
+
+	//
+	// this.ondBlclick_Model = function (position) {
+	// 	console.log("更新视角中心点:", position)
+	// 	controls.target.set(position.x, position.y, position.z);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 	controls.update();
+	// }
+
+//endregion
+
+
+// --------------------- 项目 -----------------------
+//region Description
+	// 本地调试模式
+	this.LocalRun = function ( parkId = 0 ) {
+		window.parkId = parkId
+		new _Storage(function(result) {
+			console.log(result);
+			// scene = result.scene
+			loadJson(result,"000000"+parkId)
+		});
+	}
+
+	var loadProject_Map = []; //ProjectID 映射
+	var Now_ProjectID = ""; // 当前 ProjectID
+	this.load = function (){
+		console.log("加载完毕")
+	}
+
+	// 加载项目
+	this.LoadProject = function (ProjectID) {
+		if(Now_ProjectID == ProjectID){ return }
+
+
+		console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "加载 ProjectID:", ProjectID)
+
+		// 是否加载与缓存过
+		if (loadProject_Map[ProjectID] == undefined) {
+			var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
+			httpRequest.open('GET', './GetProject?T_ViewID=' + ProjectID, true);//第二步:打开连接  将请求参数写在url中  ps:"./Ptest.php?name=test&nameone=testone"
+			httpRequest.send();//第三步:发送请求  将请求参数写在URL中
+			httpRequest.onreadystatechange = function () {
+				if (httpRequest.readyState == 4 && httpRequest.status == 200) {
+					var json = JSON.parse(httpRequest.responseText);//获取到json字符串,还需解析
+					console.log(json);
+					if (json.Code != 200) {
+						console.log("ProjectID 错误!", ProjectID)
+						return "ProjectID 错误!"
+					}
+					var json = JSON.parse(json.Data.T_url);//获取到json字符串,还需解析
+					console.log(json);
+					window.parkList = json
+					window.parkId = 0
+					// 如果需要兼容低版本的浏览器,需要判断一下FileReader对象是否存在。
+					if (window.FileReader) {
+						blobLoad(ProjectID, window.parkList[window.parkId].T_url)
+					} else {
+						console.log('你的浏览器不支持读取文件');
+						loadProject_Map[ProjectID] = src
+					}
+				}
+			};
+		} else {
+
+			console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "缓存加载 ProjectID:", ProjectID)
+			f_load(ProjectID)
+
+		}
+	}
+	this.LoadLocal = function () {
+
+
+		var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
+		httpRequest.open('GET', './static/LocalProject/main.json', true);//第二步:打开连接  将请求参数写在url中  ps:"./Ptest.php?name=test&nameone=testone"
+		httpRequest.responseType = 'json'; // 设置响应类型为 JSON
+		httpRequest.onload = function() {
+			if (httpRequest.status >= 200 && httpRequest.status < 300) {
+				var jsonArray = httpRequest.response; // 直接获取 JSON 数组
+				console.log("jsonArray:",jsonArray);
+				window.parkList = jsonArray
+				window.parkId = 0
+				// 如果需要兼容低版本的浏览器,需要判断一下FileReader对象是否存在。
+				if (window.FileReader) {
+					blobLoad("00000", window.parkList[window.parkId].T_url)
+				} else {
+					console.log('你的浏览器不支持读取文件');
+					loadProject_Map[ProjectID] = src
+				}
+			}
+		};
+		httpRequest.send();//第三步:发送请求  将请求参数写在URL中
+
+	}
+	// 解压并更新舞台
+	function f_load(ProjectID) {
+		console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "开始加载 file:", loadProject_Map[ProjectID])
+
+		// 解压模型
+		// var promise = fetch('../static/16635717199e4e03e8-8850-4152-9c08-9c203c882f7a.zip')
+		var promise = fetch(loadProject_Map[ProjectID])
+			.then((d) => d.arrayBuffer())
+		promise = promise.then(function (data) {
+			//响应的内容
+			console.log("data:", data);
+			const decompressed = unzipSync(new Uint8Array(data), {
+				// You may optionally supply a filter for files. By default, all files in a
+				// ZIP archive are extracted, but a filter can save resources by telling
+				// the library not to decompress certain files
+				filter(file) {
+					// Don't decompress the massive image or any files larger than 10 MiB
+					return file;
+				}
+			});
+			console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "file:", decompressed['app.json'])
+			var obj = JSON.parse(new TextDecoder().decode(decompressed['app.json']));
+			loadJson(obj,ProjectID);// 加载场景
+			// self.setSize(dom_width, dom_height);
+			// orbitControls() // 鼠标移动 -轨道控制
+			Now_ProjectID = ProjectID
+			console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "JSON obj:", obj)
+
+		}).catch(function (err) {
+			console.log(err);
+		})
+
+	}
+
+	// 导入 json
+	function loadJson(json,ProjectID) {
+
+		let loader = new THREE.ObjectLoader(); // 加载
+
+		console.log("loadJson:", json)
+		project = json.project;
+
+		// if (project.vr !== undefined) renderer.xr.enabled = project.vr;
+		// if (project.shadows !== undefined) renderer.shadowMap.enabled = project.shadows;
+		// if (project.shadowType !== undefined) renderer.shadowMap.type = project.shadowType;
+		// if (project.toneMapping !== undefined) renderer.toneMapping = project.toneMapping;
+		// if (project.toneMappingExposure !== undefined) renderer.toneMappingExposure = project.toneMappingExposure;
+		// if (project.physicallyCorrectLights !== undefined) renderer.physicallyCorrectLights = project.physicallyCorrectLights;
+		console.log("rendererCss2:", rendererCss2)
+		if ( rendererCss2 !== null ) {
+			rendererCss2.clean()
+			rendererCss3.clean()
+		}
+
+		scene = loader.parse(json.scene);
+
+		// camera = loader.parse(json.camera);
+		// console.log("json.camera:",json.camera)
+		// console.log("camera:",camera)
+		camera.position.x = json.camera.object.matrix[12]
+		camera.position.y = json.camera.object.matrix[13]
+		camera.position.z = json.camera.object.matrix[14]
+		// camera.aspect = dom_width / dom_height;
+		// camera.updateProjectionMatrix();
+		// renderer.setSize(dom_width, dom_height);
+		// 中心点
+		controls.target.set(0, 0, 0);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+		controls.update();
+
+
+
+		//////--------------
+		// ScenePlane   场景平面
+		if ( project.sceneplane !== undefined ){
+			// console.log("sceneplane:",project.sceneplane)
+
+			switch ( project.sceneplane ) {
+				case 'None':
+					window.ScenePlane = undefined
+					break;
+
+				case 'Grass':  //草地平面
+					const gt = new THREE.TextureLoader().load( './static/js/img/cd.jpg' );
+					const gg = new THREE.PlaneGeometry( 300, 300 );
+					const gm = new THREE.MeshPhongMaterial( { color: 0xffffff, map: gt } );
+
+					window.ScenePlane = new THREE.Mesh( gg, gm );
+					window.ScenePlane.rotation.x = - Math.PI / 2;
+					// window.ScenePlane.rotation.y = -0.1
+					window.ScenePlane.material.map.repeat.set( 64, 64 );
+					window.ScenePlane.material.map.wrapS = THREE.RepeatWrapping;
+					window.ScenePlane.material.map.wrapT = THREE.RepeatWrapping;
+					window.ScenePlane.material.map.colorSpace = THREE.SRGBColorSpace;
+					// note that because the ground does not cast a shadow, .castShadow is left false
+					window.ScenePlane.receiveShadow = true;
+
+					break;
+
+			}
+
+		}
+
+
+
+		//// ------------
+
+		events = {
+			init: [],
+			keydown: [],
+			keyup: [],
+			onMouseMoveIn: [],  //鼠标移入
+			onMouseMoveOut: [],  //鼠标移入
+			onClick: [],  //鼠标单击
+			onDblclick: [],  //鼠标双击
+			onBackclick: [],  //鼠标右击  退后
+			renderer: [],
+			update: []
+		};
+		window.Getevents = function () {
+			return events
+		}
+
+
+		var scriptWrapParams = 'iot3d,renderer,scene,camera';
+		var scriptWrapResultObj = {};
+
+		for (var eventKey in events) {
+
+			scriptWrapParams += ',' + eventKey;
+			scriptWrapResultObj[eventKey] = eventKey;
+
+		}
+
+		var scriptWrapResult = JSON.stringify(scriptWrapResultObj).replace(/\"/g, '');
+
+
+		//执行代码
+
+		// 递归遍历 所有脚本 并添加
+		function traverseChildrenScript(object) {
+			if ( object.scripts !== undefined && object.scripts.length > 0 ) {
+
+				console.log("addObjectScript:",object.uuid,object.scripts)
+				object.scriptsf = []
+				var scripts = object.scripts;
+				for ( var i = 0; i < scripts.length; i ++ ) {
+					//  每个 script 代码
+					var script = scripts[i];
+
+					var functions = ( new Function( scriptWrapParams, script.source + '\nreturn ' + scriptWrapResult + ';' ).bind( object ) )( this, renderer, scene, camera );
+					// console.log("functions.name",functions)
+					object.scriptsf.push(functions)
+					for ( var name in functions ) {
+						// console.log("functions.name",name)
+						if ( functions[ name ] === undefined ) continue;
+
+						if ( events[ name ] === undefined ) {
+							console.warn( 'APP.Player: Event type not supported (', name, ')' );
+							continue;
+						}
+						if(name !== "update"){
+							events[ name ].push( functions[ name ].bind( object ) );
+						}else {
+							var subSN = SeekParameterNodes(object)
+							// console.log("subSN:",subSN)
+							// 订阅
+							pubSub.subscribe(subSN, data => {
+								// console.log("subSN:",subSN,data);
+								functions["update"].bind( object )( data )
+							})
+						}
+
+
+					}
+				}
+
+
+			}
+
+			// 遍历当前对象的所有子对象
+			object.children.forEach(child => {
+				traverseChildrenScript(child); // 递归调用
+			});
+		}
+		traverseChildrenScript(scene)
+
+
+		// console.log("events:", events)
+		dispatch(events.init, arguments);
+
+		console.log("加载完成 ProjectID:",ProjectID)
+		// 场景加载后 视角归为
+		self.setSize(window.innerWidth, window.innerHeight);
+		orbitControls_maxDistance()  // 计算场景最远距离,并控制参数
+		OutlinePass_inte()  // 鼠标选择初始化
+
+	};
+
+	// 文件缓存本地
+	function blobLoad(ProjectID, src) {
+		self.loading_open() //
+		console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "文件缓存本地 :", ProjectID)
+		// let self = this;
+		const req = new XMLHttpRequest();
+		req.open("GET", src, true);
+		req.responseType = "blob";
+		req.onload = function () {
+			// Onload is triggered even on 404
+			// so we need to check the status code
+			if (this.status === 200) {
+				const videoBlob = this.response;
+
+				console.log("videoBlob:", videoBlob)
+				const blobSrc = URL.createObjectURL(videoBlob); // IE10+
+				console.log("blobSrc:", blobSrc)
+				loadProject_Map[ProjectID] = blobSrc
+				console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "文件缓存本地完成 ProjectID:", ProjectID)
+				f_load(ProjectID)
+			}
+			self.loading_close()
+		};
+		//监听进度事件
+		req.addEventListener("progress", function (evt) {
+			if (evt.lengthComputable) {
+				var percentComplete = evt.loaded / evt.total;
+				//进度
+				console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "ProjectID:", ProjectID, " 进度:", (percentComplete * 100) + "%")
+				self.loading_text("模型加载中... "+parseInt(percentComplete * 100) + "%") // 替换内容
+			}
+		}, false);
+		req.onerror = function () {
+			// Error
+			console.log("blobLoad Error!", src)
+			loadProject_Map[ProjectID] = src
+		};
+		req.send();
+	}
+
+
+//endregion
+
+
+// --------------------- 脚本 -----------------------
+//region Description
+	// var time, startTime;
+	// startTime = performance.now();
+
+	// // 创建一个每秒执行一次的定时循环
+	// setInterval(function() {
+	// 	// 在这里编写需要重复执行的代码
+	// 	time = performance.now();
+	// 	const rendertime = { time: time - startTime }
+	// 	// console.log("renderer:",rendertime)
+	// 	try {
+	// 		dispatch( events.renderer, rendertime);
+	// 	} catch ( e ) {
+	// 		console.error( ( e.message || e ), ( e.stack || '' ) );
+	// 	}
+	//
+	// }, 1000); // 1000 毫秒 = 1 秒
+	function dispatch( array, event ) {
+
+		for ( var i = 0, l = array.length; i < l; i ++ ) {
+
+			array[ i ]( event );
+
+		}
+
+	}
+
+
+	//
+	// function animate() {
+	//
+	// 	time = performance.now();
+	//
+	//
+	//
+	// 	prevTime = time;
+	//
+	// }
+
+	// this.play = function () {
+	//
+	// 	if ( renderer.xr.enabled ) dom.append( vrButton );
+	//
+	// 	startTime = prevTime = performance.now();
+	//
+	// 	document.addEventListener( 'keydown', oneyDown );
+	// 	document.addEventListener( 'keyup', onKeyUp );
+
+	// 	document.addEventListener( 'pointerup', onPointerUp );
+	// 	document.addEventListener( 'pointermove', onPointerMove );
+	//
+	// 	// dispatch( events.start, arguments );
+	//
+	// 	renderer.setAnimationLoop( animate );
+	//
+	// };
+
+
+	this.render = function ( time ) {
+
+		dispatch( events.update, { time: time * 1000, delta: 0 /* TODO */ } );
+
+		renderer.render( scene, camera );
+
+	};
+
+	this.dispose = function () {
+
+		renderer.dispose();
+
+		camera = undefined;
+		scene = undefined;
+
+	};
+
+	//
+
+	function onKeyDown( event ) {
+
+		dispatch( events.keydown, event );
+
+	}
+
+	function onKeyUp( event ) {
+
+		dispatch( events.keyup, event );
+
+	}
+
+	// function onPointerDown( event ) {
+	// 	dispatch( events.pointerdown, event );
+	//
+	// }
+	//
+	// function onPointerUp( event ) {
+	// 	dispatch( events.pointerup, event );
+	//
+	// }
+	//
+	function onBackclick( event ) {
+		dispatch( events.onBackclick, event );
+	}
+
+	function upDate( event ) {
+
+		try {
+			dispatch( events.update, event );
+		} catch ( e ) {
+			console.error( ( e.message || e ), ( e.stack || '' ) );
+		}
+
+	}
+
+//endregion
+// --------------------- 功能 -----------------------
+//region Description
+// 	this.BackgroundScene = function () {
+// 		var urls = [
+// 			'./static/js/img/posx.jpg',
+// 			'./static/js/img/negx.jpg',
+// 			'./static/js/img/posy.jpg',
+// 			'./static/js/img/negy.jpg',
+// 			'./static/js/img/posz.jpg',
+// 			'./static/js/img/negz.jpg'
+// 		];
+//
+// 		var cubeLoader = new THREE.CubeTextureLoader();
+// 		scene.background = cubeLoader.load(urls);
+// 		setTimeout(function(){
+// 			render()
+// 		},3000);
+//
+// 	}
+	// 屏幕坐标与世界坐标
+	this.scene_3Dto2D = function (position) {
+		const worldVector = new THREE.Vector3(
+			position.x,
+			position.y,
+			position.z
+		);
+		const standardVec = worldVector.project(camera);
+		const centerX = dom_width / 2;
+		const centerY = dom_height / 2;
+		const screenX = Math.round(centerX * standardVec.x + centerX);
+		const screenY = Math.round(-centerY * standardVec.y + centerY);
+		console.log("screen:", screenX, screenY)
+		return screenX, screenY
+	}
+
+
+//endregion
+	console.log("window.runmode:",window.runmode)
+	if(window.runmode === undefined) window.runmode = 2
+	// 开始 加载场景
+	if (window.runmode === 0){
+		this.LocalRun()// 本地调试模式
+	}else if(window.runmode === 1){
+		this.LoadProject(window.T_ViewID)  // 在线
+	}else if(window.runmode === 2){
+		this.LoadLocal()  // 离线
+	}
+}
+export {IOT3D};
+
+
+new IOT3D();

+ 102 - 0
public/archive/static/build/aaa.js

@@ -0,0 +1,102 @@
+
+import * as THREE from 'three';
+
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+let camera, controls, scene, renderer;
+
+init();
+//render(); // remove when using next line for animation loop (requestAnimationFrame)
+animate();
+
+function init() {
+
+	scene = new THREE.Scene();
+	scene.background = new THREE.Color( 0xcccccc );
+	scene.fog = new THREE.FogExp2( 0xcccccc, 0.002 );
+
+	renderer = new THREE.WebGLRenderer( { antialias: true } );
+	renderer.setPixelRatio( window.devicePixelRatio );
+	renderer.setSize( window.innerWidth, window.innerHeight );
+	document.body.appendChild( renderer.domElement );
+
+	camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 1000 );
+	camera.position.set( 400, 200, 0 );
+
+	// controls
+
+	controls = new OrbitControls( camera, renderer.domElement );
+	controls.listenToKeyEvents( window ); // optional
+
+	//controls.addEventListener( 'change', render ); // call this only in static scenes (i.e., if there is no animation loop)
+
+	controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
+	controls.dampingFactor = 0.05;
+
+	controls.screenSpacePanning = false;
+
+	controls.minDistance = 100;
+	controls.maxDistance = 500;
+
+	controls.maxPolarAngle = Math.PI / 2;
+
+	// world
+
+	const geometry = new THREE.CylinderGeometry( 0, 10, 30, 4, 1 );
+	const material = new THREE.MeshPhongMaterial( { color: 0xffffff, flatShading: true } );
+
+	for ( let i = 0; i < 500; i ++ ) {
+
+		const mesh = new THREE.Mesh( geometry, material );
+		mesh.position.x = Math.random() * 1600 - 800;
+		mesh.position.y = 0;
+		mesh.position.z = Math.random() * 1600 - 800;
+		mesh.updateMatrix();
+		mesh.matrixAutoUpdate = false;
+		scene.add( mesh );
+
+	}
+
+	// lights
+
+	const dirLight1 = new THREE.DirectionalLight( 0xffffff );
+	dirLight1.position.set( 1, 1, 1 );
+	scene.add( dirLight1 );
+
+	const dirLight2 = new THREE.DirectionalLight( 0x002288 );
+	dirLight2.position.set( - 1, - 1, - 1 );
+	scene.add( dirLight2 );
+
+	const ambientLight = new THREE.AmbientLight( 0x222222 );
+	scene.add( ambientLight );
+
+	//
+
+	window.addEventListener( 'resize', onWindowResize );
+
+}
+
+function onWindowResize() {
+
+	camera.aspect = window.innerWidth / window.innerHeight;
+	camera.updateProjectionMatrix();
+
+	renderer.setSize( window.innerWidth, window.innerHeight );
+
+}
+
+function animate() {
+
+	requestAnimationFrame( animate );
+
+	controls.update(); // only required if controls.enableDamping = true, or if controls.autoRotate = true
+
+	render();
+
+}
+
+function render() {
+
+	renderer.render( scene, camera );
+
+}

+ 915 - 0
public/archive/static/build/ccc.js

@@ -0,0 +1,915 @@
+class Object3D extends EventDispatcher {
+
+	constructor() {
+
+		super();
+
+		this.isObject3D = true;
+
+		Object.defineProperty( this, 'id', { value: _object3DId ++ } );
+
+		this.uuid = generateUUID();
+
+		this.name = '';
+		this.type = 'Object3D';
+
+		this.parent = null;
+		this.children = [];
+
+		this.up = Object3D.DefaultUp.clone();
+
+		const position = new Vector3();
+		const rotation = new Euler();
+		const quaternion = new Quaternion();
+		const scale = new Vector3( 1, 1, 1 );
+
+		function onRotationChange() {
+
+			quaternion.setFromEuler( rotation, false );
+
+		}
+
+		function onQuaternionChange() {
+
+			rotation.setFromQuaternion( quaternion, undefined, false );
+
+		}
+
+		rotation._onChange( onRotationChange );
+		quaternion._onChange( onQuaternionChange );
+
+		Object.defineProperties( this, {
+			position: {
+				configurable: true,
+				enumerable: true,
+				value: position
+			},
+			rotation: {
+				configurable: true,
+				enumerable: true,
+				value: rotation
+			},
+			quaternion: {
+				configurable: true,
+				enumerable: true,
+				value: quaternion
+			},
+			scale: {
+				configurable: true,
+				enumerable: true,
+				value: scale
+			},
+			modelViewMatrix: {
+				value: new Matrix4()
+			},
+			normalMatrix: {
+				value: new Matrix3()
+			}
+		} );
+
+		this.matrix = new Matrix4();
+		this.matrixWorld = new Matrix4();
+
+		this.matrixAutoUpdate = Object3D.DefaultMatrixAutoUpdate;
+		this.matrixWorldNeedsUpdate = false;
+
+		this.matrixWorldAutoUpdate = Object3D.DefaultMatrixWorldAutoUpdate; // checked by the renderer
+
+		this.layers = new Layers();
+		this.visible = true;
+		this.choice = null;
+
+		this.castShadow = false;
+		this.receiveShadow = false;
+
+		this.frustumCulled = true;
+		this.renderOrder = 0;
+
+		this.animations = [];
+
+		this.userData = {};
+
+	}
+
+	onBeforeRender( /* renderer, scene, camera, geometry, material, group */ ) {}
+
+	onAfterRender( /* renderer, scene, camera, geometry, material, group */ ) {}
+
+	applyMatrix4( matrix ) {
+
+		if ( this.matrixAutoUpdate ) this.updateMatrix();
+
+		this.matrix.premultiply( matrix );
+
+		this.matrix.decompose( this.position, this.quaternion, this.scale );
+
+	}
+
+	applyQuaternion( q ) {
+
+		this.quaternion.premultiply( q );
+
+		return this;
+
+	}
+
+	setRotationFromAxisAngle( axis, angle ) {
+
+		// assumes axis is normalized
+
+		this.quaternion.setFromAxisAngle( axis, angle );
+
+	}
+
+	setRotationFromEuler( euler ) {
+
+		this.quaternion.setFromEuler( euler, true );
+
+	}
+
+	setRotationFromMatrix( m ) {
+
+		// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
+
+		this.quaternion.setFromRotationMatrix( m );
+
+	}
+
+	setRotationFromQuaternion( q ) {
+
+		// assumes q is normalized
+
+		this.quaternion.copy( q );
+
+	}
+
+	rotateOnAxis( axis, angle ) {
+
+		// rotate object on axis in object space
+		// axis is assumed to be normalized
+
+		_q1.setFromAxisAngle( axis, angle );
+
+		this.quaternion.multiply( _q1 );
+
+		return this;
+
+	}
+
+	rotateOnWorldAxis( axis, angle ) {
+
+		// rotate object on axis in world space
+		// axis is assumed to be normalized
+		// method assumes no rotated parent
+
+		_q1.setFromAxisAngle( axis, angle );
+
+		this.quaternion.premultiply( _q1 );
+
+		return this;
+
+	}
+
+	rotateX( angle ) {
+
+		return this.rotateOnAxis( _xAxis, angle );
+
+	}
+
+	rotateY( angle ) {
+
+		return this.rotateOnAxis( _yAxis, angle );
+
+	}
+
+	rotateZ( angle ) {
+
+		return this.rotateOnAxis( _zAxis, angle );
+
+	}
+
+	translateOnAxis( axis, distance ) {
+
+		// translate object by distance along axis in object space
+		// axis is assumed to be normalized
+
+		_v1$4.copy( axis ).applyQuaternion( this.quaternion );
+
+		this.position.add( _v1$4.multiplyScalar( distance ) );
+
+		return this;
+
+	}
+
+	translateX( distance ) {
+
+		return this.translateOnAxis( _xAxis, distance );
+
+	}
+
+	translateY( distance ) {
+
+		return this.translateOnAxis( _yAxis, distance );
+
+	}
+
+	translateZ( distance ) {
+
+		return this.translateOnAxis( _zAxis, distance );
+
+	}
+
+	localToWorld( vector ) {
+
+		return vector.applyMatrix4( this.matrixWorld );
+
+	}
+
+	worldToLocal( vector ) {
+
+		return vector.applyMatrix4( _m1$1.copy( this.matrixWorld ).invert() );
+
+	}
+
+	lookAt( x, y, z ) {
+
+		// This method does not support objects having non-uniformly-scaled parent(s)
+
+		if ( x.isVector3 ) {
+
+			_target.copy( x );
+
+		} else {
+
+			_target.set( x, y, z );
+
+		}
+
+		const parent = this.parent;
+
+		this.updateWorldMatrix( true, false );
+
+		_position$3.setFromMatrixPosition( this.matrixWorld );
+
+		if ( this.isCamera || this.isLight ) {
+
+			_m1$1.lookAt( _position$3, _target, this.up );
+
+		} else {
+
+			_m1$1.lookAt( _target, _position$3, this.up );
+
+		}
+
+		this.quaternion.setFromRotationMatrix( _m1$1 );
+
+		if ( parent ) {
+
+			_m1$1.extractRotation( parent.matrixWorld );
+			_q1.setFromRotationMatrix( _m1$1 );
+			this.quaternion.premultiply( _q1.invert() );
+
+		}
+
+	}
+
+	add( object ) {
+
+		if ( arguments.length > 1 ) {
+
+			for ( let i = 0; i < arguments.length; i ++ ) {
+
+				this.add( arguments[ i ] );
+
+			}
+
+			return this;
+
+		}
+
+		if ( object === this ) {
+
+			console.error( 'THREE.Object3D.add: object can\'t be added as a child of itself.', object );
+			return this;
+
+		}
+
+		if ( object && object.isObject3D ) {
+
+			if ( object.parent !== null ) {
+
+				object.parent.remove( object );
+
+			}
+
+			object.parent = this;
+			this.children.push( object );
+
+			object.dispatchEvent( _addedEvent );
+
+		} else {
+
+			console.error( 'THREE.Object3D.add: object not an instance of THREE.Object3D.', object );
+
+		}
+
+		return this;
+
+	}
+
+	remove( object ) {
+
+		if ( arguments.length > 1 ) {
+
+			for ( let i = 0; i < arguments.length; i ++ ) {
+
+				this.remove( arguments[ i ] );
+
+			}
+
+			return this;
+
+		}
+
+		const index = this.children.indexOf( object );
+
+		if ( index !== - 1 ) {
+
+			object.parent = null;
+			this.children.splice( index, 1 );
+
+			object.dispatchEvent( _removedEvent );
+
+		}
+
+		return this;
+
+	}
+
+	removeFromParent() {
+
+		const parent = this.parent;
+
+		if ( parent !== null ) {
+
+			parent.remove( this );
+
+		}
+
+		return this;
+
+	}
+
+	clear() {
+
+		for ( let i = 0; i < this.children.length; i ++ ) {
+
+			const object = this.children[ i ];
+
+			object.parent = null;
+
+			object.dispatchEvent( _removedEvent );
+
+		}
+
+		this.children.length = 0;
+
+		return this;
+
+
+	}
+
+	attach( object ) {
+
+		// adds object as a child of this, while maintaining the object's world transform
+
+		// Note: This method does not support scene graphs having non-uniformly-scaled nodes(s)
+
+		this.updateWorldMatrix( true, false );
+
+		_m1$1.copy( this.matrixWorld ).invert();
+
+		if ( object.parent !== null ) {
+
+			object.parent.updateWorldMatrix( true, false );
+
+			_m1$1.multiply( object.parent.matrixWorld );
+
+		}
+
+		object.applyMatrix4( _m1$1 );
+
+		this.add( object );
+
+		object.updateWorldMatrix( false, true );
+
+		return this;
+
+	}
+
+	getObjectById( id ) {
+
+		return this.getObjectByProperty( 'id', id );
+
+	}
+
+	getObjectByName( name ) {
+
+		return this.getObjectByProperty( 'name', name );
+
+	}
+
+	getObjectByProperty( name, value ) {
+
+		if ( this[ name ] === value ) return this;
+
+		for ( let i = 0, l = this.children.length; i < l; i ++ ) {
+
+			const child = this.children[ i ];
+			const object = child.getObjectByProperty( name, value );
+
+			if ( object !== undefined ) {
+
+				return object;
+
+			}
+
+		}
+
+		return undefined;
+
+	}
+
+	getWorldPosition( target ) {
+
+		this.updateWorldMatrix( true, false );
+
+		return target.setFromMatrixPosition( this.matrixWorld );
+
+	}
+
+	getWorldQuaternion( target ) {
+
+		this.updateWorldMatrix( true, false );
+
+		this.matrixWorld.decompose( _position$3, target, _scale$2 );
+
+		return target;
+
+	}
+
+	getWorldScale( target ) {
+
+		this.updateWorldMatrix( true, false );
+
+		this.matrixWorld.decompose( _position$3, _quaternion$2, target );
+
+		return target;
+
+	}
+
+	getWorldDirection( target ) {
+
+		this.updateWorldMatrix( true, false );
+
+		const e = this.matrixWorld.elements;
+
+		return target.set( e[ 8 ], e[ 9 ], e[ 10 ] ).normalize();
+
+	}
+
+	raycast( /* raycaster, intersects */ ) {}
+
+	traverse( callback ) {
+
+		callback( this );
+
+		const children = this.children;
+
+		for ( let i = 0, l = children.length; i < l; i ++ ) {
+
+			children[ i ].traverse( callback );
+
+		}
+
+	}
+
+	traverseVisible( callback ) {
+
+		if ( this.visible === false ) return;
+
+		callback( this );
+
+		const children = this.children;
+
+		for ( let i = 0, l = children.length; i < l; i ++ ) {
+
+			children[ i ].traverseVisible( callback );
+
+		}
+
+	}
+
+	traverseAncestors( callback ) {
+
+		const parent = this.parent;
+
+		if ( parent !== null ) {
+
+			callback( parent );
+
+			parent.traverseAncestors( callback );
+
+		}
+
+	}
+
+	updateMatrix() {
+
+		this.matrix.compose( this.position, this.quaternion, this.scale );
+
+		this.matrixWorldNeedsUpdate = true;
+
+	}
+
+	updateMatrixWorld( force ) {
+
+		if ( this.matrixAutoUpdate ) this.updateMatrix();
+
+		if ( this.matrixWorldNeedsUpdate || force ) {
+
+			if ( this.parent === null ) {
+
+				this.matrixWorld.copy( this.matrix );
+
+			} else {
+
+				this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
+
+			}
+
+			this.matrixWorldNeedsUpdate = false;
+
+			force = true;
+
+		}
+
+		// update children
+
+		const children = this.children;
+
+		for ( let i = 0, l = children.length; i < l; i ++ ) {
+
+			const child = children[ i ];
+
+			if ( child.matrixWorldAutoUpdate === true || force === true ) {
+
+				child.updateMatrixWorld( force );
+
+			}
+
+		}
+
+	}
+
+	updateWorldMatrix( updateParents, updateChildren ) {
+
+		const parent = this.parent;
+
+		if ( updateParents === true && parent !== null && parent.matrixWorldAutoUpdate === true ) {
+
+			parent.updateWorldMatrix( true, false );
+
+		}
+
+		if ( this.matrixAutoUpdate ) this.updateMatrix();
+
+		if ( this.parent === null ) {
+
+			this.matrixWorld.copy( this.matrix );
+
+		} else {
+
+			this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
+
+		}
+
+		// update children
+
+		if ( updateChildren === true ) {
+
+			const children = this.children;
+
+			for ( let i = 0, l = children.length; i < l; i ++ ) {
+
+				const child = children[ i ];
+
+				if ( child.matrixWorldAutoUpdate === true ) {
+
+					child.updateWorldMatrix( false, true );
+
+				}
+
+			}
+
+		}
+
+	}
+
+	toJSON( meta ) {
+
+		// meta is a string when called from JSON.stringify
+		const isRootObject = ( meta === undefined || typeof meta === 'string' );
+
+		const output = {};
+
+		// meta is a hash used to collect geometries, materials.
+		// not providing it implies that this is the root object
+		// being serialized.
+		if ( isRootObject ) {
+
+			// initialize meta obj
+			meta = {
+				geometries: {},
+				materials: {},
+				textures: {},
+				images: {},
+				shapes: {},
+				skeletons: {},
+				animations: {},
+				nodes: {}
+			};
+
+			output.metadata = {
+				version: 4.5,
+				type: 'Object',
+				generator: 'Object3D.toJSON'
+			};
+
+		}
+
+		// standard Object3D serialization
+
+		const object = {};
+
+		object.uuid = this.uuid;
+		object.type = this.type;
+		object.choice = this.choice;
+
+		if ( this.name !== '' ) object.name = this.name;
+		if ( this.castShadow === true ) object.castShadow = true;
+		if ( this.receiveShadow === true ) object.receiveShadow = true;
+		if ( this.visible === false ) object.visible = false;
+		// if ( this.choice === false ) object.choice = false;
+		if ( this.frustumCulled === false ) object.frustumCulled = false;
+		if ( this.renderOrder !== 0 ) object.renderOrder = this.renderOrder;
+		if ( JSON.stringify( this.userData ) !== '{}' ) object.userData = this.userData;
+
+		object.layers = this.layers.mask;
+		object.matrix = this.matrix.toArray();
+
+		if ( this.matrixAutoUpdate === false ) object.matrixAutoUpdate = false;
+
+		// object specific properties
+
+		if ( this.isInstancedMesh ) {
+
+			object.type = 'InstancedMesh';
+			object.count = this.count;
+			object.instanceMatrix = this.instanceMatrix.toJSON();
+			if ( this.instanceColor !== null ) object.instanceColor = this.instanceColor.toJSON();
+
+		}
+
+		//
+
+		function serialize( library, element ) {
+
+			if ( library[ element.uuid ] === undefined ) {
+
+				library[ element.uuid ] = element.toJSON( meta );
+
+			}
+
+			return element.uuid;
+
+		}
+
+		if ( this.isScene ) {
+
+			if ( this.background ) {
+
+				if ( this.background.isColor ) {
+
+					object.background = this.background.toJSON();
+
+				} else if ( this.background.isTexture ) {
+
+					object.background = this.background.toJSON( meta ).uuid;
+
+				}
+
+			}
+
+			if ( this.environment && this.environment.isTexture && this.environment.isRenderTargetTexture !== true ) {
+
+				object.environment = this.environment.toJSON( meta ).uuid;
+
+			}
+
+		} else if ( this.isMesh || this.isLine || this.isPoints ) {
+
+			object.geometry = serialize( meta.geometries, this.geometry );
+
+			const parameters = this.geometry.parameters;
+
+			if ( parameters !== undefined && parameters.shapes !== undefined ) {
+
+				const shapes = parameters.shapes;
+
+				if ( Array.isArray( shapes ) ) {
+
+					for ( let i = 0, l = shapes.length; i < l; i ++ ) {
+
+						const shape = shapes[ i ];
+
+						serialize( meta.shapes, shape );
+
+					}
+
+				} else {
+
+					serialize( meta.shapes, shapes );
+
+				}
+
+			}
+
+		}
+
+		if ( this.isSkinnedMesh ) {
+
+			object.bindMode = this.bindMode;
+			object.bindMatrix = this.bindMatrix.toArray();
+
+			if ( this.skeleton !== undefined ) {
+
+				serialize( meta.skeletons, this.skeleton );
+
+				object.skeleton = this.skeleton.uuid;
+
+			}
+
+		}
+
+		if ( this.material !== undefined ) {
+
+			if ( Array.isArray( this.material ) ) {
+
+				const uuids = [];
+
+				for ( let i = 0, l = this.material.length; i < l; i ++ ) {
+
+					uuids.push( serialize( meta.materials, this.material[ i ] ) );
+
+				}
+
+				object.material = uuids;
+
+			} else {
+
+				object.material = serialize( meta.materials, this.material );
+
+			}
+
+		}
+
+		//
+
+		if ( this.children.length > 0 ) {
+
+			object.children = [];
+
+			for ( let i = 0; i < this.children.length; i ++ ) {
+
+				object.children.push( this.children[ i ].toJSON( meta ).object );
+
+			}
+
+		}
+
+		//
+
+		if ( this.animations.length > 0 ) {
+
+			object.animations = [];
+
+			for ( let i = 0; i < this.animations.length; i ++ ) {
+
+				const animation = this.animations[ i ];
+
+				object.animations.push( serialize( meta.animations, animation ) );
+
+			}
+
+		}
+
+		if ( isRootObject ) {
+
+			const geometries = extractFromCache( meta.geometries );
+			const materials = extractFromCache( meta.materials );
+			const textures = extractFromCache( meta.textures );
+			const images = extractFromCache( meta.images );
+			const shapes = extractFromCache( meta.shapes );
+			const skeletons = extractFromCache( meta.skeletons );
+			const animations = extractFromCache( meta.animations );
+			const nodes = extractFromCache( meta.nodes );
+
+			if ( geometries.length > 0 ) output.geometries = geometries;
+			if ( materials.length > 0 ) output.materials = materials;
+			if ( textures.length > 0 ) output.textures = textures;
+			if ( images.length > 0 ) output.images = images;
+			if ( shapes.length > 0 ) output.shapes = shapes;
+			if ( skeletons.length > 0 ) output.skeletons = skeletons;
+			if ( animations.length > 0 ) output.animations = animations;
+			if ( nodes.length > 0 ) output.nodes = nodes;
+
+		}
+
+		output.object = object;
+
+		return output;
+
+		// extract data from the cache hash
+		// remove metadata on each item
+		// and return as array
+		function extractFromCache( cache ) {
+
+			const values = [];
+			for ( const key in cache ) {
+
+				const data = cache[ key ];
+				delete data.metadata;
+				values.push( data );
+
+			}
+
+			return values;
+
+		}
+
+	}
+
+	clone( recursive ) {
+
+		return new this.constructor().copy( this, recursive );
+
+	}
+
+	copy( source, recursive = true ) {
+
+		this.name = source.name;
+
+		this.up.copy( source.up );
+
+		this.position.copy( source.position );
+		this.rotation.order = source.rotation.order;
+		this.quaternion.copy( source.quaternion );
+		this.scale.copy( source.scale );
+
+		this.matrix.copy( source.matrix );
+		this.matrixWorld.copy( source.matrixWorld );
+
+		this.matrixAutoUpdate = source.matrixAutoUpdate;
+		this.matrixWorldNeedsUpdate = source.matrixWorldNeedsUpdate;
+
+		this.matrixWorldAutoUpdate = source.matrixWorldAutoUpdate;
+
+		this.layers.mask = source.layers.mask;
+		this.visible = source.visible;
+		this.choice = source.choice;
+
+		this.castShadow = source.castShadow;
+		this.receiveShadow = source.receiveShadow;
+
+		this.frustumCulled = source.frustumCulled;
+		this.renderOrder = source.renderOrder;
+
+		this.userData = JSON.parse( JSON.stringify( source.userData ) );
+
+		if ( recursive === true ) {
+
+			for ( let i = 0; i < source.children.length; i ++ ) {
+
+				const child = source.children[ i ];
+				this.add( child.clone() );
+
+			}
+
+		}
+
+		return this;
+
+	}
+
+}

File diff suppressed because it is too large
+ 305 - 0
public/archive/static/build/es-module-shims.js


File diff suppressed because it is too large
+ 9773 - 0
public/archive/static/build/three.cjs


File diff suppressed because it is too large
+ 13350 - 0
public/archive/static/build/three.module.js


+ 1529 - 0
public/archive/static/js/IOT3D-.js

@@ -0,0 +1,1529 @@
+
+
+import * as THREE from 'three';
+// import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+import {unzipSync} from './libs/fflate.module.js';
+import {FontLoader} from './jsm/loaders/FontLoader.js';
+import {CinematicCamera} from './jsm/cameras/CinematicCamera.js';
+import { OrbitControls } from './jsm/controls/OrbitControls.js';
+import { EffectComposer } from './jsm/postprocessing/EffectComposer.js';
+import { RenderPass } from './jsm/postprocessing/RenderPass.js';
+import { ShaderPass } from './jsm/postprocessing/ShaderPass.js';
+import { OutlinePass } from './jsm/postprocessing/OutlinePass.js';
+import { CSS3DRenderer  } from './jsm/renderers/CSS3DRenderer.js';
+import { CSS2DRenderer } from './jsm/renderers/CSS2DRenderer.js';
+import { Storage as _Storage } from './libs/Storage.js';
+
+// --------------------- 折叠 -----------------------
+//region Description
+
+//endregion
+
+// iot3d = new IOT3D("iot3d",false); // 创建应用  (div_id,运行模式)
+function IOT3D(iot3d,runmode = 1) {
+	window.iot3d = this
+	window.runmode = runmode
+	window.THREE = THREE; // Used by APP Scripts.
+	window.FontLoader = FontLoader; // Used by APP Scripts.
+	// window.VRButton = VRButton; // Used by APP Scripts.
+	window.OrbitControls = OrbitControls; // Used by APP Scripts.
+	window.CinematicCamera = CinematicCamera; // Used by APP Scripts.
+	window.EffectComposer = EffectComposer; // Used by APP Scripts.
+	window.RenderPass = RenderPass; // Used by APP Scripts.
+	window.ShaderPass = ShaderPass; // Used by APP Scripts.
+	window.OutlinePass = OutlinePass; // Used by APP Scripts.
+
+
+	this.loading_open = function (time){
+		//1000为遮罩层显示时长,若不传则一直显示,须调用关闭方法
+		$.mask_fullscreen(time);
+	}
+	this.loading_close = function (){
+		//关闭遮罩层
+		$.mask_close_all();
+	}
+	this.loading_text = function (text){
+		//关闭遮罩层
+		$.mask_text(text);
+	}
+	this.loading_html = function (html){
+		//关闭遮罩层
+		$.mask_html(html);
+	}
+	//
+	var self;
+
+	THREE.Cache.enabled = true; //这是一个全局属性,只需要设置一次,供内部使用FileLoader的所有加载器使用。
+	// 项目
+	// var IOT3D_Url = "https://iot3d.baozhida.cn"
+	// var IOT3D_Url = "https://iot3d.baozhida.cn"
+	// if(url.indexOf("127.0.0.1") != -1){
+	// 	IOT3D_Url = "http://127.0.0.1:6210"
+	// }else {
+	// 	IOT3D_Url = "https://iot3d.baozhida.cn"
+	// }
+	var clock = new THREE.Clock();
+	// 公共
+	var dom_width = 500, dom_height = 500;
+	var camera, scene, dom, renderer, rendererCss2, rendererCss3,controls;
+	var events = {};
+	window.parkId = 0;   // 园区ID
+	var project;  // 项目配置
+	/// =-  选取
+	var raycaster, mouse, INTERSECTED = null;
+
+	// 变量初始化
+	self = this;
+	dom_width = window.innerWidth;
+	dom_height = window.innerHeight;
+	self.width = dom_width;
+	self.height = dom_width;
+	// var vrButton = VRButton.createButton( renderer ); // eslint-disable-line no-undef
+
+
+	// --------------------- 初始化 -----------------------
+	//region Description
+
+	// 舞台
+	scene = new THREE.Scene();
+	// 相机
+	camera = new THREE.PerspectiveCamera(45, dom_width / dom_height, 0.1, 3000);
+	camera.position.set(-10, 10, 30);
+	camera.lookAt(scene.position);
+
+	window.mixer = new THREE.AnimationMixer( scene ); // 动画
+
+
+	// HTML dom
+	dom = document.getElementById(iot3d);
+
+	//渲染器
+	renderer = new THREE.WebGLRenderer({antialias: true});
+	renderer.setPixelRatio(window.devicePixelRatio); // TODO: Use setPixelRatio()
+	// renderer.outputEncoding = THREE.sRGBEncoding;
+	renderer.shadowMap.enabled = true;// 阴影
+	renderer.setSize( dom_width, dom_height );
+	renderer.setAnimationLoop( animate );
+	dom.appendChild(renderer.domElement);
+
+
+	// rendererCss3
+	rendererCss3 = new CSS3DRenderer();
+	rendererCss3.setSize( dom.innerWidth, dom.innerHeight );
+	rendererCss3.domElement.style.position = 'absolute';
+	rendererCss3.domElement.style.top = 0;
+	dom.appendChild( rendererCss3.domElement );
+
+	// rendererCss2
+	rendererCss2 = new CSS2DRenderer();
+	rendererCss2.setSize( dom.innerWidth, dom.innerHeight );
+	rendererCss2.domElement.style.position = 'absolute';
+	rendererCss2.domElement.style.top = '0px';
+	dom.appendChild( rendererCss2.domElement );
+
+	// 加载到 网页
+	document.body.appendChild(dom);
+	window.addEventListener('resize', function () {
+		self.setSize(window.innerWidth, window.innerHeight);
+	});
+
+	/// =-  选取
+	raycaster = new THREE.Raycaster();
+	mouse = new THREE.Vector2();
+
+
+	var selectedObjects = [],compose,renderPass,outlinePass;
+
+	// 鼠标移动 -轨道控制
+	// var AutomaticRotationPerspective = [1,2,20]  //自动旋转视角  0 【关闭】  1【360度旋转[1,旋转速度{1~9,2},俯视角度{0~100,20}]】
+	var AutomaticRotationPerspectiveInterval = undefined  // 定时任务
+	var AutomaticRotationPerspectiveTally = 0 // 计数
+	// - 鼠标移动视角
+	controls = new OrbitControls(camera, renderer.domElement);
+	// controls.addEventListener('change', animate); // use if there is no animation loop
+	controls.dampingFactor = 0.25;
+	controls.minDistance = 5;  // 最小距离
+	controls.maxDistance = 1000;
+	controls.screenSpacePanning = false; // 允许相机平移
+	// 设置最大和最小角度
+	controls.maxPolarAngle = Math.PI / 2; // 最大角度 (90度) - 可视化平面
+	controls.minPolarAngle = 0;             // 最小角度 (0度) - 直接向下
+	controls.target.set(0, 0, -2.2);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// controls.position0.set(200, 200, 500 )
+	controls.update();
+	// 检查 鼠标是否有操作
+	renderer.domElement.addEventListener( 'pointerdown', function () {
+		if(AutomaticRotationPerspectiveInterval !== undefined){
+			clearInterval(AutomaticRotationPerspectiveInterval); // 停止定时任务的执行
+		}
+		AutomaticRotationPerspectiveTally = 0
+		AutomaticRotationPerspectiveInterval = undefined
+	} );
+	// 定时 开始触发 自动旋转视角
+	setInterval(() => {
+		if(project.autoangle === undefined || project.autoangle === "None") return; // 直接跳过
+
+		if(AutomaticRotationPerspectiveInterval === undefined){
+			AutomaticRotationPerspectiveTally += 1
+			if(AutomaticRotationPerspectiveTally === 3){
+				console.log("project.autoangle:",project.autoangle)
+				switch (project.autoangle) {
+					case "Angle360":  // 360度旋转
+						self.AroundRotation(scene,project.autoangle_speed,project.autoangle_angle)
+						break;
+					case "Regainstate":  // 回到原始视角
+						self.Focus(self.GetScene())
+						break;
+				}
+
+			}
+		}
+	}, 1000); // 每秒
+
+	// 数据订阅
+	window.pubSub = {
+		list: {},
+		// 订阅
+		subscribe: function(key, fn) {
+			if (!this.list[key]) this.list[key] = [];
+			this.list[key].push(fn);
+		},
+		//取消订阅
+		unsubscribe: function(key, fn) {
+			let fnList = this.list[key];
+			if (!fnList) return false;
+			if (!fn) { // 不传入指定的方法,清空所用 key 下的订阅
+				fnList && (fnList.length = 0);
+			} else {
+				fnList.forEach((item, index) => {
+					item === fn && fnList.splice(index, 1);
+				});
+			}
+		},
+		// 发布
+		publish: function(key, ...args) {
+			if(this.list[key] === undefined) return;
+			for (let fn of this.list[key]) fn.call(this, ...args);
+		}
+	}
+
+	// this.storage.get(fxx)
+	//
+	// function fxx(xxx) {
+	// 	console.log("fxx-------------------------",xxx)
+	// }
+
+	// 测试
+	this.Test = function (rotationSpeed = 0.001) {
+		console.log("Test-------------------------")
+
+
+
+		return
+	}
+
+
+// 测试
+
+	function cubeDr(a, x, y, z) {
+		var cubeGeo = new THREE.BoxGeometry(a, a, a);
+		var cubeMat = new THREE.MeshPhongMaterial({
+			color: 0xfff000 * Math.random()
+		});
+		var cube = new THREE.Mesh(cubeGeo, cubeMat);
+		cube.position.set(x, y, z);
+		cube.castShadow = true;
+		scene.add(cube);
+		return cube;
+	}
+
+
+
+
+//endregion
+
+// --------------------- 核心 -----------------------
+//region Description
+
+
+
+	// 核心方法
+	//region Description
+	// 舞台
+	this.GetScene = function () {
+		return scene
+	}
+	// 相机
+	this.GetCamera = function () {
+		return camera
+	}
+	//渲染器
+	this.GetRenderer = function () {
+		return renderer
+	}
+	// 获取 UUID 模型
+	this.GetModelByUuid = function (uuid) {
+		return scene.getObjectByProperty('uuid', uuid, true);
+	}
+	// 获取 UUID 模型 内部函数
+	this.Model = function (uuid,fun) {
+		fun(scene.getObjectByProperty('uuid', uuid, true));
+	}
+	// 设置显示比例
+	this.setPixelRatio = function (pixelRatio) {
+		renderer.setPixelRatio(pixelRatio);
+	};
+	// 设置大小
+	this.setSize = function (width, height) {
+		dom_width = width;
+		dom_height = height;
+
+		if (camera) {
+			camera.aspect = dom_width / dom_height;
+			camera.updateProjectionMatrix();
+		}
+		renderer.setSize(width, height);
+
+		if ( rendererCss3 !== null ) {
+			rendererCss3.setSize( dom.offsetWidth, dom.offsetHeight );
+		}
+
+		if ( rendererCss2 !== null ) {
+			rendererCss2.setSize( dom.offsetWidth, dom.offsetHeight );
+		}
+		self.width = dom_width;
+		self.height = dom_width;
+	};
+	//endregion
+
+	// 鼠标
+	//region Description
+
+	//更新视角中心点
+	this.orbitControls_target = function (position) {
+		console.log("更新视角中心点:", position)
+		controls.target.set(position.x, position.y, position.z);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+		controls.update();
+	}
+	// 计算场景最远距离,并控制参数
+	function orbitControls_maxDistance() {
+		// 计算场景中的包围盒
+		function calculateSceneBoundingBox(scene) {
+			const box = new THREE.Box3();
+
+			scene.traverse((object) => {
+				if (object.isMesh) {
+					// 更新包围盒以包含当前对象的包围盒
+					const objectBox = new THREE.Box3().setFromObject(object);
+					box.union(objectBox);
+				}
+			});
+
+			return box;
+		}
+		// 计算场景的包围盒
+		const boundingBox = calculateSceneBoundingBox(scene);
+		// 获取包围盒的尺寸
+		const size = new THREE.Vector3();
+		boundingBox.getSize(size);
+		// 获取包围盒的中心
+		const center = new THREE.Vector3();
+		boundingBox.getCenter(center);
+		// 计算最大尺寸距离 (从中心到某个角的距离)
+		const maxDistance = center.distanceTo(boundingBox.max);
+		// console.log("从中心到最远角的距离:", maxDistance);
+		controls.maxDistance = maxDistance * 10;
+	}
+
+
+	// 鼠标选择初始化
+	var OutlinePass_selectedObjects_Map = new Map();
+	function OutlinePass_inte(){
+		// console.log("OutlinePass_inte")
+		// 清空所有选项
+		selectedObjects = []
+		self.Model_Selected_Clear()
+
+		camera.lookAt(scene.position);
+
+		compose = new EffectComposer(renderer);
+		renderPass = new RenderPass(scene, camera);
+
+		outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth,window.innerHeight),scene,camera);
+		outlinePass.renderToScreen = true;
+		outlinePass.selectedObjects = selectedObjects;
+
+		compose.addPass(renderPass);
+		compose.addPass(outlinePass);
+
+		// https://threejs.org/examples/?q=webgl_postprocessing_outline#webgl_postprocessing_outline
+		outlinePass.renderToScreen = true;
+		outlinePass.edgeStrength = 3 //粗   0.01, 10
+		outlinePass.edgeGlow = 1 //发光  0.0, 1
+		outlinePass.edgeThickness = 2 //光晕粗   1, 4
+		outlinePass.pulsePeriod = 0 //闪烁  0.0, 5
+		outlinePass.usePatternTexture = false //是否使用贴图
+		let visibleEdgeColor = '#00a1fd';  // 选择颜色
+		let hiddenEdgeColor = '#00a1fd';  //遮挡部分颜色
+		outlinePass.visibleEdgeColor.set(visibleEdgeColor);
+		outlinePass.hiddenEdgeColor.set(hiddenEdgeColor);
+
+		// let light = new THREE.AmbientLight(0x333333);
+		// scene.add(light);
+		//
+		// 这里没有 渲染会报错
+		let light = new THREE.SpotLight(0xFFFFFF);
+		light.position.set(0, 40, 30);
+		light.castShadow = true;
+		light.shadow.mapSize.height = 1;
+		light.shadow.mapSize.width = 1;
+		light.angle = 0;
+		scene.add(light);
+
+		// light = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
+		// light.position.set(0, 200, 0);
+		// scene.add(light);
+
+
+
+		// const light = new THREE.DirectionalLight( 0xffffff, 0.6 );
+		// light.position.set( 1, 1, 1 );
+		// light.castShadow = true;
+		// light.shadow.mapSize.width = 1024;
+		// light.shadow.mapSize.height = 1024;
+		//
+		// const d = 10;
+		//
+		// light.shadow.camera.left = - d;
+		// light.shadow.camera.right = d;
+		// light.shadow.camera.top = d;
+		// light.shadow.camera.bottom = - d;
+		// light.shadow.camera.far = 1000;
+		//
+		// scene.add( light );
+	}
+	// 刷新
+	function OutlinePass_selectedObjects_Refresh() {
+		if (outlinePass == undefined) return;
+		selectedObjects = [];
+		OutlinePass_selectedObjects_Map.forEach(function(value, key) {
+			// console.log(key, value);
+			selectedObjects.push( value );
+		})
+
+		if (INTERSECTED != null){
+			if(!OutlinePass_selectedObjects_Map.has(INTERSECTED.uuid)){
+				selectedObjects.push( INTERSECTED );
+			}
+		}
+
+		// console.log("selectedObjects:",selectedObjects)
+		outlinePass.selectedObjects = selectedObjects;
+
+		// render()
+	}
+	// 选中配置   选择颜色 ,遮挡部分颜色
+	this.Model_Selected_Config = function(visibleEdgeColor="#00ff18",hiddenEdgeColor="#ff0000") {
+		outlinePass.visibleEdgeColor.set(visibleEdgeColor);
+		outlinePass.hiddenEdgeColor.set(hiddenEdgeColor);
+	}
+	// 添加
+	this.Model_Selected_Add = function(Model) {
+		OutlinePass_selectedObjects_Map.set(Model.uuid, Model)
+		OutlinePass_selectedObjects_Refresh()
+	}
+	// 删除
+	this.Model_Selected_Del = function(Model) {
+		OutlinePass_selectedObjects_Map.delete(Model.uuid)
+		OutlinePass_selectedObjects_Refresh()
+	}
+	// 清空
+	this.Model_Selected_Clear = function() {
+		OutlinePass_selectedObjects_Map.clear()
+		OutlinePass_selectedObjects_Refresh()
+	}
+	//endregion
+
+
+	//  ------------------------------ 运动  ---------------------------------------------
+
+
+	// 聚焦物体 -
+	var startMove_is = false
+
+	// 聚焦物体 - V1
+	// this.startFocus = function (ob,MoveTime=1) {
+	//
+	// 	if(startMove_is) {
+	// 		console.log("任务还没结束,不能开始新任务!")
+	// 		return;
+	// 	}
+	// 	startMove_is = true // 开始
+	// 	// if(ob.type != "Group") {
+	// 	// 	console.log("Group != ")
+	// 	// 	startMove_is = false
+	// 	// 	return;
+	// 	// }
+	// 	if(ob.children.length == 0) {
+	// 		console.log("children.length == 0!")
+	// 		startMove_is = false
+	// 		return;
+	// 	}
+	// 	if(ob.children[0].type != "PerspectiveCamera") {
+	// 		console.log("children[0].type != PerspectiveCamera")
+	// 		startMove_is = false
+	// 		return;
+	// 	}
+	// 	let MoveList = []
+	// 	MoveList.push([camera.position.x,camera.position.y,camera.position.z])
+	// 	// MoveList.push([ob.position.x,ob.position.y,ob.position.z])
+	// 	// MoveList.push([ob.children[0].position.x + ob.position.x, ob.children[0].position.y + ob.position.y, ob.children[0].position.z + ob.position.z])
+	// 	MoveList.push([ob.children[0].matrixWorld.elements[12], ob.children[0].matrixWorld.elements[13], ob.children[0].matrixWorld.elements[14]])
+	// 	// MoveList.push([ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14]])
+	// 	console.log("MoveList:",MoveList)
+	//
+	// 	let MoveListCurve = []
+	// 	for(var item of MoveList) {
+	// 		MoveListCurve.push(new THREE.Vector3(item[0], item[1], item[2]))
+	// 	}
+	// 	let curve = new THREE.CatmullRomCurve3(MoveListCurve)
+	//
+	// 	var curveList = curve.getPoints(20 * MoveTime)
+	// 	// console.log("curveList:",curveList)
+	//
+	// 	var testIndex = 0
+	// 	var t = setInterval(function () {
+	// 		if(!startMove_is) {
+	// 			curveList = []
+	// 			testIndex = 0
+	//
+	// 			clearTimeout(t) //停止 t 定时器
+	// 			return
+	// 		}
+	// 		// 模仿管道的镜头推进
+	// 		if (curveList.length !== 0) {
+	// 			if (testIndex < curveList.length  ) {
+	//
+	// 				const point = curveList[testIndex] //获取样条曲线指定点坐标,作为相机的位置
+	// 				const pointBox = curveList[testIndex+2] //获取样条曲线指定点坐标
+	//
+	// 				camera.position.set(point.x, point.y , point.z)
+	// 				camera.lookAt(ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14])
+	//
+	// 				controls.target.set(ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	// 				// camera.lookAt(ob.position.x,ob.position.y,ob.position.z)
+	// 				testIndex += 1
+	// 			} else {
+	// 				curveList = []
+	// 				testIndex = 0
+	//
+	// 				clearTimeout(t) //停止 t 定时器
+	// 				startMove_is = false
+	//
+	//
+	// 				// 更新视角中心点
+	// 				controls.target.set(ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	//
+	// 			}
+	// 		}
+	//
+	// 		render()
+	// 	}, 50)
+	//
+	// 	return
+	// }
+	// 围绕旋转  ,围绕对象 , 旋转速度{1~9,2} , 俯视角度{0~100,20}
+	this.AroundRotation = function (ob,anglespeed_ = 2,targetHeight = 20) {
+		// 自动计算包围盒
+		const box = new THREE.Box3().setFromObject(ob);
+		const size = box.getSize(new THREE.Vector3());
+		const center = box.getCenter(new THREE.Vector3());
+
+		// const targetHeight = 20; // 俯视角度的高度
+		const radius = Math.max(size.x, size.z) *1.2; // 基于包围盒计算半径
+		let anglespeed = 0.001 * anglespeed_; // 旋转速度
+		let angle = Math.atan2(camera.position.z - center.z, camera.position.x - center.x); // 当前相机角度
+
+		// 设置相机位置的函数
+		function updateCameraPosition() {
+			camera.position.x = radius * Math.cos(angle);
+			camera.position.z = radius * Math.sin(angle);
+			camera.position.y = targetHeight; // 固定Y坐标高度
+			camera.lookAt(center); // 始终朝向场景中心
+		}
+
+		// 定时循环
+		AutomaticRotationPerspectiveInterval = setInterval(() => {
+			angle += anglespeed; // 旋转速度
+			updateCameraPosition();
+			renderer.render(scene, camera);
+		}, 1000 / 60); // 每秒60帧
+
+	}
+	// 聚焦物体 - V2
+	this.Focus = function (ob) {
+		// 计算物体的边界盒
+		const box = new THREE.Box3().setFromObject(ob);
+		const size = box.getSize(new THREE.Vector3());
+		const distance = size.length() * 2.0; // 增加一些距离,以便能看得到
+
+		// 按照目标物体的大小和 20 度俯视角度计算相机位置
+		const pitchAngle = THREE.MathUtils.degToRad(20); // 转换为弧度 20
+		// const yawAngle = THREE.MathUtils.degToRad(angle); // 旋转 90 度
+		const yawAngle = ob.rotation._y
+
+		const targetEnd = box.getCenter(new THREE.Vector3());
+		const targetPosition = new THREE.Vector3(
+			targetEnd.x + distance * Math.sin(yawAngle),
+			targetEnd.y + distance * Math.sin(pitchAngle),
+			targetEnd.z + distance * Math.cos(yawAngle)
+		);
+
+		let initialPosition = new THREE.Vector3();
+		initialPosition.copy(camera.position); // 记录初始位置
+		// 在动画开始前,确保相机在正确的初始位置
+		// 		camera.position.copy(targetPosition);
+		// 		controls.target.copy(target);
+		// 		controls.update(); // 更新控件
+
+
+		const targetStart = new THREE.Vector3(0, 0, 0); // 初始中心点
+		// const targetEnd = new THREE.Vector3(1, 1, 1); // 目标中心点
+		targetStart.x = controls.target.x
+		targetStart.y = controls.target.y
+		targetStart.z = controls.target.z
+
+		// console.log("targetStart:",targetStart)
+		// console.log("targetEnd:",targetEnd)
+		// console.log("initialPosition:",initialPosition)
+		// console.log("targetPosition:",targetPosition)
+		const distancex = initialPosition.distanceTo(targetPosition);
+		// console.log("运动距离:", distancex);
+
+		if(startMove_is) {
+			console.log("任务还没结束,不能开始新任务!")
+			return;
+		}
+		startMove_is = true // 开始
+
+		// 动画属性
+		const animationDuration = distancex * 2; // 动画持续 2 秒
+		let animationDurationTime = 0; // 动画开始时间
+
+		var It = setInterval(function () {
+
+			animationDurationTime += 1;
+			const t = Math.min(animationDurationTime / animationDuration, 1); // 归一化
+			// console.log("t:",animationDurationTime)
+			// 插值计算新的相机位置
+			camera.position.x = THREE.MathUtils.lerp(initialPosition.x, targetPosition.x, t);
+			camera.position.y = THREE.MathUtils.lerp(initialPosition.y, targetPosition.y, t);
+			camera.position.z = THREE.MathUtils.lerp(initialPosition.z, targetPosition.z, t);
+			// 插值控制目标位置
+			controls.target.x = THREE.MathUtils.lerp(targetStart.x, targetEnd.x, t);
+			controls.target.y = THREE.MathUtils.lerp(targetStart.y, targetEnd.y, t);
+			controls.target.z = THREE.MathUtils.lerp(targetStart.z, targetEnd.z, t);
+
+
+			// 更新控件目标
+			camera.lookAt(controls.target); // 始终看向目标
+			controls.update(); // 更新控件以应用新位置和目标
+
+			// render()
+
+			if(animationDuration <= animationDurationTime || !startMove_is){
+				startMove_is = false
+				clearTimeout(It) //停止 t 定时器
+			}
+		}, 10)
+
+	}
+
+	// 聚焦物体运动 -
+	// this.startFocusMotion = function (ob,MoveList,MoveTime=1) {
+	// 	if(startMove_is) {
+	// 		console.log("任务还没结束,不能开始新任务!")
+	// 		return;
+	// 	}
+	// 	startMove_is = true // 开始
+	//
+	// 	console.log("MoveList:",MoveList)
+	//
+	// 	let MoveListCurve = []
+	// 	for(var item of MoveList) {
+	// 		MoveListCurve.push(new THREE.Vector3(item[0], item[1], item[2]))
+	// 	}
+	// 	let curve = new THREE.CatmullRomCurve3(MoveListCurve)
+	//
+	// 	var curveList = curve.getPoints(20 * MoveTime)
+	// 	// console.log("curveList:",curveList)
+	//
+	// 	var testIndex = 0
+	// 	var t = setInterval(function () {
+	// 		if(!startMove_is) {
+	// 			curveList = []
+	// 			testIndex = 0
+	//
+	// 			clearTimeout(t) //停止 t 定时器
+	// 		}
+	// 		// 模仿管道的镜头推进
+	// 		if (curveList.length !== 0) {
+	// 			if (testIndex < curveList.length  ) {
+	//
+	// 				const point = curveList[testIndex] //获取样条曲线指定点坐标,作为相机的位置
+	//
+	// 				camera.position.set(point.x, point.y , point.z)
+	//
+	// 				camera.lookAt(ob[0], ob[1], ob[2])
+	// 				controls.target.set(ob[0], ob[1], ob[2]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	// 				// camera.lookAt(ob.position.x,ob.position.y,ob.position.z)
+	// 				testIndex += 1
+	// 			} else {
+	// 				curveList = []
+	// 				testIndex = 0
+	//
+	// 				clearTimeout(t) //停止 t 定时器
+	// 				startMove_is = false
+	//
+	//
+	// 				// 更新视角中心点
+	// 				controls.target.set(ob[0], ob[1], ob[2]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	//
+	// 			}
+	// 		}
+	//
+	// 		render()
+	// 	}, 50)
+	//
+	// 	return
+	// }
+
+	// 路径移动
+	this.startMove = function (MoveList,MoveTime=1,orbitControls_target=[]) {
+
+		if(startMove_is) {
+			console.log("任务还没结束,不能开始新任务!")
+			return;
+		}
+		startMove_is = true // 开始
+		if(MoveList.length == 0) {
+			console.log("数据异常!")
+			startMove_is = false
+			return;
+		}
+		if(MoveList[0].length != 3) {
+			console.log("数据异常!")
+			startMove_is = false
+			return;
+		}
+
+
+		let MoveListCurve = []
+		for(var item of MoveList) {
+			MoveListCurve.push(new THREE.Vector3(item[0], item[1], item[2]))
+		}
+		let curve = new THREE.CatmullRomCurve3(MoveListCurve)
+
+		var curveList = curve.getPoints(20 * MoveTime)
+		// console.log("curveList:",curveList)
+
+		var testIndex = 0
+		var t = setInterval(function () {
+			if(!startMove_is) {
+				curveList = []
+				testIndex = 0
+
+				clearTimeout(t) //停止 t 定时器
+			}
+			// 模仿管道的镜头推进
+			if (curveList.length !== 0) {
+				if (testIndex < curveList.length - 2) {
+
+					const point = curveList[testIndex] //获取样条曲线指定点坐标,作为相机的位置
+					const pointBox = curveList[testIndex+2] //获取样条曲线指定点坐标
+
+					camera.position.set(point.x, point.y , point.z)
+					camera.lookAt(pointBox.x, pointBox.y , pointBox.z)
+
+					testIndex += 1
+				} else {
+					curveList = []
+					testIndex = 0
+
+					clearTimeout(t) //停止 t 定时器
+					startMove_is = false
+
+					// 更新视角中心点
+					if(orbitControls_target.length == 3){
+						controls.target.set(orbitControls_target[0], orbitControls_target[1], orbitControls_target[2]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+						controls.update();
+					}
+				}
+			}
+
+			// render()
+		}, 50)
+
+		return
+	}
+	// 停止移动
+	this.stopMove = function () {
+		startMove_is = false
+	}
+
+	// 渲染
+	//region Description
+
+	// ---------- 渲染 -----------
+	// renderer.setAnimationLoop( render );
+
+	var clock = new THREE.Clock(); // only used for animations
+
+	// function render() {
+	// 	// console.log("render")
+	//
+	// 	// 渲染方式
+	// 	if(compose != undefined) {
+	// 		if(selectedObjects.length > 0){
+	// 			compose.render()
+	// 		}else {
+	// 			renderer.render(scene, camera);
+	// 		}
+	// 	}else {
+	// 		renderer.render(scene, camera);
+	// 	}
+	//
+	// 	if ( rendererCss2 !== null ) {
+	// 		rendererCss2.render( scene, camera );
+	// 	}
+	// 	if ( rendererCss3 !== null ) {
+	// 		rendererCss3.render( scene, camera );
+	// 	}
+	//
+	// }
+	// this.Render = function () {
+	// 	render()
+	// }
+
+	// 渲染
+	// let lastRender = performance.now();
+	function animate() {
+		// let timestamp = timestamp.now();
+		// if (timestamp - lastRender < 1000 / 60) return; // 限制为 60fps
+		// lastRender = timestamp;
+
+
+		if(window.ScenePlane !== undefined){
+			scene.add( window.ScenePlane );
+		}
+		// renderer.render(scene, camera);
+		if(compose != undefined) {
+			if(selectedObjects.length > 0){
+				compose.render()
+			}else {
+				renderer.render(scene, camera);
+			}
+		}else {
+			renderer.render(scene, camera);
+		}
+
+		if ( rendererCss2 !== null ) {
+			rendererCss2.render( scene, camera );
+		}
+		if ( rendererCss3 !== null ) {
+			rendererCss3.render( scene, camera );
+		}
+		// 更新控制器
+		// controls.update(); // 仅在需要时调用,例如当你使相机移动时
+
+		if(window.ScenePlane !== undefined){
+			scene.remove( window.ScenePlane );
+		}
+
+		// Animations  动画
+		if(window.mixer != null){
+			window.mixer.update( clock.getDelta() );
+		}
+		// requestAnimationFrame(animate);
+	}
+	//endregion
+
+//endregion
+
+
+// --------------------- 鼠标事件 -----------------------
+//region Description
+	var Model_onEvents = {
+		mousemove:undefined,
+		click:undefined,
+		dblclick:undefined,
+		mousedown:undefined,
+	}
+	// 递归寻找上级可触发
+	function finde_parent_choice(Ob) {
+		if (Ob.choice) {
+			return {Ob:Ob,is:true}
+		}
+		if(Ob.parent.isScene) return null,false
+		var Obf = finde_parent_choice(Ob.parent)
+		if(Obf.is){
+			return Obf
+		}
+		return {Ob:null,is:false}
+	}
+	// 移动
+	dom.addEventListener('mousemove', onMouseMove, false);
+	function onMouseMove(event) {
+
+		// console.log("onMouseMove:",event)
+		if (event.isPrimary === false) return;
+
+		// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
+		mouse.x = (event.clientX / dom_width) * 2 - 1;
+		mouse.y = -(event.clientY / dom_height) * 2 + 1;
+
+		raycaster.setFromCamera(mouse, camera);
+		// 计算物体和射线的焦点
+		var intersects = raycaster.intersectObjects(scene.children);
+
+		// console.log("intersects:",intersects)
+		if (intersects.length > 0){
+			//
+			for (let n = 0;intersects.length > n;n++) {
+				let objectP = intersects[n].object
+				// console.log("objectP:",objectP)
+				if(objectP === undefined) break;
+				let objectF = finde_parent_choice(objectP)
+				// console.log("finde_parent_choice:",objectF)
+				if (objectF.is) {
+					if(INTERSECTED !== null && INTERSECTED.uuid === objectF.Ob.uuid) break;
+					INTERSECTED = objectF.Ob;
+					// console.log("移入:",INTERSECTED)
+					if (Model_onEvents.mousemove !== undefined) Model_onEvents.mousemove(INTERSECTED)
+					if(INTERSECTED.scriptsf !== undefined ){
+						INTERSECTED.scriptsf.forEach(scriptf => {
+							if(scriptf["onMouseMoveIn"] !== undefined){
+								scriptf.onMouseMoveIn()
+							}
+						})
+
+					}
+					break;
+				}
+
+				// for (let xn = 0; 10 > xn; xn++) {
+				// 	console.log(xn,"-objectP:",objectP)
+				// 	if(objectP == null) break;
+				// 	// if (objectP.type == "Object3D") {
+				// 	if (INTERSECTED !== objectP) {
+				// 		console.log("intersects[0].object:", intersects[0].object.parent)
+				// 		if (objectP.choice === true) {
+				// 			INTERSECTED = objectP;
+				// 			if (Model_onEvents.mousemove !== undefined) Model_onEvents.mousemove(INTERSECTED)
+				// 			break;
+				// 		}
+				// 	}
+				// 	// }
+				// 	objectP = objectP.parent
+				// }
+
+			}
+			// console.log("onMouseMove:",INTERSECTED)
+		}else {
+			// console.log("移出:",INTERSECTED)
+			if(INTERSECTED !== null && INTERSECTED.scriptsf !== undefined ){
+				INTERSECTED.scriptsf.forEach(scriptf => {
+					if(scriptf["onMouseMoveOut"] !== undefined){
+						scriptf.onMouseMoveOut()
+					}
+				})
+
+			}
+
+			INTERSECTED = null;
+
+		}
+
+
+		OutlinePass_selectedObjects_Refresh()
+		// render()  // 可优化空间
+	}
+	this.Model_onMouseMove = function (fun) {
+		Model_onEvents.mousemove = fun
+	}
+
+	//单击延时触发
+	var  clickTimeId,clickTimeIs = false;
+	// 单击
+	dom.addEventListener('click', onClick, false);
+	function onClick(event) {
+		// console.log("onClick:",INTERSECTED)
+		if(INTERSECTED === null){return}
+		// 取消上次延时未执行的方法
+		clearTimeout(clickTimeId);
+		const INTERSECTED_ = INTERSECTED
+		//执行延时
+		clickTimeId = setTimeout( function () {
+			if (INTERSECTED_) {
+				if(Model_onEvents.click != undefined) Model_onEvents.click(INTERSECTED)
+			}
+			// console.log("onClick:",INTERSECTED)
+
+			if(INTERSECTED_.scriptsf !== undefined ){
+				INTERSECTED_.scriptsf.forEach(scriptf => {
+					if(scriptf["onClick"] !== undefined){
+						scriptf.onClick()
+					}
+				})
+
+			}
+
+
+			// render()
+		}, 250);
+
+	}
+	this.Model_onClick = function (fun) {
+		Model_onEvents.click = fun
+	}
+
+	// 双击
+	dom.addEventListener('dblclick', onDblclick, false);
+	function onDblclick(event) {
+		if(INTERSECTED === null){return}
+		clearTimeout(clickTimeId); // 取消上次延时未执行的方法
+		if (INTERSECTED) {
+			if(Model_onEvents.dblclick != undefined) Model_onEvents.dblclick(INTERSECTED)
+		}
+
+		if(INTERSECTED.scriptsf !== undefined ){
+			INTERSECTED.scriptsf.forEach(scriptf => {
+				if(scriptf["onDblclick"] !== undefined){
+					scriptf.onDblclick()
+				}
+			})
+
+		}
+
+
+		// render()
+	}
+	this.Model_onDblclick = function (fun) {
+		Model_onEvents.dblclick = fun
+	}
+
+	// 右击---
+	dom.addEventListener('contextmenu', onMousedown, false);
+	function onMousedown(event) {
+		if(clickTimeIs) return;
+		clickTimeIs = true;
+		setTimeout( function () {
+			clickTimeIs = false;
+		}, 2000);  // 防止 反复触发退出
+		if(Model_onEvents.mousedown != undefined) Model_onEvents.mousedown(INTERSECTED,event)
+		onBackclick()
+		// render()
+	}
+	this.Model_onMousedown = function (fun) {
+		Model_onEvents.mousedown = fun
+	}
+
+	//
+	// this.ondBlclick_Model = function (position) {
+	// 	console.log("更新视角中心点:", position)
+	// 	controls.target.set(position.x, position.y, position.z);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 	controls.update();
+	// }
+
+//endregion
+
+
+// --------------------- 项目 -----------------------
+//region Description
+	// 本地调试模式
+	this.LocalRun = function ( parkId = 0 ) {
+		window.parkId = parkId
+		new _Storage(function(result) {
+			console.log(result);
+			// scene = result.scene
+			loadJson(result,"000000"+parkId)
+		});
+	}
+
+	var loadProject_Map = []; //ProjectID 映射
+	var Now_ProjectID = ""; // 当前 ProjectID
+	this.load = function (){
+		console.log("加载完毕")
+	}
+
+	// 加载项目
+	this.LoadProject = function (ProjectID) {
+		if(Now_ProjectID == ProjectID){ return }
+
+
+		console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "加载 ProjectID:", ProjectID)
+
+		// 是否加载与缓存过
+		if (loadProject_Map[ProjectID] == undefined) {
+			var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
+			httpRequest.open('GET', './GetProject?T_ViewID=' + ProjectID, true);//第二步:打开连接  将请求参数写在url中  ps:"./Ptest.php?name=test&nameone=testone"
+			httpRequest.send();//第三步:发送请求  将请求参数写在URL中
+			httpRequest.onreadystatechange = function () {
+				if (httpRequest.readyState == 4 && httpRequest.status == 200) {
+					var json = JSON.parse(httpRequest.responseText);//获取到json字符串,还需解析
+					console.log(json);
+					if (json.Code != 200) {
+						console.log("ProjectID 错误!", ProjectID)
+						return "ProjectID 错误!"
+					}
+					var json = JSON.parse(json.Data.T_url);//获取到json字符串,还需解析
+					console.log(json);
+					window.parkList = json
+					window.parkId = 0
+					// 如果需要兼容低版本的浏览器,需要判断一下FileReader对象是否存在。
+					if (window.FileReader) {
+						blobLoad(ProjectID, window.parkList[window.parkId].T_url)
+					} else {
+						console.log('你的浏览器不支持读取文件');
+						loadProject_Map[ProjectID] = src
+					}
+				}
+			};
+		} else {
+
+			console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "缓存加载 ProjectID:", ProjectID)
+			f_load(ProjectID)
+
+		}
+	}
+	this.LoadLocal = function () {
+
+
+		var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
+		httpRequest.open('GET', '../static/LocalProject/main.json', true);//第二步:打开连接  将请求参数写在url中  ps:"./Ptest.php?name=test&nameone=testone"
+		httpRequest.send();//第三步:发送请求  将请求参数写在URL中
+		httpRequest.onreadystatechange = function () {
+			if (httpRequest.readyState == 4 && httpRequest.status == 200) {
+				var json = JSON.parse(httpRequest.responseText);//获取到json字符串,还需解析
+				console.log(json);
+				window.parkList = json
+				window.parkId = 0
+				// 如果需要兼容低版本的浏览器,需要判断一下FileReader对象是否存在。
+				if (window.FileReader) {
+					blobLoad("00000", window.parkList[window.parkId].T_url)
+				} else {
+					console.log('你的浏览器不支持读取文件');
+					loadProject_Map[ProjectID] = src
+				}
+			}
+		};
+
+	}
+	// 解压并更新舞台
+	function f_load(ProjectID) {
+		console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "开始加载 file:", loadProject_Map[ProjectID])
+
+		// 解压模型
+		// var promise = fetch('../static/16635717199e4e03e8-8850-4152-9c08-9c203c882f7a.zip')
+		var promise = fetch(loadProject_Map[ProjectID])
+			.then((d) => d.arrayBuffer())
+		promise = promise.then(function (data) {
+			//响应的内容
+			console.log("data:", data);
+			const decompressed = unzipSync(new Uint8Array(data), {
+				// You may optionally supply a filter for files. By default, all files in a
+				// ZIP archive are extracted, but a filter can save resources by telling
+				// the library not to decompress certain files
+				filter(file) {
+					// Don't decompress the massive image or any files larger than 10 MiB
+					return file;
+				}
+			});
+			console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "file:", decompressed['app.json'])
+			var obj = JSON.parse(new TextDecoder().decode(decompressed['app.json']));
+			loadJson(obj,ProjectID);// 加载场景
+			// self.setSize(dom_width, dom_height);
+			// orbitControls() // 鼠标移动 -轨道控制
+			Now_ProjectID = ProjectID
+			console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "JSON obj:", obj)
+
+		}).catch(function (err) {
+			console.log(err);
+		})
+
+	}
+
+	// 导入 json
+	function loadJson(json,ProjectID) {
+
+		let loader = new THREE.ObjectLoader(); // 加载
+
+		console.log("loadJson:", json)
+		project = json.project;
+
+		// if (project.vr !== undefined) renderer.xr.enabled = project.vr;
+		// if (project.shadows !== undefined) renderer.shadowMap.enabled = project.shadows;
+		// if (project.shadowType !== undefined) renderer.shadowMap.type = project.shadowType;
+		// if (project.toneMapping !== undefined) renderer.toneMapping = project.toneMapping;
+		// if (project.toneMappingExposure !== undefined) renderer.toneMappingExposure = project.toneMappingExposure;
+		// if (project.physicallyCorrectLights !== undefined) renderer.physicallyCorrectLights = project.physicallyCorrectLights;
+		rendererCss2.clean()
+		rendererCss3.clean()
+		scene = loader.parse(json.scene);
+
+		// camera = loader.parse(json.camera);
+		// console.log("json.camera:",json.camera)
+		// console.log("camera:",camera)
+		camera.position.x = json.camera.object.matrix[12]
+		camera.position.y = json.camera.object.matrix[13]
+		camera.position.z = json.camera.object.matrix[14]
+		// camera.aspect = dom_width / dom_height;
+		// camera.updateProjectionMatrix();
+		// renderer.setSize(dom_width, dom_height);
+		// 中心点
+		controls.target.set(0, 0, 0);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+		controls.update();
+
+
+
+		//////--------------
+		// ScenePlane   场景平面
+		if ( project.sceneplane !== undefined ){
+			// console.log("sceneplane:",project.sceneplane)
+
+			switch ( project.sceneplane ) {
+				case 'None':
+					window.ScenePlane = undefined
+					break;
+
+				case 'Grass':  //草地平面
+					const gt = new THREE.TextureLoader().load( './static/js/img/cd.jpg' );
+					const gg = new THREE.PlaneGeometry( 300, 300 );
+					const gm = new THREE.MeshPhongMaterial( { color: 0xffffff, map: gt } );
+
+					window.ScenePlane = new THREE.Mesh( gg, gm );
+					window.ScenePlane.rotation.x = - Math.PI / 2;
+					// window.ScenePlane.rotation.y = -0.1
+					window.ScenePlane.material.map.repeat.set( 64, 64 );
+					window.ScenePlane.material.map.wrapS = THREE.RepeatWrapping;
+					window.ScenePlane.material.map.wrapT = THREE.RepeatWrapping;
+					window.ScenePlane.material.map.colorSpace = THREE.SRGBColorSpace;
+					// note that because the ground does not cast a shadow, .castShadow is left false
+					window.ScenePlane.receiveShadow = true;
+
+					break;
+
+			}
+
+		}
+
+
+
+		//// ------------
+
+		events = {
+			init: [],
+			keydown: [],
+			keyup: [],
+			onMouseMoveIn: [],  //鼠标移入
+			onMouseMoveOut: [],  //鼠标移入
+			onClick: [],  //鼠标单击
+			onDblclick: [],  //鼠标双击
+			onBackclick: [],  //鼠标右击  退后
+			renderer: [],
+			update: []
+		};
+		window.Getevents = function () {
+			return events
+		}
+
+
+		var scriptWrapParams = 'iot3d,renderer,scene,camera';
+		var scriptWrapResultObj = {};
+
+		for (var eventKey in events) {
+
+			scriptWrapParams += ',' + eventKey;
+			scriptWrapResultObj[eventKey] = eventKey;
+
+		}
+
+		var scriptWrapResult = JSON.stringify(scriptWrapResultObj).replace(/\"/g, '');
+
+
+		//执行代码
+
+		// 递归遍历 所有脚本 并添加
+
+		function traverseChildrenScript(object) {
+			if ( object.scripts !== undefined && object.scripts.length > 0 ) {
+
+				console.log("addObjectScript:",object.uuid,object.scripts)
+				object.scriptsf = []
+				var scripts = object.scripts;
+				for ( var i = 0; i < scripts.length; i ++ ) {
+					//  每个 script 代码
+					var script = scripts[i];
+
+					var functions = ( new Function( scriptWrapParams, script.source + '\nreturn ' + scriptWrapResult + ';' ).bind( object ) )( this, renderer, scene, camera );
+					// console.log("functions.name",functions)
+					object.scriptsf.push(functions)
+					for ( var name in functions ) {
+						// console.log("functions.name",name)
+						if ( functions[ name ] === undefined ) continue;
+
+						if ( events[ name ] === undefined ) {
+							console.warn( 'APP.Player: Event type not supported (', name, ')' );
+							continue;
+						}
+						if(name !== "update"){
+							events[ name ].push( functions[ name ].bind( object ) );
+						}else {
+							var subSN = SeekParameterNodes(object)
+							// console.log("subSN:",subSN)
+							// 订阅
+							pubSub.subscribe(subSN, data => {
+								// console.log("subSN:",subSN,data);
+								functions["update"].bind( object )( data )
+							})
+						}
+
+
+					}
+				}
+
+
+			}
+
+			// 遍历当前对象的所有子对象
+			object.children.forEach(child => {
+				traverseChildrenScript(child); // 递归调用
+			});
+		}
+		traverseChildrenScript(scene)
+
+
+		// console.log("events:", events)
+		dispatch(events.init, arguments);
+
+		console.log("加载完成 ProjectID:",ProjectID)
+		// 场景加载后 视角归为
+		self.setSize(window.innerWidth, window.innerHeight);
+		orbitControls_maxDistance()  // 计算场景最远距离,并控制参数
+		OutlinePass_inte()  // 鼠标选择初始化
+
+	};
+
+	// 文件缓存本地
+	function blobLoad(ProjectID, src) {
+		self.loading_open() //
+		console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "文件缓存本地 :", ProjectID)
+		// let self = this;
+		const req = new XMLHttpRequest();
+		req.open("GET", src, true);
+		req.responseType = "blob";
+		req.onload = function () {
+			// Onload is triggered even on 404
+			// so we need to check the status code
+			if (this.status === 200) {
+				const videoBlob = this.response;
+
+				console.log("videoBlob:", videoBlob)
+				const blobSrc = URL.createObjectURL(videoBlob); // IE10+
+				console.log("blobSrc:", blobSrc)
+				loadProject_Map[ProjectID] = blobSrc
+				console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "文件缓存本地完成 ProjectID:", ProjectID)
+				f_load(ProjectID)
+			}
+			self.loading_close()
+		};
+		//监听进度事件
+		req.addEventListener("progress", function (evt) {
+			if (evt.lengthComputable) {
+				var percentComplete = evt.loaded / evt.total;
+				//进度
+				console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "ProjectID:", ProjectID, " 进度:", (percentComplete * 100) + "%")
+				self.loading_text("模型加载中... "+parseInt(percentComplete * 100) + "%") // 替换内容
+			}
+		}, false);
+		req.onerror = function () {
+			// Error
+			console.log("blobLoad Error!", src)
+			loadProject_Map[ProjectID] = src
+		};
+		req.send();
+	}
+
+
+//endregion
+
+
+// --------------------- 脚本 -----------------------
+//region Description
+	// var time, startTime;
+	// startTime = performance.now();
+
+	// // 创建一个每秒执行一次的定时循环
+	// setInterval(function() {
+	// 	// 在这里编写需要重复执行的代码
+	// 	time = performance.now();
+	// 	const rendertime = { time: time - startTime }
+	// 	// console.log("renderer:",rendertime)
+	// 	try {
+	// 		dispatch( events.renderer, rendertime);
+	// 	} catch ( e ) {
+	// 		console.error( ( e.message || e ), ( e.stack || '' ) );
+	// 	}
+	//
+	// }, 1000); // 1000 毫秒 = 1 秒
+	function dispatch( array, event ) {
+
+		for ( var i = 0, l = array.length; i < l; i ++ ) {
+
+			array[ i ]( event );
+
+		}
+
+	}
+
+
+	//
+	// function animate() {
+	//
+	// 	time = performance.now();
+	//
+	//
+	//
+	// 	prevTime = time;
+	//
+	// }
+
+	// this.play = function () {
+	//
+	// 	if ( renderer.xr.enabled ) dom.append( vrButton );
+	//
+	// 	startTime = prevTime = performance.now();
+	//
+	// 	document.addEventListener( 'keydown', oneyDown );
+	// 	document.addEventListener( 'keyup', onKeyUp );
+
+	// 	document.addEventListener( 'pointerup', onPointerUp );
+	// 	document.addEventListener( 'pointermove', onPointerMove );
+	//
+	// 	// dispatch( events.start, arguments );
+	//
+	// 	renderer.setAnimationLoop( animate );
+	//
+	// };
+
+
+	this.render = function ( time ) {
+
+		dispatch( events.update, { time: time * 1000, delta: 0 /* TODO */ } );
+
+		renderer.render( scene, camera );
+
+	};
+
+	this.dispose = function () {
+
+		renderer.dispose();
+
+		camera = undefined;
+		scene = undefined;
+
+	};
+
+	//
+
+	function onKeyDown( event ) {
+
+		dispatch( events.keydown, event );
+
+	}
+
+	function onKeyUp( event ) {
+
+		dispatch( events.keyup, event );
+
+	}
+
+	// function onPointerDown( event ) {
+	// 	dispatch( events.pointerdown, event );
+	//
+	// }
+	//
+	// function onPointerUp( event ) {
+	// 	dispatch( events.pointerup, event );
+	//
+	// }
+	//
+	function onBackclick( event ) {
+		dispatch( events.onBackclick, event );
+	}
+
+	function upDate( event ) {
+
+		try {
+			dispatch( events.update, event );
+		} catch ( e ) {
+			console.error( ( e.message || e ), ( e.stack || '' ) );
+		}
+
+	}
+
+//endregion
+// --------------------- 功能 -----------------------
+//region Description
+// 	this.BackgroundScene = function () {
+// 		var urls = [
+// 			'./static/js/img/posx.jpg',
+// 			'./static/js/img/negx.jpg',
+// 			'./static/js/img/posy.jpg',
+// 			'./static/js/img/negy.jpg',
+// 			'./static/js/img/posz.jpg',
+// 			'./static/js/img/negz.jpg'
+// 		];
+//
+// 		var cubeLoader = new THREE.CubeTextureLoader();
+// 		scene.background = cubeLoader.load(urls);
+// 		setTimeout(function(){
+// 			render()
+// 		},3000);
+//
+// 	}
+	// 屏幕坐标与世界坐标
+	this.scene_3Dto2D = function (position) {
+		const worldVector = new THREE.Vector3(
+			position.x,
+			position.y,
+			position.z
+		);
+		const standardVec = worldVector.project(camera);
+		const centerX = dom_width / 2;
+		const centerY = dom_height / 2;
+		const screenX = Math.round(centerX * standardVec.x + centerX);
+		const screenY = Math.round(-centerY * standardVec.y + centerY);
+		console.log("screen:", screenX, screenY)
+		return screenX, screenY
+	}
+
+
+//endregion
+
+
+	// 开始 加载场景
+	if (runmode === 0){
+		this.LocalRun()// 本地调试模式
+	}else if(runmode === 1){
+		this.LoadProject(window.T_ViewID)  // 在线
+	}else if(runmode === 2){
+		this.LoadLocal()  // 离线
+	}
+}
+export {IOT3D};

+ 1526 - 0
public/archive/static/js/IOT3D.js

@@ -0,0 +1,1526 @@
+
+import * as THREE from '../build/three.module.js';
+import {unzipSync} from './libs/fflate.module.js';
+import {FontLoader} from './jsm/loaders/FontLoader.js';
+import {CinematicCamera} from './jsm/cameras/CinematicCamera.js';
+import { OrbitControls } from './jsm/controls/OrbitControls.js';
+import { EffectComposer } from './jsm/postprocessing/EffectComposer.js';
+import { RenderPass } from './jsm/postprocessing/RenderPass.js';
+import { ShaderPass } from './jsm/postprocessing/ShaderPass.js';
+import { OutlinePass } from './jsm/postprocessing/OutlinePass.js';
+import { CSS3DRenderer  } from './jsm/renderers/CSS3DRenderer.js';
+import { CSS2DRenderer } from './jsm/renderers/CSS2DRenderer.js';
+import { Storage as _Storage } from './libs/Storage.js';
+
+// --------------------- 折叠 -----------------------
+//region Description
+
+//endregion
+
+// iot3d = new IOT3D("iot3d",false); // 创建应用  (div_id,运行模式)
+function IOT3D(iot3d,runmode = 1) {
+	window.iot3d = this
+	window.runmode = runmode
+	window.THREE = THREE; // Used by APP Scripts.
+	window.FontLoader = FontLoader; // Used by APP Scripts.
+	// window.VRButton = VRButton; // Used by APP Scripts.
+	window.OrbitControls = OrbitControls; // Used by APP Scripts.
+	window.CinematicCamera = CinematicCamera; // Used by APP Scripts.
+	window.EffectComposer = EffectComposer; // Used by APP Scripts.
+	window.RenderPass = RenderPass; // Used by APP Scripts.
+	window.ShaderPass = ShaderPass; // Used by APP Scripts.
+	window.OutlinePass = OutlinePass; // Used by APP Scripts.
+
+
+	this.loading_open = function (time){
+		//1000为遮罩层显示时长,若不传则一直显示,须调用关闭方法
+		$.mask_fullscreen(time);
+	}
+	this.loading_close = function (){
+		//关闭遮罩层
+		$.mask_close_all();
+	}
+	this.loading_text = function (text){
+		//关闭遮罩层
+		$.mask_text(text);
+	}
+	this.loading_html = function (html){
+		//关闭遮罩层
+		$.mask_html(html);
+	}
+	//
+	var self;
+
+	THREE.Cache.enabled = true; //这是一个全局属性,只需要设置一次,供内部使用FileLoader的所有加载器使用。
+	// 项目
+	// var IOT3D_Url = "https://iot3d.baozhida.cn"
+	// var IOT3D_Url = "https://iot3d.baozhida.cn"
+	// if(url.indexOf("127.0.0.1") != -1){
+	// 	IOT3D_Url = "http://127.0.0.1:6210"
+	// }else {
+	// 	IOT3D_Url = "https://iot3d.baozhida.cn"
+	// }
+	var clock = new THREE.Clock();
+	// 公共
+	var dom_width = 500, dom_height = 500;
+	var camera, scene, dom, renderer, rendererCss2, rendererCss3,controls;
+	var events = {};
+	window.parkId = 0;   // 园区ID
+	var project;  // 项目配置
+	/// =-  选取
+	var raycaster, mouse, INTERSECTED = null;
+
+	// 变量初始化
+	self = this;
+	dom_width = window.innerWidth;
+	dom_height = window.innerHeight;
+	self.width = dom_width;
+	self.height = dom_width;
+	// var vrButton = VRButton.createButton( renderer ); // eslint-disable-line no-undef
+
+
+	// --------------------- 初始化 -----------------------
+	//region Description
+
+	// 舞台
+	scene = new THREE.Scene();
+	// 相机
+	camera = new THREE.PerspectiveCamera(45, dom_width / dom_height, 0.1, 3000);
+	camera.position.set(-10, 10, 30);
+	camera.lookAt(scene.position);
+
+	window.mixer = new THREE.AnimationMixer( scene ); // 动画
+
+
+	// HTML dom
+	dom = document.getElementById(iot3d);
+
+	//渲染器
+	renderer = new THREE.WebGLRenderer({antialias: true});
+	renderer.setPixelRatio(window.devicePixelRatio); // TODO: Use setPixelRatio()
+	// renderer.outputEncoding = THREE.sRGBEncoding;
+	renderer.shadowMap.enabled = true;// 阴影
+	renderer.setSize( dom_width, dom_height );
+	renderer.setAnimationLoop( animate );
+	dom.appendChild(renderer.domElement);
+
+
+	// rendererCss3
+	rendererCss3 = new CSS3DRenderer();
+	rendererCss3.setSize( dom.innerWidth, dom.innerHeight );
+	rendererCss3.domElement.style.position = 'absolute';
+	rendererCss3.domElement.style.top = 0;
+	dom.appendChild( rendererCss3.domElement );
+
+	// rendererCss2
+	rendererCss2 = new CSS2DRenderer();
+	rendererCss2.setSize( dom.innerWidth, dom.innerHeight );
+	rendererCss2.domElement.style.position = 'absolute';
+	rendererCss2.domElement.style.top = '0px';
+	dom.appendChild( rendererCss2.domElement );
+
+	// 加载到 网页
+	document.body.appendChild(dom);
+	window.addEventListener('resize', function () {
+		self.setSize(window.innerWidth, window.innerHeight);
+	});
+
+	/// =-  选取
+	raycaster = new THREE.Raycaster();
+	mouse = new THREE.Vector2();
+
+
+	var selectedObjects = [],compose,renderPass,outlinePass;
+
+	// 鼠标移动 -轨道控制
+	// var AutomaticRotationPerspective = [1,2,20]  //自动旋转视角  0 【关闭】  1【360度旋转[1,旋转速度{1~9,2},俯视角度{0~100,20}]】
+	var AutomaticRotationPerspectiveInterval = undefined  // 定时任务
+	var AutomaticRotationPerspectiveTally = 0 // 计数
+	// - 鼠标移动视角
+	controls = new OrbitControls(camera, renderer.domElement);
+	// controls.addEventListener('change', animate); // use if there is no animation loop
+	controls.dampingFactor = 0.25;
+	controls.minDistance = 5;  // 最小距离
+	controls.maxDistance = 1000;
+	controls.screenSpacePanning = false; // 允许相机平移
+	// 设置最大和最小角度
+	controls.maxPolarAngle = Math.PI / 2; // 最大角度 (90度) - 可视化平面
+	controls.minPolarAngle = 0;             // 最小角度 (0度) - 直接向下
+	controls.target.set(0, 0, -2.2);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// controls.position0.set(200, 200, 500 )
+	controls.update();
+	// 检查 鼠标是否有操作
+	renderer.domElement.addEventListener( 'pointerdown', function () {
+		if(AutomaticRotationPerspectiveInterval !== undefined){
+			clearInterval(AutomaticRotationPerspectiveInterval); // 停止定时任务的执行
+		}
+		AutomaticRotationPerspectiveTally = 0
+		AutomaticRotationPerspectiveInterval = undefined
+	} );
+	// 定时 开始触发 自动旋转视角
+	setInterval(() => {
+		if(project.autoangle === undefined || project.autoangle === "None") return; // 直接跳过
+
+		if(AutomaticRotationPerspectiveInterval === undefined){
+			AutomaticRotationPerspectiveTally += 1
+			if(AutomaticRotationPerspectiveTally === 3){
+				console.log("project.autoangle:",project.autoangle)
+				switch (project.autoangle) {
+					case "Angle360":  // 360度旋转
+						self.AroundRotation(scene,project.autoangle_speed,project.autoangle_angle)
+						break;
+					case "Regainstate":  // 回到原始视角
+						self.Focus(self.GetScene())
+						break;
+				}
+
+			}
+		}
+	}, 1000); // 每秒
+
+	// 数据订阅
+	window.pubSub = {
+		list: {},
+		// 订阅
+		subscribe: function(key, fn) {
+			if (!this.list[key]) this.list[key] = [];
+			this.list[key].push(fn);
+		},
+		//取消订阅
+		unsubscribe: function(key, fn) {
+			let fnList = this.list[key];
+			if (!fnList) return false;
+			if (!fn) { // 不传入指定的方法,清空所用 key 下的订阅
+				fnList && (fnList.length = 0);
+			} else {
+				fnList.forEach((item, index) => {
+					item === fn && fnList.splice(index, 1);
+				});
+			}
+		},
+		// 发布
+		publish: function(key, ...args) {
+			if(this.list[key] === undefined) return;
+			for (let fn of this.list[key]) fn.call(this, ...args);
+		}
+	}
+
+	// this.storage.get(fxx)
+	//
+	// function fxx(xxx) {
+	// 	console.log("fxx-------------------------",xxx)
+	// }
+
+	// 测试
+	this.Test = function (rotationSpeed = 0.001) {
+		console.log("Test-------------------------")
+
+
+
+		return
+	}
+
+
+// 测试
+
+	function cubeDr(a, x, y, z) {
+		var cubeGeo = new THREE.BoxGeometry(a, a, a);
+		var cubeMat = new THREE.MeshPhongMaterial({
+			color: 0xfff000 * Math.random()
+		});
+		var cube = new THREE.Mesh(cubeGeo, cubeMat);
+		cube.position.set(x, y, z);
+		cube.castShadow = true;
+		scene.add(cube);
+		return cube;
+	}
+
+
+
+
+//endregion
+
+// --------------------- 核心 -----------------------
+//region Description
+
+
+
+	// 核心方法
+	//region Description
+	// 舞台
+	this.GetScene = function () {
+		return scene
+	}
+	// 相机
+	this.GetCamera = function () {
+		return camera
+	}
+	//渲染器
+	this.GetRenderer = function () {
+		return renderer
+	}
+	// 获取 UUID 模型
+	this.GetModelByUuid = function (uuid) {
+		return scene.getObjectByProperty('uuid', uuid, true);
+	}
+	// 获取 UUID 模型 内部函数
+	this.Model = function (uuid,fun) {
+		fun(scene.getObjectByProperty('uuid', uuid, true));
+	}
+	// 设置显示比例
+	this.setPixelRatio = function (pixelRatio) {
+		renderer.setPixelRatio(pixelRatio);
+	};
+	// 设置大小
+	this.setSize = function (width, height) {
+		dom_width = width;
+		dom_height = height;
+
+		if (camera) {
+			camera.aspect = dom_width / dom_height;
+			camera.updateProjectionMatrix();
+		}
+		renderer.setSize(width, height);
+
+		if ( rendererCss3 !== null ) {
+			rendererCss3.setSize( dom.offsetWidth, dom.offsetHeight );
+		}
+
+		if ( rendererCss2 !== null ) {
+			rendererCss2.setSize( dom.offsetWidth, dom.offsetHeight );
+		}
+		self.width = dom_width;
+		self.height = dom_width;
+	};
+	//endregion
+
+	// 鼠标
+	//region Description
+
+	//更新视角中心点
+	this.orbitControls_target = function (position) {
+		console.log("更新视角中心点:", position)
+		controls.target.set(position.x, position.y, position.z);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+		controls.update();
+	}
+	// 计算场景最远距离,并控制参数
+	function orbitControls_maxDistance() {
+		// 计算场景中的包围盒
+		function calculateSceneBoundingBox(scene) {
+			const box = new THREE.Box3();
+
+			scene.traverse((object) => {
+				if (object.isMesh) {
+					// 更新包围盒以包含当前对象的包围盒
+					const objectBox = new THREE.Box3().setFromObject(object);
+					box.union(objectBox);
+				}
+			});
+
+			return box;
+		}
+		// 计算场景的包围盒
+		const boundingBox = calculateSceneBoundingBox(scene);
+		// 获取包围盒的尺寸
+		const size = new THREE.Vector3();
+		boundingBox.getSize(size);
+		// 获取包围盒的中心
+		const center = new THREE.Vector3();
+		boundingBox.getCenter(center);
+		// 计算最大尺寸距离 (从中心到某个角的距离)
+		const maxDistance = center.distanceTo(boundingBox.max);
+		// console.log("从中心到最远角的距离:", maxDistance);
+		controls.maxDistance = maxDistance * 10;
+	}
+
+
+	// 鼠标选择初始化
+	var OutlinePass_selectedObjects_Map = new Map();
+	function OutlinePass_inte(){
+		// console.log("OutlinePass_inte")
+		// 清空所有选项
+		selectedObjects = []
+		self.Model_Selected_Clear()
+
+		camera.lookAt(scene.position);
+
+		compose = new EffectComposer(renderer);
+		renderPass = new RenderPass(scene, camera);
+
+		outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth,window.innerHeight),scene,camera);
+		outlinePass.renderToScreen = true;
+		outlinePass.selectedObjects = selectedObjects;
+
+		compose.addPass(renderPass);
+		compose.addPass(outlinePass);
+
+		// https://threejs.org/examples/?q=webgl_postprocessing_outline#webgl_postprocessing_outline
+		outlinePass.renderToScreen = true;
+		outlinePass.edgeStrength = 3 //粗   0.01, 10
+		outlinePass.edgeGlow = 1 //发光  0.0, 1
+		outlinePass.edgeThickness = 2 //光晕粗   1, 4
+		outlinePass.pulsePeriod = 0 //闪烁  0.0, 5
+		outlinePass.usePatternTexture = false //是否使用贴图
+		let visibleEdgeColor = '#00a1fd';  // 选择颜色
+		let hiddenEdgeColor = '#00a1fd';  //遮挡部分颜色
+		outlinePass.visibleEdgeColor.set(visibleEdgeColor);
+		outlinePass.hiddenEdgeColor.set(hiddenEdgeColor);
+
+		// let light = new THREE.AmbientLight(0x333333);
+		// scene.add(light);
+		//
+		// 这里没有 渲染会报错
+		let light = new THREE.SpotLight(0xFFFFFF);
+		light.position.set(0, 40, 30);
+		light.castShadow = true;
+		light.shadow.mapSize.height = 1;
+		light.shadow.mapSize.width = 1;
+		light.angle = 0;
+		scene.add(light);
+
+		// light = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
+		// light.position.set(0, 200, 0);
+		// scene.add(light);
+
+
+
+		// const light = new THREE.DirectionalLight( 0xffffff, 0.6 );
+		// light.position.set( 1, 1, 1 );
+		// light.castShadow = true;
+		// light.shadow.mapSize.width = 1024;
+		// light.shadow.mapSize.height = 1024;
+		//
+		// const d = 10;
+		//
+		// light.shadow.camera.left = - d;
+		// light.shadow.camera.right = d;
+		// light.shadow.camera.top = d;
+		// light.shadow.camera.bottom = - d;
+		// light.shadow.camera.far = 1000;
+		//
+		// scene.add( light );
+	}
+	// 刷新
+	function OutlinePass_selectedObjects_Refresh() {
+		if (outlinePass == undefined) return;
+		selectedObjects = [];
+		OutlinePass_selectedObjects_Map.forEach(function(value, key) {
+			// console.log(key, value);
+			selectedObjects.push( value );
+		})
+
+		if (INTERSECTED != null){
+			if(!OutlinePass_selectedObjects_Map.has(INTERSECTED.uuid)){
+				selectedObjects.push( INTERSECTED );
+			}
+		}
+
+		// console.log("selectedObjects:",selectedObjects)
+		outlinePass.selectedObjects = selectedObjects;
+
+		// render()
+	}
+	// 选中配置   选择颜色 ,遮挡部分颜色
+	this.Model_Selected_Config = function(visibleEdgeColor="#00ff18",hiddenEdgeColor="#ff0000") {
+		outlinePass.visibleEdgeColor.set(visibleEdgeColor);
+		outlinePass.hiddenEdgeColor.set(hiddenEdgeColor);
+	}
+	// 添加
+	this.Model_Selected_Add = function(Model) {
+		OutlinePass_selectedObjects_Map.set(Model.uuid, Model)
+		OutlinePass_selectedObjects_Refresh()
+	}
+	// 删除
+	this.Model_Selected_Del = function(Model) {
+		OutlinePass_selectedObjects_Map.delete(Model.uuid)
+		OutlinePass_selectedObjects_Refresh()
+	}
+	// 清空
+	this.Model_Selected_Clear = function() {
+		OutlinePass_selectedObjects_Map.clear()
+		OutlinePass_selectedObjects_Refresh()
+	}
+	//endregion
+
+
+	//  ------------------------------ 运动  ---------------------------------------------
+
+
+	// 聚焦物体 -
+	var startMove_is = false
+
+	// 聚焦物体 - V1
+	// this.startFocus = function (ob,MoveTime=1) {
+	//
+	// 	if(startMove_is) {
+	// 		console.log("任务还没结束,不能开始新任务!")
+	// 		return;
+	// 	}
+	// 	startMove_is = true // 开始
+	// 	// if(ob.type != "Group") {
+	// 	// 	console.log("Group != ")
+	// 	// 	startMove_is = false
+	// 	// 	return;
+	// 	// }
+	// 	if(ob.children.length == 0) {
+	// 		console.log("children.length == 0!")
+	// 		startMove_is = false
+	// 		return;
+	// 	}
+	// 	if(ob.children[0].type != "PerspectiveCamera") {
+	// 		console.log("children[0].type != PerspectiveCamera")
+	// 		startMove_is = false
+	// 		return;
+	// 	}
+	// 	let MoveList = []
+	// 	MoveList.push([camera.position.x,camera.position.y,camera.position.z])
+	// 	// MoveList.push([ob.position.x,ob.position.y,ob.position.z])
+	// 	// MoveList.push([ob.children[0].position.x + ob.position.x, ob.children[0].position.y + ob.position.y, ob.children[0].position.z + ob.position.z])
+	// 	MoveList.push([ob.children[0].matrixWorld.elements[12], ob.children[0].matrixWorld.elements[13], ob.children[0].matrixWorld.elements[14]])
+	// 	// MoveList.push([ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14]])
+	// 	console.log("MoveList:",MoveList)
+	//
+	// 	let MoveListCurve = []
+	// 	for(var item of MoveList) {
+	// 		MoveListCurve.push(new THREE.Vector3(item[0], item[1], item[2]))
+	// 	}
+	// 	let curve = new THREE.CatmullRomCurve3(MoveListCurve)
+	//
+	// 	var curveList = curve.getPoints(20 * MoveTime)
+	// 	// console.log("curveList:",curveList)
+	//
+	// 	var testIndex = 0
+	// 	var t = setInterval(function () {
+	// 		if(!startMove_is) {
+	// 			curveList = []
+	// 			testIndex = 0
+	//
+	// 			clearTimeout(t) //停止 t 定时器
+	// 			return
+	// 		}
+	// 		// 模仿管道的镜头推进
+	// 		if (curveList.length !== 0) {
+	// 			if (testIndex < curveList.length  ) {
+	//
+	// 				const point = curveList[testIndex] //获取样条曲线指定点坐标,作为相机的位置
+	// 				const pointBox = curveList[testIndex+2] //获取样条曲线指定点坐标
+	//
+	// 				camera.position.set(point.x, point.y , point.z)
+	// 				camera.lookAt(ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14])
+	//
+	// 				controls.target.set(ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	// 				// camera.lookAt(ob.position.x,ob.position.y,ob.position.z)
+	// 				testIndex += 1
+	// 			} else {
+	// 				curveList = []
+	// 				testIndex = 0
+	//
+	// 				clearTimeout(t) //停止 t 定时器
+	// 				startMove_is = false
+	//
+	//
+	// 				// 更新视角中心点
+	// 				controls.target.set(ob.matrixWorld.elements[12], ob.matrixWorld.elements[13], ob.matrixWorld.elements[14]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	//
+	// 			}
+	// 		}
+	//
+	// 		render()
+	// 	}, 50)
+	//
+	// 	return
+	// }
+	// 围绕旋转  ,围绕对象 , 旋转速度{1~9,2} , 俯视角度{0~100,20}
+	this.AroundRotation = function (ob,anglespeed_ = 2,targetHeight = 20) {
+		// 自动计算包围盒
+		const box = new THREE.Box3().setFromObject(ob);
+		const size = box.getSize(new THREE.Vector3());
+		const center = box.getCenter(new THREE.Vector3());
+
+		// const targetHeight = 20; // 俯视角度的高度
+		const radius = Math.max(size.x, size.z) *1.2; // 基于包围盒计算半径
+		let anglespeed = 0.001 * anglespeed_; // 旋转速度
+		let angle = Math.atan2(camera.position.z - center.z, camera.position.x - center.x); // 当前相机角度
+
+		// 设置相机位置的函数
+		function updateCameraPosition() {
+			camera.position.x = radius * Math.cos(angle);
+			camera.position.z = radius * Math.sin(angle);
+			camera.position.y = targetHeight; // 固定Y坐标高度
+			camera.lookAt(center); // 始终朝向场景中心
+		}
+
+		// 定时循环
+		AutomaticRotationPerspectiveInterval = setInterval(() => {
+			angle += anglespeed; // 旋转速度
+			updateCameraPosition();
+			renderer.render(scene, camera);
+		}, 1000 / 60); // 每秒60帧
+
+	}
+	// 聚焦物体 - V2
+	this.Focus = function (ob) {
+		// 计算物体的边界盒
+		const box = new THREE.Box3().setFromObject(ob);
+		const size = box.getSize(new THREE.Vector3());
+		const distance = size.length() * 2.0; // 增加一些距离,以便能看得到
+
+		// 按照目标物体的大小和 20 度俯视角度计算相机位置
+		const pitchAngle = THREE.MathUtils.degToRad(20); // 转换为弧度 20
+		// const yawAngle = THREE.MathUtils.degToRad(angle); // 旋转 90 度
+		const yawAngle = ob.rotation._y
+
+		const targetEnd = box.getCenter(new THREE.Vector3());
+		const targetPosition = new THREE.Vector3(
+			targetEnd.x + distance * Math.sin(yawAngle),
+			targetEnd.y + distance * Math.sin(pitchAngle),
+			targetEnd.z + distance * Math.cos(yawAngle)
+		);
+
+		let initialPosition = new THREE.Vector3();
+		initialPosition.copy(camera.position); // 记录初始位置
+		// 在动画开始前,确保相机在正确的初始位置
+		// 		camera.position.copy(targetPosition);
+		// 		controls.target.copy(target);
+		// 		controls.update(); // 更新控件
+
+
+		const targetStart = new THREE.Vector3(0, 0, 0); // 初始中心点
+		// const targetEnd = new THREE.Vector3(1, 1, 1); // 目标中心点
+		targetStart.x = controls.target.x
+		targetStart.y = controls.target.y
+		targetStart.z = controls.target.z
+
+		// console.log("targetStart:",targetStart)
+		// console.log("targetEnd:",targetEnd)
+		// console.log("initialPosition:",initialPosition)
+		// console.log("targetPosition:",targetPosition)
+		const distancex = initialPosition.distanceTo(targetPosition);
+		// console.log("运动距离:", distancex);
+
+		if(startMove_is) {
+			console.log("任务还没结束,不能开始新任务!")
+			return;
+		}
+		startMove_is = true // 开始
+
+		// 动画属性
+		const animationDuration = distancex * 2; // 动画持续 2 秒
+		let animationDurationTime = 0; // 动画开始时间
+
+		var It = setInterval(function () {
+
+			animationDurationTime += 1;
+			const t = Math.min(animationDurationTime / animationDuration, 1); // 归一化
+			// console.log("t:",animationDurationTime)
+			// 插值计算新的相机位置
+			camera.position.x = THREE.MathUtils.lerp(initialPosition.x, targetPosition.x, t);
+			camera.position.y = THREE.MathUtils.lerp(initialPosition.y, targetPosition.y, t);
+			camera.position.z = THREE.MathUtils.lerp(initialPosition.z, targetPosition.z, t);
+			// 插值控制目标位置
+			controls.target.x = THREE.MathUtils.lerp(targetStart.x, targetEnd.x, t);
+			controls.target.y = THREE.MathUtils.lerp(targetStart.y, targetEnd.y, t);
+			controls.target.z = THREE.MathUtils.lerp(targetStart.z, targetEnd.z, t);
+
+
+			// 更新控件目标
+			camera.lookAt(controls.target); // 始终看向目标
+			controls.update(); // 更新控件以应用新位置和目标
+
+			// render()
+
+			if(animationDuration <= animationDurationTime || !startMove_is){
+				startMove_is = false
+				clearTimeout(It) //停止 t 定时器
+			}
+		}, 10)
+
+	}
+
+	// 聚焦物体运动 -
+	// this.startFocusMotion = function (ob,MoveList,MoveTime=1) {
+	// 	if(startMove_is) {
+	// 		console.log("任务还没结束,不能开始新任务!")
+	// 		return;
+	// 	}
+	// 	startMove_is = true // 开始
+	//
+	// 	console.log("MoveList:",MoveList)
+	//
+	// 	let MoveListCurve = []
+	// 	for(var item of MoveList) {
+	// 		MoveListCurve.push(new THREE.Vector3(item[0], item[1], item[2]))
+	// 	}
+	// 	let curve = new THREE.CatmullRomCurve3(MoveListCurve)
+	//
+	// 	var curveList = curve.getPoints(20 * MoveTime)
+	// 	// console.log("curveList:",curveList)
+	//
+	// 	var testIndex = 0
+	// 	var t = setInterval(function () {
+	// 		if(!startMove_is) {
+	// 			curveList = []
+	// 			testIndex = 0
+	//
+	// 			clearTimeout(t) //停止 t 定时器
+	// 		}
+	// 		// 模仿管道的镜头推进
+	// 		if (curveList.length !== 0) {
+	// 			if (testIndex < curveList.length  ) {
+	//
+	// 				const point = curveList[testIndex] //获取样条曲线指定点坐标,作为相机的位置
+	//
+	// 				camera.position.set(point.x, point.y , point.z)
+	//
+	// 				camera.lookAt(ob[0], ob[1], ob[2])
+	// 				controls.target.set(ob[0], ob[1], ob[2]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	// 				// camera.lookAt(ob.position.x,ob.position.y,ob.position.z)
+	// 				testIndex += 1
+	// 			} else {
+	// 				curveList = []
+	// 				testIndex = 0
+	//
+	// 				clearTimeout(t) //停止 t 定时器
+	// 				startMove_is = false
+	//
+	//
+	// 				// 更新视角中心点
+	// 				controls.target.set(ob[0], ob[1], ob[2]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 				controls.update();
+	//
+	// 			}
+	// 		}
+	//
+	// 		render()
+	// 	}, 50)
+	//
+	// 	return
+	// }
+
+	// 路径移动
+	this.startMove = function (MoveList,MoveTime=1,orbitControls_target=[]) {
+
+		if(startMove_is) {
+			console.log("任务还没结束,不能开始新任务!")
+			return;
+		}
+		startMove_is = true // 开始
+		if(MoveList.length == 0) {
+			console.log("数据异常!")
+			startMove_is = false
+			return;
+		}
+		if(MoveList[0].length != 3) {
+			console.log("数据异常!")
+			startMove_is = false
+			return;
+		}
+
+
+		let MoveListCurve = []
+		for(var item of MoveList) {
+			MoveListCurve.push(new THREE.Vector3(item[0], item[1], item[2]))
+		}
+		let curve = new THREE.CatmullRomCurve3(MoveListCurve)
+
+		var curveList = curve.getPoints(20 * MoveTime)
+		// console.log("curveList:",curveList)
+
+		var testIndex = 0
+		var t = setInterval(function () {
+			if(!startMove_is) {
+				curveList = []
+				testIndex = 0
+
+				clearTimeout(t) //停止 t 定时器
+			}
+			// 模仿管道的镜头推进
+			if (curveList.length !== 0) {
+				if (testIndex < curveList.length - 2) {
+
+					const point = curveList[testIndex] //获取样条曲线指定点坐标,作为相机的位置
+					const pointBox = curveList[testIndex+2] //获取样条曲线指定点坐标
+
+					camera.position.set(point.x, point.y , point.z)
+					camera.lookAt(pointBox.x, pointBox.y , pointBox.z)
+
+					testIndex += 1
+				} else {
+					curveList = []
+					testIndex = 0
+
+					clearTimeout(t) //停止 t 定时器
+					startMove_is = false
+
+					// 更新视角中心点
+					if(orbitControls_target.length == 3){
+						controls.target.set(orbitControls_target[0], orbitControls_target[1], orbitControls_target[2]);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+						controls.update();
+					}
+				}
+			}
+
+			// render()
+		}, 50)
+
+		return
+	}
+	// 停止移动
+	this.stopMove = function () {
+		startMove_is = false
+	}
+
+	// 渲染
+	//region Description
+
+	// ---------- 渲染 -----------
+	// renderer.setAnimationLoop( render );
+
+	var clock = new THREE.Clock(); // only used for animations
+
+	// function render() {
+	// 	// console.log("render")
+	//
+	// 	// 渲染方式
+	// 	if(compose != undefined) {
+	// 		if(selectedObjects.length > 0){
+	// 			compose.render()
+	// 		}else {
+	// 			renderer.render(scene, camera);
+	// 		}
+	// 	}else {
+	// 		renderer.render(scene, camera);
+	// 	}
+	//
+	// 	if ( rendererCss2 !== null ) {
+	// 		rendererCss2.render( scene, camera );
+	// 	}
+	// 	if ( rendererCss3 !== null ) {
+	// 		rendererCss3.render( scene, camera );
+	// 	}
+	//
+	// }
+	// this.Render = function () {
+	// 	render()
+	// }
+
+	// 渲染
+	// let lastRender = performance.now();
+	function animate() {
+		// let timestamp = timestamp.now();
+		// if (timestamp - lastRender < 1000 / 60) return; // 限制为 60fps
+		// lastRender = timestamp;
+
+
+		if(window.ScenePlane !== undefined){
+			scene.add( window.ScenePlane );
+		}
+		// renderer.render(scene, camera);
+		if(compose != undefined) {
+			if(selectedObjects.length > 0){
+				compose.render()
+			}else {
+				renderer.render(scene, camera);
+			}
+		}else {
+			renderer.render(scene, camera);
+		}
+
+		if ( rendererCss2 !== null ) {
+			rendererCss2.render( scene, camera );
+		}
+		if ( rendererCss3 !== null ) {
+			rendererCss3.render( scene, camera );
+		}
+		// 更新控制器
+		// controls.update(); // 仅在需要时调用,例如当你使相机移动时
+
+		if(window.ScenePlane !== undefined){
+			scene.remove( window.ScenePlane );
+		}
+
+		// Animations  动画
+		if(window.mixer != null){
+			window.mixer.update( clock.getDelta() );
+		}
+		// requestAnimationFrame(animate);
+	}
+	//endregion
+
+//endregion
+
+
+// --------------------- 鼠标事件 -----------------------
+//region Description
+	var Model_onEvents = {
+		mousemove:undefined,
+		click:undefined,
+		dblclick:undefined,
+		mousedown:undefined,
+	}
+	// 递归寻找上级可触发
+	function finde_parent_choice(Ob) {
+		if (Ob.choice) {
+			return {Ob:Ob,is:true}
+		}
+		if(Ob.parent.isScene) return null,false
+		var Obf = finde_parent_choice(Ob.parent)
+		if(Obf.is){
+			return Obf
+		}
+		return {Ob:null,is:false}
+	}
+	// 移动
+	dom.addEventListener('mousemove', onMouseMove, false);
+	function onMouseMove(event) {
+
+		// console.log("onMouseMove:",event)
+		if (event.isPrimary === false) return;
+
+		// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
+		mouse.x = (event.clientX / dom_width) * 2 - 1;
+		mouse.y = -(event.clientY / dom_height) * 2 + 1;
+
+		raycaster.setFromCamera(mouse, camera);
+		// 计算物体和射线的焦点
+		var intersects = raycaster.intersectObjects(scene.children);
+
+		// console.log("intersects:",intersects)
+		if (intersects.length > 0){
+			//
+			for (let n = 0;intersects.length > n;n++) {
+				let objectP = intersects[n].object
+				// console.log("objectP:",objectP)
+				if(objectP === undefined) break;
+				let objectF = finde_parent_choice(objectP)
+				// console.log("finde_parent_choice:",objectF)
+				if (objectF.is) {
+					if(INTERSECTED !== null && INTERSECTED.uuid === objectF.Ob.uuid) break;
+					INTERSECTED = objectF.Ob;
+					// console.log("移入:",INTERSECTED)
+					if (Model_onEvents.mousemove !== undefined) Model_onEvents.mousemove(INTERSECTED)
+					if(INTERSECTED.scriptsf !== undefined ){
+						INTERSECTED.scriptsf.forEach(scriptf => {
+							if(scriptf["onMouseMoveIn"] !== undefined){
+								scriptf.onMouseMoveIn()
+							}
+						})
+
+					}
+					break;
+				}
+
+				// for (let xn = 0; 10 > xn; xn++) {
+				// 	console.log(xn,"-objectP:",objectP)
+				// 	if(objectP == null) break;
+				// 	// if (objectP.type == "Object3D") {
+				// 	if (INTERSECTED !== objectP) {
+				// 		console.log("intersects[0].object:", intersects[0].object.parent)
+				// 		if (objectP.choice === true) {
+				// 			INTERSECTED = objectP;
+				// 			if (Model_onEvents.mousemove !== undefined) Model_onEvents.mousemove(INTERSECTED)
+				// 			break;
+				// 		}
+				// 	}
+				// 	// }
+				// 	objectP = objectP.parent
+				// }
+
+			}
+			// console.log("onMouseMove:",INTERSECTED)
+		}else {
+			// console.log("移出:",INTERSECTED)
+			if(INTERSECTED !== null && INTERSECTED.scriptsf !== undefined ){
+				INTERSECTED.scriptsf.forEach(scriptf => {
+					if(scriptf["onMouseMoveOut"] !== undefined){
+						scriptf.onMouseMoveOut()
+					}
+				})
+
+			}
+
+			INTERSECTED = null;
+
+		}
+
+
+		OutlinePass_selectedObjects_Refresh()
+		// render()  // 可优化空间
+	}
+	this.Model_onMouseMove = function (fun) {
+		Model_onEvents.mousemove = fun
+	}
+
+	//单击延时触发
+	var  clickTimeId,clickTimeIs = false;
+	// 单击
+	dom.addEventListener('click', onClick, false);
+	function onClick(event) {
+		// console.log("onClick:",INTERSECTED)
+		if(INTERSECTED === null){return}
+		// 取消上次延时未执行的方法
+		clearTimeout(clickTimeId);
+		const INTERSECTED_ = INTERSECTED
+		//执行延时
+		clickTimeId = setTimeout( function () {
+			if (INTERSECTED_) {
+				if(Model_onEvents.click != undefined) Model_onEvents.click(INTERSECTED)
+			}
+			// console.log("onClick:",INTERSECTED)
+
+			if(INTERSECTED_.scriptsf !== undefined ){
+				INTERSECTED_.scriptsf.forEach(scriptf => {
+					if(scriptf["onClick"] !== undefined){
+						scriptf.onClick()
+					}
+				})
+
+			}
+
+
+			// render()
+		}, 250);
+
+	}
+	this.Model_onClick = function (fun) {
+		Model_onEvents.click = fun
+	}
+
+	// 双击
+	dom.addEventListener('dblclick', onDblclick, false);
+	function onDblclick(event) {
+		if(INTERSECTED === null){return}
+		clearTimeout(clickTimeId); // 取消上次延时未执行的方法
+		if (INTERSECTED) {
+			if(Model_onEvents.dblclick != undefined) Model_onEvents.dblclick(INTERSECTED)
+		}
+
+		if(INTERSECTED.scriptsf !== undefined ){
+			INTERSECTED.scriptsf.forEach(scriptf => {
+				if(scriptf["onDblclick"] !== undefined){
+					scriptf.onDblclick()
+				}
+			})
+
+		}
+
+
+		// render()
+	}
+	this.Model_onDblclick = function (fun) {
+		Model_onEvents.dblclick = fun
+	}
+
+	// 右击---
+	dom.addEventListener('contextmenu', onMousedown, false);
+	function onMousedown(event) {
+		if(clickTimeIs) return;
+		clickTimeIs = true;
+		setTimeout( function () {
+			clickTimeIs = false;
+		}, 2000);  // 防止 反复触发退出
+		if(Model_onEvents.mousedown != undefined) Model_onEvents.mousedown(INTERSECTED,event)
+		onBackclick()
+		// render()
+	}
+	this.Model_onMousedown = function (fun) {
+		Model_onEvents.mousedown = fun
+	}
+
+	//
+	// this.ondBlclick_Model = function (position) {
+	// 	console.log("更新视角中心点:", position)
+	// 	controls.target.set(position.x, position.y, position.z);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+	// 	controls.update();
+	// }
+
+//endregion
+
+
+// --------------------- 项目 -----------------------
+//region Description
+	// 本地调试模式
+	this.LocalRun = function ( parkId = 0 ) {
+		window.parkId = parkId
+		new _Storage(function(result) {
+			console.log(result);
+			// scene = result.scene
+			loadJson(result,"000000"+parkId)
+		});
+	}
+
+	var loadProject_Map = []; //ProjectID 映射
+	var Now_ProjectID = ""; // 当前 ProjectID
+	this.load = function (){
+		console.log("加载完毕")
+	}
+
+	// 加载项目
+	this.LoadProject = function (ProjectID) {
+		if(Now_ProjectID == ProjectID){ return }
+
+
+		console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "加载 ProjectID:", ProjectID)
+
+		// 是否加载与缓存过
+		if (loadProject_Map[ProjectID] == undefined) {
+			var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
+			httpRequest.open('GET', './GetProject?T_ViewID=' + ProjectID, true);//第二步:打开连接  将请求参数写在url中  ps:"./Ptest.php?name=test&nameone=testone"
+			httpRequest.send();//第三步:发送请求  将请求参数写在URL中
+			httpRequest.onreadystatechange = function () {
+				if (httpRequest.readyState == 4 && httpRequest.status == 200) {
+					var json = JSON.parse(httpRequest.responseText);//获取到json字符串,还需解析
+					console.log(json);
+					if (json.Code != 200) {
+						console.log("ProjectID 错误!", ProjectID)
+						return "ProjectID 错误!"
+					}
+					var json = JSON.parse(json.Data.T_url);//获取到json字符串,还需解析
+					console.log(json);
+					window.parkList = json
+					window.parkId = 0
+					// 如果需要兼容低版本的浏览器,需要判断一下FileReader对象是否存在。
+					if (window.FileReader) {
+						blobLoad(ProjectID, window.parkList[window.parkId].T_url)
+					} else {
+						console.log('你的浏览器不支持读取文件');
+						loadProject_Map[ProjectID] = src
+					}
+				}
+			};
+		} else {
+
+			console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "缓存加载 ProjectID:", ProjectID)
+			f_load(ProjectID)
+
+		}
+	}
+	this.LoadLocal = function () {
+
+
+		var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
+		httpRequest.open('GET', '../static/LocalProject/main.json', true);//第二步:打开连接  将请求参数写在url中  ps:"./Ptest.php?name=test&nameone=testone"
+		httpRequest.send();//第三步:发送请求  将请求参数写在URL中
+		httpRequest.onreadystatechange = function () {
+			if (httpRequest.readyState == 4 && httpRequest.status == 200) {
+				var json = JSON.parse(httpRequest.responseText);//获取到json字符串,还需解析
+				console.log(json);
+				window.parkList = json
+				window.parkId = 0
+				// 如果需要兼容低版本的浏览器,需要判断一下FileReader对象是否存在。
+				if (window.FileReader) {
+					blobLoad("00000", window.parkList[window.parkId].T_url)
+				} else {
+					console.log('你的浏览器不支持读取文件');
+					loadProject_Map[ProjectID] = src
+				}
+			}
+		};
+
+	}
+	// 解压并更新舞台
+	function f_load(ProjectID) {
+		console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "开始加载 file:", loadProject_Map[ProjectID])
+
+		// 解压模型
+		// var promise = fetch('../static/16635717199e4e03e8-8850-4152-9c08-9c203c882f7a.zip')
+		var promise = fetch(loadProject_Map[ProjectID])
+			.then((d) => d.arrayBuffer())
+		promise = promise.then(function (data) {
+			//响应的内容
+			console.log("data:", data);
+			const decompressed = unzipSync(new Uint8Array(data), {
+				// You may optionally supply a filter for files. By default, all files in a
+				// ZIP archive are extracted, but a filter can save resources by telling
+				// the library not to decompress certain files
+				filter(file) {
+					// Don't decompress the massive image or any files larger than 10 MiB
+					return file;
+				}
+			});
+			console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "file:", decompressed['app.json'])
+			var obj = JSON.parse(new TextDecoder().decode(decompressed['app.json']));
+			loadJson(obj,ProjectID);// 加载场景
+			// self.setSize(dom_width, dom_height);
+			// orbitControls() // 鼠标移动 -轨道控制
+			Now_ProjectID = ProjectID
+			console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "JSON obj:", obj)
+
+		}).catch(function (err) {
+			console.log(err);
+		})
+
+	}
+
+	// 导入 json
+	function loadJson(json,ProjectID) {
+
+		let loader = new THREE.ObjectLoader(); // 加载
+
+		console.log("loadJson:", json)
+		project = json.project;
+
+		// if (project.vr !== undefined) renderer.xr.enabled = project.vr;
+		// if (project.shadows !== undefined) renderer.shadowMap.enabled = project.shadows;
+		// if (project.shadowType !== undefined) renderer.shadowMap.type = project.shadowType;
+		// if (project.toneMapping !== undefined) renderer.toneMapping = project.toneMapping;
+		// if (project.toneMappingExposure !== undefined) renderer.toneMappingExposure = project.toneMappingExposure;
+		// if (project.physicallyCorrectLights !== undefined) renderer.physicallyCorrectLights = project.physicallyCorrectLights;
+		rendererCss2.clean()
+		rendererCss3.clean()
+		scene = loader.parse(json.scene);
+
+		// camera = loader.parse(json.camera);
+		// console.log("json.camera:",json.camera)
+		// console.log("camera:",camera)
+		camera.position.x = json.camera.object.matrix[12]
+		camera.position.y = json.camera.object.matrix[13]
+		camera.position.z = json.camera.object.matrix[14]
+		// camera.aspect = dom_width / dom_height;
+		// camera.updateProjectionMatrix();
+		// renderer.setSize(dom_width, dom_height);
+		// 中心点
+		controls.target.set(0, 0, 0);  // 控件的焦点,.object围绕它运行。它可以随时手动更新以更改控件的焦点。
+		controls.update();
+
+
+
+		//////--------------
+		// ScenePlane   场景平面
+		if ( project.sceneplane !== undefined ){
+			// console.log("sceneplane:",project.sceneplane)
+
+			switch ( project.sceneplane ) {
+				case 'None':
+					window.ScenePlane = undefined
+					break;
+
+				case 'Grass':  //草地平面
+					const gt = new THREE.TextureLoader().load( './static/js/img/cd.jpg' );
+					const gg = new THREE.PlaneGeometry( 300, 300 );
+					const gm = new THREE.MeshPhongMaterial( { color: 0xffffff, map: gt } );
+
+					window.ScenePlane = new THREE.Mesh( gg, gm );
+					window.ScenePlane.rotation.x = - Math.PI / 2;
+					// window.ScenePlane.rotation.y = -0.1
+					window.ScenePlane.material.map.repeat.set( 64, 64 );
+					window.ScenePlane.material.map.wrapS = THREE.RepeatWrapping;
+					window.ScenePlane.material.map.wrapT = THREE.RepeatWrapping;
+					window.ScenePlane.material.map.colorSpace = THREE.SRGBColorSpace;
+					// note that because the ground does not cast a shadow, .castShadow is left false
+					window.ScenePlane.receiveShadow = true;
+
+					break;
+
+			}
+
+		}
+
+
+
+		//// ------------
+
+		events = {
+			init: [],
+			keydown: [],
+			keyup: [],
+			onMouseMoveIn: [],  //鼠标移入
+			onMouseMoveOut: [],  //鼠标移入
+			onClick: [],  //鼠标单击
+			onDblclick: [],  //鼠标双击
+			onBackclick: [],  //鼠标右击  退后
+			renderer: [],
+			update: []
+		};
+		window.Getevents = function () {
+			return events
+		}
+
+
+		var scriptWrapParams = 'iot3d,renderer,scene,camera';
+		var scriptWrapResultObj = {};
+
+		for (var eventKey in events) {
+
+			scriptWrapParams += ',' + eventKey;
+			scriptWrapResultObj[eventKey] = eventKey;
+
+		}
+
+		var scriptWrapResult = JSON.stringify(scriptWrapResultObj).replace(/\"/g, '');
+
+
+		//执行代码
+
+		// 递归遍历 所有脚本 并添加
+
+		function traverseChildrenScript(object) {
+			if ( object.scripts !== undefined && object.scripts.length > 0 ) {
+
+				console.log("addObjectScript:",object.uuid,object.scripts)
+				object.scriptsf = []
+				var scripts = object.scripts;
+				for ( var i = 0; i < scripts.length; i ++ ) {
+					//  每个 script 代码
+					var script = scripts[i];
+
+					var functions = ( new Function( scriptWrapParams, script.source + '\nreturn ' + scriptWrapResult + ';' ).bind( object ) )( this, renderer, scene, camera );
+					// console.log("functions.name",functions)
+					object.scriptsf.push(functions)
+					for ( var name in functions ) {
+						// console.log("functions.name",name)
+						if ( functions[ name ] === undefined ) continue;
+
+						if ( events[ name ] === undefined ) {
+							console.warn( 'APP.Player: Event type not supported (', name, ')' );
+							continue;
+						}
+						if(name !== "update"){
+							events[ name ].push( functions[ name ].bind( object ) );
+						}else {
+							var subSN = SeekParameterNodes(object)
+							// console.log("subSN:",subSN)
+							// 订阅
+							pubSub.subscribe(subSN, data => {
+								// console.log("subSN:",subSN,data);
+								functions["update"].bind( object )( data )
+							})
+						}
+
+
+					}
+				}
+
+
+			}
+
+			// 遍历当前对象的所有子对象
+			object.children.forEach(child => {
+				traverseChildrenScript(child); // 递归调用
+			});
+		}
+		traverseChildrenScript(scene)
+
+
+		// console.log("events:", events)
+		dispatch(events.init, arguments);
+
+		console.log("加载完成 ProjectID:",ProjectID)
+		// 场景加载后 视角归为
+		self.setSize(window.innerWidth, window.innerHeight);
+		orbitControls_maxDistance()  // 计算场景最远距离,并控制参数
+		OutlinePass_inte()  // 鼠标选择初始化
+
+	};
+
+	// 文件缓存本地
+	function blobLoad(ProjectID, src) {
+		self.loading_open() //
+		console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "文件缓存本地 :", ProjectID)
+		// let self = this;
+		const req = new XMLHttpRequest();
+		req.open("GET", src, true);
+		req.responseType = "blob";
+		req.onload = function () {
+			// Onload is triggered even on 404
+			// so we need to check the status code
+			if (this.status === 200) {
+				const videoBlob = this.response;
+
+				console.log("videoBlob:", videoBlob)
+				const blobSrc = URL.createObjectURL(videoBlob); // IE10+
+				console.log("blobSrc:", blobSrc)
+				loadProject_Map[ProjectID] = blobSrc
+				console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "文件缓存本地完成 ProjectID:", ProjectID)
+				f_load(ProjectID)
+			}
+			self.loading_close()
+		};
+		//监听进度事件
+		req.addEventListener("progress", function (evt) {
+			if (evt.lengthComputable) {
+				var percentComplete = evt.loaded / evt.total;
+				//进度
+				console.log('[' + /\d\d\:\d\d\:\d\d/.exec(new Date())[0] + ']', "ProjectID:", ProjectID, " 进度:", (percentComplete * 100) + "%")
+				self.loading_text("模型加载中... "+parseInt(percentComplete * 100) + "%") // 替换内容
+			}
+		}, false);
+		req.onerror = function () {
+			// Error
+			console.log("blobLoad Error!", src)
+			loadProject_Map[ProjectID] = src
+		};
+		req.send();
+	}
+
+
+//endregion
+
+
+// --------------------- 脚本 -----------------------
+//region Description
+	// var time, startTime;
+	// startTime = performance.now();
+
+	// // 创建一个每秒执行一次的定时循环
+	// setInterval(function() {
+	// 	// 在这里编写需要重复执行的代码
+	// 	time = performance.now();
+	// 	const rendertime = { time: time - startTime }
+	// 	// console.log("renderer:",rendertime)
+	// 	try {
+	// 		dispatch( events.renderer, rendertime);
+	// 	} catch ( e ) {
+	// 		console.error( ( e.message || e ), ( e.stack || '' ) );
+	// 	}
+	//
+	// }, 1000); // 1000 毫秒 = 1 秒
+	function dispatch( array, event ) {
+
+		for ( var i = 0, l = array.length; i < l; i ++ ) {
+
+			array[ i ]( event );
+
+		}
+
+	}
+
+
+	//
+	// function animate() {
+	//
+	// 	time = performance.now();
+	//
+	//
+	//
+	// 	prevTime = time;
+	//
+	// }
+
+	// this.play = function () {
+	//
+	// 	if ( renderer.xr.enabled ) dom.append( vrButton );
+	//
+	// 	startTime = prevTime = performance.now();
+	//
+	// 	document.addEventListener( 'keydown', oneyDown );
+	// 	document.addEventListener( 'keyup', onKeyUp );
+
+	// 	document.addEventListener( 'pointerup', onPointerUp );
+	// 	document.addEventListener( 'pointermove', onPointerMove );
+	//
+	// 	// dispatch( events.start, arguments );
+	//
+	// 	renderer.setAnimationLoop( animate );
+	//
+	// };
+
+
+	this.render = function ( time ) {
+
+		dispatch( events.update, { time: time * 1000, delta: 0 /* TODO */ } );
+
+		renderer.render( scene, camera );
+
+	};
+
+	this.dispose = function () {
+
+		renderer.dispose();
+
+		camera = undefined;
+		scene = undefined;
+
+	};
+
+	//
+
+	function onKeyDown( event ) {
+
+		dispatch( events.keydown, event );
+
+	}
+
+	function onKeyUp( event ) {
+
+		dispatch( events.keyup, event );
+
+	}
+
+	// function onPointerDown( event ) {
+	// 	dispatch( events.pointerdown, event );
+	//
+	// }
+	//
+	// function onPointerUp( event ) {
+	// 	dispatch( events.pointerup, event );
+	//
+	// }
+	//
+	function onBackclick( event ) {
+		dispatch( events.onBackclick, event );
+	}
+
+	function upDate( event ) {
+
+		try {
+			dispatch( events.update, event );
+		} catch ( e ) {
+			console.error( ( e.message || e ), ( e.stack || '' ) );
+		}
+
+	}
+
+//endregion
+// --------------------- 功能 -----------------------
+//region Description
+// 	this.BackgroundScene = function () {
+// 		var urls = [
+// 			'./static/js/img/posx.jpg',
+// 			'./static/js/img/negx.jpg',
+// 			'./static/js/img/posy.jpg',
+// 			'./static/js/img/negy.jpg',
+// 			'./static/js/img/posz.jpg',
+// 			'./static/js/img/negz.jpg'
+// 		];
+//
+// 		var cubeLoader = new THREE.CubeTextureLoader();
+// 		scene.background = cubeLoader.load(urls);
+// 		setTimeout(function(){
+// 			render()
+// 		},3000);
+//
+// 	}
+	// 屏幕坐标与世界坐标
+	this.scene_3Dto2D = function (position) {
+		const worldVector = new THREE.Vector3(
+			position.x,
+			position.y,
+			position.z
+		);
+		const standardVec = worldVector.project(camera);
+		const centerX = dom_width / 2;
+		const centerY = dom_height / 2;
+		const screenX = Math.round(centerX * standardVec.x + centerX);
+		const screenY = Math.round(-centerY * standardVec.y + centerY);
+		console.log("screen:", screenX, screenY)
+		return screenX, screenY
+	}
+
+
+//endregion
+
+
+	// 开始 加载场景
+	if (runmode === 0){
+		this.LocalRun()// 本地调试模式
+	}else if(runmode === 1){
+		this.LoadProject(window.T_ViewID)  // 在线
+	}else if(runmode === 2){
+		this.LoadLocal()  // 离线
+	}
+}
+export {IOT3D};

+ 201 - 0
public/archive/static/js/VRButton.js

@@ -0,0 +1,201 @@
+class VRButton {
+
+	static createButton( renderer ) {
+
+		const button = document.createElement( 'button' );
+
+		function showEnterVR( /*device*/ ) {
+
+			let currentSession = null;
+
+			async function onSessionStarted( session ) {
+
+				session.addEventListener( 'end', onSessionEnded );
+
+				await renderer.xr.setSession( session );
+				button.textContent = 'EXIT VR';
+
+				currentSession = session;
+
+			}
+
+			function onSessionEnded( /*event*/ ) {
+
+				currentSession.removeEventListener( 'end', onSessionEnded );
+
+				button.textContent = 'ENTER VR';
+
+				currentSession = null;
+
+			}
+
+			//
+
+			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 () {
+
+				if ( currentSession === null ) {
+
+					// WebXR's requestReferenceSpace only works if the corresponding feature
+					// was requested at session creation time. For simplicity, just ask for
+					// the interesting ones as optional features, but be aware that the
+					// requestReferenceSpace call will fail if it turns out to be unavailable.
+					// ('local' is always available for immersive sessions and doesn't need to
+					// be requested separately.)
+
+					const sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking', 'layers' ] };
+					navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted );
+
+				} else {
+
+					currentSession.end();
+
+				}
+
+			};
+
+		}
+
+		function disableButton() {
+
+			button.style.display = '';
+
+			button.style.cursor = 'auto';
+			button.style.left = 'calc(50% - 75px)';
+			button.style.width = '150px';
+
+			button.onmouseenter = null;
+			button.onmouseleave = null;
+
+			button.onclick = null;
+
+		}
+
+		function showWebXRNotFound() {
+
+			disableButton();
+
+			button.textContent = 'VR NOT SUPPORTED';
+
+		}
+
+		function showVRNotAllowed( exception ) {
+
+			disableButton();
+
+			console.warn( 'Exception when trying to call xr.isSessionSupported', exception );
+
+			button.textContent = 'VR NOT ALLOWED';
+
+		}
+
+		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 = 'rgba(0,0,0,0.1)';
+			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 ) {
+
+			button.id = 'VRButton';
+			button.style.display = 'none';
+
+			stylizeElement( button );
+
+			navigator.xr.isSessionSupported( 'immersive-vr' ).then( function ( supported ) {
+
+				supported ? showEnterVR() : showWebXRNotFound();
+
+				if ( supported && VRButton.xrSessionIsGranted ) {
+
+					button.click();
+
+				}
+
+			} ).catch( showVRNotAllowed );
+
+			return button;
+
+		} else {
+
+			const message = document.createElement( 'a' );
+
+			if ( window.isSecureContext === false ) {
+
+				message.href = document.location.href.replace( /^http:/, 'https:' );
+				message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message
+
+			} else {
+
+				message.href = 'https://immersiveweb.dev/';
+				message.innerHTML = 'WEBXR NOT AVAILABLE';
+
+			}
+
+			message.style.left = 'calc(50% - 90px)';
+			message.style.width = '180px';
+			message.style.textDecoration = 'none';
+
+			stylizeElement( message );
+
+			return message;
+
+		}
+
+	}
+
+	static xrSessionIsGranted = false;
+
+	static registerSessionGrantedListener() {
+
+		if ( 'xr' in navigator ) {
+
+			// WebXRViewer (based on Firefox) has a bug where addEventListener
+			// throws a silent exception and aborts execution entirely.
+			if ( /WebXRViewer\//i.test( navigator.userAgent ) ) return;
+
+			navigator.xr.addEventListener( 'sessiongranted', () => {
+
+				VRButton.xrSessionIsGranted = true;
+
+			} );
+
+		}
+
+	}
+
+}
+
+VRButton.registerSessionGrantedListener();
+
+export { VRButton };

File diff suppressed because it is too large
+ 305 - 0
public/archive/static/js/es-module-shims.js


BIN
public/archive/static/js/fonts/172397064437d19568-688d-4032-a8e8-cf561dae4f3a.7z


File diff suppressed because it is too large
+ 88 - 0
public/archive/static/js/fonts/172397064437d19568-688d-4032-a8e8-cf561dae4f3a.json


File diff suppressed because it is too large
+ 0 - 0
public/archive/static/js/fonts/FZYaoTi_Regular.json


File diff suppressed because it is too large
+ 0 - 0
public/archive/static/js/fonts/STKaiti_Regular.json


File diff suppressed because it is too large
+ 0 - 0
public/archive/static/js/fonts/helvetiker_regular.typeface.json


BIN
public/archive/static/js/img/bk/Dusk/nx.png


BIN
public/archive/static/js/img/bk/Dusk/ny.png


BIN
public/archive/static/js/img/bk/Dusk/nz.png


BIN
public/archive/static/js/img/bk/Dusk/px.png


BIN
public/archive/static/js/img/bk/Dusk/py.png


BIN
public/archive/static/js/img/bk/Dusk/pz.png


BIN
public/archive/static/js/img/bk/Sky/negx.jpg


BIN
public/archive/static/js/img/bk/Sky/negy.jpg


BIN
public/archive/static/js/img/bk/Sky/negz.jpg


BIN
public/archive/static/js/img/bk/Sky/posx.jpg


BIN
public/archive/static/js/img/bk/Sky/posy.jpg


BIN
public/archive/static/js/img/bk/Sky/posz.jpg


BIN
public/archive/static/js/img/bk/Starry/nx.jpg


BIN
public/archive/static/js/img/bk/Starry/ny.jpg


BIN
public/archive/static/js/img/bk/Starry/nz.jpg


BIN
public/archive/static/js/img/bk/Starry/px.jpg


BIN
public/archive/static/js/img/bk/Starry/py.jpg


BIN
public/archive/static/js/img/bk/Starry/pz.jpg


BIN
public/archive/static/js/img/bk/f557c3a477552a1ba6268aaffa1cb18c.png


BIN
public/archive/static/js/img/brick-wall.jpg


BIN
public/archive/static/js/img/cd.jpg


BIN
public/archive/static/js/img/grasslight-big-nm.jpg


File diff suppressed because it is too large
+ 1 - 0
public/archive/static/js/jquery.min.js


+ 114 - 0
public/archive/static/js/jsm/animation/AnimationClipCreator.js

@@ -0,0 +1,114 @@
+import {
+	AnimationClip,
+	BooleanKeyframeTrack,
+	ColorKeyframeTrack,
+	NumberKeyframeTrack,
+	Vector3,
+	VectorKeyframeTrack
+} from 'three';
+
+class AnimationClipCreator {
+
+	static CreateRotationAnimation( period, axis = 'x' ) {
+
+		const times = [ 0, period ], values = [ 0, 360 ];
+
+		const trackName = '.rotation[' + axis + ']';
+
+		const track = new NumberKeyframeTrack( trackName, times, values );
+
+		return new AnimationClip( null, period, [ track ] );
+
+	}
+
+	static CreateScaleAxisAnimation( period, axis = 'x' ) {
+
+		const times = [ 0, period ], values = [ 0, 1 ];
+
+		const trackName = '.scale[' + axis + ']';
+
+		const track = new NumberKeyframeTrack( trackName, times, values );
+
+		return new AnimationClip( null, period, [ track ] );
+
+	}
+
+	static CreateShakeAnimation( duration, shakeScale ) {
+
+		const times = [], values = [], tmp = new Vector3();
+
+		for ( let i = 0; i < duration * 10; i ++ ) {
+
+			times.push( i / 10 );
+
+			tmp.set( Math.random() * 2.0 - 1.0, Math.random() * 2.0 - 1.0, Math.random() * 2.0 - 1.0 ).
+				multiply( shakeScale ).
+				toArray( values, values.length );
+
+		}
+
+		const trackName = '.position';
+
+		const track = new VectorKeyframeTrack( trackName, times, values );
+
+		return new AnimationClip( null, duration, [ track ] );
+
+	}
+
+	static CreatePulsationAnimation( duration, pulseScale ) {
+
+		const times = [], values = [], tmp = new Vector3();
+
+		for ( let i = 0; i < duration * 10; i ++ ) {
+
+			times.push( i / 10 );
+
+			const scaleFactor = Math.random() * pulseScale;
+			tmp.set( scaleFactor, scaleFactor, scaleFactor ).
+				toArray( values, values.length );
+
+		}
+
+		const trackName = '.scale';
+
+		const track = new VectorKeyframeTrack( trackName, times, values );
+
+		return new AnimationClip( null, duration, [ track ] );
+
+	}
+
+	static CreateVisibilityAnimation( duration ) {
+
+		const times = [ 0, duration / 2, duration ], values = [ true, false, true ];
+
+		const trackName = '.visible';
+
+		const track = new BooleanKeyframeTrack( trackName, times, values );
+
+		return new AnimationClip( null, duration, [ track ] );
+
+	}
+
+	static CreateMaterialColorAnimation( duration, colors ) {
+
+		const times = [], values = [],
+			timeStep = duration / colors.length;
+
+		for ( let i = 0; i <= colors.length; i ++ ) {
+
+			times.push( i * timeStep );
+			values.push( colors[ i % colors.length ] );
+
+		}
+
+		const trackName = '.material[0].color';
+
+		const track = new ColorKeyframeTrack( trackName, times, values );
+
+		return new AnimationClip( null, duration, [ track ] );
+
+	}
+
+}
+
+export { AnimationClipCreator };

+ 458 - 0
public/archive/static/js/jsm/animation/CCDIKSolver.js

@@ -0,0 +1,458 @@
+import {
+	BufferAttribute,
+	BufferGeometry,
+	Color,
+	Line,
+	LineBasicMaterial,
+	Matrix4,
+	Mesh,
+	MeshBasicMaterial,
+	Object3D,
+	Quaternion,
+	SphereGeometry,
+	Vector3
+} from 'three';
+
+const _q = new Quaternion();
+const _targetPos = new Vector3();
+const _targetVec = new Vector3();
+const _effectorPos = new Vector3();
+const _effectorVec = new Vector3();
+const _linkPos = new Vector3();
+const _invLinkQ = new Quaternion();
+const _linkScale = new Vector3();
+const _axis = new Vector3();
+const _vector = new Vector3();
+const _matrix = new Matrix4();
+
+
+/**
+ * CCD Algorithm
+ *  - https://sites.google.com/site/auraliusproject/ccd-algorithm
+ *
+ * // ik parameter example
+ * //
+ * // target, effector, index in links are bone index in skeleton.bones.
+ * // the bones relation should be
+ * // <-- parent                                  child -->
+ * // links[ n ], links[ n - 1 ], ..., links[ 0 ], effector
+ * iks = [ {
+ *	target: 1,
+ *	effector: 2,
+ *	links: [ { index: 5, limitation: new Vector3( 1, 0, 0 ) }, { index: 4, enabled: false }, { index : 3 } ],
+ *	iteration: 10,
+ *	minAngle: 0.0,
+ *	maxAngle: 1.0,
+ * } ];
+ */
+
+class CCDIKSolver {
+
+	/**
+	 * @param {THREE.SkinnedMesh} mesh
+	 * @param {Array<Object>} iks
+	 */
+	constructor( mesh, iks = [] ) {
+
+		this.mesh = mesh;
+		this.iks = iks;
+
+		this._valid();
+
+	}
+
+	/**
+	 * Update all IK bones.
+	 *
+	 * @return {CCDIKSolver}
+	 */
+	update() {
+
+		const iks = this.iks;
+
+		for ( let i = 0, il = iks.length; i < il; i ++ ) {
+
+			this.updateOne( iks[ i ] );
+
+		}
+
+		return this;
+
+	}
+
+	/**
+	 * Update one IK bone
+	 *
+	 * @param {Object} ik parameter
+	 * @return {CCDIKSolver}
+	 */
+	updateOne( ik ) {
+
+		const bones = this.mesh.skeleton.bones;
+
+		// for reference overhead reduction in loop
+		const math = Math;
+
+		const effector = bones[ ik.effector ];
+		const target = bones[ ik.target ];
+
+		// don't use getWorldPosition() here for the performance
+		// because it calls updateMatrixWorld( true ) inside.
+		_targetPos.setFromMatrixPosition( target.matrixWorld );
+
+		const links = ik.links;
+		const iteration = ik.iteration !== undefined ? ik.iteration : 1;
+
+		for ( let i = 0; i < iteration; i ++ ) {
+
+			let rotated = false;
+
+			for ( let j = 0, jl = links.length; j < jl; j ++ ) {
+
+				const link = bones[ links[ j ].index ];
+
+				// skip this link and following links.
+				// this skip is used for MMD performance optimization.
+				if ( links[ j ].enabled === false ) break;
+
+				const limitation = links[ j ].limitation;
+				const rotationMin = links[ j ].rotationMin;
+				const rotationMax = links[ j ].rotationMax;
+
+				// don't use getWorldPosition/Quaternion() here for the performance
+				// because they call updateMatrixWorld( true ) inside.
+				link.matrixWorld.decompose( _linkPos, _invLinkQ, _linkScale );
+				_invLinkQ.invert();
+				_effectorPos.setFromMatrixPosition( effector.matrixWorld );
+
+				// work in link world
+				_effectorVec.subVectors( _effectorPos, _linkPos );
+				_effectorVec.applyQuaternion( _invLinkQ );
+				_effectorVec.normalize();
+
+				_targetVec.subVectors( _targetPos, _linkPos );
+				_targetVec.applyQuaternion( _invLinkQ );
+				_targetVec.normalize();
+
+				let angle = _targetVec.dot( _effectorVec );
+
+				if ( angle > 1.0 ) {
+
+					angle = 1.0;
+
+				} else if ( angle < - 1.0 ) {
+
+					angle = - 1.0;
+
+				}
+
+				angle = math.acos( angle );
+
+				// skip if changing angle is too small to prevent vibration of bone
+				if ( angle < 1e-5 ) continue;
+
+				if ( ik.minAngle !== undefined && angle < ik.minAngle ) {
+
+					angle = ik.minAngle;
+
+				}
+
+				if ( ik.maxAngle !== undefined && angle > ik.maxAngle ) {
+
+					angle = ik.maxAngle;
+
+				}
+
+				_axis.crossVectors( _effectorVec, _targetVec );
+				_axis.normalize();
+
+				_q.setFromAxisAngle( _axis, angle );
+				link.quaternion.multiply( _q );
+
+				// TODO: re-consider the limitation specification
+				if ( limitation !== undefined ) {
+
+					let c = link.quaternion.w;
+
+					if ( c > 1.0 ) c = 1.0;
+
+					const c2 = math.sqrt( 1 - c * c );
+					link.quaternion.set( limitation.x * c2,
+					                     limitation.y * c2,
+					                     limitation.z * c2,
+					                     c );
+
+				}
+
+				if ( rotationMin !== undefined ) {
+
+					link.rotation.setFromVector3( _vector.setFromEuler( link.rotation ).max( rotationMin ) );
+
+				}
+
+				if ( rotationMax !== undefined ) {
+
+					link.rotation.setFromVector3( _vector.setFromEuler( link.rotation ).min( rotationMax ) );
+
+				}
+
+				link.updateMatrixWorld( true );
+
+				rotated = true;
+
+			}
+
+			if ( ! rotated ) break;
+
+		}
+
+		return this;
+
+	}
+
+	/**
+	 * Creates Helper
+	 *
+	 * @return {CCDIKHelper}
+	 */
+	createHelper() {
+
+		return new CCDIKHelper( this.mesh, this.mesh.geometry.userData.MMD.iks );
+
+	}
+
+	// private methods
+
+	_valid() {
+
+		const iks = this.iks;
+		const bones = this.mesh.skeleton.bones;
+
+		for ( let i = 0, il = iks.length; i < il; i ++ ) {
+
+			const ik = iks[ i ];
+			const effector = bones[ ik.effector ];
+			const links = ik.links;
+			let link0, link1;
+
+			link0 = effector;
+
+			for ( let j = 0, jl = links.length; j < jl; j ++ ) {
+
+				link1 = bones[ links[ j ].index ];
+
+				if ( link0.parent !== link1 ) {
+
+					console.warn( 'THREE.CCDIKSolver: bone ' + link0.name + ' is not the child of bone ' + link1.name );
+
+				}
+
+				link0 = link1;
+
+			}
+
+		}
+
+	}
+
+}
+
+function getPosition( bone, matrixWorldInv ) {
+
+	return _vector
+		.setFromMatrixPosition( bone.matrixWorld )
+		.applyMatrix4( matrixWorldInv );
+
+}
+
+function setPositionOfBoneToAttributeArray( array, index, bone, matrixWorldInv ) {
+
+	const v = getPosition( bone, matrixWorldInv );
+
+	array[ index * 3 + 0 ] = v.x;
+	array[ index * 3 + 1 ] = v.y;
+	array[ index * 3 + 2 ] = v.z;
+
+}
+
+/**
+ * Visualize IK bones
+ *
+ * @param {SkinnedMesh} mesh
+ * @param {Array<Object>} iks
+ */
+class CCDIKHelper extends Object3D {
+
+	constructor( mesh, iks = [] ) {
+
+		super();
+
+		this.root = mesh;
+		this.iks = iks;
+
+		this.matrix.copy( mesh.matrixWorld );
+		this.matrixAutoUpdate = false;
+
+		this.sphereGeometry = new SphereGeometry( 0.25, 16, 8 );
+
+		this.targetSphereMaterial = new MeshBasicMaterial( {
+			color: new Color( 0xff8888 ),
+			depthTest: false,
+			depthWrite: false,
+			transparent: true
+		} );
+
+		this.effectorSphereMaterial = new MeshBasicMaterial( {
+			color: new Color( 0x88ff88 ),
+			depthTest: false,
+			depthWrite: false,
+			transparent: true
+		} );
+
+		this.linkSphereMaterial = new MeshBasicMaterial( {
+			color: new Color( 0x8888ff ),
+			depthTest: false,
+			depthWrite: false,
+			transparent: true
+		} );
+
+		this.lineMaterial = new LineBasicMaterial( {
+			color: new Color( 0xff0000 ),
+			depthTest: false,
+			depthWrite: false,
+			transparent: true
+		} );
+
+		this._init();
+
+	}
+
+	/**
+	 * Updates IK bones visualization.
+	 */
+	updateMatrixWorld( force ) {
+
+		const mesh = this.root;
+
+		if ( this.visible ) {
+
+			let offset = 0;
+
+			const iks = this.iks;
+			const bones = mesh.skeleton.bones;
+
+			_matrix.copy( mesh.matrixWorld ).invert();
+
+			for ( let i = 0, il = iks.length; i < il; i ++ ) {
+
+				const ik = iks[ i ];
+
+				const targetBone = bones[ ik.target ];
+				const effectorBone = bones[ ik.effector ];
+
+				const targetMesh = this.children[ offset ++ ];
+				const effectorMesh = this.children[ offset ++ ];
+
+				targetMesh.position.copy( getPosition( targetBone, _matrix ) );
+				effectorMesh.position.copy( getPosition( effectorBone, _matrix ) );
+
+				for ( let j = 0, jl = ik.links.length; j < jl; j ++ ) {
+
+					const link = ik.links[ j ];
+					const linkBone = bones[ link.index ];
+
+					const linkMesh = this.children[ offset ++ ];
+
+					linkMesh.position.copy( getPosition( linkBone, _matrix ) );
+
+				}
+
+				const line = this.children[ offset ++ ];
+				const array = line.geometry.attributes.position.array;
+
+				setPositionOfBoneToAttributeArray( array, 0, targetBone, _matrix );
+				setPositionOfBoneToAttributeArray( array, 1, effectorBone, _matrix );
+
+				for ( let j = 0, jl = ik.links.length; j < jl; j ++ ) {
+
+					const link = ik.links[ j ];
+					const linkBone = bones[ link.index ];
+					setPositionOfBoneToAttributeArray( array, j + 2, linkBone, _matrix );
+
+				}
+
+				line.geometry.attributes.position.needsUpdate = true;
+
+			}
+
+		}
+
+		this.matrix.copy( mesh.matrixWorld );
+
+		super.updateMatrixWorld( force );
+
+	}
+
+	// private method
+
+	_init() {
+
+		const scope = this;
+		const iks = this.iks;
+
+		function createLineGeometry( ik ) {
+
+			const geometry = new BufferGeometry();
+			const vertices = new Float32Array( ( 2 + ik.links.length ) * 3 );
+			geometry.setAttribute( 'position', new BufferAttribute( vertices, 3 ) );
+
+			return geometry;
+
+		}
+
+		function createTargetMesh() {
+
+			return new Mesh( scope.sphereGeometry, scope.targetSphereMaterial );
+
+		}
+
+		function createEffectorMesh() {
+
+			return new Mesh( scope.sphereGeometry, scope.effectorSphereMaterial );
+
+		}
+
+		function createLinkMesh() {
+
+			return new Mesh( scope.sphereGeometry, scope.linkSphereMaterial );
+
+		}
+
+		function createLine( ik ) {
+
+			return new Line( createLineGeometry( ik ), scope.lineMaterial );
+
+		}
+
+		for ( let i = 0, il = iks.length; i < il; i ++ ) {
+
+			const ik = iks[ i ];
+
+			this.add( createTargetMesh() );
+			this.add( createEffectorMesh() );
+
+			for ( let j = 0, jl = ik.links.length; j < jl; j ++ ) {
+
+				this.add( createLinkMesh() );
+
+			}
+
+			this.add( createLine( ik ) );
+
+		}
+
+	}
+
+}
+
+export { CCDIKSolver, CCDIKHelper };

+ 1207 - 0
public/archive/static/js/jsm/animation/MMDAnimationHelper.js

@@ -0,0 +1,1207 @@
+import {
+	AnimationMixer,
+	Object3D,
+	Quaternion,
+	Vector3
+} from 'three';
+import { CCDIKSolver } from '../animation/CCDIKSolver.js';
+import { MMDPhysics } from '../animation/MMDPhysics.js';
+
+/**
+ * MMDAnimationHelper handles animation of MMD assets loaded by MMDLoader
+ * with MMD special features as IK, Grant, and Physics.
+ *
+ * Dependencies
+ *  - ammo.js https://github.com/kripken/ammo.js
+ *  - MMDPhysics
+ *  - CCDIKSolver
+ *
+ * TODO
+ *  - more precise grant skinning support.
+ */
+class MMDAnimationHelper {
+
+	/**
+	 * @param {Object} params - (optional)
+	 * @param {boolean} params.sync - Whether animation durations of added objects are synched. Default is true.
+	 * @param {Number} params.afterglow - Default is 0.0.
+	 * @param {boolean} params.resetPhysicsOnLoop - Default is true.
+	 */
+	constructor( params = {} ) {
+
+		this.meshes = [];
+
+		this.camera = null;
+		this.cameraTarget = new Object3D();
+		this.cameraTarget.name = 'target';
+
+		this.audio = null;
+		this.audioManager = null;
+
+		this.objects = new WeakMap();
+
+		this.configuration = {
+			sync: params.sync !== undefined ? params.sync : true,
+			afterglow: params.afterglow !== undefined ? params.afterglow : 0.0,
+			resetPhysicsOnLoop: params.resetPhysicsOnLoop !== undefined ? params.resetPhysicsOnLoop : true,
+			pmxAnimation: params.pmxAnimation !== undefined ? params.pmxAnimation : false
+		};
+
+		this.enabled = {
+			animation: true,
+			ik: true,
+			grant: true,
+			physics: true,
+			cameraAnimation: true
+		};
+
+		this.onBeforePhysics = function ( /* mesh */ ) {};
+
+		// experimental
+		this.sharedPhysics = false;
+		this.masterPhysics = null;
+
+	}
+
+	/**
+	 * Adds an Three.js Object to helper and setups animation.
+	 * The anmation durations of added objects are synched
+	 * if this.configuration.sync is true.
+	 *
+	 * @param {THREE.SkinnedMesh|THREE.Camera|THREE.Audio} object
+	 * @param {Object} params - (optional)
+	 * @param {THREE.AnimationClip|Array<THREE.AnimationClip>} params.animation - Only for THREE.SkinnedMesh and THREE.Camera. Default is undefined.
+	 * @param {boolean} params.physics - Only for THREE.SkinnedMesh. Default is true.
+	 * @param {Integer} params.warmup - Only for THREE.SkinnedMesh and physics is true. Default is 60.
+	 * @param {Number} params.unitStep - Only for THREE.SkinnedMesh and physics is true. Default is 1 / 65.
+	 * @param {Integer} params.maxStepNum - Only for THREE.SkinnedMesh and physics is true. Default is 3.
+	 * @param {Vector3} params.gravity - Only for THREE.SkinnedMesh and physics is true. Default ( 0, - 9.8 * 10, 0 ).
+	 * @param {Number} params.delayTime - Only for THREE.Audio. Default is 0.0.
+	 * @return {MMDAnimationHelper}
+	 */
+	add( object, params = {} ) {
+
+		if ( object.isSkinnedMesh ) {
+
+			this._addMesh( object, params );
+
+		} else if ( object.isCamera ) {
+
+			this._setupCamera( object, params );
+
+		} else if ( object.type === 'Audio' ) {
+
+			this._setupAudio( object, params );
+
+		} else {
+
+			throw new Error( 'THREE.MMDAnimationHelper.add: '
+				+ 'accepts only '
+				+ 'THREE.SkinnedMesh or '
+				+ 'THREE.Camera or '
+				+ 'THREE.Audio instance.' );
+
+		}
+
+		if ( this.configuration.sync ) this._syncDuration();
+
+		return this;
+
+	}
+
+	/**
+	 * Removes an Three.js Object from helper.
+	 *
+	 * @param {THREE.SkinnedMesh|THREE.Camera|THREE.Audio} object
+	 * @return {MMDAnimationHelper}
+	 */
+	remove( object ) {
+
+		if ( object.isSkinnedMesh ) {
+
+			this._removeMesh( object );
+
+		} else if ( object.isCamera ) {
+
+			this._clearCamera( object );
+
+		} else if ( object.type === 'Audio' ) {
+
+			this._clearAudio( object );
+
+		} else {
+
+			throw new Error( 'THREE.MMDAnimationHelper.remove: '
+				+ 'accepts only '
+				+ 'THREE.SkinnedMesh or '
+				+ 'THREE.Camera or '
+				+ 'THREE.Audio instance.' );
+
+		}
+
+		if ( this.configuration.sync ) this._syncDuration();
+
+		return this;
+
+	}
+
+	/**
+	 * Updates the animation.
+	 *
+	 * @param {Number} delta
+	 * @return {MMDAnimationHelper}
+	 */
+	update( delta ) {
+
+		if ( this.audioManager !== null ) this.audioManager.control( delta );
+
+		for ( let i = 0; i < this.meshes.length; i ++ ) {
+
+			this._animateMesh( this.meshes[ i ], delta );
+
+		}
+
+		if ( this.sharedPhysics ) this._updateSharedPhysics( delta );
+
+		if ( this.camera !== null ) this._animateCamera( this.camera, delta );
+
+		return this;
+
+	}
+
+	/**
+	 * Changes the pose of SkinnedMesh as VPD specifies.
+	 *
+	 * @param {THREE.SkinnedMesh} mesh
+	 * @param {Object} vpd - VPD content parsed MMDParser
+	 * @param {Object} params - (optional)
+	 * @param {boolean} params.resetPose - Default is true.
+	 * @param {boolean} params.ik - Default is true.
+	 * @param {boolean} params.grant - Default is true.
+	 * @return {MMDAnimationHelper}
+	 */
+	pose( mesh, vpd, params = {} ) {
+
+		if ( params.resetPose !== false ) mesh.pose();
+
+		const bones = mesh.skeleton.bones;
+		const boneParams = vpd.bones;
+
+		const boneNameDictionary = {};
+
+		for ( let i = 0, il = bones.length; i < il; i ++ ) {
+
+			boneNameDictionary[ bones[ i ].name ] = i;
+
+		}
+
+		const vector = new Vector3();
+		const quaternion = new Quaternion();
+
+		for ( let i = 0, il = boneParams.length; i < il; i ++ ) {
+
+			const boneParam = boneParams[ i ];
+			const boneIndex = boneNameDictionary[ boneParam.name ];
+
+			if ( boneIndex === undefined ) continue;
+
+			const bone = bones[ boneIndex ];
+			bone.position.add( vector.fromArray( boneParam.translation ) );
+			bone.quaternion.multiply( quaternion.fromArray( boneParam.quaternion ) );
+
+		}
+
+		mesh.updateMatrixWorld( true );
+
+		// PMX animation system special path
+		if ( this.configuration.pmxAnimation &&
+			mesh.geometry.userData.MMD && mesh.geometry.userData.MMD.format === 'pmx' ) {
+
+			const sortedBonesData = this._sortBoneDataArray( mesh.geometry.userData.MMD.bones.slice() );
+			const ikSolver = params.ik !== false ? this._createCCDIKSolver( mesh ) : null;
+			const grantSolver = params.grant !== false ? this.createGrantSolver( mesh ) : null;
+			this._animatePMXMesh( mesh, sortedBonesData, ikSolver, grantSolver );
+
+		} else {
+
+			if ( params.ik !== false ) {
+
+				this._createCCDIKSolver( mesh ).update();
+
+			}
+
+			if ( params.grant !== false ) {
+
+				this.createGrantSolver( mesh ).update();
+
+			}
+
+		}
+
+		return this;
+
+	}
+
+	/**
+	 * Enabes/Disables an animation feature.
+	 *
+	 * @param {string} key
+	 * @param {boolean} enabled
+	 * @return {MMDAnimationHelper}
+	 */
+	enable( key, enabled ) {
+
+		if ( this.enabled[ key ] === undefined ) {
+
+			throw new Error( 'THREE.MMDAnimationHelper.enable: '
+				+ 'unknown key ' + key );
+
+		}
+
+		this.enabled[ key ] = enabled;
+
+		if ( key === 'physics' ) {
+
+			for ( let i = 0, il = this.meshes.length; i < il; i ++ ) {
+
+				this._optimizeIK( this.meshes[ i ], enabled );
+
+			}
+
+		}
+
+		return this;
+
+	}
+
+	/**
+	 * Creates an GrantSolver instance.
+	 *
+	 * @param {THREE.SkinnedMesh} mesh
+	 * @return {GrantSolver}
+	 */
+	createGrantSolver( mesh ) {
+
+		return new GrantSolver( mesh, mesh.geometry.userData.MMD.grants );
+
+	}
+
+	// private methods
+
+	_addMesh( mesh, params ) {
+
+		if ( this.meshes.indexOf( mesh ) >= 0 ) {
+
+			throw new Error( 'THREE.MMDAnimationHelper._addMesh: '
+				+ 'SkinnedMesh \'' + mesh.name + '\' has already been added.' );
+
+		}
+
+		this.meshes.push( mesh );
+		this.objects.set( mesh, { looped: false } );
+
+		this._setupMeshAnimation( mesh, params.animation );
+
+		if ( params.physics !== false ) {
+
+			this._setupMeshPhysics( mesh, params );
+
+		}
+
+		return this;
+
+	}
+
+	_setupCamera( camera, params ) {
+
+		if ( this.camera === camera ) {
+
+			throw new Error( 'THREE.MMDAnimationHelper._setupCamera: '
+				+ 'Camera \'' + camera.name + '\' has already been set.' );
+
+		}
+
+		if ( this.camera ) this.clearCamera( this.camera );
+
+		this.camera = camera;
+
+		camera.add( this.cameraTarget );
+
+		this.objects.set( camera, {} );
+
+		if ( params.animation !== undefined ) {
+
+			this._setupCameraAnimation( camera, params.animation );
+
+		}
+
+		return this;
+
+	}
+
+	_setupAudio( audio, params ) {
+
+		if ( this.audio === audio ) {
+
+			throw new Error( 'THREE.MMDAnimationHelper._setupAudio: '
+				+ 'Audio \'' + audio.name + '\' has already been set.' );
+
+		}
+
+		if ( this.audio ) this.clearAudio( this.audio );
+
+		this.audio = audio;
+		this.audioManager = new AudioManager( audio, params );
+
+		this.objects.set( this.audioManager, {
+			duration: this.audioManager.duration
+		} );
+
+		return this;
+
+	}
+
+	_removeMesh( mesh ) {
+
+		let found = false;
+		let writeIndex = 0;
+
+		for ( let i = 0, il = this.meshes.length; i < il; i ++ ) {
+
+			if ( this.meshes[ i ] === mesh ) {
+
+				this.objects.delete( mesh );
+				found = true;
+
+				continue;
+
+			}
+
+			this.meshes[ writeIndex ++ ] = this.meshes[ i ];
+
+		}
+
+		if ( ! found ) {
+
+			throw new Error( 'THREE.MMDAnimationHelper._removeMesh: '
+				+ 'SkinnedMesh \'' + mesh.name + '\' has not been added yet.' );
+
+		}
+
+		this.meshes.length = writeIndex;
+
+		return this;
+
+	}
+
+	_clearCamera( camera ) {
+
+		if ( camera !== this.camera ) {
+
+			throw new Error( 'THREE.MMDAnimationHelper._clearCamera: '
+				+ 'Camera \'' + camera.name + '\' has not been set yet.' );
+
+		}
+
+		this.camera.remove( this.cameraTarget );
+
+		this.objects.delete( this.camera );
+		this.camera = null;
+
+		return this;
+
+	}
+
+	_clearAudio( audio ) {
+
+		if ( audio !== this.audio ) {
+
+			throw new Error( 'THREE.MMDAnimationHelper._clearAudio: '
+				+ 'Audio \'' + audio.name + '\' has not been set yet.' );
+
+		}
+
+		this.objects.delete( this.audioManager );
+
+		this.audio = null;
+		this.audioManager = null;
+
+		return this;
+
+	}
+
+	_setupMeshAnimation( mesh, animation ) {
+
+		const objects = this.objects.get( mesh );
+
+		if ( animation !== undefined ) {
+
+			const animations = Array.isArray( animation )
+				? animation : [ animation ];
+
+			objects.mixer = new AnimationMixer( mesh );
+
+			for ( let i = 0, il = animations.length; i < il; i ++ ) {
+
+				objects.mixer.clipAction( animations[ i ] ).play();
+
+			}
+
+			// TODO: find a workaround not to access ._clip looking like a private property
+			objects.mixer.addEventListener( 'loop', function ( event ) {
+
+				const tracks = event.action._clip.tracks;
+
+				if ( tracks.length > 0 && tracks[ 0 ].name.slice( 0, 6 ) !== '.bones' ) return;
+
+				objects.looped = true;
+
+			} );
+
+		}
+
+		objects.ikSolver = this._createCCDIKSolver( mesh );
+		objects.grantSolver = this.createGrantSolver( mesh );
+
+		return this;
+
+	}
+
+	_setupCameraAnimation( camera, animation ) {
+
+		const animations = Array.isArray( animation )
+			? animation : [ animation ];
+
+		const objects = this.objects.get( camera );
+
+		objects.mixer = new AnimationMixer( camera );
+
+		for ( let i = 0, il = animations.length; i < il; i ++ ) {
+
+			objects.mixer.clipAction( animations[ i ] ).play();
+
+		}
+
+	}
+
+	_setupMeshPhysics( mesh, params ) {
+
+		const objects = this.objects.get( mesh );
+
+		// shared physics is experimental
+
+		if ( params.world === undefined && this.sharedPhysics ) {
+
+			const masterPhysics = this._getMasterPhysics();
+
+			if ( masterPhysics !== null ) world = masterPhysics.world; // eslint-disable-line no-undef
+
+		}
+
+		objects.physics = this._createMMDPhysics( mesh, params );
+
+		if ( objects.mixer && params.animationWarmup !== false ) {
+
+			this._animateMesh( mesh, 0 );
+			objects.physics.reset();
+
+		}
+
+		objects.physics.warmup( params.warmup !== undefined ? params.warmup : 60 );
+
+		this._optimizeIK( mesh, true );
+
+	}
+
+	_animateMesh( mesh, delta ) {
+
+		const objects = this.objects.get( mesh );
+
+		const mixer = objects.mixer;
+		const ikSolver = objects.ikSolver;
+		const grantSolver = objects.grantSolver;
+		const physics = objects.physics;
+		const looped = objects.looped;
+
+		if ( mixer && this.enabled.animation ) {
+
+			// alternate solution to save/restore bones but less performant?
+			//mesh.pose();
+			//this._updatePropertyMixersBuffer( mesh );
+
+			this._restoreBones( mesh );
+
+			mixer.update( delta );
+
+			this._saveBones( mesh );
+
+			// PMX animation system special path
+			if ( this.configuration.pmxAnimation &&
+				mesh.geometry.userData.MMD && mesh.geometry.userData.MMD.format === 'pmx' ) {
+
+				if ( ! objects.sortedBonesData ) objects.sortedBonesData = this._sortBoneDataArray( mesh.geometry.userData.MMD.bones.slice() );
+
+				this._animatePMXMesh(
+					mesh,
+					objects.sortedBonesData,
+					ikSolver && this.enabled.ik ? ikSolver : null,
+					grantSolver && this.enabled.grant ? grantSolver : null
+				);
+
+			} else {
+
+				if ( ikSolver && this.enabled.ik ) {
+
+					mesh.updateMatrixWorld( true );
+					ikSolver.update();
+
+				}
+
+				if ( grantSolver && this.enabled.grant ) {
+
+					grantSolver.update();
+
+				}
+
+			}
+
+		}
+
+		if ( looped === true && this.enabled.physics ) {
+
+			if ( physics && this.configuration.resetPhysicsOnLoop ) physics.reset();
+
+			objects.looped = false;
+
+		}
+
+		if ( physics && this.enabled.physics && ! this.sharedPhysics ) {
+
+			this.onBeforePhysics( mesh );
+			physics.update( delta );
+
+		}
+
+	}
+
+	// Sort bones in order by 1. transformationClass and 2. bone index.
+	// In PMX animation system, bone transformations should be processed
+	// in this order.
+	_sortBoneDataArray( boneDataArray ) {
+
+		return boneDataArray.sort( function ( a, b ) {
+
+			if ( a.transformationClass !== b.transformationClass ) {
+
+				return a.transformationClass - b.transformationClass;
+
+			} else {
+
+				return a.index - b.index;
+
+			}
+
+		} );
+
+	}
+
+	// PMX Animation system is a bit too complex and doesn't great match to
+	// Three.js Animation system. This method attempts to simulate it as much as
+	// possible but doesn't perfectly simulate.
+	// This method is more costly than the regular one so
+	// you are recommended to set constructor parameter "pmxAnimation: true"
+	// only if your PMX model animation doesn't work well.
+	// If you need better method you would be required to write your own.
+	_animatePMXMesh( mesh, sortedBonesData, ikSolver, grantSolver ) {
+
+		_quaternionIndex = 0;
+		_grantResultMap.clear();
+
+		for ( let i = 0, il = sortedBonesData.length; i < il; i ++ ) {
+
+			updateOne( mesh, sortedBonesData[ i ].index, ikSolver, grantSolver );
+
+		}
+
+		mesh.updateMatrixWorld( true );
+		return this;
+
+	}
+
+	_animateCamera( camera, delta ) {
+
+		const mixer = this.objects.get( camera ).mixer;
+
+		if ( mixer && this.enabled.cameraAnimation ) {
+
+			mixer.update( delta );
+
+			camera.updateProjectionMatrix();
+
+			camera.up.set( 0, 1, 0 );
+			camera.up.applyQuaternion( camera.quaternion );
+			camera.lookAt( this.cameraTarget.position );
+
+		}
+
+	}
+
+	_optimizeIK( mesh, physicsEnabled ) {
+
+		const iks = mesh.geometry.userData.MMD.iks;
+		const bones = mesh.geometry.userData.MMD.bones;
+
+		for ( let i = 0, il = iks.length; i < il; i ++ ) {
+
+			const ik = iks[ i ];
+			const links = ik.links;
+
+			for ( let j = 0, jl = links.length; j < jl; j ++ ) {
+
+				const link = links[ j ];
+
+				if ( physicsEnabled === true ) {
+
+					// disable IK of the bone the corresponding rigidBody type of which is 1 or 2
+					// because its rotation will be overriden by physics
+					link.enabled = bones[ link.index ].rigidBodyType > 0 ? false : true;
+
+				} else {
+
+					link.enabled = true;
+
+				}
+
+			}
+
+		}
+
+	}
+
+	_createCCDIKSolver( mesh ) {
+
+		if ( CCDIKSolver === undefined ) {
+
+			throw new Error( 'THREE.MMDAnimationHelper: Import CCDIKSolver.' );
+
+		}
+
+		return new CCDIKSolver( mesh, mesh.geometry.userData.MMD.iks );
+
+	}
+
+	_createMMDPhysics( mesh, params ) {
+
+		if ( MMDPhysics === undefined ) {
+
+			throw new Error( 'THREE.MMDPhysics: Import MMDPhysics.' );
+
+		}
+
+		return new MMDPhysics(
+			mesh,
+			mesh.geometry.userData.MMD.rigidBodies,
+			mesh.geometry.userData.MMD.constraints,
+			params );
+
+	}
+
+	/*
+	 * Detects the longest duration and then sets it to them to sync.
+	 * TODO: Not to access private properties ( ._actions and ._clip )
+	 */
+	_syncDuration() {
+
+		let max = 0.0;
+
+		const objects = this.objects;
+		const meshes = this.meshes;
+		const camera = this.camera;
+		const audioManager = this.audioManager;
+
+		// get the longest duration
+
+		for ( let i = 0, il = meshes.length; i < il; i ++ ) {
+
+			const mixer = this.objects.get( meshes[ i ] ).mixer;
+
+			if ( mixer === undefined ) continue;
+
+			for ( let j = 0; j < mixer._actions.length; j ++ ) {
+
+				const clip = mixer._actions[ j ]._clip;
+
+				if ( ! objects.has( clip ) ) {
+
+					objects.set( clip, {
+						duration: clip.duration
+					} );
+
+				}
+
+				max = Math.max( max, objects.get( clip ).duration );
+
+			}
+
+		}
+
+		if ( camera !== null ) {
+
+			const mixer = this.objects.get( camera ).mixer;
+
+			if ( mixer !== undefined ) {
+
+				for ( let i = 0, il = mixer._actions.length; i < il; i ++ ) {
+
+					const clip = mixer._actions[ i ]._clip;
+
+					if ( ! objects.has( clip ) ) {
+
+						objects.set( clip, {
+							duration: clip.duration
+						} );
+
+					}
+
+					max = Math.max( max, objects.get( clip ).duration );
+
+				}
+
+			}
+
+		}
+
+		if ( audioManager !== null ) {
+
+			max = Math.max( max, objects.get( audioManager ).duration );
+
+		}
+
+		max += this.configuration.afterglow;
+
+		// update the duration
+
+		for ( let i = 0, il = this.meshes.length; i < il; i ++ ) {
+
+			const mixer = this.objects.get( this.meshes[ i ] ).mixer;
+
+			if ( mixer === undefined ) continue;
+
+			for ( let j = 0, jl = mixer._actions.length; j < jl; j ++ ) {
+
+				mixer._actions[ j ]._clip.duration = max;
+
+			}
+
+		}
+
+		if ( camera !== null ) {
+
+			const mixer = this.objects.get( camera ).mixer;
+
+			if ( mixer !== undefined ) {
+
+				for ( let i = 0, il = mixer._actions.length; i < il; i ++ ) {
+
+					mixer._actions[ i ]._clip.duration = max;
+
+				}
+
+			}
+
+		}
+
+		if ( audioManager !== null ) {
+
+			audioManager.duration = max;
+
+		}
+
+	}
+
+	// workaround
+
+	_updatePropertyMixersBuffer( mesh ) {
+
+		const mixer = this.objects.get( mesh ).mixer;
+
+		const propertyMixers = mixer._bindings;
+		const accuIndex = mixer._accuIndex;
+
+		for ( let i = 0, il = propertyMixers.length; i < il; i ++ ) {
+
+			const propertyMixer = propertyMixers[ i ];
+			const buffer = propertyMixer.buffer;
+			const stride = propertyMixer.valueSize;
+			const offset = ( accuIndex + 1 ) * stride;
+
+			propertyMixer.binding.getValue( buffer, offset );
+
+		}
+
+	}
+
+	/*
+	 * Avoiding these two issues by restore/save bones before/after mixer animation.
+	 *
+	 * 1. PropertyMixer used by AnimationMixer holds cache value in .buffer.
+	 *    Calculating IK, Grant, and Physics after mixer animation can break
+	 *    the cache coherency.
+	 *
+	 * 2. Applying Grant two or more times without reset the posing breaks model.
+	 */
+	_saveBones( mesh ) {
+
+		const objects = this.objects.get( mesh );
+
+		const bones = mesh.skeleton.bones;
+
+		let backupBones = objects.backupBones;
+
+		if ( backupBones === undefined ) {
+
+			backupBones = new Float32Array( bones.length * 7 );
+			objects.backupBones = backupBones;
+
+		}
+
+		for ( let i = 0, il = bones.length; i < il; i ++ ) {
+
+			const bone = bones[ i ];
+			bone.position.toArray( backupBones, i * 7 );
+			bone.quaternion.toArray( backupBones, i * 7 + 3 );
+
+		}
+
+	}
+
+	_restoreBones( mesh ) {
+
+		const objects = this.objects.get( mesh );
+
+		const backupBones = objects.backupBones;
+
+		if ( backupBones === undefined ) return;
+
+		const bones = mesh.skeleton.bones;
+
+		for ( let i = 0, il = bones.length; i < il; i ++ ) {
+
+			const bone = bones[ i ];
+			bone.position.fromArray( backupBones, i * 7 );
+			bone.quaternion.fromArray( backupBones, i * 7 + 3 );
+
+		}
+
+	}
+
+	// experimental
+
+	_getMasterPhysics() {
+
+		if ( this.masterPhysics !== null ) return this.masterPhysics;
+
+		for ( let i = 0, il = this.meshes.length; i < il; i ++ ) {
+
+			const physics = this.meshes[ i ].physics;
+
+			if ( physics !== undefined && physics !== null ) {
+
+				this.masterPhysics = physics;
+				return this.masterPhysics;
+
+			}
+
+		}
+
+		return null;
+
+	}
+
+	_updateSharedPhysics( delta ) {
+
+		if ( this.meshes.length === 0 || ! this.enabled.physics || ! this.sharedPhysics ) return;
+
+		const physics = this._getMasterPhysics();
+
+		if ( physics === null ) return;
+
+		for ( let i = 0, il = this.meshes.length; i < il; i ++ ) {
+
+			const p = this.meshes[ i ].physics;
+
+			if ( p !== null && p !== undefined ) {
+
+				p.updateRigidBodies();
+
+			}
+
+		}
+
+		physics.stepSimulation( delta );
+
+		for ( let i = 0, il = this.meshes.length; i < il; i ++ ) {
+
+			const p = this.meshes[ i ].physics;
+
+			if ( p !== null && p !== undefined ) {
+
+				p.updateBones();
+
+			}
+
+		}
+
+	}
+
+}
+
+// Keep working quaternions for less GC
+const _quaternions = [];
+let _quaternionIndex = 0;
+
+function getQuaternion() {
+
+	if ( _quaternionIndex >= _quaternions.length ) {
+
+		_quaternions.push( new Quaternion() );
+
+	}
+
+	return _quaternions[ _quaternionIndex ++ ];
+
+}
+
+// Save rotation whose grant and IK are already applied
+// used by grant children
+const _grantResultMap = new Map();
+
+function updateOne( mesh, boneIndex, ikSolver, grantSolver ) {
+
+	const bones = mesh.skeleton.bones;
+	const bonesData = mesh.geometry.userData.MMD.bones;
+	const boneData = bonesData[ boneIndex ];
+	const bone = bones[ boneIndex ];
+
+	// Return if already updated by being referred as a grant parent.
+	if ( _grantResultMap.has( boneIndex ) ) return;
+
+	const quaternion = getQuaternion();
+
+	// Initialize grant result here to prevent infinite loop.
+	// If it's referred before updating with actual result later
+	// result without applyting IK or grant is gotten
+	// but better than composing of infinite loop.
+	_grantResultMap.set( boneIndex, quaternion.copy( bone.quaternion ) );
+
+	// @TODO: Support global grant and grant position
+	if ( grantSolver && boneData.grant &&
+		! boneData.grant.isLocal && boneData.grant.affectRotation ) {
+
+		const parentIndex = boneData.grant.parentIndex;
+		const ratio = boneData.grant.ratio;
+
+		if ( ! _grantResultMap.has( parentIndex ) ) {
+
+			updateOne( mesh, parentIndex, ikSolver, grantSolver );
+
+		}
+
+		grantSolver.addGrantRotation( bone, _grantResultMap.get( parentIndex ), ratio );
+
+	}
+
+	if ( ikSolver && boneData.ik ) {
+
+		// @TODO: Updating world matrices every time solving an IK bone is
+		// costly. Optimize if possible.
+		mesh.updateMatrixWorld( true );
+		ikSolver.updateOne( boneData.ik );
+
+		// No confident, but it seems the grant results with ik links should be updated?
+		const links = boneData.ik.links;
+
+		for ( let i = 0, il = links.length; i < il; i ++ ) {
+
+			const link = links[ i ];
+
+			if ( link.enabled === false ) continue;
+
+			const linkIndex = link.index;
+
+			if ( _grantResultMap.has( linkIndex ) ) {
+
+				_grantResultMap.set( linkIndex, _grantResultMap.get( linkIndex ).copy( bones[ linkIndex ].quaternion ) );
+
+			}
+
+		}
+
+	}
+
+	// Update with the actual result here
+	quaternion.copy( bone.quaternion );
+
+}
+
+//
+
+class AudioManager {
+
+	/**
+	 * @param {THREE.Audio} audio
+	 * @param {Object} params - (optional)
+	 * @param {Nuumber} params.delayTime
+	 */
+	constructor( audio, params = {} ) {
+
+		this.audio = audio;
+
+		this.elapsedTime = 0.0;
+		this.currentTime = 0.0;
+		this.delayTime = params.delayTime !== undefined
+			? params.delayTime : 0.0;
+
+		this.audioDuration = this.audio.buffer.duration;
+		this.duration = this.audioDuration + this.delayTime;
+
+	}
+
+	/**
+	 * @param {Number} delta
+	 * @return {AudioManager}
+	 */
+	control( delta ) {
+
+		this.elapsed += delta;
+		this.currentTime += delta;
+
+		if ( this._shouldStopAudio() ) this.audio.stop();
+		if ( this._shouldStartAudio() ) this.audio.play();
+
+		return this;
+
+	}
+
+	// private methods
+
+	_shouldStartAudio() {
+
+		if ( this.audio.isPlaying ) return false;
+
+		while ( this.currentTime >= this.duration ) {
+
+			this.currentTime -= this.duration;
+
+		}
+
+		if ( this.currentTime < this.delayTime ) return false;
+
+		// 'duration' can be bigger than 'audioDuration + delayTime' because of sync configuration
+		if ( ( this.currentTime - this.delayTime ) > this.audioDuration ) return false;
+
+		return true;
+
+	}
+
+	_shouldStopAudio() {
+
+		return this.audio.isPlaying &&
+			this.currentTime >= this.duration;
+
+	}
+
+}
+
+const _q = new Quaternion();
+
+/**
+ * Solver for Grant (Fuyo in Japanese. I just google translated because
+ * Fuyo may be MMD specific term and may not be common word in 3D CG terms.)
+ * Grant propagates a bone's transform to other bones transforms even if
+ * they are not children.
+ * @param {THREE.SkinnedMesh} mesh
+ * @param {Array<Object>} grants
+ */
+class GrantSolver {
+
+	constructor( mesh, grants = [] ) {
+
+		this.mesh = mesh;
+		this.grants = grants;
+
+	}
+
+	/**
+	 * Solve all the grant bones
+	 * @return {GrantSolver}
+	 */
+	update() {
+
+		const grants = this.grants;
+
+		for ( let i = 0, il = grants.length; i < il; i ++ ) {
+
+			this.updateOne( grants[ i ] );
+
+		}
+
+		return this;
+
+	}
+
+	/**
+	 * Solve a grant bone
+	 * @param {Object} grant - grant parameter
+	 * @return {GrantSolver}
+	 */
+	updateOne( grant ) {
+
+		const bones = this.mesh.skeleton.bones;
+		const bone = bones[ grant.index ];
+		const parentBone = bones[ grant.parentIndex ];
+
+		if ( grant.isLocal ) {
+
+			// TODO: implement
+			if ( grant.affectPosition ) {
+
+			}
+
+			// TODO: implement
+			if ( grant.affectRotation ) {
+
+			}
+
+		} else {
+
+			// TODO: implement
+			if ( grant.affectPosition ) {
+
+			}
+
+			if ( grant.affectRotation ) {
+
+				this.addGrantRotation( bone, parentBone.quaternion, grant.ratio );
+
+			}
+
+		}
+
+		return this;
+
+	}
+
+	addGrantRotation( bone, q, ratio ) {
+
+		_q.set( 0, 0, 0, 1 );
+		_q.slerp( q, ratio );
+		bone.quaternion.multiply( _q );
+
+		return this;
+
+	}
+
+}
+
+export { MMDAnimationHelper };

+ 1381 - 0
public/archive/static/js/jsm/animation/MMDPhysics.js

@@ -0,0 +1,1381 @@
+import {
+	Bone,
+	BoxGeometry,
+	CapsuleGeometry,
+	Color,
+	Euler,
+	Matrix4,
+	Mesh,
+	MeshBasicMaterial,
+	Object3D,
+	Quaternion,
+	SphereGeometry,
+	Vector3
+} from 'three';
+
+/**
+ * Dependencies
+ *  - Ammo.js https://github.com/kripken/ammo.js
+ *
+ * MMDPhysics calculates physics with Ammo(Bullet based JavaScript Physics engine)
+ * for MMD model loaded by MMDLoader.
+ *
+ * TODO
+ *  - Physics in Worker
+ */
+
+/* global Ammo */
+
+class MMDPhysics {
+
+	/**
+	 * @param {THREE.SkinnedMesh} mesh
+	 * @param {Array<Object>} rigidBodyParams
+	 * @param {Array<Object>} (optional) constraintParams
+	 * @param {Object} params - (optional)
+	 * @param {Number} params.unitStep - Default is 1 / 65.
+	 * @param {Integer} params.maxStepNum - Default is 3.
+	 * @param {Vector3} params.gravity - Default is ( 0, - 9.8 * 10, 0 )
+	 */
+	constructor( mesh, rigidBodyParams, constraintParams = [], params = {} ) {
+
+		if ( typeof Ammo === 'undefined' ) {
+
+			throw new Error( 'THREE.MMDPhysics: Import ammo.js https://github.com/kripken/ammo.js' );
+
+		}
+
+		this.manager = new ResourceManager();
+
+		this.mesh = mesh;
+
+		/*
+		 * I don't know why but 1/60 unitStep easily breaks models
+		 * so I set it 1/65 so far.
+		 * Don't set too small unitStep because
+		 * the smaller unitStep can make the performance worse.
+		 */
+		this.unitStep = ( params.unitStep !== undefined ) ? params.unitStep : 1 / 65;
+		this.maxStepNum = ( params.maxStepNum !== undefined ) ? params.maxStepNum : 3;
+		this.gravity = new Vector3( 0, - 9.8 * 10, 0 );
+
+		if ( params.gravity !== undefined ) this.gravity.copy( params.gravity );
+
+		this.world = params.world !== undefined ? params.world : null; // experimental
+
+		this.bodies = [];
+		this.constraints = [];
+
+		this._init( mesh, rigidBodyParams, constraintParams );
+
+	}
+
+	/**
+	 * Advances Physics calculation and updates bones.
+	 *
+	 * @param {Number} delta - time in second
+	 * @return {MMDPhysics}
+	 */
+	update( delta ) {
+
+		const manager = this.manager;
+		const mesh = this.mesh;
+
+		// rigid bodies and constrains are for
+		// mesh's world scale (1, 1, 1).
+		// Convert to (1, 1, 1) if it isn't.
+
+		let isNonDefaultScale = false;
+
+		const position = manager.allocThreeVector3();
+		const quaternion = manager.allocThreeQuaternion();
+		const scale = manager.allocThreeVector3();
+
+		mesh.matrixWorld.decompose( position, quaternion, scale );
+
+		if ( scale.x !== 1 || scale.y !== 1 || scale.z !== 1 ) {
+
+			isNonDefaultScale = true;
+
+		}
+
+		let parent;
+
+		if ( isNonDefaultScale ) {
+
+			parent = mesh.parent;
+
+			if ( parent !== null ) mesh.parent = null;
+
+			scale.copy( this.mesh.scale );
+
+			mesh.scale.set( 1, 1, 1 );
+			mesh.updateMatrixWorld( true );
+
+		}
+
+		// calculate physics and update bones
+
+		this._updateRigidBodies();
+		this._stepSimulation( delta );
+		this._updateBones();
+
+		// restore mesh if converted above
+
+		if ( isNonDefaultScale ) {
+
+			if ( parent !== null ) mesh.parent = parent;
+
+			mesh.scale.copy( scale );
+
+		}
+
+		manager.freeThreeVector3( scale );
+		manager.freeThreeQuaternion( quaternion );
+		manager.freeThreeVector3( position );
+
+		return this;
+
+	}
+
+	/**
+	 * Resets rigid bodies transorm to current bone's.
+	 *
+	 * @return {MMDPhysics}
+	 */
+	reset() {
+
+		for ( let i = 0, il = this.bodies.length; i < il; i ++ ) {
+
+			this.bodies[ i ].reset();
+
+		}
+
+		return this;
+
+	}
+
+	/**
+	 * Warm ups Rigid bodies. Calculates cycles steps.
+	 *
+	 * @param {Integer} cycles
+	 * @return {MMDPhysics}
+	 */
+	warmup( cycles ) {
+
+		for ( let i = 0; i < cycles; i ++ ) {
+
+			this.update( 1 / 60 );
+
+		}
+
+		return this;
+
+	}
+
+	/**
+	 * Sets gravity.
+	 *
+	 * @param {Vector3} gravity
+	 * @return {MMDPhysicsHelper}
+	 */
+	setGravity( gravity ) {
+
+		this.world.setGravity( new Ammo.btVector3( gravity.x, gravity.y, gravity.z ) );
+		this.gravity.copy( gravity );
+
+		return this;
+
+	}
+
+	/**
+	 * Creates MMDPhysicsHelper
+	 *
+	 * @return {MMDPhysicsHelper}
+	 */
+	createHelper() {
+
+		return new MMDPhysicsHelper( this.mesh, this );
+
+	}
+
+	// private methods
+
+	_init( mesh, rigidBodyParams, constraintParams ) {
+
+		const manager = this.manager;
+
+		// rigid body/constraint parameters are for
+		// mesh's default world transform as position(0, 0, 0),
+		// quaternion(0, 0, 0, 1) and scale(0, 0, 0)
+
+		const parent = mesh.parent;
+
+		if ( parent !== null ) mesh.parent = null;
+
+		const currentPosition = manager.allocThreeVector3();
+		const currentQuaternion = manager.allocThreeQuaternion();
+		const currentScale = manager.allocThreeVector3();
+
+		currentPosition.copy( mesh.position );
+		currentQuaternion.copy( mesh.quaternion );
+		currentScale.copy( mesh.scale );
+
+		mesh.position.set( 0, 0, 0 );
+		mesh.quaternion.set( 0, 0, 0, 1 );
+		mesh.scale.set( 1, 1, 1 );
+
+		mesh.updateMatrixWorld( true );
+
+		if ( this.world === null ) {
+
+			this.world = this._createWorld();
+			this.setGravity( this.gravity );
+
+		}
+
+		this._initRigidBodies( rigidBodyParams );
+		this._initConstraints( constraintParams );
+
+		if ( parent !== null ) mesh.parent = parent;
+
+		mesh.position.copy( currentPosition );
+		mesh.quaternion.copy( currentQuaternion );
+		mesh.scale.copy( currentScale );
+
+		mesh.updateMatrixWorld( true );
+
+		this.reset();
+
+		manager.freeThreeVector3( currentPosition );
+		manager.freeThreeQuaternion( currentQuaternion );
+		manager.freeThreeVector3( currentScale );
+
+	}
+
+	_createWorld() {
+
+		const config = new Ammo.btDefaultCollisionConfiguration();
+		const dispatcher = new Ammo.btCollisionDispatcher( config );
+		const cache = new Ammo.btDbvtBroadphase();
+		const solver = new Ammo.btSequentialImpulseConstraintSolver();
+		const world = new Ammo.btDiscreteDynamicsWorld( dispatcher, cache, solver, config );
+		return world;
+
+	}
+
+	_initRigidBodies( rigidBodies ) {
+
+		for ( let i = 0, il = rigidBodies.length; i < il; i ++ ) {
+
+			this.bodies.push( new RigidBody(
+				this.mesh, this.world, rigidBodies[ i ], this.manager ) );
+
+		}
+
+	}
+
+	_initConstraints( constraints ) {
+
+		for ( let i = 0, il = constraints.length; i < il; i ++ ) {
+
+			const params = constraints[ i ];
+			const bodyA = this.bodies[ params.rigidBodyIndex1 ];
+			const bodyB = this.bodies[ params.rigidBodyIndex2 ];
+			this.constraints.push( new Constraint( this.mesh, this.world, bodyA, bodyB, params, this.manager ) );
+
+		}
+
+	}
+
+	_stepSimulation( delta ) {
+
+		const unitStep = this.unitStep;
+		let stepTime = delta;
+		let maxStepNum = ( ( delta / unitStep ) | 0 ) + 1;
+
+		if ( stepTime < unitStep ) {
+
+			stepTime = unitStep;
+			maxStepNum = 1;
+
+		}
+
+		if ( maxStepNum > this.maxStepNum ) {
+
+			maxStepNum = this.maxStepNum;
+
+		}
+
+		this.world.stepSimulation( stepTime, maxStepNum, unitStep );
+
+	}
+
+	_updateRigidBodies() {
+
+		for ( let i = 0, il = this.bodies.length; i < il; i ++ ) {
+
+			this.bodies[ i ].updateFromBone();
+
+		}
+
+	}
+
+	_updateBones() {
+
+		for ( let i = 0, il = this.bodies.length; i < il; i ++ ) {
+
+			this.bodies[ i ].updateBone();
+
+		}
+
+	}
+
+}
+
+/**
+ * This manager's responsibilies are
+ *
+ * 1. manage Ammo.js and Three.js object resources and
+ *    improve the performance and the memory consumption by
+ *    reusing objects.
+ *
+ * 2. provide simple Ammo object operations.
+ */
+class ResourceManager {
+
+	constructor() {
+
+		// for Three.js
+		this.threeVector3s = [];
+		this.threeMatrix4s = [];
+		this.threeQuaternions = [];
+		this.threeEulers = [];
+
+		// for Ammo.js
+		this.transforms = [];
+		this.quaternions = [];
+		this.vector3s = [];
+
+	}
+
+	allocThreeVector3() {
+
+		return ( this.threeVector3s.length > 0 )
+			? this.threeVector3s.pop()
+			: new Vector3();
+
+	}
+
+	freeThreeVector3( v ) {
+
+		this.threeVector3s.push( v );
+
+	}
+
+	allocThreeMatrix4() {
+
+		return ( this.threeMatrix4s.length > 0 )
+			? this.threeMatrix4s.pop()
+			: new Matrix4();
+
+	}
+
+	freeThreeMatrix4( m ) {
+
+		this.threeMatrix4s.push( m );
+
+	}
+
+	allocThreeQuaternion() {
+
+		return ( this.threeQuaternions.length > 0 )
+			? this.threeQuaternions.pop()
+			: new Quaternion();
+
+	}
+
+	freeThreeQuaternion( q ) {
+
+		this.threeQuaternions.push( q );
+
+	}
+
+	allocThreeEuler() {
+
+		return ( this.threeEulers.length > 0 )
+			? this.threeEulers.pop()
+			: new Euler();
+
+	}
+
+	freeThreeEuler( e ) {
+
+		this.threeEulers.push( e );
+
+	}
+
+	allocTransform() {
+
+		return ( this.transforms.length > 0 )
+			? this.transforms.pop()
+			: new Ammo.btTransform();
+
+	}
+
+	freeTransform( t ) {
+
+		this.transforms.push( t );
+
+	}
+
+	allocQuaternion() {
+
+		return ( this.quaternions.length > 0 )
+			? this.quaternions.pop()
+			: new Ammo.btQuaternion();
+
+	}
+
+	freeQuaternion( q ) {
+
+		this.quaternions.push( q );
+
+	}
+
+	allocVector3() {
+
+		return ( this.vector3s.length > 0 )
+			? this.vector3s.pop()
+			: new Ammo.btVector3();
+
+	}
+
+	freeVector3( v ) {
+
+		this.vector3s.push( v );
+
+	}
+
+	setIdentity( t ) {
+
+		t.setIdentity();
+
+	}
+
+	getBasis( t ) {
+
+		var q = this.allocQuaternion();
+		t.getBasis().getRotation( q );
+		return q;
+
+	}
+
+	getBasisAsMatrix3( t ) {
+
+		var q = this.getBasis( t );
+		var m = this.quaternionToMatrix3( q );
+		this.freeQuaternion( q );
+		return m;
+
+	}
+
+	getOrigin( t ) {
+
+		return t.getOrigin();
+
+	}
+
+	setOrigin( t, v ) {
+
+		t.getOrigin().setValue( v.x(), v.y(), v.z() );
+
+	}
+
+	copyOrigin( t1, t2 ) {
+
+		var o = t2.getOrigin();
+		this.setOrigin( t1, o );
+
+	}
+
+	setBasis( t, q ) {
+
+		t.setRotation( q );
+
+	}
+
+	setBasisFromMatrix3( t, m ) {
+
+		var q = this.matrix3ToQuaternion( m );
+		this.setBasis( t, q );
+		this.freeQuaternion( q );
+
+	}
+
+	setOriginFromArray3( t, a ) {
+
+		t.getOrigin().setValue( a[ 0 ], a[ 1 ], a[ 2 ] );
+
+	}
+
+	setOriginFromThreeVector3( t, v ) {
+
+		t.getOrigin().setValue( v.x, v.y, v.z );
+
+	}
+
+	setBasisFromArray3( t, a ) {
+
+		var thQ = this.allocThreeQuaternion();
+		var thE = this.allocThreeEuler();
+		thE.set( a[ 0 ], a[ 1 ], a[ 2 ] );
+		this.setBasisFromThreeQuaternion( t, thQ.setFromEuler( thE ) );
+
+		this.freeThreeEuler( thE );
+		this.freeThreeQuaternion( thQ );
+
+	}
+
+	setBasisFromThreeQuaternion( t, a ) {
+
+		var q = this.allocQuaternion();
+
+		q.setX( a.x );
+		q.setY( a.y );
+		q.setZ( a.z );
+		q.setW( a.w );
+		this.setBasis( t, q );
+
+		this.freeQuaternion( q );
+
+	}
+
+	multiplyTransforms( t1, t2 ) {
+
+		var t = this.allocTransform();
+		this.setIdentity( t );
+
+		var m1 = this.getBasisAsMatrix3( t1 );
+		var m2 = this.getBasisAsMatrix3( t2 );
+
+		var o1 = this.getOrigin( t1 );
+		var o2 = this.getOrigin( t2 );
+
+		var v1 = this.multiplyMatrix3ByVector3( m1, o2 );
+		var v2 = this.addVector3( v1, o1 );
+		this.setOrigin( t, v2 );
+
+		var m3 = this.multiplyMatrices3( m1, m2 );
+		this.setBasisFromMatrix3( t, m3 );
+
+		this.freeVector3( v1 );
+		this.freeVector3( v2 );
+
+		return t;
+
+	}
+
+	inverseTransform( t ) {
+
+		var t2 = this.allocTransform();
+
+		var m1 = this.getBasisAsMatrix3( t );
+		var o = this.getOrigin( t );
+
+		var m2 = this.transposeMatrix3( m1 );
+		var v1 = this.negativeVector3( o );
+		var v2 = this.multiplyMatrix3ByVector3( m2, v1 );
+
+		this.setOrigin( t2, v2 );
+		this.setBasisFromMatrix3( t2, m2 );
+
+		this.freeVector3( v1 );
+		this.freeVector3( v2 );
+
+		return t2;
+
+	}
+
+	multiplyMatrices3( m1, m2 ) {
+
+		var m3 = [];
+
+		var v10 = this.rowOfMatrix3( m1, 0 );
+		var v11 = this.rowOfMatrix3( m1, 1 );
+		var v12 = this.rowOfMatrix3( m1, 2 );
+
+		var v20 = this.columnOfMatrix3( m2, 0 );
+		var v21 = this.columnOfMatrix3( m2, 1 );
+		var v22 = this.columnOfMatrix3( m2, 2 );
+
+		m3[ 0 ] = this.dotVectors3( v10, v20 );
+		m3[ 1 ] = this.dotVectors3( v10, v21 );
+		m3[ 2 ] = this.dotVectors3( v10, v22 );
+		m3[ 3 ] = this.dotVectors3( v11, v20 );
+		m3[ 4 ] = this.dotVectors3( v11, v21 );
+		m3[ 5 ] = this.dotVectors3( v11, v22 );
+		m3[ 6 ] = this.dotVectors3( v12, v20 );
+		m3[ 7 ] = this.dotVectors3( v12, v21 );
+		m3[ 8 ] = this.dotVectors3( v12, v22 );
+
+		this.freeVector3( v10 );
+		this.freeVector3( v11 );
+		this.freeVector3( v12 );
+		this.freeVector3( v20 );
+		this.freeVector3( v21 );
+		this.freeVector3( v22 );
+
+		return m3;
+
+	}
+
+	addVector3( v1, v2 ) {
+
+		var v = this.allocVector3();
+		v.setValue( v1.x() + v2.x(), v1.y() + v2.y(), v1.z() + v2.z() );
+		return v;
+
+	}
+
+	dotVectors3( v1, v2 ) {
+
+		return v1.x() * v2.x() + v1.y() * v2.y() + v1.z() * v2.z();
+
+	}
+
+	rowOfMatrix3( m, i ) {
+
+		var v = this.allocVector3();
+		v.setValue( m[ i * 3 + 0 ], m[ i * 3 + 1 ], m[ i * 3 + 2 ] );
+		return v;
+
+	}
+
+	columnOfMatrix3( m, i ) {
+
+		var v = this.allocVector3();
+		v.setValue( m[ i + 0 ], m[ i + 3 ], m[ i + 6 ] );
+		return v;
+
+	}
+
+	negativeVector3( v ) {
+
+		var v2 = this.allocVector3();
+		v2.setValue( - v.x(), - v.y(), - v.z() );
+		return v2;
+
+	}
+
+	multiplyMatrix3ByVector3( m, v ) {
+
+		var v4 = this.allocVector3();
+
+		var v0 = this.rowOfMatrix3( m, 0 );
+		var v1 = this.rowOfMatrix3( m, 1 );
+		var v2 = this.rowOfMatrix3( m, 2 );
+		var x = this.dotVectors3( v0, v );
+		var y = this.dotVectors3( v1, v );
+		var z = this.dotVectors3( v2, v );
+
+		v4.setValue( x, y, z );
+
+		this.freeVector3( v0 );
+		this.freeVector3( v1 );
+		this.freeVector3( v2 );
+
+		return v4;
+
+	}
+
+	transposeMatrix3( m ) {
+
+		var m2 = [];
+		m2[ 0 ] = m[ 0 ];
+		m2[ 1 ] = m[ 3 ];
+		m2[ 2 ] = m[ 6 ];
+		m2[ 3 ] = m[ 1 ];
+		m2[ 4 ] = m[ 4 ];
+		m2[ 5 ] = m[ 7 ];
+		m2[ 6 ] = m[ 2 ];
+		m2[ 7 ] = m[ 5 ];
+		m2[ 8 ] = m[ 8 ];
+		return m2;
+
+	}
+
+	quaternionToMatrix3( q ) {
+
+		var m = [];
+
+		var x = q.x();
+		var y = q.y();
+		var z = q.z();
+		var w = q.w();
+
+		var xx = x * x;
+		var yy = y * y;
+		var zz = z * z;
+
+		var xy = x * y;
+		var yz = y * z;
+		var zx = z * x;
+
+		var xw = x * w;
+		var yw = y * w;
+		var zw = z * w;
+
+		m[ 0 ] = 1 - 2 * ( yy + zz );
+		m[ 1 ] = 2 * ( xy - zw );
+		m[ 2 ] = 2 * ( zx + yw );
+		m[ 3 ] = 2 * ( xy + zw );
+		m[ 4 ] = 1 - 2 * ( zz + xx );
+		m[ 5 ] = 2 * ( yz - xw );
+		m[ 6 ] = 2 * ( zx - yw );
+		m[ 7 ] = 2 * ( yz + xw );
+		m[ 8 ] = 1 - 2 * ( xx + yy );
+
+		return m;
+
+	}
+
+	matrix3ToQuaternion( m ) {
+
+		var t = m[ 0 ] + m[ 4 ] + m[ 8 ];
+		var s, x, y, z, w;
+
+		if ( t > 0 ) {
+
+			s = Math.sqrt( t + 1.0 ) * 2;
+			w = 0.25 * s;
+			x = ( m[ 7 ] - m[ 5 ] ) / s;
+			y = ( m[ 2 ] - m[ 6 ] ) / s;
+			z = ( m[ 3 ] - m[ 1 ] ) / s;
+
+		} else if ( ( m[ 0 ] > m[ 4 ] ) && ( m[ 0 ] > m[ 8 ] ) ) {
+
+			s = Math.sqrt( 1.0 + m[ 0 ] - m[ 4 ] - m[ 8 ] ) * 2;
+			w = ( m[ 7 ] - m[ 5 ] ) / s;
+			x = 0.25 * s;
+			y = ( m[ 1 ] + m[ 3 ] ) / s;
+			z = ( m[ 2 ] + m[ 6 ] ) / s;
+
+		} else if ( m[ 4 ] > m[ 8 ] ) {
+
+			s = Math.sqrt( 1.0 + m[ 4 ] - m[ 0 ] - m[ 8 ] ) * 2;
+			w = ( m[ 2 ] - m[ 6 ] ) / s;
+			x = ( m[ 1 ] + m[ 3 ] ) / s;
+			y = 0.25 * s;
+			z = ( m[ 5 ] + m[ 7 ] ) / s;
+
+		} else {
+
+			s = Math.sqrt( 1.0 + m[ 8 ] - m[ 0 ] - m[ 4 ] ) * 2;
+			w = ( m[ 3 ] - m[ 1 ] ) / s;
+			x = ( m[ 2 ] + m[ 6 ] ) / s;
+			y = ( m[ 5 ] + m[ 7 ] ) / s;
+			z = 0.25 * s;
+
+		}
+
+		var q = this.allocQuaternion();
+		q.setX( x );
+		q.setY( y );
+		q.setZ( z );
+		q.setW( w );
+		return q;
+
+	}
+
+}
+
+/**
+ * @param {THREE.SkinnedMesh} mesh
+ * @param {Ammo.btDiscreteDynamicsWorld} world
+ * @param {Object} params
+ * @param {ResourceManager} manager
+ */
+class RigidBody {
+
+	constructor( mesh, world, params, manager ) {
+
+		this.mesh = mesh;
+		this.world = world;
+		this.params = params;
+		this.manager = manager;
+
+		this.body = null;
+		this.bone = null;
+		this.boneOffsetForm = null;
+		this.boneOffsetFormInverse = null;
+
+		this._init();
+
+	}
+
+	/**
+	 * Resets rigid body transform to the current bone's.
+	 *
+	 * @return {RigidBody}
+	 */
+	reset() {
+
+		this._setTransformFromBone();
+		return this;
+
+	}
+
+	/**
+	 * Updates rigid body's transform from the current bone.
+	 *
+	 * @return {RidigBody}
+	 */
+	updateFromBone() {
+
+		if ( this.params.boneIndex !== - 1 && this.params.type === 0 ) {
+
+			this._setTransformFromBone();
+
+		}
+
+		return this;
+
+	}
+
+	/**
+	 * Updates bone from the current ridid body's transform.
+	 *
+	 * @return {RidigBody}
+	 */
+	updateBone() {
+
+		if ( this.params.type === 0 || this.params.boneIndex === - 1 ) {
+
+			return this;
+
+		}
+
+		this._updateBoneRotation();
+
+		if ( this.params.type === 1 ) {
+
+			this._updateBonePosition();
+
+		}
+
+		this.bone.updateMatrixWorld( true );
+
+		if ( this.params.type === 2 ) {
+
+			this._setPositionFromBone();
+
+		}
+
+		return this;
+
+	}
+
+	// private methods
+
+	_init() {
+
+		function generateShape( p ) {
+
+			switch ( p.shapeType ) {
+
+				case 0:
+					return new Ammo.btSphereShape( p.width );
+
+				case 1:
+					return new Ammo.btBoxShape( new Ammo.btVector3( p.width, p.height, p.depth ) );
+
+				case 2:
+					return new Ammo.btCapsuleShape( p.width, p.height );
+
+				default:
+					throw new Error( 'unknown shape type ' + p.shapeType );
+
+			}
+
+		}
+
+		const manager = this.manager;
+		const params = this.params;
+		const bones = this.mesh.skeleton.bones;
+		const bone = ( params.boneIndex === - 1 )
+			? new Bone()
+			: bones[ params.boneIndex ];
+
+		const shape = generateShape( params );
+		const weight = ( params.type === 0 ) ? 0 : params.weight;
+		const localInertia = manager.allocVector3();
+		localInertia.setValue( 0, 0, 0 );
+
+		if ( weight !== 0 ) {
+
+			shape.calculateLocalInertia( weight, localInertia );
+
+		}
+
+		const boneOffsetForm = manager.allocTransform();
+		manager.setIdentity( boneOffsetForm );
+		manager.setOriginFromArray3( boneOffsetForm, params.position );
+		manager.setBasisFromArray3( boneOffsetForm, params.rotation );
+
+		const vector = manager.allocThreeVector3();
+		const boneForm = manager.allocTransform();
+		manager.setIdentity( boneForm );
+		manager.setOriginFromThreeVector3( boneForm, bone.getWorldPosition( vector ) );
+
+		const form = manager.multiplyTransforms( boneForm, boneOffsetForm );
+		const state = new Ammo.btDefaultMotionState( form );
+
+		const info = new Ammo.btRigidBodyConstructionInfo( weight, state, shape, localInertia );
+		info.set_m_friction( params.friction );
+		info.set_m_restitution( params.restitution );
+
+		const body = new Ammo.btRigidBody( info );
+
+		if ( params.type === 0 ) {
+
+			body.setCollisionFlags( body.getCollisionFlags() | 2 );
+
+			/*
+			 * It'd be better to comment out this line though in general I should call this method
+			 * because I'm not sure why but physics will be more like MMD's
+			 * if I comment out.
+			 */
+			body.setActivationState( 4 );
+
+		}
+
+		body.setDamping( params.positionDamping, params.rotationDamping );
+		body.setSleepingThresholds( 0, 0 );
+
+		this.world.addRigidBody( body, 1 << params.groupIndex, params.groupTarget );
+
+		this.body = body;
+		this.bone = bone;
+		this.boneOffsetForm = boneOffsetForm;
+		this.boneOffsetFormInverse = manager.inverseTransform( boneOffsetForm );
+
+		manager.freeVector3( localInertia );
+		manager.freeTransform( form );
+		manager.freeTransform( boneForm );
+		manager.freeThreeVector3( vector );
+
+	}
+
+	_getBoneTransform() {
+
+		const manager = this.manager;
+		const p = manager.allocThreeVector3();
+		const q = manager.allocThreeQuaternion();
+		const s = manager.allocThreeVector3();
+
+		this.bone.matrixWorld.decompose( p, q, s );
+
+		const tr = manager.allocTransform();
+		manager.setOriginFromThreeVector3( tr, p );
+		manager.setBasisFromThreeQuaternion( tr, q );
+
+		const form = manager.multiplyTransforms( tr, this.boneOffsetForm );
+
+		manager.freeTransform( tr );
+		manager.freeThreeVector3( s );
+		manager.freeThreeQuaternion( q );
+		manager.freeThreeVector3( p );
+
+		return form;
+
+	}
+
+	_getWorldTransformForBone() {
+
+		const manager = this.manager;
+		const tr = this.body.getCenterOfMassTransform();
+		return manager.multiplyTransforms( tr, this.boneOffsetFormInverse );
+
+	}
+
+	_setTransformFromBone() {
+
+		const manager = this.manager;
+		const form = this._getBoneTransform();
+
+		// TODO: check the most appropriate way to set
+		//this.body.setWorldTransform( form );
+		this.body.setCenterOfMassTransform( form );
+		this.body.getMotionState().setWorldTransform( form );
+
+		manager.freeTransform( form );
+
+	}
+
+	_setPositionFromBone() {
+
+		const manager = this.manager;
+		const form = this._getBoneTransform();
+
+		const tr = manager.allocTransform();
+		this.body.getMotionState().getWorldTransform( tr );
+		manager.copyOrigin( tr, form );
+
+		// TODO: check the most appropriate way to set
+		//this.body.setWorldTransform( tr );
+		this.body.setCenterOfMassTransform( tr );
+		this.body.getMotionState().setWorldTransform( tr );
+
+		manager.freeTransform( tr );
+		manager.freeTransform( form );
+
+	}
+
+	_updateBoneRotation() {
+
+		const manager = this.manager;
+
+		const tr = this._getWorldTransformForBone();
+		const q = manager.getBasis( tr );
+
+		const thQ = manager.allocThreeQuaternion();
+		const thQ2 = manager.allocThreeQuaternion();
+		const thQ3 = manager.allocThreeQuaternion();
+
+		thQ.set( q.x(), q.y(), q.z(), q.w() );
+		thQ2.setFromRotationMatrix( this.bone.matrixWorld );
+		thQ2.conjugate();
+		thQ2.multiply( thQ );
+
+		//this.bone.quaternion.multiply( thQ2 );
+
+		thQ3.setFromRotationMatrix( this.bone.matrix );
+
+		// Renormalizing quaternion here because repeatedly transforming
+		// quaternion continuously accumulates floating point error and
+		// can end up being overflow. See #15335
+		this.bone.quaternion.copy( thQ2.multiply( thQ3 ).normalize() );
+
+		manager.freeThreeQuaternion( thQ );
+		manager.freeThreeQuaternion( thQ2 );
+		manager.freeThreeQuaternion( thQ3 );
+
+		manager.freeQuaternion( q );
+		manager.freeTransform( tr );
+
+	}
+
+	_updateBonePosition() {
+
+		const manager = this.manager;
+
+		const tr = this._getWorldTransformForBone();
+
+		const thV = manager.allocThreeVector3();
+
+		const o = manager.getOrigin( tr );
+		thV.set( o.x(), o.y(), o.z() );
+
+		if ( this.bone.parent ) {
+
+			this.bone.parent.worldToLocal( thV );
+
+		}
+
+		this.bone.position.copy( thV );
+
+		manager.freeThreeVector3( thV );
+
+		manager.freeTransform( tr );
+
+	}
+
+}
+
+//
+
+class Constraint {
+
+	/**
+	 * @param {THREE.SkinnedMesh} mesh
+	 * @param {Ammo.btDiscreteDynamicsWorld} world
+	 * @param {RigidBody} bodyA
+	 * @param {RigidBody} bodyB
+	 * @param {Object} params
+	 * @param {ResourceManager} manager
+	 */
+	constructor( mesh, world, bodyA, bodyB, params, manager ) {
+
+		this.mesh = mesh;
+		this.world = world;
+		this.bodyA = bodyA;
+		this.bodyB = bodyB;
+		this.params = params;
+		this.manager = manager;
+
+		this.constraint = null;
+
+		this._init();
+
+	}
+
+	// private method
+
+	_init() {
+
+		const manager = this.manager;
+		const params = this.params;
+		const bodyA = this.bodyA;
+		const bodyB = this.bodyB;
+
+		const form = manager.allocTransform();
+		manager.setIdentity( form );
+		manager.setOriginFromArray3( form, params.position );
+		manager.setBasisFromArray3( form, params.rotation );
+
+		const formA = manager.allocTransform();
+		const formB = manager.allocTransform();
+
+		bodyA.body.getMotionState().getWorldTransform( formA );
+		bodyB.body.getMotionState().getWorldTransform( formB );
+
+		const formInverseA = manager.inverseTransform( formA );
+		const formInverseB = manager.inverseTransform( formB );
+
+		const formA2 = manager.multiplyTransforms( formInverseA, form );
+		const formB2 = manager.multiplyTransforms( formInverseB, form );
+
+		const constraint = new Ammo.btGeneric6DofSpringConstraint( bodyA.body, bodyB.body, formA2, formB2, true );
+
+		const lll = manager.allocVector3();
+		const lul = manager.allocVector3();
+		const all = manager.allocVector3();
+		const aul = manager.allocVector3();
+
+		lll.setValue( params.translationLimitation1[ 0 ],
+		              params.translationLimitation1[ 1 ],
+		              params.translationLimitation1[ 2 ] );
+		lul.setValue( params.translationLimitation2[ 0 ],
+		              params.translationLimitation2[ 1 ],
+		              params.translationLimitation2[ 2 ] );
+		all.setValue( params.rotationLimitation1[ 0 ],
+		              params.rotationLimitation1[ 1 ],
+		              params.rotationLimitation1[ 2 ] );
+		aul.setValue( params.rotationLimitation2[ 0 ],
+		              params.rotationLimitation2[ 1 ],
+		              params.rotationLimitation2[ 2 ] );
+
+		constraint.setLinearLowerLimit( lll );
+		constraint.setLinearUpperLimit( lul );
+		constraint.setAngularLowerLimit( all );
+		constraint.setAngularUpperLimit( aul );
+
+		for ( let i = 0; i < 3; i ++ ) {
+
+			if ( params.springPosition[ i ] !== 0 ) {
+
+				constraint.enableSpring( i, true );
+				constraint.setStiffness( i, params.springPosition[ i ] );
+
+			}
+
+		}
+
+		for ( let i = 0; i < 3; i ++ ) {
+
+			if ( params.springRotation[ i ] !== 0 ) {
+
+				constraint.enableSpring( i + 3, true );
+				constraint.setStiffness( i + 3, params.springRotation[ i ] );
+
+			}
+
+		}
+
+		/*
+		 * Currently(10/31/2016) official ammo.js doesn't support
+		 * btGeneric6DofSpringConstraint.setParam method.
+		 * You need custom ammo.js (add the method into idl) if you wanna use.
+		 * By setting this parameter, physics will be more like MMD's
+		 */
+		if ( constraint.setParam !== undefined ) {
+
+			for ( let i = 0; i < 6; i ++ ) {
+
+				constraint.setParam( 2, 0.475, i );
+
+			}
+
+		}
+
+		this.world.addConstraint( constraint, true );
+		this.constraint = constraint;
+
+		manager.freeTransform( form );
+		manager.freeTransform( formA );
+		manager.freeTransform( formB );
+		manager.freeTransform( formInverseA );
+		manager.freeTransform( formInverseB );
+		manager.freeTransform( formA2 );
+		manager.freeTransform( formB2 );
+		manager.freeVector3( lll );
+		manager.freeVector3( lul );
+		manager.freeVector3( all );
+		manager.freeVector3( aul );
+
+	}
+
+}
+
+//
+
+const _position = new Vector3();
+const _quaternion = new Quaternion();
+const _scale = new Vector3();
+const _matrixWorldInv = new Matrix4();
+
+class MMDPhysicsHelper extends Object3D {
+
+	/**
+	 * Visualize Rigid bodies
+	 *
+	 * @param {THREE.SkinnedMesh} mesh
+	 * @param {Physics} physics
+	 */
+	constructor( mesh, physics ) {
+
+		super();
+
+		this.root = mesh;
+		this.physics = physics;
+
+		this.matrix.copy( mesh.matrixWorld );
+		this.matrixAutoUpdate = false;
+
+		this.materials = [];
+
+		this.materials.push(
+			new MeshBasicMaterial( {
+				color: new Color( 0xff8888 ),
+				wireframe: true,
+				depthTest: false,
+				depthWrite: false,
+				opacity: 0.25,
+				transparent: true
+			} )
+		);
+
+		this.materials.push(
+			new MeshBasicMaterial( {
+				color: new Color( 0x88ff88 ),
+				wireframe: true,
+				depthTest: false,
+				depthWrite: false,
+				opacity: 0.25,
+				transparent: true
+			} )
+		);
+
+		this.materials.push(
+			new MeshBasicMaterial( {
+				color: new Color( 0x8888ff ),
+				wireframe: true,
+				depthTest: false,
+				depthWrite: false,
+				opacity: 0.25,
+				transparent: true
+			} )
+		);
+
+		this._init();
+
+	}
+
+	/**
+	 * Updates Rigid Bodies visualization.
+	 */
+	updateMatrixWorld( force ) {
+
+		var mesh = this.root;
+
+		if ( this.visible ) {
+
+			var bodies = this.physics.bodies;
+
+			_matrixWorldInv
+				.copy( mesh.matrixWorld )
+				.decompose( _position, _quaternion, _scale )
+				.compose( _position, _quaternion, _scale.set( 1, 1, 1 ) )
+				.invert();
+
+			for ( var i = 0, il = bodies.length; i < il; i ++ ) {
+
+				var body = bodies[ i ].body;
+				var child = this.children[ i ];
+
+				var tr = body.getCenterOfMassTransform();
+				var origin = tr.getOrigin();
+				var rotation = tr.getRotation();
+
+				child.position
+					.set( origin.x(), origin.y(), origin.z() )
+					.applyMatrix4( _matrixWorldInv );
+
+				child.quaternion
+					.setFromRotationMatrix( _matrixWorldInv )
+					.multiply(
+						_quaternion.set( rotation.x(), rotation.y(), rotation.z(), rotation.w() )
+					);
+
+			}
+
+		}
+
+		this.matrix
+			.copy( mesh.matrixWorld )
+			.decompose( _position, _quaternion, _scale )
+			.compose( _position, _quaternion, _scale.set( 1, 1, 1 ) );
+
+		super.updateMatrixWorld( force );
+
+	}
+
+	// private method
+
+	_init() {
+
+		var bodies = this.physics.bodies;
+
+		function createGeometry( param ) {
+
+			switch ( param.shapeType ) {
+
+				case 0:
+					return new SphereGeometry( param.width, 16, 8 );
+
+				case 1:
+					return new BoxGeometry( param.width * 2, param.height * 2, param.depth * 2, 8, 8, 8 );
+
+				case 2:
+					return new CapsuleGeometry( param.width, param.height, 8, 16 );
+
+				default:
+					return null;
+
+			}
+
+		}
+
+		for ( var i = 0, il = bodies.length; i < il; i ++ ) {
+
+			var param = bodies[ i ].params;
+			this.add( new Mesh( createGeometry( param ), this.materials[ param.type ] ) );
+
+		}
+
+	}
+
+}
+
+export { MMDPhysics };

+ 209 - 0
public/archive/static/js/jsm/cameras/CinematicCamera.js

@@ -0,0 +1,209 @@
+import {
+	Mesh,
+	OrthographicCamera,
+	PerspectiveCamera,
+	PlaneGeometry,
+	Scene,
+	ShaderMaterial,
+	UniformsUtils,
+	WebGLRenderTarget
+} from 'three';
+
+import { BokehShader } from '../shaders/BokehShader2.js';
+import { BokehDepthShader } from '../shaders/BokehShader2.js';
+
+class CinematicCamera extends PerspectiveCamera {
+
+	constructor( fov, aspect, near, far ) {
+
+		super( fov, aspect, near, far );
+
+		this.type = 'CinematicCamera';
+
+		this.postprocessing = { enabled: true };
+		this.shaderSettings = {
+			rings: 3,
+			samples: 4
+		};
+
+		const depthShader = BokehDepthShader;
+
+		this.materialDepth = new ShaderMaterial( {
+			uniforms: depthShader.uniforms,
+			vertexShader: depthShader.vertexShader,
+			fragmentShader: depthShader.fragmentShader
+		} );
+
+		this.materialDepth.uniforms[ 'mNear' ].value = near;
+		this.materialDepth.uniforms[ 'mFar' ].value = far;
+
+		// In case of cinematicCamera, having a default lens set is important
+		this.setLens();
+
+		this.initPostProcessing();
+
+	}
+
+	// providing fnumber and coc(Circle of Confusion) as extra arguments
+	// In case of cinematicCamera, having a default lens set is important
+	// if fnumber and coc are not provided, cinematicCamera tries to act as a basic PerspectiveCamera
+	setLens( focalLength = 35, filmGauge = 35, fNumber = 8, coc = 0.019 ) {
+
+		this.filmGauge = filmGauge;
+
+		this.setFocalLength( focalLength );
+
+		this.fNumber = fNumber;
+		this.coc = coc;
+
+		// fNumber is focalLength by aperture
+		this.aperture = focalLength / this.fNumber;
+
+		// hyperFocal is required to calculate depthOfField when a lens tries to focus at a distance with given fNumber and focalLength
+		this.hyperFocal = ( focalLength * focalLength ) / ( this.aperture * this.coc );
+
+	}
+
+	linearize( depth ) {
+
+		const zfar = this.far;
+		const znear = this.near;
+		return - zfar * znear / ( depth * ( zfar - znear ) - zfar );
+
+	}
+
+	smoothstep( near, far, depth ) {
+
+		const x = this.saturate( ( depth - near ) / ( far - near ) );
+		return x * x * ( 3 - 2 * x );
+
+	}
+
+	saturate( x ) {
+
+		return Math.max( 0, Math.min( 1, x ) );
+
+	}
+
+	// function for focusing at a distance from the camera
+	focusAt( focusDistance = 20 ) {
+
+		const focalLength = this.getFocalLength();
+
+		// distance from the camera (normal to frustrum) to focus on
+		this.focus = focusDistance;
+
+		// the nearest point from the camera which is in focus (unused)
+		this.nearPoint = ( this.hyperFocal * this.focus ) / ( this.hyperFocal + ( this.focus - focalLength ) );
+
+		// the farthest point from the camera which is in focus (unused)
+		this.farPoint = ( this.hyperFocal * this.focus ) / ( this.hyperFocal - ( this.focus - focalLength ) );
+
+		// the gap or width of the space in which is everything is in focus (unused)
+		this.depthOfField = this.farPoint - this.nearPoint;
+
+		// Considering minimum distance of focus for a standard lens (unused)
+		if ( this.depthOfField < 0 ) this.depthOfField = 0;
+
+		this.sdistance = this.smoothstep( this.near, this.far, this.focus );
+
+		this.ldistance = this.linearize( 1 -	this.sdistance );
+
+		this.postprocessing.bokeh_uniforms[ 'focalDepth' ].value = this.ldistance;
+
+	}
+
+	initPostProcessing() {
+
+		if ( this.postprocessing.enabled ) {
+
+			this.postprocessing.scene = new Scene();
+
+			this.postprocessing.camera = new OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2,	window.innerHeight / 2, window.innerHeight / - 2, - 10000, 10000 );
+
+			this.postprocessing.scene.add( this.postprocessing.camera );
+
+			this.postprocessing.rtTextureDepth = new WebGLRenderTarget( window.innerWidth, window.innerHeight );
+			this.postprocessing.rtTextureColor = new WebGLRenderTarget( window.innerWidth, window.innerHeight );
+
+			const bokeh_shader = BokehShader;
+
+			this.postprocessing.bokeh_uniforms = UniformsUtils.clone( bokeh_shader.uniforms );
+
+			this.postprocessing.bokeh_uniforms[ 'tColor' ].value = this.postprocessing.rtTextureColor.texture;
+			this.postprocessing.bokeh_uniforms[ 'tDepth' ].value = this.postprocessing.rtTextureDepth.texture;
+
+			this.postprocessing.bokeh_uniforms[ 'manualdof' ].value = 0;
+			this.postprocessing.bokeh_uniforms[ 'shaderFocus' ].value = 0;
+
+			this.postprocessing.bokeh_uniforms[ 'fstop' ].value = 2.8;
+
+			this.postprocessing.bokeh_uniforms[ 'showFocus' ].value = 1;
+
+			this.postprocessing.bokeh_uniforms[ 'focalDepth' ].value = 0.1;
+
+			//console.log( this.postprocessing.bokeh_uniforms[ "focalDepth" ].value );
+
+			this.postprocessing.bokeh_uniforms[ 'znear' ].value = this.near;
+			this.postprocessing.bokeh_uniforms[ 'zfar' ].value = this.near;
+
+
+			this.postprocessing.bokeh_uniforms[ 'textureWidth' ].value = window.innerWidth;
+
+			this.postprocessing.bokeh_uniforms[ 'textureHeight' ].value = window.innerHeight;
+
+			this.postprocessing.materialBokeh = new ShaderMaterial( {
+				uniforms: this.postprocessing.bokeh_uniforms,
+				vertexShader: bokeh_shader.vertexShader,
+				fragmentShader: bokeh_shader.fragmentShader,
+				defines: {
+					RINGS: this.shaderSettings.rings,
+					SAMPLES: this.shaderSettings.samples,
+					DEPTH_PACKING: 1
+				}
+			} );
+
+			this.postprocessing.quad = new Mesh( new PlaneGeometry( window.innerWidth, window.innerHeight ), this.postprocessing.materialBokeh );
+			this.postprocessing.quad.position.z = - 500;
+			this.postprocessing.scene.add( this.postprocessing.quad );
+
+		}
+
+	}
+
+	renderCinematic( scene, renderer ) {
+
+		if ( this.postprocessing.enabled ) {
+
+			const currentRenderTarget = renderer.getRenderTarget();
+
+			renderer.clear();
+
+			// Render scene into texture
+
+			scene.overrideMaterial = null;
+			renderer.setRenderTarget( this.postprocessing.rtTextureColor );
+			renderer.clear();
+			renderer.render( scene, this );
+
+			// Render depth into texture
+
+			scene.overrideMaterial = this.materialDepth;
+			renderer.setRenderTarget( this.postprocessing.rtTextureDepth );
+			renderer.clear();
+			renderer.render( scene, this );
+
+			// Render bokeh composite
+
+			renderer.setRenderTarget( null );
+			renderer.render( this.postprocessing.scene, this.postprocessing.camera );
+
+			renderer.setRenderTarget( currentRenderTarget );
+
+		}
+
+	}
+
+}
+
+export { CinematicCamera };

+ 91 - 0
public/archive/static/js/jsm/capabilities/WebGL.js

@@ -0,0 +1,91 @@
+class WebGL {
+
+	static isWebGLAvailable() {
+
+		try {
+
+			const canvas = document.createElement( 'canvas' );
+			return !! ( window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ) );
+
+		} catch ( e ) {
+
+			return false;
+
+		}
+
+	}
+
+	static isWebGL2Available() {
+
+		try {
+
+			const canvas = document.createElement( 'canvas' );
+			return !! ( window.WebGL2RenderingContext && canvas.getContext( 'webgl2' ) );
+
+		} catch ( e ) {
+
+			return false;
+
+		}
+
+	}
+
+	static getWebGLErrorMessage() {
+
+		return this.getErrorMessage( 1 );
+
+	}
+
+	static getWebGL2ErrorMessage() {
+
+		return this.getErrorMessage( 2 );
+
+	}
+
+	static getErrorMessage( version ) {
+
+		const names = {
+			1: 'WebGL',
+			2: 'WebGL 2'
+		};
+
+		const contexts = {
+			1: window.WebGLRenderingContext,
+			2: window.WebGL2RenderingContext
+		};
+
+		let message = 'Your $0 does not seem to support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation" style="color:#000">$1</a>';
+
+		const element = document.createElement( 'div' );
+		element.id = 'webglmessage';
+		element.style.fontFamily = 'monospace';
+		element.style.fontSize = '13px';
+		element.style.fontWeight = 'normal';
+		element.style.textAlign = 'center';
+		element.style.background = '#fff';
+		element.style.color = '#000';
+		element.style.padding = '1.5em';
+		element.style.width = '400px';
+		element.style.margin = '5em auto 0';
+
+		if ( contexts[ version ] ) {
+
+			message = message.replace( '$0', 'graphics card' );
+
+		} else {
+
+			message = message.replace( '$0', 'browser' );
+
+		}
+
+		message = message.replace( '$1', names[ version ] );
+
+		element.innerHTML = message;
+
+		return element;
+
+	}
+
+}
+
+export default WebGL;

+ 39 - 0
public/archive/static/js/jsm/capabilities/WebGPU.js

@@ -0,0 +1,39 @@
+if ( window.GPUShaderStage === undefined ) {
+
+	window.GPUShaderStage = { VERTEX: 1, FRAGMENT: 2, COMPUTE: 4 };
+
+}
+
+class WebGPU {
+
+	static isAvailable() {
+
+		return ( navigator.gpu !== undefined );
+
+	}
+
+	static getErrorMessage() {
+
+		const message = 'Your browser does not support <a href="https://gpuweb.github.io/gpuweb/" style="color:blue">WebGPU</a>';
+
+		const element = document.createElement( 'div' );
+		element.id = 'webgpumessage';
+		element.style.fontFamily = 'monospace';
+		element.style.fontSize = '13px';
+		element.style.fontWeight = 'normal';
+		element.style.textAlign = 'center';
+		element.style.background = '#fff';
+		element.style.color = '#000';
+		element.style.padding = '1.5em';
+		element.style.width = '400px';
+		element.style.margin = '5em auto 0';
+
+		element.innerHTML = message;
+
+		return element;
+
+	}
+
+}
+
+export default WebGPU;

+ 3216 - 0
public/archive/static/js/jsm/controls/ArcballControls.js

@@ -0,0 +1,3216 @@
+import {
+	GridHelper,
+	EllipseCurve,
+	BufferGeometry,
+	Line,
+	LineBasicMaterial,
+	Raycaster,
+	Group,
+	Box3,
+	Sphere,
+	Quaternion,
+	Vector2,
+	Vector3,
+	Matrix4,
+	MathUtils,
+	EventDispatcher
+} from 'three';
+
+//trackball state
+const STATE = {
+
+	IDLE: Symbol(),
+	ROTATE: Symbol(),
+	PAN: Symbol(),
+	SCALE: Symbol(),
+	FOV: Symbol(),
+	FOCUS: Symbol(),
+	ZROTATE: Symbol(),
+	TOUCH_MULTI: Symbol(),
+	ANIMATION_FOCUS: Symbol(),
+	ANIMATION_ROTATE: Symbol()
+
+};
+
+const INPUT = {
+
+	NONE: Symbol(),
+	ONE_FINGER: Symbol(),
+	ONE_FINGER_SWITCHED: Symbol(),
+	TWO_FINGER: Symbol(),
+	MULT_FINGER: Symbol(),
+	CURSOR: Symbol()
+
+};
+
+//cursor center coordinates
+const _center = {
+
+	x: 0,
+	y: 0
+
+};
+
+//transformation matrices for gizmos and camera
+const _transformation = {
+
+	camera: new Matrix4(),
+	gizmos: new Matrix4()
+
+};
+
+//events
+const _changeEvent = { type: 'change' };
+const _startEvent = { type: 'start' };
+const _endEvent = { type: 'end' };
+
+const _raycaster = new Raycaster();
+const _offset = new Vector3();
+
+const _gizmoMatrixStateTemp = new Matrix4();
+const _cameraMatrixStateTemp = new Matrix4();
+const _scalePointTemp = new Vector3();
+/**
+ *
+ * @param {Camera} camera Virtual camera used in the scene
+ * @param {HTMLElement} domElement Renderer's dom element
+ * @param {Scene} scene The scene to be rendered
+ */
+class ArcballControls extends EventDispatcher {
+
+	constructor( camera, domElement, scene = null ) {
+
+		super();
+		this.camera = null;
+		this.domElement = domElement;
+		this.scene = scene;
+		this.target = new Vector3();
+		this._currentTarget = new Vector3();
+		this.radiusFactor = 0.67;
+
+		this.mouseActions = [];
+		this._mouseOp = null;
+
+
+		//global vectors and matrices that are used in some operations to avoid creating new objects every time (e.g. every time cursor moves)
+		this._v2_1 = new Vector2();
+		this._v3_1 = new Vector3();
+		this._v3_2 = new Vector3();
+
+		this._m4_1 = new Matrix4();
+		this._m4_2 = new Matrix4();
+
+		this._quat = new Quaternion();
+
+		//transformation matrices
+		this._translationMatrix = new Matrix4(); //matrix for translation operation
+		this._rotationMatrix = new Matrix4(); //matrix for rotation operation
+		this._scaleMatrix = new Matrix4(); //matrix for scaling operation
+
+		this._rotationAxis = new Vector3(); //axis for rotate operation
+
+
+		//camera state
+		this._cameraMatrixState = new Matrix4();
+		this._cameraProjectionState = new Matrix4();
+
+		this._fovState = 1;
+		this._upState = new Vector3();
+		this._zoomState = 1;
+		this._nearPos = 0;
+		this._farPos = 0;
+
+		this._gizmoMatrixState = new Matrix4();
+
+		//initial values
+		this._up0 = new Vector3();
+		this._zoom0 = 1;
+		this._fov0 = 0;
+		this._initialNear = 0;
+		this._nearPos0 = 0;
+		this._initialFar = 0;
+		this._farPos0 = 0;
+		this._cameraMatrixState0 = new Matrix4();
+		this._gizmoMatrixState0 = new Matrix4();
+
+		//pointers array
+		this._button = - 1;
+		this._touchStart = [];
+		this._touchCurrent = [];
+		this._input = INPUT.NONE;
+
+		//two fingers touch interaction
+		this._switchSensibility = 32;	//minimum movement to be performed to fire single pan start after the second finger has been released
+		this._startFingerDistance = 0; //distance between two fingers
+		this._currentFingerDistance = 0;
+		this._startFingerRotation = 0; //amount of rotation performed with two fingers
+		this._currentFingerRotation = 0;
+
+		//double tap
+		this._devPxRatio = 0;
+		this._downValid = true;
+		this._nclicks = 0;
+		this._downEvents = [];
+		this._downStart = 0;	//pointerDown time
+		this._clickStart = 0;	//first click time
+		this._maxDownTime = 250;
+		this._maxInterval = 300;
+		this._posThreshold = 24;
+		this._movementThreshold = 24;
+
+		//cursor positions
+		this._currentCursorPosition = new Vector3();
+		this._startCursorPosition = new Vector3();
+
+		//grid
+		this._grid = null; //grid to be visualized during pan operation
+		this._gridPosition = new Vector3();
+
+		//gizmos
+		this._gizmos = new Group();
+		this._curvePts = 128;
+
+
+		//animations
+		this._timeStart = - 1; //initial time
+		this._animationId = - 1;
+
+		//focus animation
+		this.focusAnimationTime = 500; //duration of focus animation in ms
+
+		//rotate animation
+		this._timePrev = 0; //time at which previous rotate operation has been detected
+		this._timeCurrent = 0; //time at which current rotate operation has been detected
+		this._anglePrev = 0; //angle of previous rotation
+		this._angleCurrent = 0; //angle of current rotation
+		this._cursorPosPrev = new Vector3();	//cursor position when previous rotate operation has been detected
+		this._cursorPosCurr = new Vector3();//cursor position when current rotate operation has been detected
+		this._wPrev = 0; //angular velocity of the previous rotate operation
+		this._wCurr = 0; //angular velocity of the current rotate operation
+
+
+		//parameters
+		this.adjustNearFar = false;
+		this.scaleFactor = 1.1;	//zoom/distance multiplier
+		this.dampingFactor = 25;
+		this.wMax = 20;	//maximum angular velocity allowed
+		this.enableAnimations = true; //if animations should be performed
+		this.enableGrid = false; //if grid should be showed during pan operation
+		this.cursorZoom = false;	//if wheel zoom should be cursor centered
+		this.minFov = 5;
+		this.maxFov = 90;
+
+		this.enabled = true;
+		this.enablePan = true;
+		this.enableRotate = true;
+		this.enableZoom = true;
+		this.enableGizmos = true;
+
+		this.minDistance = 0;
+		this.maxDistance = Infinity;
+		this.minZoom = 0;
+		this.maxZoom = Infinity;
+
+		//trackball parameters
+		this._tbRadius = 1;
+
+		//FSA
+		this._state = STATE.IDLE;
+
+		this.setCamera( camera );
+
+		if ( this.scene != null ) {
+
+			this.scene.add( this._gizmos );
+
+		}
+
+		this.domElement.style.touchAction = 'none';
+		this._devPxRatio = window.devicePixelRatio;
+
+		this.initializeMouseActions();
+
+		this.domElement.addEventListener( 'contextmenu', this.onContextMenu );
+		this.domElement.addEventListener( 'wheel', this.onWheel );
+		this.domElement.addEventListener( 'pointerdown', this.onPointerDown );
+		this.domElement.addEventListener( 'pointercancel', this.onPointerCancel );
+
+		window.addEventListener( 'resize', this.onWindowResize );
+
+	}
+
+	//listeners
+
+	onWindowResize = () => {
+
+		const scale = ( this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z ) / 3;
+		this._tbRadius = this.calculateTbRadius( this.camera );
+
+		const newRadius = this._tbRadius / scale;
+		const curve = new EllipseCurve( 0, 0, newRadius, newRadius );
+		const points = curve.getPoints( this._curvePts );
+		const curveGeometry = new BufferGeometry().setFromPoints( points );
+
+
+		for ( const gizmo in this._gizmos.children ) {
+
+			this._gizmos.children[ gizmo ].geometry = curveGeometry;
+
+		}
+
+		this.dispatchEvent( _changeEvent );
+
+	};
+
+	onContextMenu = ( event ) => {
+
+		if ( ! this.enabled ) {
+
+			return;
+
+		}
+
+		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+
+			if ( this.mouseActions[ i ].mouse == 2 ) {
+
+				//prevent only if button 2 is actually used
+				event.preventDefault();
+				break;
+
+			}
+
+		}
+
+	};
+
+	onPointerCancel = () => {
+
+		this._touchStart.splice( 0, this._touchStart.length );
+		this._touchCurrent.splice( 0, this._touchCurrent.length );
+		this._input = INPUT.NONE;
+
+	};
+
+	onPointerDown = ( event ) => {
+
+		if ( event.button == 0 && event.isPrimary ) {
+
+			this._downValid = true;
+			this._downEvents.push( event );
+			this._downStart = performance.now();
+
+		} else {
+
+			this._downValid = false;
+
+		}
+
+		if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) {
+
+			this._touchStart.push( event );
+			this._touchCurrent.push( event );
+
+			switch ( this._input ) {
+
+				case INPUT.NONE:
+
+					//singleStart
+					this._input = INPUT.ONE_FINGER;
+					this.onSinglePanStart( event, 'ROTATE' );
+
+					window.addEventListener( 'pointermove', this.onPointerMove );
+					window.addEventListener( 'pointerup', this.onPointerUp );
+
+					break;
+
+				case INPUT.ONE_FINGER:
+				case INPUT.ONE_FINGER_SWITCHED:
+
+					//doubleStart
+					this._input = INPUT.TWO_FINGER;
+
+					this.onRotateStart();
+					this.onPinchStart();
+					this.onDoublePanStart();
+
+					break;
+
+				case INPUT.TWO_FINGER:
+
+					//multipleStart
+					this._input = INPUT.MULT_FINGER;
+					this.onTriplePanStart( event );
+					break;
+
+			}
+
+		} else if ( event.pointerType != 'touch' && this._input == INPUT.NONE ) {
+
+			let modifier = null;
+
+			if ( event.ctrlKey || event.metaKey ) {
+
+				modifier = 'CTRL';
+
+			} else if ( event.shiftKey ) {
+
+				modifier = 'SHIFT';
+
+			}
+
+			this._mouseOp = this.getOpFromAction( event.button, modifier );
+			if ( this._mouseOp != null ) {
+
+				window.addEventListener( 'pointermove', this.onPointerMove );
+				window.addEventListener( 'pointerup', this.onPointerUp );
+
+				//singleStart
+				this._input = INPUT.CURSOR;
+				this._button = event.button;
+				this.onSinglePanStart( event, this._mouseOp );
+
+			}
+
+		}
+
+	};
+
+	onPointerMove = ( event ) => {
+
+		if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) {
+
+			switch ( this._input ) {
+
+				case INPUT.ONE_FINGER:
+
+					//singleMove
+					this.updateTouchEvent( event );
+
+					this.onSinglePanMove( event, STATE.ROTATE );
+					break;
+
+				case INPUT.ONE_FINGER_SWITCHED:
+
+					const movement = this.calculatePointersDistance( this._touchCurrent[ 0 ], event ) * this._devPxRatio;
+
+					if ( movement >= this._switchSensibility ) {
+
+						//singleMove
+						this._input = INPUT.ONE_FINGER;
+						this.updateTouchEvent( event );
+
+						this.onSinglePanStart( event, 'ROTATE' );
+						break;
+
+					}
+
+					break;
+
+				case INPUT.TWO_FINGER:
+
+					//rotate/pan/pinchMove
+					this.updateTouchEvent( event );
+
+					this.onRotateMove();
+					this.onPinchMove();
+					this.onDoublePanMove();
+
+					break;
+
+				case INPUT.MULT_FINGER:
+
+					//multMove
+					this.updateTouchEvent( event );
+
+					this.onTriplePanMove( event );
+					break;
+
+			}
+
+		} else if ( event.pointerType != 'touch' && this._input == INPUT.CURSOR ) {
+
+			let modifier = null;
+
+			if ( event.ctrlKey || event.metaKey ) {
+
+				modifier = 'CTRL';
+
+			} else if ( event.shiftKey ) {
+
+				modifier = 'SHIFT';
+
+			}
+
+			const mouseOpState = this.getOpStateFromAction( this._button, modifier );
+
+			if ( mouseOpState != null ) {
+
+				this.onSinglePanMove( event, mouseOpState );
+
+			}
+
+		}
+
+		//checkDistance
+		if ( this._downValid ) {
+
+			const movement = this.calculatePointersDistance( this._downEvents[ this._downEvents.length - 1 ], event ) * this._devPxRatio;
+			if ( movement > this._movementThreshold ) {
+
+				this._downValid = false;
+
+			}
+
+		}
+
+	};
+
+	onPointerUp = ( event ) => {
+
+		if ( event.pointerType == 'touch' && this._input != INPUT.CURSOR ) {
+
+			const nTouch = this._touchCurrent.length;
+
+			for ( let i = 0; i < nTouch; i ++ ) {
+
+				if ( this._touchCurrent[ i ].pointerId == event.pointerId ) {
+
+					this._touchCurrent.splice( i, 1 );
+					this._touchStart.splice( i, 1 );
+					break;
+
+				}
+
+			}
+
+			switch ( this._input ) {
+
+				case INPUT.ONE_FINGER:
+				case INPUT.ONE_FINGER_SWITCHED:
+
+					//singleEnd
+					window.removeEventListener( 'pointermove', this.onPointerMove );
+					window.removeEventListener( 'pointerup', this.onPointerUp );
+
+					this._input = INPUT.NONE;
+					this.onSinglePanEnd();
+
+					break;
+
+				case INPUT.TWO_FINGER:
+
+					//doubleEnd
+					this.onDoublePanEnd( event );
+					this.onPinchEnd( event );
+					this.onRotateEnd( event );
+
+					//switching to singleStart
+					this._input = INPUT.ONE_FINGER_SWITCHED;
+
+					break;
+
+				case INPUT.MULT_FINGER:
+
+					if ( this._touchCurrent.length == 0 ) {
+
+						window.removeEventListener( 'pointermove', this.onPointerMove );
+						window.removeEventListener( 'pointerup', this.onPointerUp );
+
+						//multCancel
+						this._input = INPUT.NONE;
+						this.onTriplePanEnd();
+
+					}
+
+					break;
+
+			}
+
+		} else if ( event.pointerType != 'touch' && this._input == INPUT.CURSOR ) {
+
+			window.removeEventListener( 'pointermove', this.onPointerMove );
+			window.removeEventListener( 'pointerup', this.onPointerUp );
+
+			this._input = INPUT.NONE;
+			this.onSinglePanEnd();
+			this._button = - 1;
+
+		}
+
+		if ( event.isPrimary ) {
+
+			if ( this._downValid ) {
+
+				const downTime = event.timeStamp - this._downEvents[ this._downEvents.length - 1 ].timeStamp;
+
+				if ( downTime <= this._maxDownTime ) {
+
+					if ( this._nclicks == 0 ) {
+
+						//first valid click detected
+						this._nclicks = 1;
+						this._clickStart = performance.now();
+
+					} else {
+
+						const clickInterval = event.timeStamp - this._clickStart;
+						const movement = this.calculatePointersDistance( this._downEvents[ 1 ], this._downEvents[ 0 ] ) * this._devPxRatio;
+
+						if ( clickInterval <= this._maxInterval && movement <= this._posThreshold ) {
+
+							//second valid click detected
+							//fire double tap and reset values
+							this._nclicks = 0;
+							this._downEvents.splice( 0, this._downEvents.length );
+							this.onDoubleTap( event );
+
+						} else {
+
+							//new 'first click'
+							this._nclicks = 1;
+							this._downEvents.shift();
+							this._clickStart = performance.now();
+
+						}
+
+					}
+
+				} else {
+
+					this._downValid = false;
+					this._nclicks = 0;
+					this._downEvents.splice( 0, this._downEvents.length );
+
+				}
+
+			} else {
+
+				this._nclicks = 0;
+				this._downEvents.splice( 0, this._downEvents.length );
+
+			}
+
+		}
+
+	};
+
+	onWheel = ( event ) => {
+
+		if ( this.enabled && this.enableZoom ) {
+
+			let modifier = null;
+
+			if ( event.ctrlKey || event.metaKey ) {
+
+				modifier = 'CTRL';
+
+			} else if ( event.shiftKey ) {
+
+				modifier = 'SHIFT';
+
+			}
+
+			const mouseOp = this.getOpFromAction( 'WHEEL', modifier );
+
+			if ( mouseOp != null ) {
+
+				event.preventDefault();
+				this.dispatchEvent( _startEvent );
+
+				const notchDeltaY = 125; //distance of one notch of mouse wheel
+				let sgn = event.deltaY / notchDeltaY;
+
+				let size = 1;
+
+				if ( sgn > 0 ) {
+
+					size = 1 / this.scaleFactor;
+
+				} else if ( sgn < 0 ) {
+
+					size = this.scaleFactor;
+
+				}
+
+				switch ( mouseOp ) {
+
+					case 'ZOOM':
+
+						this.updateTbState( STATE.SCALE, true );
+
+						if ( sgn > 0 ) {
+
+							size = 1 / ( Math.pow( this.scaleFactor, sgn ) );
+
+						} else if ( sgn < 0 ) {
+
+							size = Math.pow( this.scaleFactor, - sgn );
+
+						}
+
+						if ( this.cursorZoom && this.enablePan ) {
+
+							let scalePoint;
+
+							if ( this.camera.isOrthographicCamera ) {
+
+								scalePoint = this.unprojectOnTbPlane( this.camera, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.camera.quaternion ).multiplyScalar( 1 / this.camera.zoom ).add( this._gizmos.position );
+
+							} else if ( this.camera.isPerspectiveCamera ) {
+
+								scalePoint = this.unprojectOnTbPlane( this.camera, event.clientX, event.clientY, this.domElement ).applyQuaternion( this.camera.quaternion ).add( this._gizmos.position );
+
+							}
+
+							this.applyTransformMatrix( this.scale( size, scalePoint ) );
+
+						} else {
+
+							this.applyTransformMatrix( this.scale( size, this._gizmos.position ) );
+
+						}
+
+						if ( this._grid != null ) {
+
+							this.disposeGrid();
+							this.drawGrid();
+
+						}
+
+						this.updateTbState( STATE.IDLE, false );
+
+						this.dispatchEvent( _changeEvent );
+						this.dispatchEvent( _endEvent );
+
+						break;
+
+					case 'FOV':
+
+						if ( this.camera.isPerspectiveCamera ) {
+
+							this.updateTbState( STATE.FOV, true );
+
+
+							//Vertigo effect
+
+							//	  fov / 2
+							//		|\
+							//		| \
+							//		|  \
+							//	x	|	\
+							//		| 	 \
+							//		| 	  \
+							//		| _ _ _\
+							//			y
+
+							//check for iOs shift shortcut
+							if ( event.deltaX != 0 ) {
+
+								sgn = event.deltaX / notchDeltaY;
+
+								size = 1;
+
+								if ( sgn > 0 ) {
+
+									size = 1 / ( Math.pow( this.scaleFactor, sgn ) );
+
+								} else if ( sgn < 0 ) {
+
+									size = Math.pow( this.scaleFactor, - sgn );
+
+								}
+
+							}
+
+							this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
+							const x = this._v3_1.distanceTo( this._gizmos.position );
+							let xNew = x / size;	//distance between camera and gizmos if scale(size, scalepoint) would be performed
+
+							//check min and max distance
+							xNew = MathUtils.clamp( xNew, this.minDistance, this.maxDistance );
+
+							const y = x * Math.tan( MathUtils.DEG2RAD * this.camera.fov * 0.5 );
+
+							//calculate new fov
+							let newFov = MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 );
+
+							//check min and max fov
+							if ( newFov > this.maxFov ) {
+
+								newFov = this.maxFov;
+
+							} else if ( newFov < this.minFov ) {
+
+								newFov = this.minFov;
+
+							}
+
+							const newDistance = y / Math.tan( MathUtils.DEG2RAD * ( newFov / 2 ) );
+							size = x / newDistance;
+
+							this.setFov( newFov );
+							this.applyTransformMatrix( this.scale( size, this._gizmos.position, false ) );
+
+						}
+
+						if ( this._grid != null ) {
+
+							this.disposeGrid();
+							this.drawGrid();
+
+						}
+
+						this.updateTbState( STATE.IDLE, false );
+
+						this.dispatchEvent( _changeEvent );
+						this.dispatchEvent( _endEvent );
+
+						break;
+
+				}
+
+			}
+
+		}
+
+	};
+
+	onSinglePanStart = ( event, operation ) => {
+
+		if ( this.enabled ) {
+
+			this.dispatchEvent( _startEvent );
+
+			this.setCenter( event.clientX, event.clientY );
+
+			switch ( operation ) {
+
+				case 'PAN':
+
+					if ( ! this.enablePan ) {
+
+						return;
+
+					}
+
+					if ( this._animationId != - 1 ) {
+
+						cancelAnimationFrame( this._animationId );
+						this._animationId = - 1;
+						this._timeStart = - 1;
+
+						this.activateGizmos( false );
+						this.dispatchEvent( _changeEvent );
+
+					}
+
+					this.updateTbState( STATE.PAN, true );
+					this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
+					if ( this.enableGrid ) {
+
+						this.drawGrid();
+						this.dispatchEvent( _changeEvent );
+
+					}
+
+					break;
+
+				case 'ROTATE':
+
+					if ( ! this.enableRotate ) {
+
+						return;
+
+					}
+
+					if ( this._animationId != - 1 ) {
+
+						cancelAnimationFrame( this._animationId );
+						this._animationId = - 1;
+						this._timeStart = - 1;
+
+					}
+
+					this.updateTbState( STATE.ROTATE, true );
+					this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
+					this.activateGizmos( true );
+					if ( this.enableAnimations ) {
+
+						this._timePrev = this._timeCurrent = performance.now();
+						this._angleCurrent = this._anglePrev = 0;
+						this._cursorPosPrev.copy( this._startCursorPosition );
+						this._cursorPosCurr.copy( this._cursorPosPrev );
+						this._wCurr = 0;
+						this._wPrev = this._wCurr;
+
+					}
+
+					this.dispatchEvent( _changeEvent );
+					break;
+
+				case 'FOV':
+
+					if ( ! this.camera.isPerspectiveCamera || ! this.enableZoom ) {
+
+						return;
+
+					}
+
+					if ( this._animationId != - 1 ) {
+
+						cancelAnimationFrame( this._animationId );
+						this._animationId = - 1;
+						this._timeStart = - 1;
+
+						this.activateGizmos( false );
+						this.dispatchEvent( _changeEvent );
+
+					}
+
+					this.updateTbState( STATE.FOV, true );
+					this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+					this._currentCursorPosition.copy( this._startCursorPosition );
+					break;
+
+				case 'ZOOM':
+
+					if ( ! this.enableZoom ) {
+
+						return;
+
+					}
+
+					if ( this._animationId != - 1 ) {
+
+						cancelAnimationFrame( this._animationId );
+						this._animationId = - 1;
+						this._timeStart = - 1;
+
+						this.activateGizmos( false );
+						this.dispatchEvent( _changeEvent );
+
+					}
+
+					this.updateTbState( STATE.SCALE, true );
+					this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+					this._currentCursorPosition.copy( this._startCursorPosition );
+					break;
+
+			}
+
+		}
+
+	};
+
+	onSinglePanMove = ( event, opState ) => {
+
+		if ( this.enabled ) {
+
+			const restart = opState != this._state;
+			this.setCenter( event.clientX, event.clientY );
+
+			switch ( opState ) {
+
+				case STATE.PAN:
+
+					if ( this.enablePan ) {
+
+						if ( restart ) {
+
+							//switch to pan operation
+
+							this.dispatchEvent( _endEvent );
+							this.dispatchEvent( _startEvent );
+
+							this.updateTbState( opState, true );
+							this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
+							if ( this.enableGrid ) {
+
+								this.drawGrid();
+
+							}
+
+							this.activateGizmos( false );
+
+						} else {
+
+							//continue with pan operation
+							this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ) );
+							this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition ) );
+
+						}
+
+					}
+
+					break;
+
+				case STATE.ROTATE:
+
+					if ( this.enableRotate ) {
+
+						if ( restart ) {
+
+							//switch to rotate operation
+
+							this.dispatchEvent( _endEvent );
+							this.dispatchEvent( _startEvent );
+
+							this.updateTbState( opState, true );
+							this._startCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
+
+							if ( this.enableGrid ) {
+
+								this.disposeGrid();
+
+							}
+
+							this.activateGizmos( true );
+
+						} else {
+
+							//continue with rotate operation
+							this._currentCursorPosition.copy( this.unprojectOnTbSurface( this.camera, _center.x, _center.y, this.domElement, this._tbRadius ) );
+
+							const distance = this._startCursorPosition.distanceTo( this._currentCursorPosition );
+							const angle = this._startCursorPosition.angleTo( this._currentCursorPosition );
+							const amount = Math.max( distance / this._tbRadius, angle ); //effective rotation angle
+
+							this.applyTransformMatrix( this.rotate( this.calculateRotationAxis( this._startCursorPosition, this._currentCursorPosition ), amount ) );
+
+							if ( this.enableAnimations ) {
+
+								this._timePrev = this._timeCurrent;
+								this._timeCurrent = performance.now();
+								this._anglePrev = this._angleCurrent;
+								this._angleCurrent = amount;
+								this._cursorPosPrev.copy( this._cursorPosCurr );
+								this._cursorPosCurr.copy( this._currentCursorPosition );
+								this._wPrev = this._wCurr;
+								this._wCurr = this.calculateAngularSpeed( this._anglePrev, this._angleCurrent, this._timePrev, this._timeCurrent );
+
+							}
+
+						}
+
+					}
+
+					break;
+
+				case STATE.SCALE:
+
+					if ( this.enableZoom ) {
+
+						if ( restart ) {
+
+							//switch to zoom operation
+
+							this.dispatchEvent( _endEvent );
+							this.dispatchEvent( _startEvent );
+
+							this.updateTbState( opState, true );
+							this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+							this._currentCursorPosition.copy( this._startCursorPosition );
+
+							if ( this.enableGrid ) {
+
+								this.disposeGrid();
+
+							}
+
+							this.activateGizmos( false );
+
+						} else {
+
+							//continue with zoom operation
+							const screenNotches = 8;	//how many wheel notches corresponds to a full screen pan
+							this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+
+							const movement = this._currentCursorPosition.y - this._startCursorPosition.y;
+
+							let size = 1;
+
+							if ( movement < 0 ) {
+
+								size = 1 / ( Math.pow( this.scaleFactor, - movement * screenNotches ) );
+
+							} else if ( movement > 0 ) {
+
+								size = Math.pow( this.scaleFactor, movement * screenNotches );
+
+							}
+
+							this._v3_1.setFromMatrixPosition( this._gizmoMatrixState );
+
+							this.applyTransformMatrix( this.scale( size, this._v3_1 ) );
+
+						}
+
+					}
+
+					break;
+
+				case STATE.FOV:
+
+					if ( this.enableZoom && this.camera.isPerspectiveCamera ) {
+
+						if ( restart ) {
+
+							//switch to fov operation
+
+							this.dispatchEvent( _endEvent );
+							this.dispatchEvent( _startEvent );
+
+							this.updateTbState( opState, true );
+							this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+							this._currentCursorPosition.copy( this._startCursorPosition );
+
+							if ( this.enableGrid ) {
+
+								this.disposeGrid();
+
+							}
+
+							this.activateGizmos( false );
+
+						} else {
+
+							//continue with fov operation
+							const screenNotches = 8;	//how many wheel notches corresponds to a full screen pan
+							this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+
+							const movement = this._currentCursorPosition.y - this._startCursorPosition.y;
+
+							let size = 1;
+
+							if ( movement < 0 ) {
+
+								size = 1 / ( Math.pow( this.scaleFactor, - movement * screenNotches ) );
+
+							} else if ( movement > 0 ) {
+
+								size = Math.pow( this.scaleFactor, movement * screenNotches );
+
+							}
+
+							this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
+							const x = this._v3_1.distanceTo( this._gizmos.position );
+							let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed
+
+							//check min and max distance
+							xNew = MathUtils.clamp( xNew, this.minDistance, this.maxDistance );
+
+							const y = x * Math.tan( MathUtils.DEG2RAD * this._fovState * 0.5 );
+
+							//calculate new fov
+							let newFov = MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 );
+
+							//check min and max fov
+							newFov = MathUtils.clamp( newFov, this.minFov, this.maxFov );
+
+							const newDistance = y / Math.tan( MathUtils.DEG2RAD * ( newFov / 2 ) );
+							size = x / newDistance;
+							this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
+
+							this.setFov( newFov );
+							this.applyTransformMatrix( this.scale( size, this._v3_2, false ) );
+
+							//adjusting distance
+							_offset.copy( this._gizmos.position ).sub( this.camera.position ).normalize().multiplyScalar( newDistance / x );
+							this._m4_1.makeTranslation( _offset.x, _offset.y, _offset.z );
+
+						}
+
+					}
+
+					break;
+
+			}
+
+			this.dispatchEvent( _changeEvent );
+
+		}
+
+	};
+
+	onSinglePanEnd = () => {
+
+		if ( this._state == STATE.ROTATE ) {
+
+
+			if ( ! this.enableRotate ) {
+
+				return;
+
+			}
+
+			if ( this.enableAnimations ) {
+
+				//perform rotation animation
+				const deltaTime = ( performance.now() - this._timeCurrent );
+				if ( deltaTime < 120 ) {
+
+					const w = Math.abs( ( this._wPrev + this._wCurr ) / 2 );
+
+					const self = this;
+					this._animationId = window.requestAnimationFrame( function ( t ) {
+
+						self.updateTbState( STATE.ANIMATION_ROTATE, true );
+						const rotationAxis = self.calculateRotationAxis( self._cursorPosPrev, self._cursorPosCurr );
+
+						self.onRotationAnim( t, rotationAxis, Math.min( w, self.wMax ) );
+
+					} );
+
+				} else {
+
+					//cursor has been standing still for over 120 ms since last movement
+					this.updateTbState( STATE.IDLE, false );
+					this.activateGizmos( false );
+					this.dispatchEvent( _changeEvent );
+
+				}
+
+			} else {
+
+				this.updateTbState( STATE.IDLE, false );
+				this.activateGizmos( false );
+				this.dispatchEvent( _changeEvent );
+
+			}
+
+		} else if ( this._state == STATE.PAN || this._state == STATE.IDLE ) {
+
+			this.updateTbState( STATE.IDLE, false );
+
+			if ( this.enableGrid ) {
+
+				this.disposeGrid();
+
+			}
+
+			this.activateGizmos( false );
+			this.dispatchEvent( _changeEvent );
+
+
+		}
+
+		this.dispatchEvent( _endEvent );
+
+	};
+
+	onDoubleTap = ( event ) => {
+
+		if ( this.enabled && this.enablePan && this.scene != null ) {
+
+			this.dispatchEvent( _startEvent );
+
+			this.setCenter( event.clientX, event.clientY );
+			const hitP = this.unprojectOnObj( this.getCursorNDC( _center.x, _center.y, this.domElement ), this.camera );
+
+			if ( hitP != null && this.enableAnimations ) {
+
+				const self = this;
+				if ( this._animationId != - 1 ) {
+
+					window.cancelAnimationFrame( this._animationId );
+
+				}
+
+				this._timeStart = - 1;
+				this._animationId = window.requestAnimationFrame( function ( t ) {
+
+					self.updateTbState( STATE.ANIMATION_FOCUS, true );
+					self.onFocusAnim( t, hitP, self._cameraMatrixState, self._gizmoMatrixState );
+
+				} );
+
+			} else if ( hitP != null && ! this.enableAnimations ) {
+
+				this.updateTbState( STATE.FOCUS, true );
+				this.focus( hitP, this.scaleFactor );
+				this.updateTbState( STATE.IDLE, false );
+				this.dispatchEvent( _changeEvent );
+
+			}
+
+		}
+
+		this.dispatchEvent( _endEvent );
+
+	};
+
+	onDoublePanStart = () => {
+
+		if ( this.enabled && this.enablePan ) {
+
+			this.dispatchEvent( _startEvent );
+
+			this.updateTbState( STATE.PAN, true );
+
+			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
+			this._startCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement, true ) );
+			this._currentCursorPosition.copy( this._startCursorPosition );
+
+			this.activateGizmos( false );
+
+		}
+
+	};
+
+	onDoublePanMove = () => {
+
+		if ( this.enabled && this.enablePan ) {
+
+			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
+
+			if ( this._state != STATE.PAN ) {
+
+				this.updateTbState( STATE.PAN, true );
+				this._startCursorPosition.copy( this._currentCursorPosition );
+
+			}
+
+			this._currentCursorPosition.copy( this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement, true ) );
+			this.applyTransformMatrix( this.pan( this._startCursorPosition, this._currentCursorPosition, true ) );
+			this.dispatchEvent( _changeEvent );
+
+		}
+
+	};
+
+	onDoublePanEnd = () => {
+
+		this.updateTbState( STATE.IDLE, false );
+		this.dispatchEvent( _endEvent );
+
+	};
+
+
+	onRotateStart = () => {
+
+		if ( this.enabled && this.enableRotate ) {
+
+			this.dispatchEvent( _startEvent );
+
+			this.updateTbState( STATE.ZROTATE, true );
+
+			//this._startFingerRotation = event.rotation;
+
+			this._startFingerRotation = this.getAngle( this._touchCurrent[ 1 ], this._touchCurrent[ 0 ] ) + this.getAngle( this._touchStart[ 1 ], this._touchStart[ 0 ] );
+			this._currentFingerRotation = this._startFingerRotation;
+
+			this.camera.getWorldDirection( this._rotationAxis ); //rotation axis
+
+			if ( ! this.enablePan && ! this.enableZoom ) {
+
+				this.activateGizmos( true );
+
+			}
+
+		}
+
+	};
+
+	onRotateMove = () => {
+
+		if ( this.enabled && this.enableRotate ) {
+
+			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
+			let rotationPoint;
+
+			if ( this._state != STATE.ZROTATE ) {
+
+				this.updateTbState( STATE.ZROTATE, true );
+				this._startFingerRotation = this._currentFingerRotation;
+
+			}
+
+			//this._currentFingerRotation = event.rotation;
+			this._currentFingerRotation = this.getAngle( this._touchCurrent[ 1 ], this._touchCurrent[ 0 ] ) + this.getAngle( this._touchStart[ 1 ], this._touchStart[ 0 ] );
+
+			if ( ! this.enablePan ) {
+
+				rotationPoint = new Vector3().setFromMatrixPosition( this._gizmoMatrixState );
+
+			} else {
+
+				this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
+				rotationPoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement ).applyQuaternion( this.camera.quaternion ).multiplyScalar( 1 / this.camera.zoom ).add( this._v3_2 );
+
+			}
+
+			const amount = MathUtils.DEG2RAD * ( this._startFingerRotation - this._currentFingerRotation );
+
+			this.applyTransformMatrix( this.zRotate( rotationPoint, amount ) );
+			this.dispatchEvent( _changeEvent );
+
+		}
+
+	};
+
+	onRotateEnd = () => {
+
+		this.updateTbState( STATE.IDLE, false );
+		this.activateGizmos( false );
+		this.dispatchEvent( _endEvent );
+
+	};
+
+	onPinchStart = () => {
+
+		if ( this.enabled && this.enableZoom ) {
+
+			this.dispatchEvent( _startEvent );
+			this.updateTbState( STATE.SCALE, true );
+
+			this._startFingerDistance = this.calculatePointersDistance( this._touchCurrent[ 0 ], this._touchCurrent[ 1 ] );
+			this._currentFingerDistance = this._startFingerDistance;
+
+			this.activateGizmos( false );
+
+		}
+
+	};
+
+	onPinchMove = () => {
+
+		if ( this.enabled && this.enableZoom ) {
+
+			this.setCenter( ( this._touchCurrent[ 0 ].clientX + this._touchCurrent[ 1 ].clientX ) / 2, ( this._touchCurrent[ 0 ].clientY + this._touchCurrent[ 1 ].clientY ) / 2 );
+			const minDistance = 12; //minimum distance between fingers (in css pixels)
+
+			if ( this._state != STATE.SCALE ) {
+
+				this._startFingerDistance = this._currentFingerDistance;
+				this.updateTbState( STATE.SCALE, true );
+
+			}
+
+			this._currentFingerDistance = Math.max( this.calculatePointersDistance( this._touchCurrent[ 0 ], this._touchCurrent[ 1 ] ), minDistance * this._devPxRatio );
+			const amount = this._currentFingerDistance / this._startFingerDistance;
+
+			let scalePoint;
+
+			if ( ! this.enablePan ) {
+
+				scalePoint = this._gizmos.position;
+
+			} else {
+
+				if ( this.camera.isOrthographicCamera ) {
+
+					scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement )
+						.applyQuaternion( this.camera.quaternion )
+						.multiplyScalar( 1 / this.camera.zoom )
+						.add( this._gizmos.position );
+
+				} else if ( this.camera.isPerspectiveCamera ) {
+
+					scalePoint = this.unprojectOnTbPlane( this.camera, _center.x, _center.y, this.domElement )
+						.applyQuaternion( this.camera.quaternion )
+						.add( this._gizmos.position );
+
+				}
+
+			}
+
+			this.applyTransformMatrix( this.scale( amount, scalePoint ) );
+			this.dispatchEvent( _changeEvent );
+
+		}
+
+	};
+
+	onPinchEnd = () => {
+
+		this.updateTbState( STATE.IDLE, false );
+		this.dispatchEvent( _endEvent );
+
+	};
+
+	onTriplePanStart = () => {
+
+		if ( this.enabled && this.enableZoom ) {
+
+			this.dispatchEvent( _startEvent );
+
+			this.updateTbState( STATE.SCALE, true );
+
+			//const center = event.center;
+			let clientX = 0;
+			let clientY = 0;
+			const nFingers = this._touchCurrent.length;
+
+			for ( let i = 0; i < nFingers; i ++ ) {
+
+				clientX += this._touchCurrent[ i ].clientX;
+				clientY += this._touchCurrent[ i ].clientY;
+
+			}
+
+			this.setCenter( clientX / nFingers, clientY / nFingers );
+
+			this._startCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+			this._currentCursorPosition.copy( this._startCursorPosition );
+
+		}
+
+	};
+
+	onTriplePanMove = () => {
+
+		if ( this.enabled && this.enableZoom ) {
+
+			//	  fov / 2
+			//		|\
+			//		| \
+			//		|  \
+			//	x	|	\
+			//		| 	 \
+			//		| 	  \
+			//		| _ _ _\
+			//			y
+
+			//const center = event.center;
+			let clientX = 0;
+			let clientY = 0;
+			const nFingers = this._touchCurrent.length;
+
+			for ( let i = 0; i < nFingers; i ++ ) {
+
+				clientX += this._touchCurrent[ i ].clientX;
+				clientY += this._touchCurrent[ i ].clientY;
+
+			}
+
+			this.setCenter( clientX / nFingers, clientY / nFingers );
+
+			const screenNotches = 8;	//how many wheel notches corresponds to a full screen pan
+			this._currentCursorPosition.setY( this.getCursorNDC( _center.x, _center.y, this.domElement ).y * 0.5 );
+
+			const movement = this._currentCursorPosition.y - this._startCursorPosition.y;
+
+			let size = 1;
+
+			if ( movement < 0 ) {
+
+				size = 1 / ( Math.pow( this.scaleFactor, - movement * screenNotches ) );
+
+			} else if ( movement > 0 ) {
+
+				size = Math.pow( this.scaleFactor, movement * screenNotches );
+
+			}
+
+			this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
+			const x = this._v3_1.distanceTo( this._gizmos.position );
+			let xNew = x / size; //distance between camera and gizmos if scale(size, scalepoint) would be performed
+
+			//check min and max distance
+			xNew = MathUtils.clamp( xNew, this.minDistance, this.maxDistance );
+
+			const y = x * Math.tan( MathUtils.DEG2RAD * this._fovState * 0.5 );
+
+			//calculate new fov
+			let newFov = MathUtils.RAD2DEG * ( Math.atan( y / xNew ) * 2 );
+
+			//check min and max fov
+			newFov = MathUtils.clamp( newFov, this.minFov, this.maxFov );
+
+			const newDistance = y / Math.tan( MathUtils.DEG2RAD * ( newFov / 2 ) );
+			size = x / newDistance;
+			this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
+
+			this.setFov( newFov );
+			this.applyTransformMatrix( this.scale( size, this._v3_2, false ) );
+
+			//adjusting distance
+			_offset.copy( this._gizmos.position ).sub( this.camera.position ).normalize().multiplyScalar( newDistance / x );
+			this._m4_1.makeTranslation( _offset.x, _offset.y, _offset.z );
+
+			this.dispatchEvent( _changeEvent );
+
+		}
+
+	};
+
+	onTriplePanEnd = () => {
+
+		this.updateTbState( STATE.IDLE, false );
+		this.dispatchEvent( _endEvent );
+		//this.dispatchEvent( _changeEvent );
+
+	};
+
+	/**
+	 * Set _center's x/y coordinates
+	 * @param {Number} clientX
+	 * @param {Number} clientY
+	 */
+	setCenter = ( clientX, clientY ) => {
+
+		_center.x = clientX;
+		_center.y = clientY;
+
+	};
+
+	/**
+	 * Set default mouse actions
+	 */
+	initializeMouseActions = () => {
+
+		this.setMouseAction( 'PAN', 0, 'CTRL' );
+		this.setMouseAction( 'PAN', 2 );
+
+		this.setMouseAction( 'ROTATE', 0 );
+
+		this.setMouseAction( 'ZOOM', 'WHEEL' );
+		this.setMouseAction( 'ZOOM', 1 );
+
+		this.setMouseAction( 'FOV', 'WHEEL', 'SHIFT' );
+		this.setMouseAction( 'FOV', 1, 'SHIFT' );
+
+
+	};
+
+	/**
+	 * Compare two mouse actions
+	 * @param {Object} action1
+	 * @param {Object} action2
+	 * @returns {Boolean} True if action1 and action 2 are the same mouse action, false otherwise
+	 */
+	compareMouseAction = ( action1, action2 ) => {
+
+		if ( action1.operation == action2.operation ) {
+
+			if ( action1.mouse == action2.mouse && action1.key == action2.key ) {
+
+				return true;
+
+			} else {
+
+				return false;
+
+			}
+
+		} else {
+
+			return false;
+
+		}
+
+	};
+
+	/**
+	 * Set a new mouse action by specifying the operation to be performed and a mouse/key combination. In case of conflict, replaces the existing one
+	 * @param {String} operation The operation to be performed ('PAN', 'ROTATE', 'ZOOM', 'FOV)
+	 * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches
+	 * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed
+	 * @returns {Boolean} True if the mouse action has been successfully added, false otherwise
+	 */
+	setMouseAction = ( operation, mouse, key = null ) => {
+
+		const operationInput = [ 'PAN', 'ROTATE', 'ZOOM', 'FOV' ];
+		const mouseInput = [ 0, 1, 2, 'WHEEL' ];
+		const keyInput = [ 'CTRL', 'SHIFT', null ];
+		let state;
+
+		if ( ! operationInput.includes( operation ) || ! mouseInput.includes( mouse ) || ! keyInput.includes( key ) ) {
+
+			//invalid parameters
+			return false;
+
+		}
+
+		if ( mouse == 'WHEEL' ) {
+
+			if ( operation != 'ZOOM' && operation != 'FOV' ) {
+
+				//cannot associate 2D operation to 1D input
+				return false;
+
+			}
+
+		}
+
+		switch ( operation ) {
+
+			case 'PAN':
+
+				state = STATE.PAN;
+				break;
+
+			case 'ROTATE':
+
+				state = STATE.ROTATE;
+				break;
+
+			case 'ZOOM':
+
+				state = STATE.SCALE;
+				break;
+
+			case 'FOV':
+
+				state = STATE.FOV;
+				break;
+
+		}
+
+		const action = {
+
+			operation: operation,
+			mouse: mouse,
+			key: key,
+			state: state
+
+		};
+
+		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+
+			if ( this.mouseActions[ i ].mouse == action.mouse && this.mouseActions[ i ].key == action.key ) {
+
+				this.mouseActions.splice( i, 1, action );
+				return true;
+
+			}
+
+		}
+
+		this.mouseActions.push( action );
+		return true;
+
+	};
+
+	/**
+	 * Remove a mouse action by specifying its mouse/key combination
+	 * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches
+	 * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed
+	 * @returns {Boolean} True if the operation has been succesfully removed, false otherwise
+	 */
+	unsetMouseAction = ( mouse, key = null ) => {
+
+		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+
+			if ( this.mouseActions[ i ].mouse == mouse && this.mouseActions[ i ].key == key ) {
+
+				this.mouseActions.splice( i, 1 );
+				return true;
+
+			}
+
+		}
+
+		return false;
+
+	};
+
+	/**
+	 * Return the operation associated to a mouse/keyboard combination
+	 * @param {*} mouse A mouse button (0, 1, 2) or 'WHEEL' for wheel notches
+	 * @param {*} key The keyboard modifier ('CTRL', 'SHIFT') or null if key is not needed
+	 * @returns The operation if it has been found, null otherwise
+	 */
+	getOpFromAction = ( mouse, key ) => {
+
+		let action;
+
+		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+
+			action = this.mouseActions[ i ];
+			if ( action.mouse == mouse && action.key == key ) {
+
+				return action.operation;
+
+			}
+
+		}
+
+		if ( key != null ) {
+
+			for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+
+				action = this.mouseActions[ i ];
+				if ( action.mouse == mouse && action.key == null ) {
+
+					return action.operation;
+
+				}
+
+			}
+
+		}
+
+		return null;
+
+	};
+
+	/**
+	 * Get the operation associated to mouse and key combination and returns the corresponding FSA state
+	 * @param {Number} mouse Mouse button
+	 * @param {String} key Keyboard modifier
+	 * @returns The FSA state obtained from the operation associated to mouse/keyboard combination
+	 */
+	getOpStateFromAction = ( mouse, key ) => {
+
+		let action;
+
+		for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+
+			action = this.mouseActions[ i ];
+			if ( action.mouse == mouse && action.key == key ) {
+
+				return action.state;
+
+			}
+
+		}
+
+		if ( key != null ) {
+
+			for ( let i = 0; i < this.mouseActions.length; i ++ ) {
+
+				action = this.mouseActions[ i ];
+				if ( action.mouse == mouse && action.key == null ) {
+
+					return action.state;
+
+				}
+
+			}
+
+		}
+
+		return null;
+
+	};
+
+	/**
+	 * Calculate the angle between two pointers
+	 * @param {PointerEvent} p1
+	 * @param {PointerEvent} p2
+	 * @returns {Number} The angle between two pointers in degrees
+	 */
+	getAngle = ( p1, p2 ) => {
+
+		return Math.atan2( p2.clientY - p1.clientY, p2.clientX - p1.clientX ) * 180 / Math.PI;
+
+	};
+
+	/**
+	 * Update a PointerEvent inside current pointerevents array
+	 * @param {PointerEvent} event
+	 */
+	updateTouchEvent = ( event ) => {
+
+		for ( let i = 0; i < this._touchCurrent.length; i ++ ) {
+
+			if ( this._touchCurrent[ i ].pointerId == event.pointerId ) {
+
+				this._touchCurrent.splice( i, 1, event );
+				break;
+
+			}
+
+		}
+
+	};
+
+	/**
+	 * Apply a transformation matrix, to the camera and gizmos
+	 * @param {Object} transformation Object containing matrices to apply to camera and gizmos
+	 */
+	applyTransformMatrix( transformation ) {
+
+		if ( transformation.camera != null ) {
+
+			this._m4_1.copy( this._cameraMatrixState ).premultiply( transformation.camera );
+			this._m4_1.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
+			this.camera.updateMatrix();
+
+			//update camera up vector
+			if ( this._state == STATE.ROTATE || this._state == STATE.ZROTATE || this._state == STATE.ANIMATION_ROTATE ) {
+
+				this.camera.up.copy( this._upState ).applyQuaternion( this.camera.quaternion );
+
+			}
+
+		}
+
+		if ( transformation.gizmos != null ) {
+
+			this._m4_1.copy( this._gizmoMatrixState ).premultiply( transformation.gizmos );
+			this._m4_1.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+			this._gizmos.updateMatrix();
+
+		}
+
+		if ( this._state == STATE.SCALE || this._state == STATE.FOCUS || this._state == STATE.ANIMATION_FOCUS ) {
+
+			this._tbRadius = this.calculateTbRadius( this.camera );
+
+			if ( this.adjustNearFar ) {
+
+				const cameraDistance = this.camera.position.distanceTo( this._gizmos.position );
+
+				const bb = new Box3();
+				bb.setFromObject( this._gizmos );
+				const sphere = new Sphere();
+				bb.getBoundingSphere( sphere );
+
+				const adjustedNearPosition = Math.max( this._nearPos0, sphere.radius + sphere.center.length() );
+				const regularNearPosition = cameraDistance - this._initialNear;
+
+				const minNearPos = Math.min( adjustedNearPosition, regularNearPosition );
+				this.camera.near = cameraDistance - minNearPos;
+
+
+				const adjustedFarPosition = Math.min( this._farPos0, - sphere.radius + sphere.center.length() );
+				const regularFarPosition = cameraDistance - this._initialFar;
+
+				const minFarPos = Math.min( adjustedFarPosition, regularFarPosition );
+				this.camera.far = cameraDistance - minFarPos;
+
+				this.camera.updateProjectionMatrix();
+
+			} else {
+
+				let update = false;
+
+				if ( this.camera.near != this._initialNear ) {
+
+					this.camera.near = this._initialNear;
+					update = true;
+
+				}
+
+				if ( this.camera.far != this._initialFar ) {
+
+					this.camera.far = this._initialFar;
+					update = true;
+
+				}
+
+				if ( update ) {
+
+					this.camera.updateProjectionMatrix();
+
+				}
+
+			}
+
+		}
+
+	}
+
+	/**
+	 * Calculate the angular speed
+	 * @param {Number} p0 Position at t0
+	 * @param {Number} p1 Position at t1
+	 * @param {Number} t0 Initial time in milliseconds
+	 * @param {Number} t1 Ending time in milliseconds
+	 */
+	calculateAngularSpeed = ( p0, p1, t0, t1 ) => {
+
+		const s = p1 - p0;
+		const t = ( t1 - t0 ) / 1000;
+		if ( t == 0 ) {
+
+			return 0;
+
+		}
+
+		return s / t;
+
+	};
+
+	/**
+	 * Calculate the distance between two pointers
+	 * @param {PointerEvent} p0 The first pointer
+	 * @param {PointerEvent} p1 The second pointer
+	 * @returns {number} The distance between the two pointers
+	 */
+	calculatePointersDistance = ( p0, p1 ) => {
+
+		return Math.sqrt( Math.pow( p1.clientX - p0.clientX, 2 ) + Math.pow( p1.clientY - p0.clientY, 2 ) );
+
+	};
+
+	/**
+	 * Calculate the rotation axis as the vector perpendicular between two vectors
+	 * @param {Vector3} vec1 The first vector
+	 * @param {Vector3} vec2 The second vector
+	 * @returns {Vector3} The normalized rotation axis
+	 */
+	calculateRotationAxis = ( vec1, vec2 ) => {
+
+		this._rotationMatrix.extractRotation( this._cameraMatrixState );
+		this._quat.setFromRotationMatrix( this._rotationMatrix );
+
+		this._rotationAxis.crossVectors( vec1, vec2 ).applyQuaternion( this._quat );
+		return this._rotationAxis.normalize().clone();
+
+	};
+
+	/**
+	 * Calculate the trackball radius so that gizmo's diamater will be 2/3 of the minimum side of the camera frustum
+	 * @param {Camera} camera
+	 * @returns {Number} The trackball radius
+	 */
+	calculateTbRadius = ( camera ) => {
+
+		const distance = camera.position.distanceTo( this._gizmos.position );
+
+		if ( camera.type == 'PerspectiveCamera' ) {
+
+			const halfFovV = MathUtils.DEG2RAD * camera.fov * 0.5; //vertical fov/2 in radians
+			const halfFovH = Math.atan( ( camera.aspect ) * Math.tan( halfFovV ) ); //horizontal fov/2 in radians
+			return Math.tan( Math.min( halfFovV, halfFovH ) ) * distance * this.radiusFactor;
+
+		} else if ( camera.type == 'OrthographicCamera' ) {
+
+			return Math.min( camera.top, camera.right ) * this.radiusFactor;
+
+		}
+
+	};
+
+	/**
+	 * Focus operation consist of positioning the point of interest in front of the camera and a slightly zoom in
+	 * @param {Vector3} point The point of interest
+	 * @param {Number} size Scale factor
+	 * @param {Number} amount Amount of operation to be completed (used for focus animations, default is complete full operation)
+	 */
+	focus = ( point, size, amount = 1 ) => {
+
+		//move center of camera (along with gizmos) towards point of interest
+		_offset.copy( point ).sub( this._gizmos.position ).multiplyScalar( amount );
+		this._translationMatrix.makeTranslation( _offset.x, _offset.y, _offset.z );
+
+		_gizmoMatrixStateTemp.copy( this._gizmoMatrixState );
+		this._gizmoMatrixState.premultiply( this._translationMatrix );
+		this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+
+		_cameraMatrixStateTemp.copy( this._cameraMatrixState );
+		this._cameraMatrixState.premultiply( this._translationMatrix );
+		this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
+
+		//apply zoom
+		if ( this.enableZoom ) {
+
+			this.applyTransformMatrix( this.scale( size, this._gizmos.position ) );
+
+		}
+
+		this._gizmoMatrixState.copy( _gizmoMatrixStateTemp );
+		this._cameraMatrixState.copy( _cameraMatrixStateTemp );
+
+	};
+
+	/**
+	 * Draw a grid and add it to the scene
+	 */
+	drawGrid = () => {
+
+		if ( this.scene != null ) {
+
+			const color = 0x888888;
+			const multiplier = 3;
+			let size, divisions, maxLength, tick;
+
+			if ( this.camera.isOrthographicCamera ) {
+
+				const width = this.camera.right - this.camera.left;
+				const height = this.camera.bottom - this.camera.top;
+
+				maxLength = Math.max( width, height );
+				tick = maxLength / 20;
+
+				size = maxLength / this.camera.zoom * multiplier;
+				divisions = size / tick * this.camera.zoom;
+
+			} else if ( this.camera.isPerspectiveCamera ) {
+
+				const distance = this.camera.position.distanceTo( this._gizmos.position );
+				const halfFovV = MathUtils.DEG2RAD * this.camera.fov * 0.5;
+				const halfFovH = Math.atan( ( this.camera.aspect ) * Math.tan( halfFovV ) );
+
+				maxLength = Math.tan( Math.max( halfFovV, halfFovH ) ) * distance * 2;
+				tick = maxLength / 20;
+
+				size = maxLength * multiplier;
+				divisions = size / tick;
+
+			}
+
+			if ( this._grid == null ) {
+
+				this._grid = new GridHelper( size, divisions, color, color );
+				this._grid.position.copy( this._gizmos.position );
+				this._gridPosition.copy( this._grid.position );
+				this._grid.quaternion.copy( this.camera.quaternion );
+				this._grid.rotateX( Math.PI * 0.5 );
+
+				this.scene.add( this._grid );
+
+			}
+
+		}
+
+	};
+
+	/**
+	 * Remove all listeners, stop animations and clean scene
+	 */
+	dispose = () => {
+
+		if ( this._animationId != - 1 ) {
+
+			window.cancelAnimationFrame( this._animationId );
+
+		}
+
+		this.domElement.removeEventListener( 'pointerdown', this.onPointerDown );
+		this.domElement.removeEventListener( 'pointercancel', this.onPointerCancel );
+		this.domElement.removeEventListener( 'wheel', this.onWheel );
+		this.domElement.removeEventListener( 'contextmenu', this.onContextMenu );
+
+		window.removeEventListener( 'pointermove', this.onPointerMove );
+		window.removeEventListener( 'pointerup', this.onPointerUp );
+
+		window.removeEventListener( 'resize', this.onWindowResize );
+
+		if ( this.scene !== null ) this.scene.remove( this._gizmos );
+		this.disposeGrid();
+
+	};
+
+	/**
+	 * remove the grid from the scene
+	 */
+	disposeGrid = () => {
+
+		if ( this._grid != null && this.scene != null ) {
+
+			this.scene.remove( this._grid );
+			this._grid = null;
+
+		}
+
+	};
+
+	/**
+	 * Compute the easing out cubic function for ease out effect in animation
+	 * @param {Number} t The absolute progress of the animation in the bound of 0 (beginning of the) and 1 (ending of animation)
+	 * @returns {Number} Result of easing out cubic at time t
+	 */
+	easeOutCubic = ( t ) => {
+
+		return 1 - Math.pow( 1 - t, 3 );
+
+	};
+
+	/**
+	 * Make rotation gizmos more or less visible
+	 * @param {Boolean} isActive If true, make gizmos more visible
+	 */
+	activateGizmos = ( isActive ) => {
+
+		const gizmoX = this._gizmos.children[ 0 ];
+		const gizmoY = this._gizmos.children[ 1 ];
+		const gizmoZ = this._gizmos.children[ 2 ];
+
+		if ( isActive ) {
+
+			gizmoX.material.setValues( { opacity: 1 } );
+			gizmoY.material.setValues( { opacity: 1 } );
+			gizmoZ.material.setValues( { opacity: 1 } );
+
+		} else {
+
+			gizmoX.material.setValues( { opacity: 0.6 } );
+			gizmoY.material.setValues( { opacity: 0.6 } );
+			gizmoZ.material.setValues( { opacity: 0.6 } );
+
+		}
+
+	};
+
+	/**
+	 * Calculate the cursor position in NDC
+	 * @param {number} x Cursor horizontal coordinate within the canvas
+	 * @param {number} y Cursor vertical coordinate within the canvas
+	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
+	 * @returns {Vector2} Cursor normalized position inside the canvas
+	 */
+	getCursorNDC = ( cursorX, cursorY, canvas ) => {
+
+		const canvasRect = canvas.getBoundingClientRect();
+		this._v2_1.setX( ( ( cursorX - canvasRect.left ) / canvasRect.width ) * 2 - 1 );
+		this._v2_1.setY( ( ( canvasRect.bottom - cursorY ) / canvasRect.height ) * 2 - 1 );
+		return this._v2_1.clone();
+
+	};
+
+	/**
+	 * Calculate the cursor position inside the canvas x/y coordinates with the origin being in the center of the canvas
+	 * @param {Number} x Cursor horizontal coordinate within the canvas
+	 * @param {Number} y Cursor vertical coordinate within the canvas
+	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
+	 * @returns {Vector2} Cursor position inside the canvas
+	 */
+	getCursorPosition = ( cursorX, cursorY, canvas ) => {
+
+		this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) );
+		this._v2_1.x *= ( this.camera.right - this.camera.left ) * 0.5;
+		this._v2_1.y *= ( this.camera.top - this.camera.bottom ) * 0.5;
+		return this._v2_1.clone();
+
+	};
+
+	/**
+	 * Set the camera to be controlled
+	 * @param {Camera} camera The virtual camera to be controlled
+	 */
+	setCamera = ( camera ) => {
+
+		camera.lookAt( this.target );
+		camera.updateMatrix();
+
+		//setting state
+		if ( camera.type == 'PerspectiveCamera' ) {
+
+			this._fov0 = camera.fov;
+			this._fovState = camera.fov;
+
+		}
+
+		this._cameraMatrixState0.copy( camera.matrix );
+		this._cameraMatrixState.copy( this._cameraMatrixState0 );
+		this._cameraProjectionState.copy( camera.projectionMatrix );
+		this._zoom0 = camera.zoom;
+		this._zoomState = this._zoom0;
+
+		this._initialNear = camera.near;
+		this._nearPos0 = camera.position.distanceTo( this.target ) - camera.near;
+		this._nearPos = this._initialNear;
+
+		this._initialFar = camera.far;
+		this._farPos0 = camera.position.distanceTo( this.target ) - camera.far;
+		this._farPos = this._initialFar;
+
+		this._up0.copy( camera.up );
+		this._upState.copy( camera.up );
+
+		this.camera = camera;
+		this.camera.updateProjectionMatrix();
+
+		//making gizmos
+		this._tbRadius = this.calculateTbRadius( camera );
+		this.makeGizmos( this.target, this._tbRadius );
+
+	};
+
+	/**
+	 * Set gizmos visibility
+	 * @param {Boolean} value Value of gizmos visibility
+	 */
+	setGizmosVisible( value ) {
+
+		this._gizmos.visible = value;
+		this.dispatchEvent( _changeEvent );
+
+	}
+
+	/**
+	 * Set gizmos radius factor and redraws gizmos
+	 * @param {Float} value Value of radius factor
+	 */
+	setTbRadius( value ) {
+
+		this.radiusFactor = value;
+		this._tbRadius = this.calculateTbRadius( this.camera );
+
+		const curve = new EllipseCurve( 0, 0, this._tbRadius, this._tbRadius );
+		const points = curve.getPoints( this._curvePts );
+		const curveGeometry = new BufferGeometry().setFromPoints( points );
+
+
+		for ( const gizmo in this._gizmos.children ) {
+
+			this._gizmos.children[ gizmo ].geometry = curveGeometry;
+
+		}
+
+		this.dispatchEvent( _changeEvent );
+
+	}
+
+	/**
+	 * Creates the rotation gizmos matching trackball center and radius
+	 * @param {Vector3} tbCenter The trackball center
+	 * @param {number} tbRadius The trackball radius
+	 */
+	makeGizmos = ( tbCenter, tbRadius ) => {
+
+		const curve = new EllipseCurve( 0, 0, tbRadius, tbRadius );
+		const points = curve.getPoints( this._curvePts );
+
+		//geometry
+		const curveGeometry = new BufferGeometry().setFromPoints( points );
+
+		//material
+		const curveMaterialX = new LineBasicMaterial( { color: 0xff8080, fog: false, transparent: true, opacity: 0.6 } );
+		const curveMaterialY = new LineBasicMaterial( { color: 0x80ff80, fog: false, transparent: true, opacity: 0.6 } );
+		const curveMaterialZ = new LineBasicMaterial( { color: 0x8080ff, fog: false, transparent: true, opacity: 0.6 } );
+
+		//line
+		const gizmoX = new Line( curveGeometry, curveMaterialX );
+		const gizmoY = new Line( curveGeometry, curveMaterialY );
+		const gizmoZ = new Line( curveGeometry, curveMaterialZ );
+
+		const rotation = Math.PI * 0.5;
+		gizmoX.rotation.x = rotation;
+		gizmoY.rotation.y = rotation;
+
+
+		//setting state
+		this._gizmoMatrixState0.identity().setPosition( tbCenter );
+		this._gizmoMatrixState.copy( this._gizmoMatrixState0 );
+
+		if ( this.camera.zoom !== 1 ) {
+
+			//adapt gizmos size to camera zoom
+			const size = 1 / this.camera.zoom;
+			this._scaleMatrix.makeScale( size, size, size );
+			this._translationMatrix.makeTranslation( - tbCenter.x, - tbCenter.y, - tbCenter.z );
+
+			this._gizmoMatrixState.premultiply( this._translationMatrix ).premultiply( this._scaleMatrix );
+			this._translationMatrix.makeTranslation( tbCenter.x, tbCenter.y, tbCenter.z );
+			this._gizmoMatrixState.premultiply( this._translationMatrix );
+
+		}
+
+		this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+
+		//
+
+		this._gizmos.traverse( function ( object ) {
+
+			if ( object.isLine ) {
+
+				object.geometry.dispose();
+				object.material.dispose();
+
+			}
+
+		} );
+
+		this._gizmos.clear();
+
+		//
+
+		this._gizmos.add( gizmoX );
+		this._gizmos.add( gizmoY );
+		this._gizmos.add( gizmoZ );
+
+	};
+
+	/**
+	 * Perform animation for focus operation
+	 * @param {Number} time Instant in which this function is called as performance.now()
+	 * @param {Vector3} point Point of interest for focus operation
+	 * @param {Matrix4} cameraMatrix Camera matrix
+	 * @param {Matrix4} gizmoMatrix Gizmos matrix
+	 */
+	onFocusAnim = ( time, point, cameraMatrix, gizmoMatrix ) => {
+
+		if ( this._timeStart == - 1 ) {
+
+			//animation start
+			this._timeStart = time;
+
+		}
+
+		if ( this._state == STATE.ANIMATION_FOCUS ) {
+
+			const deltaTime = time - this._timeStart;
+			const animTime = deltaTime / this.focusAnimationTime;
+
+			this._gizmoMatrixState.copy( gizmoMatrix );
+
+			if ( animTime >= 1 ) {
+
+				//animation end
+
+				this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+
+				this.focus( point, this.scaleFactor );
+
+				this._timeStart = - 1;
+				this.updateTbState( STATE.IDLE, false );
+				this.activateGizmos( false );
+
+				this.dispatchEvent( _changeEvent );
+
+			} else {
+
+				const amount = this.easeOutCubic( animTime );
+				const size = ( ( 1 - amount ) + ( this.scaleFactor * amount ) );
+
+				this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+				this.focus( point, size, amount );
+
+				this.dispatchEvent( _changeEvent );
+				const self = this;
+				this._animationId = window.requestAnimationFrame( function ( t ) {
+
+					self.onFocusAnim( t, point, cameraMatrix, gizmoMatrix.clone() );
+
+				} );
+
+			}
+
+		} else {
+
+			//interrupt animation
+
+			this._animationId = - 1;
+			this._timeStart = - 1;
+
+		}
+
+	};
+
+	/**
+	 * Perform animation for rotation operation
+	 * @param {Number} time Instant in which this function is called as performance.now()
+	 * @param {Vector3} rotationAxis Rotation axis
+	 * @param {number} w0 Initial angular velocity
+	 */
+	onRotationAnim = ( time, rotationAxis, w0 ) => {
+
+		if ( this._timeStart == - 1 ) {
+
+			//animation start
+			this._anglePrev = 0;
+			this._angleCurrent = 0;
+			this._timeStart = time;
+
+		}
+
+		if ( this._state == STATE.ANIMATION_ROTATE ) {
+
+			//w = w0 + alpha * t
+			const deltaTime = ( time - this._timeStart ) / 1000;
+			const w = w0 + ( ( - this.dampingFactor ) * deltaTime );
+
+			if ( w > 0 ) {
+
+				//tetha = 0.5 * alpha * t^2 + w0 * t + tetha0
+				this._angleCurrent = 0.5 * ( - this.dampingFactor ) * Math.pow( deltaTime, 2 ) + w0 * deltaTime + 0;
+				this.applyTransformMatrix( this.rotate( rotationAxis, this._angleCurrent ) );
+				this.dispatchEvent( _changeEvent );
+				const self = this;
+				this._animationId = window.requestAnimationFrame( function ( t ) {
+
+					self.onRotationAnim( t, rotationAxis, w0 );
+
+				} );
+
+			} else {
+
+				this._animationId = - 1;
+				this._timeStart = - 1;
+
+				this.updateTbState( STATE.IDLE, false );
+				this.activateGizmos( false );
+
+				this.dispatchEvent( _changeEvent );
+
+			}
+
+		} else {
+
+			//interrupt animation
+
+			this._animationId = - 1;
+			this._timeStart = - 1;
+
+			if ( this._state != STATE.ROTATE ) {
+
+				this.activateGizmos( false );
+				this.dispatchEvent( _changeEvent );
+
+			}
+
+		}
+
+	};
+
+
+	/**
+	 * Perform pan operation moving camera between two points
+	 * @param {Vector3} p0 Initial point
+	 * @param {Vector3} p1 Ending point
+	 * @param {Boolean} adjust If movement should be adjusted considering camera distance (Perspective only)
+	 */
+	pan = ( p0, p1, adjust = false ) => {
+
+		const movement = p0.clone().sub( p1 );
+
+		if ( this.camera.isOrthographicCamera ) {
+
+			//adjust movement amount
+			movement.multiplyScalar( 1 / this.camera.zoom );
+
+		} else if ( this.camera.isPerspectiveCamera && adjust ) {
+
+			//adjust movement amount
+			this._v3_1.setFromMatrixPosition( this._cameraMatrixState0 );	//camera's initial position
+			this._v3_2.setFromMatrixPosition( this._gizmoMatrixState0 );	//gizmo's initial position
+			const distanceFactor = this._v3_1.distanceTo( this._v3_2 ) / this.camera.position.distanceTo( this._gizmos.position );
+			movement.multiplyScalar( 1 / distanceFactor );
+
+		}
+
+		this._v3_1.set( movement.x, movement.y, 0 ).applyQuaternion( this.camera.quaternion );
+
+		this._m4_1.makeTranslation( this._v3_1.x, this._v3_1.y, this._v3_1.z );
+
+		this.setTransformationMatrices( this._m4_1, this._m4_1 );
+		return _transformation;
+
+	};
+
+	/**
+	 * Reset trackball
+	 */
+	reset = () => {
+
+		this.camera.zoom = this._zoom0;
+
+		if ( this.camera.isPerspectiveCamera ) {
+
+			this.camera.fov = this._fov0;
+
+		}
+
+		this.camera.near = this._nearPos;
+		this.camera.far = this._farPos;
+		this._cameraMatrixState.copy( this._cameraMatrixState0 );
+		this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
+		this.camera.up.copy( this._up0 );
+
+		this.camera.updateMatrix();
+		this.camera.updateProjectionMatrix();
+
+		this._gizmoMatrixState.copy( this._gizmoMatrixState0 );
+		this._gizmoMatrixState0.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+		this._gizmos.updateMatrix();
+
+		this._tbRadius = this.calculateTbRadius( this.camera );
+		this.makeGizmos( this._gizmos.position, this._tbRadius );
+
+		this.camera.lookAt( this._gizmos.position );
+
+		this.updateTbState( STATE.IDLE, false );
+
+		this.dispatchEvent( _changeEvent );
+
+	};
+
+	/**
+	 * Rotate the camera around an axis passing by trackball's center
+	 * @param {Vector3} axis Rotation axis
+	 * @param {number} angle Angle in radians
+	 * @returns {Object} Object with 'camera' field containing transformation matrix resulting from the operation to be applied to the camera
+	 */
+	rotate = ( axis, angle ) => {
+
+		const point = this._gizmos.position; //rotation center
+		this._translationMatrix.makeTranslation( - point.x, - point.y, - point.z );
+		this._rotationMatrix.makeRotationAxis( axis, - angle );
+
+		//rotate camera
+		this._m4_1.makeTranslation( point.x, point.y, point.z );
+		this._m4_1.multiply( this._rotationMatrix );
+		this._m4_1.multiply( this._translationMatrix );
+
+		this.setTransformationMatrices( this._m4_1 );
+
+		return _transformation;
+
+	};
+
+	copyState = () => {
+
+		let state;
+		if ( this.camera.isOrthographicCamera ) {
+
+			state = JSON.stringify( { arcballState: {
+
+				cameraFar: this.camera.far,
+				cameraMatrix: this.camera.matrix,
+				cameraNear: this.camera.near,
+				cameraUp: this.camera.up,
+				cameraZoom: this.camera.zoom,
+				gizmoMatrix: this._gizmos.matrix
+
+			} } );
+
+		} else if ( this.camera.isPerspectiveCamera ) {
+
+			state = JSON.stringify( { arcballState: {
+				cameraFar: this.camera.far,
+				cameraFov: this.camera.fov,
+				cameraMatrix: this.camera.matrix,
+				cameraNear: this.camera.near,
+				cameraUp: this.camera.up,
+				cameraZoom: this.camera.zoom,
+				gizmoMatrix: this._gizmos.matrix
+
+			} } );
+
+		}
+
+		navigator.clipboard.writeText( state );
+
+	};
+
+	pasteState = () => {
+
+		const self = this;
+		navigator.clipboard.readText().then( function resolved( value ) {
+
+			self.setStateFromJSON( value );
+
+		} );
+
+	};
+
+	/**
+	 * Save the current state of the control. This can later be recover with .reset
+	 */
+	saveState = () => {
+
+		this._cameraMatrixState0.copy( this.camera.matrix );
+		this._gizmoMatrixState0.copy( this._gizmos.matrix );
+		this._nearPos = this.camera.near;
+		this._farPos = this.camera.far;
+		this._zoom0 = this.camera.zoom;
+		this._up0.copy( this.camera.up );
+
+		if ( this.camera.isPerspectiveCamera ) {
+
+			this._fov0 = this.camera.fov;
+
+		}
+
+	};
+
+	/**
+	 * Perform uniform scale operation around a given point
+	 * @param {Number} size Scale factor
+	 * @param {Vector3} point Point around which scale
+	 * @param {Boolean} scaleGizmos If gizmos should be scaled (Perspective only)
+	 * @returns {Object} Object with 'camera' and 'gizmo' fields containing transformation matrices resulting from the operation to be applied to the camera and gizmos
+	 */
+	scale = ( size, point, scaleGizmos = true ) => {
+
+		_scalePointTemp.copy( point );
+		let sizeInverse = 1 / size;
+
+		if ( this.camera.isOrthographicCamera ) {
+
+			//camera zoom
+			this.camera.zoom = this._zoomState;
+			this.camera.zoom *= size;
+
+			//check min and max zoom
+			if ( this.camera.zoom > this.maxZoom ) {
+
+				this.camera.zoom = this.maxZoom;
+				sizeInverse = this._zoomState / this.maxZoom;
+
+			} else if ( this.camera.zoom < this.minZoom ) {
+
+				this.camera.zoom = this.minZoom;
+				sizeInverse = this._zoomState / this.minZoom;
+
+			}
+
+			this.camera.updateProjectionMatrix();
+
+			this._v3_1.setFromMatrixPosition( this._gizmoMatrixState );	//gizmos position
+
+			//scale gizmos so they appear in the same spot having the same dimension
+			this._scaleMatrix.makeScale( sizeInverse, sizeInverse, sizeInverse );
+			this._translationMatrix.makeTranslation( - this._v3_1.x, - this._v3_1.y, - this._v3_1.z );
+
+			this._m4_2.makeTranslation( this._v3_1.x, this._v3_1.y, this._v3_1.z ).multiply( this._scaleMatrix );
+			this._m4_2.multiply( this._translationMatrix );
+
+
+			//move camera and gizmos to obtain pinch effect
+			_scalePointTemp.sub( this._v3_1 );
+
+			const amount = _scalePointTemp.clone().multiplyScalar( sizeInverse );
+			_scalePointTemp.sub( amount );
+
+			this._m4_1.makeTranslation( _scalePointTemp.x, _scalePointTemp.y, _scalePointTemp.z );
+			this._m4_2.premultiply( this._m4_1 );
+
+			this.setTransformationMatrices( this._m4_1, this._m4_2 );
+			return _transformation;
+
+		} else if ( this.camera.isPerspectiveCamera ) {
+
+			this._v3_1.setFromMatrixPosition( this._cameraMatrixState );
+			this._v3_2.setFromMatrixPosition( this._gizmoMatrixState );
+
+			//move camera
+			let distance = this._v3_1.distanceTo( _scalePointTemp );
+			let amount = distance - ( distance * sizeInverse );
+
+			//check min and max distance
+			const newDistance = distance - amount;
+			if ( newDistance < this.minDistance ) {
+
+				sizeInverse = this.minDistance / distance;
+				amount = distance - ( distance * sizeInverse );
+
+			} else if ( newDistance > this.maxDistance ) {
+
+				sizeInverse = this.maxDistance / distance;
+				amount = distance - ( distance * sizeInverse );
+
+			}
+
+			_offset.copy( _scalePointTemp ).sub( this._v3_1 ).normalize().multiplyScalar( amount );
+
+			this._m4_1.makeTranslation( _offset.x, _offset.y, _offset.z );
+
+
+			if ( scaleGizmos ) {
+
+				//scale gizmos so they appear in the same spot having the same dimension
+				const pos = this._v3_2;
+
+				distance = pos.distanceTo( _scalePointTemp );
+				amount = distance - ( distance * sizeInverse );
+				_offset.copy( _scalePointTemp ).sub( this._v3_2 ).normalize().multiplyScalar( amount );
+
+				this._translationMatrix.makeTranslation( pos.x, pos.y, pos.z );
+				this._scaleMatrix.makeScale( sizeInverse, sizeInverse, sizeInverse );
+
+				this._m4_2.makeTranslation( _offset.x, _offset.y, _offset.z ).multiply( this._translationMatrix );
+				this._m4_2.multiply( this._scaleMatrix );
+
+				this._translationMatrix.makeTranslation( - pos.x, - pos.y, - pos.z );
+
+				this._m4_2.multiply( this._translationMatrix );
+				this.setTransformationMatrices( this._m4_1, this._m4_2 );
+
+
+			} else {
+
+				this.setTransformationMatrices( this._m4_1 );
+
+			}
+
+			return _transformation;
+
+		}
+
+	};
+
+	/**
+	 * Set camera fov
+	 * @param {Number} value fov to be setted
+	 */
+	setFov = ( value ) => {
+
+		if ( this.camera.isPerspectiveCamera ) {
+
+			this.camera.fov = MathUtils.clamp( value, this.minFov, this.maxFov );
+			this.camera.updateProjectionMatrix();
+
+		}
+
+	};
+
+	/**
+	 * Set values in transformation object
+	 * @param {Matrix4} camera Transformation to be applied to the camera
+	 * @param {Matrix4} gizmos Transformation to be applied to gizmos
+	 */
+	 setTransformationMatrices( camera = null, gizmos = null ) {
+
+		if ( camera != null ) {
+
+			if ( _transformation.camera != null ) {
+
+				_transformation.camera.copy( camera );
+
+			} else {
+
+				_transformation.camera = camera.clone();
+
+			}
+
+		} else {
+
+			_transformation.camera = null;
+
+		}
+
+		if ( gizmos != null ) {
+
+			if ( _transformation.gizmos != null ) {
+
+				_transformation.gizmos.copy( gizmos );
+
+			} else {
+
+				_transformation.gizmos = gizmos.clone();
+
+			}
+
+		} else {
+
+			_transformation.gizmos = null;
+
+		}
+
+	}
+
+	/**
+	 * Rotate camera around its direction axis passing by a given point by a given angle
+	 * @param {Vector3} point The point where the rotation axis is passing trough
+	 * @param {Number} angle Angle in radians
+	 * @returns The computed transormation matix
+	 */
+	zRotate = ( point, angle ) => {
+
+		this._rotationMatrix.makeRotationAxis( this._rotationAxis, angle );
+		this._translationMatrix.makeTranslation( - point.x, - point.y, - point.z );
+
+		this._m4_1.makeTranslation( point.x, point.y, point.z );
+		this._m4_1.multiply( this._rotationMatrix );
+		this._m4_1.multiply( this._translationMatrix );
+
+		this._v3_1.setFromMatrixPosition( this._gizmoMatrixState ).sub( point );	//vector from rotation center to gizmos position
+		this._v3_2.copy( this._v3_1 ).applyAxisAngle( this._rotationAxis, angle );	//apply rotation
+		this._v3_2.sub( this._v3_1 );
+
+		this._m4_2.makeTranslation( this._v3_2.x, this._v3_2.y, this._v3_2.z );
+
+		this.setTransformationMatrices( this._m4_1, this._m4_2 );
+		return _transformation;
+
+	};
+
+
+	getRaycaster() {
+
+		return _raycaster;
+
+	}
+
+
+	/**
+	 * Unproject the cursor on the 3D object surface
+	 * @param {Vector2} cursor Cursor coordinates in NDC
+	 * @param {Camera} camera Virtual camera
+	 * @returns {Vector3} The point of intersection with the model, if exist, null otherwise
+	 */
+	unprojectOnObj = ( cursor, camera ) => {
+
+		const raycaster = this.getRaycaster();
+		raycaster.near = camera.near;
+		raycaster.far = camera.far;
+		raycaster.setFromCamera( cursor, camera );
+
+		const intersect = raycaster.intersectObjects( this.scene.children, true );
+
+		for ( let i = 0; i < intersect.length; i ++ ) {
+
+			if ( intersect[ i ].object.uuid != this._gizmos.uuid && intersect[ i ].face != null ) {
+
+				return intersect[ i ].point.clone();
+
+			}
+
+		}
+
+		return null;
+
+	};
+
+	/**
+	 * Unproject the cursor on the trackball surface
+	 * @param {Camera} camera The virtual camera
+	 * @param {Number} cursorX Cursor horizontal coordinate on screen
+	 * @param {Number} cursorY Cursor vertical coordinate on screen
+	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
+	 * @param {number} tbRadius The trackball radius
+	 * @returns {Vector3} The unprojected point on the trackball surface
+	 */
+	unprojectOnTbSurface = ( camera, cursorX, cursorY, canvas, tbRadius ) => {
+
+		if ( camera.type == 'OrthographicCamera' ) {
+
+			this._v2_1.copy( this.getCursorPosition( cursorX, cursorY, canvas ) );
+			this._v3_1.set( this._v2_1.x, this._v2_1.y, 0 );
+
+			const x2 = Math.pow( this._v2_1.x, 2 );
+			const y2 = Math.pow( this._v2_1.y, 2 );
+			const r2 = Math.pow( this._tbRadius, 2 );
+
+			if ( x2 + y2 <= r2 * 0.5 ) {
+
+				//intersection with sphere
+				this._v3_1.setZ( Math.sqrt( r2 - ( x2 + y2 ) ) );
+
+			} else {
+
+				//intersection with hyperboloid
+				this._v3_1.setZ( ( r2 * 0.5 ) / ( Math.sqrt( x2 + y2 ) ) );
+
+			}
+
+			return this._v3_1;
+
+		} else if ( camera.type == 'PerspectiveCamera' ) {
+
+			//unproject cursor on the near plane
+			this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) );
+
+			this._v3_1.set( this._v2_1.x, this._v2_1.y, - 1 );
+			this._v3_1.applyMatrix4( camera.projectionMatrixInverse );
+
+			const rayDir = this._v3_1.clone().normalize(); //unprojected ray direction
+			const cameraGizmoDistance = camera.position.distanceTo( this._gizmos.position );
+			const radius2 = Math.pow( tbRadius, 2 );
+
+			//	  camera
+			//		|\
+			//		| \
+			//		|  \
+			//	h	|	\
+			//		| 	 \
+			//		| 	  \
+			//	_ _ | _ _ _\ _ _  near plane
+			//			l
+
+			const h = this._v3_1.z;
+			const l = Math.sqrt( Math.pow( this._v3_1.x, 2 ) + Math.pow( this._v3_1.y, 2 ) );
+
+			if ( l == 0 ) {
+
+				//ray aligned with camera
+				rayDir.set( this._v3_1.x, this._v3_1.y, tbRadius );
+				return rayDir;
+
+			}
+
+			const m = h / l;
+			const q = cameraGizmoDistance;
+
+			/*
+			 * calculate intersection point between unprojected ray and trackball surface
+			 *|y = m * x + q
+			 *|x^2 + y^2 = r^2
+			 *
+			 * (m^2 + 1) * x^2 + (2 * m * q) * x + q^2 - r^2 = 0
+			 */
+			let a = Math.pow( m, 2 ) + 1;
+			let b = 2 * m * q;
+			let c = Math.pow( q, 2 ) - radius2;
+			let delta = Math.pow( b, 2 ) - ( 4 * a * c );
+
+			if ( delta >= 0 ) {
+
+				//intersection with sphere
+				this._v2_1.setX( ( - b - Math.sqrt( delta ) ) / ( 2 * a ) );
+				this._v2_1.setY( m * this._v2_1.x + q );
+
+				const angle = MathUtils.RAD2DEG * this._v2_1.angle();
+
+				if ( angle >= 45 ) {
+
+					//if angle between intersection point and X' axis is >= 45°, return that point
+					//otherwise, calculate intersection point with hyperboloid
+
+					const rayLength = Math.sqrt( Math.pow( this._v2_1.x, 2 ) + Math.pow( ( cameraGizmoDistance - this._v2_1.y ), 2 ) );
+					rayDir.multiplyScalar( rayLength );
+					rayDir.z += cameraGizmoDistance;
+					return rayDir;
+
+				}
+
+			}
+
+			//intersection with hyperboloid
+			/*
+			 *|y = m * x + q
+			 *|y = (1 / x) * (r^2 / 2)
+			 *
+			 * m * x^2 + q * x - r^2 / 2 = 0
+			 */
+
+			a = m;
+			b = q;
+			c = - radius2 * 0.5;
+			delta = Math.pow( b, 2 ) - ( 4 * a * c );
+			this._v2_1.setX( ( - b - Math.sqrt( delta ) ) / ( 2 * a ) );
+			this._v2_1.setY( m * this._v2_1.x + q );
+
+			const rayLength = Math.sqrt( Math.pow( this._v2_1.x, 2 ) + Math.pow( ( cameraGizmoDistance - this._v2_1.y ), 2 ) );
+
+			rayDir.multiplyScalar( rayLength );
+			rayDir.z += cameraGizmoDistance;
+			return rayDir;
+
+		}
+
+	};
+
+
+	/**
+	 * Unproject the cursor on the plane passing through the center of the trackball orthogonal to the camera
+	 * @param {Camera} camera The virtual camera
+	 * @param {Number} cursorX Cursor horizontal coordinate on screen
+	 * @param {Number} cursorY Cursor vertical coordinate on screen
+	 * @param {HTMLElement} canvas The canvas where the renderer draws its output
+	 * @param {Boolean} initialDistance If initial distance between camera and gizmos should be used for calculations instead of current (Perspective only)
+	 * @returns {Vector3} The unprojected point on the trackball plane
+	 */
+	unprojectOnTbPlane = ( camera, cursorX, cursorY, canvas, initialDistance = false ) => {
+
+		if ( camera.type == 'OrthographicCamera' ) {
+
+			this._v2_1.copy( this.getCursorPosition( cursorX, cursorY, canvas ) );
+			this._v3_1.set( this._v2_1.x, this._v2_1.y, 0 );
+
+			return this._v3_1.clone();
+
+		} else if ( camera.type == 'PerspectiveCamera' ) {
+
+			this._v2_1.copy( this.getCursorNDC( cursorX, cursorY, canvas ) );
+
+			//unproject cursor on the near plane
+			this._v3_1.set( this._v2_1.x, this._v2_1.y, - 1 );
+			this._v3_1.applyMatrix4( camera.projectionMatrixInverse );
+
+			const rayDir = this._v3_1.clone().normalize(); //unprojected ray direction
+
+			//	  camera
+			//		|\
+			//		| \
+			//		|  \
+			//	h	|	\
+			//		| 	 \
+			//		| 	  \
+			//	_ _ | _ _ _\ _ _  near plane
+			//			l
+
+			const h = this._v3_1.z;
+			const l = Math.sqrt( Math.pow( this._v3_1.x, 2 ) + Math.pow( this._v3_1.y, 2 ) );
+			let cameraGizmoDistance;
+
+			if ( initialDistance ) {
+
+				cameraGizmoDistance = this._v3_1.setFromMatrixPosition( this._cameraMatrixState0 ).distanceTo( this._v3_2.setFromMatrixPosition( this._gizmoMatrixState0 ) );
+
+			} else {
+
+				cameraGizmoDistance = camera.position.distanceTo( this._gizmos.position );
+
+			}
+
+			/*
+			 * calculate intersection point between unprojected ray and the plane
+			 *|y = mx + q
+			 *|y = 0
+			 *
+			 * x = -q/m
+			*/
+			if ( l == 0 ) {
+
+				//ray aligned with camera
+				rayDir.set( 0, 0, 0 );
+				return rayDir;
+
+			}
+
+			const m = h / l;
+			const q = cameraGizmoDistance;
+			const x = - q / m;
+
+			const rayLength = Math.sqrt( Math.pow( q, 2 ) + Math.pow( x, 2 ) );
+			rayDir.multiplyScalar( rayLength );
+			rayDir.z = 0;
+			return rayDir;
+
+		}
+
+	};
+
+	/**
+	 * Update camera and gizmos state
+	 */
+	updateMatrixState = () => {
+
+		//update camera and gizmos state
+		this._cameraMatrixState.copy( this.camera.matrix );
+		this._gizmoMatrixState.copy( this._gizmos.matrix );
+
+		if ( this.camera.isOrthographicCamera ) {
+
+			this._cameraProjectionState.copy( this.camera.projectionMatrix );
+			this.camera.updateProjectionMatrix();
+			this._zoomState = this.camera.zoom;
+
+		} else if ( this.camera.isPerspectiveCamera ) {
+
+			this._fovState = this.camera.fov;
+
+		}
+
+	};
+
+	/**
+	 * Update the trackball FSA
+	 * @param {STATE} newState New state of the FSA
+	 * @param {Boolean} updateMatrices If matriices state should be updated
+	 */
+	updateTbState = ( newState, updateMatrices ) => {
+
+		this._state = newState;
+		if ( updateMatrices ) {
+
+			this.updateMatrixState();
+
+		}
+
+	};
+
+	update = () => {
+
+		const EPS = 0.000001;
+
+		if ( this.target.equals( this._currentTarget ) === false ) {
+
+			this._gizmos.position.copy( this.target );	//for correct radius calculation
+			this._tbRadius = this.calculateTbRadius( this.camera );
+			this.makeGizmos( this.target, this._tbRadius );
+			this._currentTarget.copy( this.target );
+
+		}
+
+		//check min/max parameters
+		if ( this.camera.isOrthographicCamera ) {
+
+			//check zoom
+			if ( this.camera.zoom > this.maxZoom || this.camera.zoom < this.minZoom ) {
+
+				const newZoom = MathUtils.clamp( this.camera.zoom, this.minZoom, this.maxZoom );
+				this.applyTransformMatrix( this.scale( newZoom / this.camera.zoom, this._gizmos.position, true ) );
+
+			}
+
+		} else if ( this.camera.isPerspectiveCamera ) {
+
+			//check distance
+			const distance = this.camera.position.distanceTo( this._gizmos.position );
+
+			if ( distance > this.maxDistance + EPS || distance < this.minDistance - EPS ) {
+
+				const newDistance = MathUtils.clamp( distance, this.minDistance, this.maxDistance );
+				this.applyTransformMatrix( this.scale( newDistance / distance, this._gizmos.position ) );
+				this.updateMatrixState();
+
+			 }
+
+			//check fov
+			if ( this.camera.fov < this.minFov || this.camera.fov > this.maxFov ) {
+
+				this.camera.fov = MathUtils.clamp( this.camera.fov, this.minFov, this.maxFov );
+				this.camera.updateProjectionMatrix();
+
+			}
+
+			const oldRadius = this._tbRadius;
+			this._tbRadius = this.calculateTbRadius( this.camera );
+
+			if ( oldRadius < this._tbRadius - EPS || oldRadius > this._tbRadius + EPS ) {
+
+				const scale = ( this._gizmos.scale.x + this._gizmos.scale.y + this._gizmos.scale.z ) / 3;
+				const newRadius = this._tbRadius / scale;
+				const curve = new EllipseCurve( 0, 0, newRadius, newRadius );
+				const points = curve.getPoints( this._curvePts );
+				const curveGeometry = new BufferGeometry().setFromPoints( points );
+
+				for ( const gizmo in this._gizmos.children ) {
+
+					this._gizmos.children[ gizmo ].geometry = curveGeometry;
+
+				}
+
+			}
+
+		}
+
+		this.camera.lookAt( this._gizmos.position );
+
+	};
+
+	setStateFromJSON = ( json ) => {
+
+		const state = JSON.parse( json );
+
+		if ( state.arcballState != undefined ) {
+
+			this._cameraMatrixState.fromArray( state.arcballState.cameraMatrix.elements );
+			this._cameraMatrixState.decompose( this.camera.position, this.camera.quaternion, this.camera.scale );
+
+			this.camera.up.copy( state.arcballState.cameraUp );
+			this.camera.near = state.arcballState.cameraNear;
+			this.camera.far = state.arcballState.cameraFar;
+
+			this.camera.zoom = state.arcballState.cameraZoom;
+
+			if ( this.camera.isPerspectiveCamera ) {
+
+				this.camera.fov = state.arcballState.cameraFov;
+
+			}
+
+			this._gizmoMatrixState.fromArray( state.arcballState.gizmoMatrix.elements );
+			this._gizmoMatrixState.decompose( this._gizmos.position, this._gizmos.quaternion, this._gizmos.scale );
+
+			this.camera.updateMatrix();
+			this.camera.updateProjectionMatrix();
+
+			this._gizmos.updateMatrix();
+
+			this._tbRadius = this.calculateTbRadius( this.camera );
+			const gizmoTmp = new Matrix4().copy( this._gizmoMatrixState0 );
+			this.makeGizmos( this._gizmos.position, this._tbRadius );
+			this._gizmoMatrixState0.copy( gizmoTmp );
+
+			this.camera.lookAt( this._gizmos.position );
+			this.updateTbState( STATE.IDLE, false );
+
+			this.dispatchEvent( _changeEvent );
+
+		}
+
+	};
+
+}
+
+export { ArcballControls };

+ 220 - 0
public/archive/static/js/jsm/controls/DragControls.js

@@ -0,0 +1,220 @@
+import {
+	EventDispatcher,
+	Matrix4,
+	Plane,
+	Raycaster,
+	Vector2,
+	Vector3
+} from 'three';
+
+const _plane = new Plane();
+const _raycaster = new Raycaster();
+
+const _pointer = new Vector2();
+const _offset = new Vector3();
+const _intersection = new Vector3();
+const _worldPosition = new Vector3();
+const _inverseMatrix = new Matrix4();
+
+class DragControls extends EventDispatcher {
+
+	constructor( _objects, _camera, _domElement ) {
+
+		super();
+
+		_domElement.style.touchAction = 'none'; // disable touch scroll
+
+		let _selected = null, _hovered = null;
+
+		const _intersections = [];
+
+		//
+
+		const scope = this;
+
+		function activate() {
+
+			_domElement.addEventListener( 'pointermove', onPointerMove );
+			_domElement.addEventListener( 'pointerdown', onPointerDown );
+			_domElement.addEventListener( 'pointerup', onPointerCancel );
+			_domElement.addEventListener( 'pointerleave', onPointerCancel );
+
+		}
+
+		function deactivate() {
+
+			_domElement.removeEventListener( 'pointermove', onPointerMove );
+			_domElement.removeEventListener( 'pointerdown', onPointerDown );
+			_domElement.removeEventListener( 'pointerup', onPointerCancel );
+			_domElement.removeEventListener( 'pointerleave', onPointerCancel );
+
+			_domElement.style.cursor = '';
+
+		}
+
+		function dispose() {
+
+			deactivate();
+
+		}
+
+		function getObjects() {
+
+			return _objects;
+
+		}
+
+		function getRaycaster() {
+
+			return _raycaster;
+
+		}
+
+		function onPointerMove( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			updatePointer( event );
+
+			_raycaster.setFromCamera( _pointer, _camera );
+
+			if ( _selected ) {
+
+				if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
+
+					_selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
+
+				}
+
+				scope.dispatchEvent( { type: 'drag', object: _selected } );
+
+				return;
+
+			}
+
+			// hover support
+
+			if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) {
+
+				_intersections.length = 0;
+
+				_raycaster.setFromCamera( _pointer, _camera );
+				_raycaster.intersectObjects( _objects, true, _intersections );
+
+				if ( _intersections.length > 0 ) {
+
+					const object = _intersections[ 0 ].object;
+
+					_plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) );
+
+					if ( _hovered !== object && _hovered !== null ) {
+
+						scope.dispatchEvent( { type: 'hoveroff', object: _hovered } );
+
+						_domElement.style.cursor = 'auto';
+						_hovered = null;
+
+					}
+
+					if ( _hovered !== object ) {
+
+						scope.dispatchEvent( { type: 'hoveron', object: object } );
+
+						_domElement.style.cursor = 'pointer';
+						_hovered = object;
+
+					}
+
+				} else {
+
+					if ( _hovered !== null ) {
+
+						scope.dispatchEvent( { type: 'hoveroff', object: _hovered } );
+
+						_domElement.style.cursor = 'auto';
+						_hovered = null;
+
+					}
+
+				}
+
+			}
+
+		}
+
+		function onPointerDown( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			updatePointer( event );
+
+			_intersections.length = 0;
+
+			_raycaster.setFromCamera( _pointer, _camera );
+			_raycaster.intersectObjects( _objects, true, _intersections );
+
+			if ( _intersections.length > 0 ) {
+
+				_selected = ( scope.transformGroup === true ) ? _objects[ 0 ] : _intersections[ 0 ].object;
+
+				_plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
+
+				if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
+
+					_inverseMatrix.copy( _selected.parent.matrixWorld ).invert();
+					_offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) );
+
+				}
+
+				_domElement.style.cursor = 'move';
+
+				scope.dispatchEvent( { type: 'dragstart', object: _selected } );
+
+			}
+
+
+		}
+
+		function onPointerCancel() {
+
+			if ( scope.enabled === false ) return;
+
+			if ( _selected ) {
+
+				scope.dispatchEvent( { type: 'dragend', object: _selected } );
+
+				_selected = null;
+
+			}
+
+			_domElement.style.cursor = _hovered ? 'pointer' : 'auto';
+
+		}
+
+		function updatePointer( event ) {
+
+			const rect = _domElement.getBoundingClientRect();
+
+			_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
+			_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
+
+		}
+
+		activate();
+
+		// API
+
+		this.enabled = true;
+		this.transformGroup = false;
+
+		this.activate = activate;
+		this.deactivate = deactivate;
+		this.dispose = dispose;
+		this.getObjects = getObjects;
+		this.getRaycaster = getRaycaster;
+
+	}
+
+}
+
+export { DragControls };

+ 325 - 0
public/archive/static/js/jsm/controls/FirstPersonControls.js

@@ -0,0 +1,325 @@
+import {
+	MathUtils,
+	Spherical,
+	Vector3
+} from 'three';
+
+const _lookDirection = new Vector3();
+const _spherical = new Spherical();
+const _target = new Vector3();
+
+class FirstPersonControls {
+
+	constructor( object, domElement ) {
+
+		this.object = object;
+		this.domElement = domElement;
+
+		// API
+
+		this.enabled = true;
+
+		this.movementSpeed = 1.0;
+		this.lookSpeed = 0.005;
+
+		this.lookVertical = true;
+		this.autoForward = false;
+
+		this.activeLook = true;
+
+		this.heightSpeed = false;
+		this.heightCoef = 1.0;
+		this.heightMin = 0.0;
+		this.heightMax = 1.0;
+
+		this.constrainVertical = false;
+		this.verticalMin = 0;
+		this.verticalMax = Math.PI;
+
+		this.mouseDragOn = false;
+
+		// internals
+
+		this.autoSpeedFactor = 0.0;
+
+		this.mouseX = 0;
+		this.mouseY = 0;
+
+		this.moveForward = false;
+		this.moveBackward = false;
+		this.moveLeft = false;
+		this.moveRight = false;
+
+		this.viewHalfX = 0;
+		this.viewHalfY = 0;
+
+		// private variables
+
+		let lat = 0;
+		let lon = 0;
+
+		//
+
+		this.handleResize = function () {
+
+			if ( this.domElement === document ) {
+
+				this.viewHalfX = window.innerWidth / 2;
+				this.viewHalfY = window.innerHeight / 2;
+
+			} else {
+
+				this.viewHalfX = this.domElement.offsetWidth / 2;
+				this.viewHalfY = this.domElement.offsetHeight / 2;
+
+			}
+
+		};
+
+		this.onMouseDown = function ( event ) {
+
+			if ( this.domElement !== document ) {
+
+				this.domElement.focus();
+
+			}
+
+			if ( this.activeLook ) {
+
+				switch ( event.button ) {
+
+					case 0: this.moveForward = true; break;
+					case 2: this.moveBackward = true; break;
+
+				}
+
+			}
+
+			this.mouseDragOn = true;
+
+		};
+
+		this.onMouseUp = function ( event ) {
+
+			if ( this.activeLook ) {
+
+				switch ( event.button ) {
+
+					case 0: this.moveForward = false; break;
+					case 2: this.moveBackward = false; break;
+
+				}
+
+			}
+
+			this.mouseDragOn = false;
+
+		};
+
+		this.onMouseMove = function ( event ) {
+
+			if ( this.domElement === document ) {
+
+				this.mouseX = event.pageX - this.viewHalfX;
+				this.mouseY = event.pageY - this.viewHalfY;
+
+			} else {
+
+				this.mouseX = event.pageX - this.domElement.offsetLeft - this.viewHalfX;
+				this.mouseY = event.pageY - this.domElement.offsetTop - this.viewHalfY;
+
+			}
+
+		};
+
+		this.onKeyDown = function ( event ) {
+
+			switch ( event.code ) {
+
+				case 'ArrowUp':
+				case 'KeyW': this.moveForward = true; break;
+
+				case 'ArrowLeft':
+				case 'KeyA': this.moveLeft = true; break;
+
+				case 'ArrowDown':
+				case 'KeyS': this.moveBackward = true; break;
+
+				case 'ArrowRight':
+				case 'KeyD': this.moveRight = true; break;
+
+				case 'KeyR': this.moveUp = true; break;
+				case 'KeyF': this.moveDown = true; break;
+
+			}
+
+		};
+
+		this.onKeyUp = function ( event ) {
+
+			switch ( event.code ) {
+
+				case 'ArrowUp':
+				case 'KeyW': this.moveForward = false; break;
+
+				case 'ArrowLeft':
+				case 'KeyA': this.moveLeft = false; break;
+
+				case 'ArrowDown':
+				case 'KeyS': this.moveBackward = false; break;
+
+				case 'ArrowRight':
+				case 'KeyD': this.moveRight = false; break;
+
+				case 'KeyR': this.moveUp = false; break;
+				case 'KeyF': this.moveDown = false; break;
+
+			}
+
+		};
+
+		this.lookAt = function ( x, y, z ) {
+
+			if ( x.isVector3 ) {
+
+				_target.copy( x );
+
+			} else {
+
+				_target.set( x, y, z );
+
+			}
+
+			this.object.lookAt( _target );
+
+			setOrientation( this );
+
+			return this;
+
+		};
+
+		this.update = function () {
+
+			const targetPosition = new Vector3();
+
+			return function update( delta ) {
+
+				if ( this.enabled === false ) return;
+
+				if ( this.heightSpeed ) {
+
+					const y = MathUtils.clamp( this.object.position.y, this.heightMin, this.heightMax );
+					const heightDelta = y - this.heightMin;
+
+					this.autoSpeedFactor = delta * ( heightDelta * this.heightCoef );
+
+				} else {
+
+					this.autoSpeedFactor = 0.0;
+
+				}
+
+				const actualMoveSpeed = delta * this.movementSpeed;
+
+				if ( this.moveForward || ( this.autoForward && ! this.moveBackward ) ) this.object.translateZ( - ( actualMoveSpeed + this.autoSpeedFactor ) );
+				if ( this.moveBackward ) this.object.translateZ( actualMoveSpeed );
+
+				if ( this.moveLeft ) this.object.translateX( - actualMoveSpeed );
+				if ( this.moveRight ) this.object.translateX( actualMoveSpeed );
+
+				if ( this.moveUp ) this.object.translateY( actualMoveSpeed );
+				if ( this.moveDown ) this.object.translateY( - actualMoveSpeed );
+
+				let actualLookSpeed = delta * this.lookSpeed;
+
+				if ( ! this.activeLook ) {
+
+					actualLookSpeed = 0;
+
+				}
+
+				let verticalLookRatio = 1;
+
+				if ( this.constrainVertical ) {
+
+					verticalLookRatio = Math.PI / ( this.verticalMax - this.verticalMin );
+
+				}
+
+				lon -= this.mouseX * actualLookSpeed;
+				if ( this.lookVertical ) lat -= this.mouseY * actualLookSpeed * verticalLookRatio;
+
+				lat = Math.max( - 85, Math.min( 85, lat ) );
+
+				let phi = MathUtils.degToRad( 90 - lat );
+				const theta = MathUtils.degToRad( lon );
+
+				if ( this.constrainVertical ) {
+
+					phi = MathUtils.mapLinear( phi, 0, Math.PI, this.verticalMin, this.verticalMax );
+
+				}
+
+				const position = this.object.position;
+
+				targetPosition.setFromSphericalCoords( 1, phi, theta ).add( position );
+
+				this.object.lookAt( targetPosition );
+
+			};
+
+		}();
+
+		this.dispose = function () {
+
+			this.domElement.removeEventListener( 'contextmenu', contextmenu );
+			this.domElement.removeEventListener( 'mousedown', _onMouseDown );
+			this.domElement.removeEventListener( 'mousemove', _onMouseMove );
+			this.domElement.removeEventListener( 'mouseup', _onMouseUp );
+
+			window.removeEventListener( 'keydown', _onKeyDown );
+			window.removeEventListener( 'keyup', _onKeyUp );
+
+		};
+
+		const _onMouseMove = this.onMouseMove.bind( this );
+		const _onMouseDown = this.onMouseDown.bind( this );
+		const _onMouseUp = this.onMouseUp.bind( this );
+		const _onKeyDown = this.onKeyDown.bind( this );
+		const _onKeyUp = this.onKeyUp.bind( this );
+
+		this.domElement.addEventListener( 'contextmenu', contextmenu );
+		this.domElement.addEventListener( 'mousemove', _onMouseMove );
+		this.domElement.addEventListener( 'mousedown', _onMouseDown );
+		this.domElement.addEventListener( 'mouseup', _onMouseUp );
+
+		window.addEventListener( 'keydown', _onKeyDown );
+		window.addEventListener( 'keyup', _onKeyUp );
+
+		function setOrientation( controls ) {
+
+			const quaternion = controls.object.quaternion;
+
+			_lookDirection.set( 0, 0, - 1 ).applyQuaternion( quaternion );
+			_spherical.setFromVector3( _lookDirection );
+
+			lat = 90 - MathUtils.radToDeg( _spherical.phi );
+			lon = MathUtils.radToDeg( _spherical.theta );
+
+		}
+
+		this.handleResize();
+
+		setOrientation( this );
+
+	}
+
+}
+
+function contextmenu( event ) {
+
+	event.preventDefault();
+
+}
+
+export { FirstPersonControls };

+ 285 - 0
public/archive/static/js/jsm/controls/FlyControls.js

@@ -0,0 +1,285 @@
+import {
+	EventDispatcher,
+	Quaternion,
+	Vector3
+} from 'three';
+
+const _changeEvent = { type: 'change' };
+
+class FlyControls extends EventDispatcher {
+
+	constructor( object, domElement ) {
+
+		super();
+
+		this.object = object;
+		this.domElement = domElement;
+
+		// API
+
+		this.movementSpeed = 1.0;
+		this.rollSpeed = 0.005;
+
+		this.dragToLook = false;
+		this.autoForward = false;
+
+		// disable default target object behavior
+
+		// internals
+
+		const scope = this;
+
+		const EPS = 0.000001;
+
+		const lastQuaternion = new Quaternion();
+		const lastPosition = new Vector3();
+
+		this.tmpQuaternion = new Quaternion();
+
+		this.mouseStatus = 0;
+
+		this.moveState = { up: 0, down: 0, left: 0, right: 0, forward: 0, back: 0, pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0 };
+		this.moveVector = new Vector3( 0, 0, 0 );
+		this.rotationVector = new Vector3( 0, 0, 0 );
+
+		this.keydown = function ( event ) {
+
+			if ( event.altKey ) {
+
+				return;
+
+			}
+			console.log("keydown:",keydown)
+			switch ( event.code ) {
+
+				case 'ShiftLeft':
+				case 'ShiftRight': this.movementSpeedMultiplier = .1; break;
+
+				case 'KeyW': this.moveState.forward = 1; break;
+				case 'KeyS': this.moveState.back = 1; break;
+
+				case 'KeyA': this.moveState.left = 1; break;
+				case 'KeyD': this.moveState.right = 1; break;
+
+				case 'KeyR': this.moveState.up = 1; break;
+				case 'KeyF': this.moveState.down = 1; break;
+
+				case 'ArrowUp': this.moveState.pitchUp = 1; break;
+				case 'ArrowDown': this.moveState.pitchDown = 1; break;
+
+				case 'ArrowLeft': this.moveState.yawLeft = 1; break;
+				case 'ArrowRight': this.moveState.yawRight = 1; break;
+
+				case 'KeyQ': this.moveState.rollLeft = 1; break;
+				case 'KeyE': this.moveState.rollRight = 1; break;
+
+			}
+
+			this.updateMovementVector();
+			this.updateRotationVector();
+
+		};
+
+		this.keyup = function ( event ) {
+			console.log("keyup:",keyup)
+			switch ( event.code ) {
+
+				case 'ShiftLeft':
+				case 'ShiftRight': this.movementSpeedMultiplier = 1; break;
+
+				case 'KeyW': this.moveState.forward = 0; break;
+				case 'KeyS': this.moveState.back = 0; break;
+
+				case 'KeyA': this.moveState.left = 0; break;
+				case 'KeyD': this.moveState.right = 0; break;
+
+				case 'KeyR': this.moveState.up = 0; break;
+				case 'KeyF': this.moveState.down = 0; break;
+
+				case 'ArrowUp': this.moveState.pitchUp = 0; break;
+				case 'ArrowDown': this.moveState.pitchDown = 0; break;
+
+				case 'ArrowLeft': this.moveState.yawLeft = 0; break;
+				case 'ArrowRight': this.moveState.yawRight = 0; break;
+
+				case 'KeyQ': this.moveState.rollLeft = 0; break;
+				case 'KeyE': this.moveState.rollRight = 0; break;
+
+			}
+
+			this.updateMovementVector();
+			this.updateRotationVector();
+
+		};
+
+		this.mousedown = function ( event ) {
+
+			if ( this.dragToLook ) {
+
+				this.mouseStatus ++;
+
+			} else {
+
+				switch ( event.button ) {
+
+					case 0: this.moveState.forward = 1; break;
+					case 2: this.moveState.back = 1; break;
+
+				}
+
+				this.updateMovementVector();
+
+			}
+
+		};
+
+		this.mousemove = function ( event ) {
+
+			if ( ! this.dragToLook || this.mouseStatus > 0 ) {
+
+				const container = this.getContainerDimensions();
+				const halfWidth = container.size[ 0 ] / 2;
+				const halfHeight = container.size[ 1 ] / 2;
+
+				this.moveState.yawLeft = - ( ( event.pageX - container.offset[ 0 ] ) - halfWidth ) / halfWidth;
+				this.moveState.pitchDown = ( ( event.pageY - container.offset[ 1 ] ) - halfHeight ) / halfHeight;
+
+				this.updateRotationVector();
+
+			}
+
+		};
+
+		this.mouseup = function ( event ) {
+
+			if ( this.dragToLook ) {
+
+				this.mouseStatus --;
+
+				this.moveState.yawLeft = this.moveState.pitchDown = 0;
+
+			} else {
+
+				switch ( event.button ) {
+
+					case 0: this.moveState.forward = 0; break;
+					case 2: this.moveState.back = 0; break;
+
+				}
+
+				this.updateMovementVector();
+
+			}
+
+			this.updateRotationVector();
+
+		};
+
+		this.update = function ( delta ) {
+
+			const moveMult = delta * scope.movementSpeed;
+			const rotMult = delta * scope.rollSpeed;
+
+			scope.object.translateX( scope.moveVector.x * moveMult );
+			scope.object.translateY( scope.moveVector.y * moveMult );
+			scope.object.translateZ( scope.moveVector.z * moveMult );
+
+			scope.tmpQuaternion.set( scope.rotationVector.x * rotMult, scope.rotationVector.y * rotMult, scope.rotationVector.z * rotMult, 1 ).normalize();
+			scope.object.quaternion.multiply( scope.tmpQuaternion );
+
+			if (
+				lastPosition.distanceToSquared( scope.object.position ) > EPS ||
+				8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS
+			) {
+
+				scope.dispatchEvent( _changeEvent );
+				lastQuaternion.copy( scope.object.quaternion );
+				lastPosition.copy( scope.object.position );
+
+			}
+
+		};
+
+		this.updateMovementVector = function () {
+
+			const forward = ( this.moveState.forward || ( this.autoForward && ! this.moveState.back ) ) ? 1 : 0;
+
+			this.moveVector.x = ( - this.moveState.left + this.moveState.right );
+			this.moveVector.y = ( - this.moveState.down + this.moveState.up );
+			this.moveVector.z = ( - forward + this.moveState.back );
+
+			//console.log( 'move:', [ this.moveVector.x, this.moveVector.y, this.moveVector.z ] );
+
+		};
+
+		this.updateRotationVector = function () {
+
+			this.rotationVector.x = ( - this.moveState.pitchDown + this.moveState.pitchUp );
+			this.rotationVector.y = ( - this.moveState.yawRight + this.moveState.yawLeft );
+			this.rotationVector.z = ( - this.moveState.rollRight + this.moveState.rollLeft );
+
+			//console.log( 'rotate:', [ this.rotationVector.x, this.rotationVector.y, this.rotationVector.z ] );
+
+		};
+
+		this.getContainerDimensions = function () {
+
+			if ( this.domElement != document ) {
+
+				return {
+					size: [ this.domElement.offsetWidth, this.domElement.offsetHeight ],
+					offset: [ this.domElement.offsetLeft, this.domElement.offsetTop ]
+				};
+
+			} else {
+
+				return {
+					size: [ window.innerWidth, window.innerHeight ],
+					offset: [ 0, 0 ]
+				};
+
+			}
+
+		};
+
+		this.dispose = function () {
+
+			this.domElement.removeEventListener( 'contextmenu', contextmenu );
+			this.domElement.removeEventListener( 'mousedown', _mousedown );
+			this.domElement.removeEventListener( 'mousemove', _mousemove );
+			this.domElement.removeEventListener( 'mouseup', _mouseup );
+
+			window.removeEventListener( 'keydown', _keydown );
+			window.removeEventListener( 'keyup', _keyup );
+
+		};
+
+		const _mousemove = this.mousemove.bind( this );
+		const _mousedown = this.mousedown.bind( this );
+		const _mouseup = this.mouseup.bind( this );
+		const _keydown = this.keydown.bind( this );
+		const _keyup = this.keyup.bind( this );
+
+		this.domElement.addEventListener( 'contextmenu', contextmenu );
+
+		this.domElement.addEventListener( 'mousemove', _mousemove );
+		this.domElement.addEventListener( 'mousedown', _mousedown );
+		this.domElement.addEventListener( 'mouseup', _mouseup );
+
+		window.addEventListener( 'keydown', _keydown );
+		window.addEventListener( 'keyup', _keyup );
+
+		this.updateMovementVector();
+		this.updateRotationVector();
+
+	}
+
+}
+
+function contextmenu( event ) {
+
+	event.preventDefault();
+
+}
+
+export { FlyControls };

+ 1247 - 0
public/archive/static/js/jsm/controls/OrbitControls.js

@@ -0,0 +1,1247 @@
+import {
+	EventDispatcher,
+	MOUSE,
+	Quaternion,
+	Spherical,
+	TOUCH,
+	Vector2,
+	Vector3
+} from 'three';
+
+// 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 left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move
+
+const _changeEvent = { type: 'change' };
+const _startEvent = { type: 'start' };
+const _endEvent = { type: 'end' };
+
+class OrbitControls extends EventDispatcher {
+
+	constructor( object, domElement ) {
+
+		super();
+
+		this.object = object;
+		this.domElement = domElement;
+		this.domElement.style.touchAction = 'none'; // disable touch scroll
+
+		// Set to false to disable this control
+		this.enabled = true;
+
+		// "target" sets the location of focus, where the object orbits around
+		this.target = new 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, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 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.05;
+
+		// 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 = true; // if false, pan orthogonal to world-space direction camera.up
+		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 orbit when fps is 60
+
+		// The four arrow keys
+		this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' };
+
+		// Mouse buttons
+		this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };
+
+		// Touch fingers
+		this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };
+
+		// for reset
+		this.target0 = this.target.clone();
+		this.position0 = this.object.position.clone();
+		this.zoom0 = this.object.zoom;
+
+		// the target DOM element for key events
+		this._domElementKeyEvents = null;
+
+		//
+		// public methods
+		//
+
+		this.getPolarAngle = function () {
+
+			return spherical.phi;
+
+		};
+
+		this.getAzimuthalAngle = function () {
+
+			return spherical.theta;
+
+		};
+
+		this.getDistance = function () {
+
+			return this.object.position.distanceTo( this.target );
+
+		};
+
+		this.listenToKeyEvents = function ( domElement ) {
+
+			domElement.addEventListener( 'keydown', onKeyDown );
+			this._domElementKeyEvents = domElement;
+
+		};
+
+		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 () {
+
+			const offset = new Vector3();
+
+			// so camera.up is the orbit axis
+			const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
+			const quatInverse = quat.clone().invert();
+
+			const lastPosition = new Vector3();
+			const lastQuaternion = new Quaternion();
+
+			const twoPI = 2 * Math.PI;
+
+			return function update() {
+
+				const 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() );
+
+				}
+
+				if ( scope.enableDamping ) {
+
+					spherical.theta += sphericalDelta.theta * scope.dampingFactor;
+					spherical.phi += sphericalDelta.phi * scope.dampingFactor;
+
+				} else {
+
+					spherical.theta += sphericalDelta.theta;
+					spherical.phi += sphericalDelta.phi;
+
+				}
+
+				// restrict theta to be between desired limits
+
+				let min = scope.minAzimuthAngle;
+				let max = scope.maxAzimuthAngle;
+
+				if ( isFinite( min ) && isFinite( max ) ) {
+
+					if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;
+
+					if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;
+
+					if ( min <= max ) {
+
+						spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );
+
+					} else {
+
+						spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ?
+							Math.max( min, spherical.theta ) :
+							Math.min( max, 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
+
+				if ( scope.enableDamping === true ) {
+
+					scope.target.addScaledVector( panOffset, scope.dampingFactor );
+
+				} else {
+
+					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 );
+
+			scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
+			scope.domElement.removeEventListener( 'pointercancel', onPointerCancel );
+			scope.domElement.removeEventListener( 'wheel', onMouseWheel );
+
+			scope.domElement.removeEventListener( 'pointermove', onPointerMove );
+			scope.domElement.removeEventListener( 'pointerup', onPointerUp );
+
+
+			if ( scope._domElementKeyEvents !== null ) {
+
+				scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
+
+			}
+
+			//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
+
+		};
+
+		//
+		// internals
+		//
+
+		const scope = this;
+
+		const STATE = {
+			NONE: - 1,
+			ROTATE: 0,
+			DOLLY: 1,
+			PAN: 2,
+			TOUCH_ROTATE: 3,
+			TOUCH_PAN: 4,
+			TOUCH_DOLLY_PAN: 5,
+			TOUCH_DOLLY_ROTATE: 6
+		};
+
+		let state = STATE.NONE;
+
+		const EPS = 0.000001;
+
+		// current position in spherical coordinates
+		const spherical = new Spherical();
+		const sphericalDelta = new Spherical();
+
+		let scale = 1;
+		const panOffset = new Vector3();
+		let zoomChanged = false;
+
+		const rotateStart = new Vector2();
+		const rotateEnd = new Vector2();
+		const rotateDelta = new Vector2();
+
+		const panStart = new Vector2();
+		const panEnd = new Vector2();
+		const panDelta = new Vector2();
+
+		const dollyStart = new Vector2();
+		const dollyEnd = new Vector2();
+		const dollyDelta = new Vector2();
+
+		const pointers = [];
+		const pointerPositions = {};
+
+		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;
+
+		}
+
+		const panLeft = function () {
+
+			const v = new Vector3();
+
+			return function panLeft( distance, objectMatrix ) {
+
+				v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
+				v.multiplyScalar( - distance );
+
+				panOffset.add( v );
+
+			};
+
+		}();
+
+		const panUp = function () {
+
+			const v = new 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
+		const pan = function () {
+
+			const offset = new Vector3();
+
+			return function pan( deltaX, deltaY ) {
+
+				const element = scope.domElement;
+
+				if ( scope.object.isPerspectiveCamera ) {
+
+					// perspective
+					const position = scope.object.position;
+					offset.copy( position ).sub( scope.target );
+					let 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 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;
+
+			}
+
+		}
+
+		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;
+
+			}
+
+		}
+
+		//
+		// event callbacks - update the object state
+		//
+
+		function handleMouseDownRotate( event ) {
+
+			rotateStart.set( event.clientX, event.clientY );
+
+		}
+
+		function handleMouseDownDolly( event ) {
+
+			dollyStart.set( event.clientX, event.clientY );
+
+		}
+
+		function handleMouseDownPan( event ) {
+
+			panStart.set( event.clientX, event.clientY );
+
+		}
+
+		function handleMouseMoveRotate( event ) {
+
+			rotateEnd.set( event.clientX, event.clientY );
+
+			rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
+
+			const element = 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 ) {
+
+			dollyEnd.set( event.clientX, event.clientY );
+
+			dollyDelta.subVectors( dollyEnd, dollyStart );
+
+			if ( dollyDelta.y > 0 ) {
+
+				dollyOut( getZoomScale() );
+
+			} else if ( dollyDelta.y < 0 ) {
+
+				dollyIn( getZoomScale() );
+
+			}
+
+			dollyStart.copy( dollyEnd );
+
+			scope.update();
+
+		}
+
+		function handleMouseMovePan( event ) {
+
+			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 ) {
+
+			if ( event.deltaY < 0 ) {
+
+				dollyIn( getZoomScale() );
+
+			} else if ( event.deltaY > 0 ) {
+
+				dollyOut( getZoomScale() );
+
+			}
+
+			scope.update();
+
+		}
+
+		function handleKeyDown( event ) {
+
+			let needsUpdate = false;
+
+			switch ( event.code ) {
+
+				case scope.keys.UP:
+					pan( 0, scope.keyPanSpeed );
+					needsUpdate = true;
+					break;
+
+				case scope.keys.BOTTOM:
+					pan( 0, - scope.keyPanSpeed );
+					needsUpdate = true;
+					break;
+
+				case scope.keys.LEFT:
+					pan( scope.keyPanSpeed, 0 );
+					needsUpdate = true;
+					break;
+
+				case scope.keys.RIGHT:
+					pan( - scope.keyPanSpeed, 0 );
+					needsUpdate = true;
+					break;
+
+			}
+
+			if ( needsUpdate ) {
+
+				// prevent the browser from scrolling on cursor keys
+				event.preventDefault();
+
+				scope.update();
+
+			}
+
+
+		}
+
+		function handleTouchStartRotate() {
+
+			if ( pointers.length === 1 ) {
+
+				rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY );
+
+			} else {
+
+				const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX );
+				const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY );
+
+				rotateStart.set( x, y );
+
+			}
+
+		}
+
+		function handleTouchStartPan() {
+
+			if ( pointers.length === 1 ) {
+
+				panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY );
+
+			} else {
+
+				const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX );
+				const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY );
+
+				panStart.set( x, y );
+
+			}
+
+		}
+
+		function handleTouchStartDolly() {
+
+			const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX;
+			const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY;
+
+			const distance = Math.sqrt( dx * dx + dy * dy );
+
+			dollyStart.set( 0, distance );
+
+		}
+
+		function handleTouchStartDollyPan() {
+
+			if ( scope.enableZoom ) handleTouchStartDolly();
+
+			if ( scope.enablePan ) handleTouchStartPan();
+
+		}
+
+		function handleTouchStartDollyRotate() {
+
+			if ( scope.enableZoom ) handleTouchStartDolly();
+
+			if ( scope.enableRotate ) handleTouchStartRotate();
+
+		}
+
+		function handleTouchMoveRotate( event ) {
+
+			if ( pointers.length == 1 ) {
+
+				rotateEnd.set( event.pageX, event.pageY );
+
+			} else {
+
+				const position = getSecondPointerPosition( event );
+
+				const x = 0.5 * ( event.pageX + position.x );
+				const y = 0.5 * ( event.pageY + position.y );
+
+				rotateEnd.set( x, y );
+
+			}
+
+			rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
+
+			const element = scope.domElement;
+
+			rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
+
+			rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
+
+			rotateStart.copy( rotateEnd );
+
+		}
+
+		function handleTouchMovePan( event ) {
+
+			if ( pointers.length === 1 ) {
+
+				panEnd.set( event.pageX, event.pageY );
+
+			} else {
+
+				const position = getSecondPointerPosition( event );
+
+				const x = 0.5 * ( event.pageX + position.x );
+				const y = 0.5 * ( event.pageY + position.y );
+
+				panEnd.set( x, y );
+
+			}
+
+			panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
+
+			pan( panDelta.x, panDelta.y );
+
+			panStart.copy( panEnd );
+
+		}
+
+		function handleTouchMoveDolly( event ) {
+
+			const position = getSecondPointerPosition( event );
+
+			const dx = event.pageX - position.x;
+			const dy = event.pageY - position.y;
+
+			const distance = Math.sqrt( dx * dx + dy * dy );
+
+			dollyEnd.set( 0, distance );
+
+			dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
+
+			dollyOut( dollyDelta.y );
+
+			dollyStart.copy( dollyEnd );
+
+		}
+
+		function handleTouchMoveDollyPan( event ) {
+
+			if ( scope.enableZoom ) handleTouchMoveDolly( event );
+
+			if ( scope.enablePan ) handleTouchMovePan( event );
+
+		}
+
+		function handleTouchMoveDollyRotate( event ) {
+
+			if ( scope.enableZoom ) handleTouchMoveDolly( event );
+
+			if ( scope.enableRotate ) handleTouchMoveRotate( event );
+
+		}
+
+		//
+		// event handlers - FSM: listen for events and reset state
+		//
+
+		function onPointerDown( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			if ( pointers.length === 0 ) {
+
+				scope.domElement.setPointerCapture( event.pointerId );
+
+				scope.domElement.addEventListener( 'pointermove', onPointerMove );
+				scope.domElement.addEventListener( 'pointerup', onPointerUp );
+
+			}
+
+			//
+
+			addPointer( event );
+
+			if ( event.pointerType === 'touch' ) {
+
+				onTouchStart( event );
+
+			} else {
+
+				onMouseDown( event );
+
+			}
+
+		}
+
+		function onPointerMove( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			if ( event.pointerType === 'touch' ) {
+
+				onTouchMove( event );
+
+			} else {
+
+				onMouseMove( event );
+
+			}
+
+		}
+
+		function onPointerUp( event ) {
+
+		    removePointer( event );
+
+		    if ( pointers.length === 0 ) {
+
+		        scope.domElement.releasePointerCapture( event.pointerId );
+
+		        scope.domElement.removeEventListener( 'pointermove', onPointerMove );
+		        scope.domElement.removeEventListener( 'pointerup', onPointerUp );
+
+		    }
+
+		    scope.dispatchEvent( _endEvent );
+
+		    state = STATE.NONE;
+
+		}
+
+		function onPointerCancel( event ) {
+
+			removePointer( event );
+
+		}
+
+		function onMouseDown( event ) {
+
+			let mouseAction;
+
+			switch ( event.button ) {
+
+				case 0:
+
+					mouseAction = scope.mouseButtons.LEFT;
+					break;
+
+				case 1:
+
+					mouseAction = scope.mouseButtons.MIDDLE;
+					break;
+
+				case 2:
+
+					mouseAction = scope.mouseButtons.RIGHT;
+					break;
+
+				default:
+
+					mouseAction = - 1;
+
+			}
+
+			switch ( mouseAction ) {
+
+				case MOUSE.DOLLY:
+
+					if ( scope.enableZoom === false ) return;
+
+					handleMouseDownDolly( event );
+
+					state = STATE.DOLLY;
+
+					break;
+
+				case MOUSE.ROTATE:
+
+					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+
+						if ( scope.enablePan === false ) return;
+
+						handleMouseDownPan( event );
+
+						state = STATE.PAN;
+
+					} else {
+
+						if ( scope.enableRotate === false ) return;
+
+						handleMouseDownRotate( event );
+
+						state = STATE.ROTATE;
+
+					}
+
+					break;
+
+				case MOUSE.PAN:
+
+					if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+
+						if ( scope.enableRotate === false ) return;
+
+						handleMouseDownRotate( event );
+
+						state = STATE.ROTATE;
+
+					} else {
+
+						if ( scope.enablePan === false ) return;
+
+						handleMouseDownPan( event );
+
+						state = STATE.PAN;
+
+					}
+
+					break;
+
+				default:
+
+					state = STATE.NONE;
+
+			}
+
+			if ( state !== STATE.NONE ) {
+
+				scope.dispatchEvent( _startEvent );
+
+			}
+
+		}
+
+		function onMouseMove( event ) {
+
+			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 onMouseWheel( event ) {
+
+			if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return;
+
+			event.preventDefault();
+
+			scope.dispatchEvent( _startEvent );
+
+			handleMouseWheel( event );
+
+			scope.dispatchEvent( _endEvent );
+
+		}
+
+		function onKeyDown( event ) {
+
+			if ( scope.enabled === false || scope.enablePan === false ) return;
+
+			handleKeyDown( event );
+
+		}
+
+		function onTouchStart( event ) {
+
+			trackPointer( event );
+
+			switch ( pointers.length ) {
+
+				case 1:
+
+					switch ( scope.touches.ONE ) {
+
+						case TOUCH.ROTATE:
+
+							if ( scope.enableRotate === false ) return;
+
+							handleTouchStartRotate();
+
+							state = STATE.TOUCH_ROTATE;
+
+							break;
+
+						case TOUCH.PAN:
+
+							if ( scope.enablePan === false ) return;
+
+							handleTouchStartPan();
+
+							state = STATE.TOUCH_PAN;
+
+							break;
+
+						default:
+
+							state = STATE.NONE;
+
+					}
+
+					break;
+
+				case 2:
+
+					switch ( scope.touches.TWO ) {
+
+						case TOUCH.DOLLY_PAN:
+
+							if ( scope.enableZoom === false && scope.enablePan === false ) return;
+
+							handleTouchStartDollyPan();
+
+							state = STATE.TOUCH_DOLLY_PAN;
+
+							break;
+
+						case TOUCH.DOLLY_ROTATE:
+
+							if ( scope.enableZoom === false && scope.enableRotate === false ) return;
+
+							handleTouchStartDollyRotate();
+
+							state = STATE.TOUCH_DOLLY_ROTATE;
+
+							break;
+
+						default:
+
+							state = STATE.NONE;
+
+					}
+
+					break;
+
+				default:
+
+					state = STATE.NONE;
+
+			}
+
+			if ( state !== STATE.NONE ) {
+
+				scope.dispatchEvent( _startEvent );
+
+			}
+
+		}
+
+		function onTouchMove( event ) {
+
+			trackPointer( event );
+
+			switch ( state ) {
+
+				case STATE.TOUCH_ROTATE:
+
+					if ( scope.enableRotate === false ) return;
+
+					handleTouchMoveRotate( event );
+
+					scope.update();
+
+					break;
+
+				case STATE.TOUCH_PAN:
+
+					if ( scope.enablePan === false ) return;
+
+					handleTouchMovePan( event );
+
+					scope.update();
+
+					break;
+
+				case STATE.TOUCH_DOLLY_PAN:
+
+					if ( scope.enableZoom === false && scope.enablePan === false ) return;
+
+					handleTouchMoveDollyPan( event );
+
+					scope.update();
+
+					break;
+
+				case STATE.TOUCH_DOLLY_ROTATE:
+
+					if ( scope.enableZoom === false && scope.enableRotate === false ) return;
+
+					handleTouchMoveDollyRotate( event );
+
+					scope.update();
+
+					break;
+
+				default:
+
+					state = STATE.NONE;
+
+			}
+
+		}
+
+		function onContextMenu( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			event.preventDefault();
+
+		}
+
+		function addPointer( event ) {
+
+			pointers.push( event );
+
+		}
+
+		function removePointer( event ) {
+
+			delete pointerPositions[ event.pointerId ];
+
+			for ( let i = 0; i < pointers.length; i ++ ) {
+
+				if ( pointers[ i ].pointerId == event.pointerId ) {
+
+					pointers.splice( i, 1 );
+					return;
+
+				}
+
+			}
+
+		}
+
+		function trackPointer( event ) {
+
+			let position = pointerPositions[ event.pointerId ];
+
+			if ( position === undefined ) {
+
+				position = new Vector2();
+				pointerPositions[ event.pointerId ] = position;
+
+			}
+
+			position.set( event.pageX, event.pageY );
+
+		}
+
+		function getSecondPointerPosition( event ) {
+
+			const pointer = ( event.pointerId === pointers[ 0 ].pointerId ) ? pointers[ 1 ] : pointers[ 0 ];
+
+			return pointerPositions[ pointer.pointerId ];
+
+		}
+
+		//
+
+		scope.domElement.addEventListener( 'contextmenu', onContextMenu );
+
+		scope.domElement.addEventListener( 'pointerdown', onPointerDown );
+		scope.domElement.addEventListener( 'pointercancel', onPointerCancel );
+		scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } );
+
+		// force an update at start
+
+		this.update();
+
+	}
+
+}
+
+
+// This set of controls performs orbiting, dollying (zooming), and panning.
+// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
+// This is very similar to OrbitControls, another set of touch behavior
+//
+//    Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate
+//    Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
+//    Pan - left mouse, or arrow keys / touch: one-finger move
+
+class MapControls extends OrbitControls {
+
+	constructor( object, domElement ) {
+
+		super( object, domElement );
+
+		this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up
+
+		this.mouseButtons.LEFT = MOUSE.PAN;
+		this.mouseButtons.RIGHT = MOUSE.ROTATE;
+
+		this.touches.ONE = TOUCH.PAN;
+		this.touches.TWO = TOUCH.DOLLY_ROTATE;
+
+	}
+
+}
+
+export { OrbitControls, MapControls };

+ 157 - 0
public/archive/static/js/jsm/controls/PointerLockControls.js

@@ -0,0 +1,157 @@
+import {
+	Euler,
+	EventDispatcher,
+	Vector3
+} from 'three';
+
+const _euler = new Euler( 0, 0, 0, 'YXZ' );
+const _vector = new Vector3();
+
+const _changeEvent = { type: 'change' };
+const _lockEvent = { type: 'lock' };
+const _unlockEvent = { type: 'unlock' };
+
+const _PI_2 = Math.PI / 2;
+
+class PointerLockControls extends EventDispatcher {
+
+	constructor( camera, domElement ) {
+
+		super();
+
+		this.domElement = domElement;
+		this.isLocked = false;
+
+		// Set to constrain the pitch of the camera
+		// Range is 0 to Math.PI radians
+		this.minPolarAngle = 0; // radians
+		this.maxPolarAngle = Math.PI; // radians
+
+		this.pointerSpeed = 1.0;
+
+		const scope = this;
+
+		function onMouseMove( event ) {
+
+			if ( scope.isLocked === false ) return;
+
+			const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
+			const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
+
+			_euler.setFromQuaternion( camera.quaternion );
+
+			_euler.y -= movementX * 0.002 * scope.pointerSpeed;
+			_euler.x -= movementY * 0.002 * scope.pointerSpeed;
+
+			_euler.x = Math.max( _PI_2 - scope.maxPolarAngle, Math.min( _PI_2 - scope.minPolarAngle, _euler.x ) );
+
+			camera.quaternion.setFromEuler( _euler );
+
+			scope.dispatchEvent( _changeEvent );
+
+		}
+
+		function onPointerlockChange() {
+
+			if ( scope.domElement.ownerDocument.pointerLockElement === scope.domElement ) {
+
+				scope.dispatchEvent( _lockEvent );
+
+				scope.isLocked = true;
+
+			} else {
+
+				scope.dispatchEvent( _unlockEvent );
+
+				scope.isLocked = false;
+
+			}
+
+		}
+
+		function onPointerlockError() {
+
+			console.error( 'THREE.PointerLockControls: Unable to use Pointer Lock API' );
+
+		}
+
+		this.connect = function () {
+
+			scope.domElement.ownerDocument.addEventListener( 'mousemove', onMouseMove );
+			scope.domElement.ownerDocument.addEventListener( 'pointerlockchange', onPointerlockChange );
+			scope.domElement.ownerDocument.addEventListener( 'pointerlockerror', onPointerlockError );
+
+		};
+
+		this.disconnect = function () {
+
+			scope.domElement.ownerDocument.removeEventListener( 'mousemove', onMouseMove );
+			scope.domElement.ownerDocument.removeEventListener( 'pointerlockchange', onPointerlockChange );
+			scope.domElement.ownerDocument.removeEventListener( 'pointerlockerror', onPointerlockError );
+
+		};
+
+		this.dispose = function () {
+
+			this.disconnect();
+
+		};
+
+		this.getObject = function () { // retaining this method for backward compatibility
+
+			return camera;
+
+		};
+
+		this.getDirection = function () {
+
+			const direction = new Vector3( 0, 0, - 1 );
+
+			return function ( v ) {
+
+				return v.copy( direction ).applyQuaternion( camera.quaternion );
+
+			};
+
+		}();
+
+		this.moveForward = function ( distance ) {
+
+			// move forward parallel to the xz-plane
+			// assumes camera.up is y-up
+
+			_vector.setFromMatrixColumn( camera.matrix, 0 );
+
+			_vector.crossVectors( camera.up, _vector );
+
+			camera.position.addScaledVector( _vector, distance );
+
+		};
+
+		this.moveRight = function ( distance ) {
+
+			_vector.setFromMatrixColumn( camera.matrix, 0 );
+
+			camera.position.addScaledVector( _vector, distance );
+
+		};
+
+		this.lock = function () {
+
+			this.domElement.requestPointerLock();
+
+		};
+
+		this.unlock = function () {
+
+			scope.domElement.ownerDocument.exitPointerLock();
+
+		};
+
+		this.connect();
+
+	}
+
+}
+
+export { PointerLockControls };

+ 802 - 0
public/archive/static/js/jsm/controls/TrackballControls.js

@@ -0,0 +1,802 @@
+import {
+	EventDispatcher,
+	MOUSE,
+	Quaternion,
+	Vector2,
+	Vector3
+} from 'three';
+
+const _changeEvent = { type: 'change' };
+const _startEvent = { type: 'start' };
+const _endEvent = { type: 'end' };
+
+class TrackballControls extends EventDispatcher {
+
+	constructor( object, domElement ) {
+
+		super();
+
+		const scope = this;
+		const STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
+
+		this.object = object;
+		this.domElement = domElement;
+		this.domElement.style.touchAction = 'none'; // disable touch scroll
+
+		// API
+
+		this.enabled = true;
+
+		this.screen = { left: 0, top: 0, width: 0, height: 0 };
+
+		this.rotateSpeed = 1.0;
+		this.zoomSpeed = 1.2;
+		this.panSpeed = 0.3;
+
+		this.noRotate = false;
+		this.noZoom = false;
+		this.noPan = false;
+
+		this.staticMoving = false;
+		this.dynamicDampingFactor = 0.2;
+
+		this.minDistance = 0;
+		this.maxDistance = Infinity;
+
+		this.keys = [ 'KeyA' /*A*/, 'KeyS' /*S*/, 'KeyD' /*D*/ ];
+
+		this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };
+
+		// internals
+
+		this.target = new Vector3();
+
+		const EPS = 0.000001;
+
+		const lastPosition = new Vector3();
+		let lastZoom = 1;
+
+		let _state = STATE.NONE,
+			_keyState = STATE.NONE,
+
+			_touchZoomDistanceStart = 0,
+			_touchZoomDistanceEnd = 0,
+
+			_lastAngle = 0;
+
+		const _eye = new Vector3(),
+
+			_movePrev = new Vector2(),
+			_moveCurr = new Vector2(),
+
+			_lastAxis = new Vector3(),
+
+			_zoomStart = new Vector2(),
+			_zoomEnd = new Vector2(),
+
+			_panStart = new Vector2(),
+			_panEnd = new Vector2(),
+
+			_pointers = [],
+			_pointerPositions = {};
+
+		// for reset
+
+		this.target0 = this.target.clone();
+		this.position0 = this.object.position.clone();
+		this.up0 = this.object.up.clone();
+		this.zoom0 = this.object.zoom;
+
+		// methods
+
+		this.handleResize = function () {
+
+			const box = scope.domElement.getBoundingClientRect();
+			// adjustments come from similar code in the jquery offset() function
+			const d = scope.domElement.ownerDocument.documentElement;
+			scope.screen.left = box.left + window.pageXOffset - d.clientLeft;
+			scope.screen.top = box.top + window.pageYOffset - d.clientTop;
+			scope.screen.width = box.width;
+			scope.screen.height = box.height;
+
+		};
+
+		const getMouseOnScreen = ( function () {
+
+			const vector = new Vector2();
+
+			return function getMouseOnScreen( pageX, pageY ) {
+
+				vector.set(
+					( pageX - scope.screen.left ) / scope.screen.width,
+					( pageY - scope.screen.top ) / scope.screen.height
+				);
+
+				return vector;
+
+			};
+
+		}() );
+
+		const getMouseOnCircle = ( function () {
+
+			const vector = new Vector2();
+
+			return function getMouseOnCircle( pageX, pageY ) {
+
+				vector.set(
+					( ( pageX - scope.screen.width * 0.5 - scope.screen.left ) / ( scope.screen.width * 0.5 ) ),
+					( ( scope.screen.height + 2 * ( scope.screen.top - pageY ) ) / scope.screen.width ) // screen.width intentional
+				);
+
+				return vector;
+
+			};
+
+		}() );
+
+		this.rotateCamera = ( function () {
+
+			const axis = new Vector3(),
+				quaternion = new Quaternion(),
+				eyeDirection = new Vector3(),
+				objectUpDirection = new Vector3(),
+				objectSidewaysDirection = new Vector3(),
+				moveDirection = new Vector3();
+
+			return function rotateCamera() {
+
+				moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 );
+				let angle = moveDirection.length();
+
+				if ( angle ) {
+
+					_eye.copy( scope.object.position ).sub( scope.target );
+
+					eyeDirection.copy( _eye ).normalize();
+					objectUpDirection.copy( scope.object.up ).normalize();
+					objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize();
+
+					objectUpDirection.setLength( _moveCurr.y - _movePrev.y );
+					objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x );
+
+					moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) );
+
+					axis.crossVectors( moveDirection, _eye ).normalize();
+
+					angle *= scope.rotateSpeed;
+					quaternion.setFromAxisAngle( axis, angle );
+
+					_eye.applyQuaternion( quaternion );
+					scope.object.up.applyQuaternion( quaternion );
+
+					_lastAxis.copy( axis );
+					_lastAngle = angle;
+
+				} else if ( ! scope.staticMoving && _lastAngle ) {
+
+					_lastAngle *= Math.sqrt( 1.0 - scope.dynamicDampingFactor );
+					_eye.copy( scope.object.position ).sub( scope.target );
+					quaternion.setFromAxisAngle( _lastAxis, _lastAngle );
+					_eye.applyQuaternion( quaternion );
+					scope.object.up.applyQuaternion( quaternion );
+
+				}
+
+				_movePrev.copy( _moveCurr );
+
+			};
+
+		}() );
+
+
+		this.zoomCamera = function () {
+
+			let factor;
+
+			if ( _state === STATE.TOUCH_ZOOM_PAN ) {
+
+				factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
+				_touchZoomDistanceStart = _touchZoomDistanceEnd;
+
+				if ( scope.object.isPerspectiveCamera ) {
+
+					_eye.multiplyScalar( factor );
+
+				} else if ( scope.object.isOrthographicCamera ) {
+
+					scope.object.zoom /= factor;
+					scope.object.updateProjectionMatrix();
+
+				} else {
+
+					console.warn( 'THREE.TrackballControls: Unsupported camera type' );
+
+				}
+
+			} else {
+
+				factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * scope.zoomSpeed;
+
+				if ( factor !== 1.0 && factor > 0.0 ) {
+
+					if ( scope.object.isPerspectiveCamera ) {
+
+						_eye.multiplyScalar( factor );
+
+					} else if ( scope.object.isOrthographicCamera ) {
+
+						scope.object.zoom /= factor;
+						scope.object.updateProjectionMatrix();
+
+					} else {
+
+						console.warn( 'THREE.TrackballControls: Unsupported camera type' );
+
+					}
+
+				}
+
+				if ( scope.staticMoving ) {
+
+					_zoomStart.copy( _zoomEnd );
+
+				} else {
+
+					_zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;
+
+				}
+
+			}
+
+		};
+
+		this.panCamera = ( function () {
+
+			const mouseChange = new Vector2(),
+				objectUp = new Vector3(),
+				pan = new Vector3();
+
+			return function panCamera() {
+
+				mouseChange.copy( _panEnd ).sub( _panStart );
+
+				if ( mouseChange.lengthSq() ) {
+
+					if ( scope.object.isOrthographicCamera ) {
+
+						const scale_x = ( scope.object.right - scope.object.left ) / scope.object.zoom / scope.domElement.clientWidth;
+						const scale_y = ( scope.object.top - scope.object.bottom ) / scope.object.zoom / scope.domElement.clientWidth;
+
+						mouseChange.x *= scale_x;
+						mouseChange.y *= scale_y;
+
+					}
+
+					mouseChange.multiplyScalar( _eye.length() * scope.panSpeed );
+
+					pan.copy( _eye ).cross( scope.object.up ).setLength( mouseChange.x );
+					pan.add( objectUp.copy( scope.object.up ).setLength( mouseChange.y ) );
+
+					scope.object.position.add( pan );
+					scope.target.add( pan );
+
+					if ( scope.staticMoving ) {
+
+						_panStart.copy( _panEnd );
+
+					} else {
+
+						_panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( scope.dynamicDampingFactor ) );
+
+					}
+
+				}
+
+			};
+
+		}() );
+
+		this.checkDistances = function () {
+
+			if ( ! scope.noZoom || ! scope.noPan ) {
+
+				if ( _eye.lengthSq() > scope.maxDistance * scope.maxDistance ) {
+
+					scope.object.position.addVectors( scope.target, _eye.setLength( scope.maxDistance ) );
+					_zoomStart.copy( _zoomEnd );
+
+				}
+
+				if ( _eye.lengthSq() < scope.minDistance * scope.minDistance ) {
+
+					scope.object.position.addVectors( scope.target, _eye.setLength( scope.minDistance ) );
+					_zoomStart.copy( _zoomEnd );
+
+				}
+
+			}
+
+		};
+
+		this.update = function () {
+
+			_eye.subVectors( scope.object.position, scope.target );
+
+			if ( ! scope.noRotate ) {
+
+				scope.rotateCamera();
+
+			}
+
+			if ( ! scope.noZoom ) {
+
+				scope.zoomCamera();
+
+			}
+
+			if ( ! scope.noPan ) {
+
+				scope.panCamera();
+
+			}
+
+			scope.object.position.addVectors( scope.target, _eye );
+
+			if ( scope.object.isPerspectiveCamera ) {
+
+				scope.checkDistances();
+
+				scope.object.lookAt( scope.target );
+
+				if ( lastPosition.distanceToSquared( scope.object.position ) > EPS ) {
+
+					scope.dispatchEvent( _changeEvent );
+
+					lastPosition.copy( scope.object.position );
+
+				}
+
+			} else if ( scope.object.isOrthographicCamera ) {
+
+				scope.object.lookAt( scope.target );
+
+				if ( lastPosition.distanceToSquared( scope.object.position ) > EPS || lastZoom !== scope.object.zoom ) {
+
+					scope.dispatchEvent( _changeEvent );
+
+					lastPosition.copy( scope.object.position );
+					lastZoom = scope.object.zoom;
+
+				}
+
+			} else {
+
+				console.warn( 'THREE.TrackballControls: Unsupported camera type' );
+
+			}
+
+		};
+
+		this.reset = function () {
+
+			_state = STATE.NONE;
+			_keyState = STATE.NONE;
+
+			scope.target.copy( scope.target0 );
+			scope.object.position.copy( scope.position0 );
+			scope.object.up.copy( scope.up0 );
+			scope.object.zoom = scope.zoom0;
+
+			scope.object.updateProjectionMatrix();
+
+			_eye.subVectors( scope.object.position, scope.target );
+
+			scope.object.lookAt( scope.target );
+
+			scope.dispatchEvent( _changeEvent );
+
+			lastPosition.copy( scope.object.position );
+			lastZoom = scope.object.zoom;
+
+		};
+
+		// listeners
+
+		function onPointerDown( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			if ( _pointers.length === 0 ) {
+
+				scope.domElement.setPointerCapture( event.pointerId );
+
+				scope.domElement.addEventListener( 'pointermove', onPointerMove );
+				scope.domElement.addEventListener( 'pointerup', onPointerUp );
+
+			}
+
+			//
+
+			addPointer( event );
+
+			if ( event.pointerType === 'touch' ) {
+
+				onTouchStart( event );
+
+			} else {
+
+				onMouseDown( event );
+
+			}
+
+		}
+
+		function onPointerMove( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			if ( event.pointerType === 'touch' ) {
+
+				onTouchMove( event );
+
+			} else {
+
+				onMouseMove( event );
+
+			}
+
+		}
+
+		function onPointerUp( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			if ( event.pointerType === 'touch' ) {
+
+				onTouchEnd( event );
+
+			} else {
+
+				onMouseUp();
+
+			}
+
+			//
+
+			removePointer( event );
+
+			if ( _pointers.length === 0 ) {
+
+				scope.domElement.releasePointerCapture( event.pointerId );
+
+				scope.domElement.removeEventListener( 'pointermove', onPointerMove );
+				scope.domElement.removeEventListener( 'pointerup', onPointerUp );
+
+			}
+
+
+		}
+
+		function onPointerCancel( event ) {
+
+			removePointer( event );
+
+		}
+
+		function keydown( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			window.removeEventListener( 'keydown', keydown );
+
+			if ( _keyState !== STATE.NONE ) {
+
+				return;
+
+			} else if ( event.code === scope.keys[ STATE.ROTATE ] && ! scope.noRotate ) {
+
+				_keyState = STATE.ROTATE;
+
+			} else if ( event.code === scope.keys[ STATE.ZOOM ] && ! scope.noZoom ) {
+
+				_keyState = STATE.ZOOM;
+
+			} else if ( event.code === scope.keys[ STATE.PAN ] && ! scope.noPan ) {
+
+				_keyState = STATE.PAN;
+
+			}
+
+		}
+
+		function keyup() {
+
+			if ( scope.enabled === false ) return;
+
+			_keyState = STATE.NONE;
+
+			window.addEventListener( 'keydown', keydown );
+
+		}
+
+		function onMouseDown( event ) {
+
+			if ( _state === STATE.NONE ) {
+
+				switch ( event.button ) {
+
+					case scope.mouseButtons.LEFT:
+						_state = STATE.ROTATE;
+						break;
+
+					case scope.mouseButtons.MIDDLE:
+						_state = STATE.ZOOM;
+						break;
+
+					case scope.mouseButtons.RIGHT:
+						_state = STATE.PAN;
+						break;
+
+				}
+
+			}
+
+			const state = ( _keyState !== STATE.NONE ) ? _keyState : _state;
+
+			if ( state === STATE.ROTATE && ! scope.noRotate ) {
+
+				_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
+				_movePrev.copy( _moveCurr );
+
+			} else if ( state === STATE.ZOOM && ! scope.noZoom ) {
+
+				_zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+				_zoomEnd.copy( _zoomStart );
+
+			} else if ( state === STATE.PAN && ! scope.noPan ) {
+
+				_panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+				_panEnd.copy( _panStart );
+
+			}
+
+			scope.dispatchEvent( _startEvent );
+
+		}
+
+		function onMouseMove( event ) {
+
+			const state = ( _keyState !== STATE.NONE ) ? _keyState : _state;
+
+			if ( state === STATE.ROTATE && ! scope.noRotate ) {
+
+				_movePrev.copy( _moveCurr );
+				_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
+
+			} else if ( state === STATE.ZOOM && ! scope.noZoom ) {
+
+				_zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+
+			} else if ( state === STATE.PAN && ! scope.noPan ) {
+
+				_panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
+
+			}
+
+		}
+
+		function onMouseUp() {
+
+			_state = STATE.NONE;
+
+			scope.dispatchEvent( _endEvent );
+
+		}
+
+		function onMouseWheel( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			if ( scope.noZoom === true ) return;
+
+			event.preventDefault();
+
+			switch ( event.deltaMode ) {
+
+				case 2:
+					// Zoom in pages
+					_zoomStart.y -= event.deltaY * 0.025;
+					break;
+
+				case 1:
+					// Zoom in lines
+					_zoomStart.y -= event.deltaY * 0.01;
+					break;
+
+				default:
+					// undefined, 0, assume pixels
+					_zoomStart.y -= event.deltaY * 0.00025;
+					break;
+
+			}
+
+			scope.dispatchEvent( _startEvent );
+			scope.dispatchEvent( _endEvent );
+
+		}
+
+		function onTouchStart( event ) {
+
+			trackPointer( event );
+
+			switch ( _pointers.length ) {
+
+				case 1:
+					_state = STATE.TOUCH_ROTATE;
+					_moveCurr.copy( getMouseOnCircle( _pointers[ 0 ].pageX, _pointers[ 0 ].pageY ) );
+					_movePrev.copy( _moveCurr );
+					break;
+
+				default: // 2 or more
+					_state = STATE.TOUCH_ZOOM_PAN;
+					const dx = _pointers[ 0 ].pageX - _pointers[ 1 ].pageX;
+					const dy = _pointers[ 0 ].pageY - _pointers[ 1 ].pageY;
+					_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
+
+					const x = ( _pointers[ 0 ].pageX + _pointers[ 1 ].pageX ) / 2;
+					const y = ( _pointers[ 0 ].pageY + _pointers[ 1 ].pageY ) / 2;
+					_panStart.copy( getMouseOnScreen( x, y ) );
+					_panEnd.copy( _panStart );
+					break;
+
+			}
+
+			scope.dispatchEvent( _startEvent );
+
+		}
+
+		function onTouchMove( event ) {
+
+			trackPointer( event );
+
+			switch ( _pointers.length ) {
+
+				case 1:
+					_movePrev.copy( _moveCurr );
+					_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
+					break;
+
+				default: // 2 or more
+
+					const position = getSecondPointerPosition( event );
+
+					const dx = event.pageX - position.x;
+					const dy = event.pageY - position.y;
+					_touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
+
+					const x = ( event.pageX + position.x ) / 2;
+					const y = ( event.pageY + position.y ) / 2;
+					_panEnd.copy( getMouseOnScreen( x, y ) );
+					break;
+
+			}
+
+		}
+
+		function onTouchEnd( event ) {
+
+			switch ( _pointers.length ) {
+
+				case 0:
+					_state = STATE.NONE;
+					break;
+
+				case 1:
+					_state = STATE.TOUCH_ROTATE;
+					_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
+					_movePrev.copy( _moveCurr );
+					break;
+
+				case 2:
+					_state = STATE.TOUCH_ZOOM_PAN;
+					_moveCurr.copy( getMouseOnCircle( event.pageX - _movePrev.x, event.pageY - _movePrev.y ) );
+					_movePrev.copy( _moveCurr );
+					break;
+
+			}
+
+			scope.dispatchEvent( _endEvent );
+
+		}
+
+		function contextmenu( event ) {
+
+			if ( scope.enabled === false ) return;
+
+			event.preventDefault();
+
+		}
+
+		function addPointer( event ) {
+
+			_pointers.push( event );
+
+		}
+
+		function removePointer( event ) {
+
+			delete _pointerPositions[ event.pointerId ];
+
+			for ( let i = 0; i < _pointers.length; i ++ ) {
+
+				if ( _pointers[ i ].pointerId == event.pointerId ) {
+
+					_pointers.splice( i, 1 );
+					return;
+
+				}
+
+			}
+
+		}
+
+		function trackPointer( event ) {
+
+			let position = _pointerPositions[ event.pointerId ];
+
+			if ( position === undefined ) {
+
+				position = new Vector2();
+				_pointerPositions[ event.pointerId ] = position;
+
+			}
+
+			position.set( event.pageX, event.pageY );
+
+		}
+
+		function getSecondPointerPosition( event ) {
+
+			const pointer = ( event.pointerId === _pointers[ 0 ].pointerId ) ? _pointers[ 1 ] : _pointers[ 0 ];
+
+			return _pointerPositions[ pointer.pointerId ];
+
+		}
+
+		this.dispose = function () {
+
+			scope.domElement.removeEventListener( 'contextmenu', contextmenu );
+
+			scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
+			scope.domElement.removeEventListener( 'pointercancel', onPointerCancel );
+			scope.domElement.removeEventListener( 'wheel', onMouseWheel );
+
+			scope.domElement.removeEventListener( 'pointermove', onPointerMove );
+			scope.domElement.removeEventListener( 'pointerup', onPointerUp );
+
+			window.removeEventListener( 'keydown', keydown );
+			window.removeEventListener( 'keyup', keyup );
+
+		};
+
+		this.domElement.addEventListener( 'contextmenu', contextmenu );
+
+		this.domElement.addEventListener( 'pointerdown', onPointerDown );
+		this.domElement.addEventListener( 'pointercancel', onPointerCancel );
+		this.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } );
+
+
+		window.addEventListener( 'keydown', keydown );
+		window.addEventListener( 'keyup', keyup );
+
+		this.handleResize();
+
+		// force an update at start
+		this.update();
+
+	}
+
+}
+
+export { TrackballControls };

+ 1558 - 0
public/archive/static/js/jsm/controls/TransformControls.js

@@ -0,0 +1,1558 @@
+import {
+	BoxGeometry,
+	BufferGeometry,
+	CylinderGeometry,
+	DoubleSide,
+	Euler,
+	Float32BufferAttribute,
+	Line,
+	LineBasicMaterial,
+	Matrix4,
+	Mesh,
+	MeshBasicMaterial,
+	Object3D,
+	OctahedronGeometry,
+	PlaneGeometry,
+	Quaternion,
+	Raycaster,
+	SphereGeometry,
+	TorusGeometry,
+	Vector3
+} from 'three';
+
+const _raycaster = new Raycaster();
+
+const _tempVector = new Vector3();
+const _tempVector2 = new Vector3();
+const _tempQuaternion = new Quaternion();
+const _unit = {
+	X: new Vector3( 1, 0, 0 ),
+	Y: new Vector3( 0, 1, 0 ),
+	Z: new Vector3( 0, 0, 1 )
+};
+
+const _changeEvent = { type: 'change' };
+const _mouseDownEvent = { type: 'mouseDown' };
+const _mouseUpEvent = { type: 'mouseUp', mode: null };
+const _objectChangeEvent = { type: 'objectChange' };
+
+class TransformControls extends Object3D {
+
+	constructor( camera, domElement ) {
+
+		super();
+
+		if ( domElement === undefined ) {
+
+			console.warn( 'THREE.TransformControls: The second parameter "domElement" is now mandatory.' );
+			domElement = document;
+
+		}
+
+		this.isTransformControls = true;
+
+		this.visible = false;
+		this.domElement = domElement;
+		this.domElement.style.touchAction = 'none'; // disable touch scroll
+
+		const _gizmo = new TransformControlsGizmo();
+		this._gizmo = _gizmo;
+		this.add( _gizmo );
+
+		const _plane = new TransformControlsPlane();
+		this._plane = _plane;
+		this.add( _plane );
+
+		const scope = this;
+
+		// Defined getter, setter and store for a property
+		function defineProperty( propName, defaultValue ) {
+
+			let propValue = defaultValue;
+
+			Object.defineProperty( scope, propName, {
+
+				get: function () {
+
+					return propValue !== undefined ? propValue : defaultValue;
+
+				},
+
+				set: function ( value ) {
+
+					if ( propValue !== value ) {
+
+						propValue = value;
+						_plane[ propName ] = value;
+						_gizmo[ propName ] = value;
+
+						scope.dispatchEvent( { type: propName + '-changed', value: value } );
+						scope.dispatchEvent( _changeEvent );
+
+					}
+
+				}
+
+			} );
+
+			scope[ propName ] = defaultValue;
+			_plane[ propName ] = defaultValue;
+			_gizmo[ propName ] = defaultValue;
+
+		}
+
+		// Define properties with getters/setter
+		// Setting the defined property will automatically trigger change event
+		// Defined properties are passed down to gizmo and plane
+
+		defineProperty( 'camera', camera );
+		defineProperty( 'object', undefined );
+		defineProperty( 'enabled', true );
+		defineProperty( 'axis', null );
+		defineProperty( 'mode', 'translate' );
+		defineProperty( 'translationSnap', null );
+		defineProperty( 'rotationSnap', null );
+		defineProperty( 'scaleSnap', null );
+		defineProperty( 'space', 'world' );
+		defineProperty( 'size', 1 );
+		defineProperty( 'dragging', false );
+		defineProperty( 'showX', true );
+		defineProperty( 'showY', true );
+		defineProperty( 'showZ', true );
+
+		// Reusable utility variables
+
+		const worldPosition = new Vector3();
+		const worldPositionStart = new Vector3();
+		const worldQuaternion = new Quaternion();
+		const worldQuaternionStart = new Quaternion();
+		const cameraPosition = new Vector3();
+		const cameraQuaternion = new Quaternion();
+		const pointStart = new Vector3();
+		const pointEnd = new Vector3();
+		const rotationAxis = new Vector3();
+		const rotationAngle = 0;
+		const eye = new Vector3();
+
+		// TODO: remove properties unused in plane and gizmo
+
+		defineProperty( 'worldPosition', worldPosition );
+		defineProperty( 'worldPositionStart', worldPositionStart );
+		defineProperty( 'worldQuaternion', worldQuaternion );
+		defineProperty( 'worldQuaternionStart', worldQuaternionStart );
+		defineProperty( 'cameraPosition', cameraPosition );
+		defineProperty( 'cameraQuaternion', cameraQuaternion );
+		defineProperty( 'pointStart', pointStart );
+		defineProperty( 'pointEnd', pointEnd );
+		defineProperty( 'rotationAxis', rotationAxis );
+		defineProperty( 'rotationAngle', rotationAngle );
+		defineProperty( 'eye', eye );
+
+		this._offset = new Vector3();
+		this._startNorm = new Vector3();
+		this._endNorm = new Vector3();
+		this._cameraScale = new Vector3();
+
+		this._parentPosition = new Vector3();
+		this._parentQuaternion = new Quaternion();
+		this._parentQuaternionInv = new Quaternion();
+		this._parentScale = new Vector3();
+
+		this._worldScaleStart = new Vector3();
+		this._worldQuaternionInv = new Quaternion();
+		this._worldScale = new Vector3();
+
+		this._positionStart = new Vector3();
+		this._quaternionStart = new Quaternion();
+		this._scaleStart = new Vector3();
+
+		this._getPointer = getPointer.bind( this );
+		this._onPointerDown = onPointerDown.bind( this );
+		this._onPointerHover = onPointerHover.bind( this );
+		this._onPointerMove = onPointerMove.bind( this );
+		this._onPointerUp = onPointerUp.bind( this );
+
+		this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.addEventListener( 'pointermove', this._onPointerHover );
+		this.domElement.addEventListener( 'pointerup', this._onPointerUp );
+
+	}
+
+	// updateMatrixWorld  updates key transformation variables
+	updateMatrixWorld() {
+
+		if ( this.object !== undefined ) {
+
+			this.object.updateMatrixWorld();
+
+			if ( this.object.parent === null ) {
+
+				console.error( 'TransformControls: The attached 3D object must be a part of the scene graph.' );
+
+			} else {
+
+				this.object.parent.matrixWorld.decompose( this._parentPosition, this._parentQuaternion, this._parentScale );
+
+			}
+
+			this.object.matrixWorld.decompose( this.worldPosition, this.worldQuaternion, this._worldScale );
+
+			this._parentQuaternionInv.copy( this._parentQuaternion ).invert();
+			this._worldQuaternionInv.copy( this.worldQuaternion ).invert();
+
+		}
+
+		this.camera.updateMatrixWorld();
+		this.camera.matrixWorld.decompose( this.cameraPosition, this.cameraQuaternion, this._cameraScale );
+
+		if ( this.camera.isOrthographicCamera ) {
+
+			this.camera.getWorldDirection( this.eye );
+
+		} else {
+
+			this.eye.copy( this.cameraPosition ).sub( this.worldPosition ).normalize();
+
+		}
+
+		super.updateMatrixWorld( this );
+
+	}
+
+	pointerHover( pointer ) {
+
+		if ( this.object === undefined || this.dragging === true ) return;
+
+		_raycaster.setFromCamera( pointer, this.camera );
+
+		const intersect = intersectObjectWithRay( this._gizmo.picker[ this.mode ], _raycaster );
+
+		if ( intersect ) {
+
+			this.axis = intersect.object.name;
+
+		} else {
+
+			this.axis = null;
+
+		}
+
+	}
+
+	pointerDown( pointer ) {
+
+		if ( this.object === undefined || this.dragging === true || pointer.button !== 0 ) return;
+
+		if ( this.axis !== null ) {
+
+			_raycaster.setFromCamera( pointer, this.camera );
+
+			const planeIntersect = intersectObjectWithRay( this._plane, _raycaster, true );
+
+			if ( planeIntersect ) {
+
+				this.object.updateMatrixWorld();
+				this.object.parent.updateMatrixWorld();
+
+				this._positionStart.copy( this.object.position );
+				this._quaternionStart.copy( this.object.quaternion );
+				this._scaleStart.copy( this.object.scale );
+
+				this.object.matrixWorld.decompose( this.worldPositionStart, this.worldQuaternionStart, this._worldScaleStart );
+
+				this.pointStart.copy( planeIntersect.point ).sub( this.worldPositionStart );
+
+			}
+
+			this.dragging = true;
+			_mouseDownEvent.mode = this.mode;
+			this.dispatchEvent( _mouseDownEvent );
+
+		}
+
+	}
+
+	pointerMove( pointer ) {
+
+		const axis = this.axis;
+		const mode = this.mode;
+		const object = this.object;
+		let space = this.space;
+
+		if ( mode === 'scale' ) {
+
+			space = 'local';
+
+		} else if ( axis === 'E' || axis === 'XYZE' || axis === 'XYZ' ) {
+
+			space = 'world';
+
+		}
+
+		if ( object === undefined || axis === null || this.dragging === false || pointer.button !== - 1 ) return;
+
+		_raycaster.setFromCamera( pointer, this.camera );
+
+		const planeIntersect = intersectObjectWithRay( this._plane, _raycaster, true );
+
+		if ( ! planeIntersect ) return;
+
+		this.pointEnd.copy( planeIntersect.point ).sub( this.worldPositionStart );
+
+		if ( mode === 'translate' ) {
+
+			// Apply translate
+
+			this._offset.copy( this.pointEnd ).sub( this.pointStart );
+
+			if ( space === 'local' && axis !== 'XYZ' ) {
+
+				this._offset.applyQuaternion( this._worldQuaternionInv );
+
+			}
+
+			if ( axis.indexOf( 'X' ) === - 1 ) this._offset.x = 0;
+			if ( axis.indexOf( 'Y' ) === - 1 ) this._offset.y = 0;
+			if ( axis.indexOf( 'Z' ) === - 1 ) this._offset.z = 0;
+
+			if ( space === 'local' && axis !== 'XYZ' ) {
+
+				this._offset.applyQuaternion( this._quaternionStart ).divide( this._parentScale );
+
+			} else {
+
+				this._offset.applyQuaternion( this._parentQuaternionInv ).divide( this._parentScale );
+
+			}
+
+			object.position.copy( this._offset ).add( this._positionStart );
+
+			// Apply translation snap
+
+			if ( this.translationSnap ) {
+
+				if ( space === 'local' ) {
+
+					object.position.applyQuaternion( _tempQuaternion.copy( this._quaternionStart ).invert() );
+
+					if ( axis.search( 'X' ) !== - 1 ) {
+
+						object.position.x = Math.round( object.position.x / this.translationSnap ) * this.translationSnap;
+
+					}
+
+					if ( axis.search( 'Y' ) !== - 1 ) {
+
+						object.position.y = Math.round( object.position.y / this.translationSnap ) * this.translationSnap;
+
+					}
+
+					if ( axis.search( 'Z' ) !== - 1 ) {
+
+						object.position.z = Math.round( object.position.z / this.translationSnap ) * this.translationSnap;
+
+					}
+
+					object.position.applyQuaternion( this._quaternionStart );
+
+				}
+
+				if ( space === 'world' ) {
+
+					if ( object.parent ) {
+
+						object.position.add( _tempVector.setFromMatrixPosition( object.parent.matrixWorld ) );
+
+					}
+
+					if ( axis.search( 'X' ) !== - 1 ) {
+
+						object.position.x = Math.round( object.position.x / this.translationSnap ) * this.translationSnap;
+
+					}
+
+					if ( axis.search( 'Y' ) !== - 1 ) {
+
+						object.position.y = Math.round( object.position.y / this.translationSnap ) * this.translationSnap;
+
+					}
+
+					if ( axis.search( 'Z' ) !== - 1 ) {
+
+						object.position.z = Math.round( object.position.z / this.translationSnap ) * this.translationSnap;
+
+					}
+
+					if ( object.parent ) {
+
+						object.position.sub( _tempVector.setFromMatrixPosition( object.parent.matrixWorld ) );
+
+					}
+
+				}
+
+			}
+
+		} else if ( mode === 'scale' ) {
+
+			if ( axis.search( 'XYZ' ) !== - 1 ) {
+
+				let d = this.pointEnd.length() / this.pointStart.length();
+
+				if ( this.pointEnd.dot( this.pointStart ) < 0 ) d *= - 1;
+
+				_tempVector2.set( d, d, d );
+
+			} else {
+
+				_tempVector.copy( this.pointStart );
+				_tempVector2.copy( this.pointEnd );
+
+				_tempVector.applyQuaternion( this._worldQuaternionInv );
+				_tempVector2.applyQuaternion( this._worldQuaternionInv );
+
+				_tempVector2.divide( _tempVector );
+
+				if ( axis.search( 'X' ) === - 1 ) {
+
+					_tempVector2.x = 1;
+
+				}
+
+				if ( axis.search( 'Y' ) === - 1 ) {
+
+					_tempVector2.y = 1;
+
+				}
+
+				if ( axis.search( 'Z' ) === - 1 ) {
+
+					_tempVector2.z = 1;
+
+				}
+
+			}
+
+			// Apply scale
+
+			object.scale.copy( this._scaleStart ).multiply( _tempVector2 );
+
+			if ( this.scaleSnap ) {
+
+				if ( axis.search( 'X' ) !== - 1 ) {
+
+					object.scale.x = Math.round( object.scale.x / this.scaleSnap ) * this.scaleSnap || this.scaleSnap;
+
+				}
+
+				if ( axis.search( 'Y' ) !== - 1 ) {
+
+					object.scale.y = Math.round( object.scale.y / this.scaleSnap ) * this.scaleSnap || this.scaleSnap;
+
+				}
+
+				if ( axis.search( 'Z' ) !== - 1 ) {
+
+					object.scale.z = Math.round( object.scale.z / this.scaleSnap ) * this.scaleSnap || this.scaleSnap;
+
+				}
+
+			}
+
+		} else if ( mode === 'rotate' ) {
+
+			this._offset.copy( this.pointEnd ).sub( this.pointStart );
+
+			const ROTATION_SPEED = 20 / this.worldPosition.distanceTo( _tempVector.setFromMatrixPosition( this.camera.matrixWorld ) );
+
+			if ( axis === 'E' ) {
+
+				this.rotationAxis.copy( this.eye );
+				this.rotationAngle = this.pointEnd.angleTo( this.pointStart );
+
+				this._startNorm.copy( this.pointStart ).normalize();
+				this._endNorm.copy( this.pointEnd ).normalize();
+
+				this.rotationAngle *= ( this._endNorm.cross( this._startNorm ).dot( this.eye ) < 0 ? 1 : - 1 );
+
+			} else if ( axis === 'XYZE' ) {
+
+				this.rotationAxis.copy( this._offset ).cross( this.eye ).normalize();
+				this.rotationAngle = this._offset.dot( _tempVector.copy( this.rotationAxis ).cross( this.eye ) ) * ROTATION_SPEED;
+
+			} else if ( axis === 'X' || axis === 'Y' || axis === 'Z' ) {
+
+				this.rotationAxis.copy( _unit[ axis ] );
+
+				_tempVector.copy( _unit[ axis ] );
+
+				if ( space === 'local' ) {
+
+					_tempVector.applyQuaternion( this.worldQuaternion );
+
+				}
+
+				this.rotationAngle = this._offset.dot( _tempVector.cross( this.eye ).normalize() ) * ROTATION_SPEED;
+
+			}
+
+			// Apply rotation snap
+
+			if ( this.rotationSnap ) this.rotationAngle = Math.round( this.rotationAngle / this.rotationSnap ) * this.rotationSnap;
+
+			// Apply rotate
+			if ( space === 'local' && axis !== 'E' && axis !== 'XYZE' ) {
+
+				object.quaternion.copy( this._quaternionStart );
+				object.quaternion.multiply( _tempQuaternion.setFromAxisAngle( this.rotationAxis, this.rotationAngle ) ).normalize();
+
+			} else {
+
+				this.rotationAxis.applyQuaternion( this._parentQuaternionInv );
+				object.quaternion.copy( _tempQuaternion.setFromAxisAngle( this.rotationAxis, this.rotationAngle ) );
+				object.quaternion.multiply( this._quaternionStart ).normalize();
+
+			}
+
+		}
+
+		this.dispatchEvent( _changeEvent );
+		this.dispatchEvent( _objectChangeEvent );
+
+	}
+
+	pointerUp( pointer ) {
+
+		if ( pointer.button !== 0 ) return;
+
+		if ( this.dragging && ( this.axis !== null ) ) {
+
+			_mouseUpEvent.mode = this.mode;
+			this.dispatchEvent( _mouseUpEvent );
+
+		}
+
+		this.dragging = false;
+		this.axis = null;
+
+	}
+
+	dispose() {
+
+		this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
+		this.domElement.removeEventListener( 'pointermove', this._onPointerHover );
+		this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
+		this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
+
+		this.traverse( function ( child ) {
+
+			if ( child.geometry ) child.geometry.dispose();
+			if ( child.material ) child.material.dispose();
+
+		} );
+
+	}
+
+	// Set current object
+	attach( object ) {
+
+		this.object = object;
+		this.visible = true;
+
+		return this;
+
+	}
+
+	// Detach from object
+	detach() {
+
+		this.object = undefined;
+		this.visible = false;
+		this.axis = null;
+
+		return this;
+
+	}
+
+	reset() {
+
+		if ( ! this.enabled ) return;
+
+		if ( this.dragging ) {
+
+			this.object.position.copy( this._positionStart );
+			this.object.quaternion.copy( this._quaternionStart );
+			this.object.scale.copy( this._scaleStart );
+
+			this.dispatchEvent( _changeEvent );
+			this.dispatchEvent( _objectChangeEvent );
+
+			this.pointStart.copy( this.pointEnd );
+
+		}
+
+	}
+
+	getRaycaster() {
+
+		return _raycaster;
+
+	}
+
+	// TODO: deprecate
+
+	getMode() {
+
+		return this.mode;
+
+	}
+
+	setMode( mode ) {
+
+		this.mode = mode;
+
+	}
+
+	setTranslationSnap( translationSnap ) {
+
+		this.translationSnap = translationSnap;
+
+	}
+
+	setRotationSnap( rotationSnap ) {
+
+		this.rotationSnap = rotationSnap;
+
+	}
+
+	setScaleSnap( scaleSnap ) {
+
+		this.scaleSnap = scaleSnap;
+
+	}
+
+	setSize( size ) {
+
+		this.size = size;
+
+	}
+
+	setSpace( space ) {
+
+		this.space = space;
+
+	}
+
+}
+
+// mouse / touch event handlers
+
+function getPointer( event ) {
+
+	if ( this.domElement.ownerDocument.pointerLockElement ) {
+
+		return {
+			x: 0,
+			y: 0,
+			button: event.button
+		};
+
+	} else {
+
+		const rect = this.domElement.getBoundingClientRect();
+
+		return {
+			x: ( event.clientX - rect.left ) / rect.width * 2 - 1,
+			y: - ( event.clientY - rect.top ) / rect.height * 2 + 1,
+			button: event.button
+		};
+
+	}
+
+}
+
+function onPointerHover( event ) {
+
+	if ( ! this.enabled ) return;
+
+	switch ( event.pointerType ) {
+
+		case 'mouse':
+		case 'pen':
+			this.pointerHover( this._getPointer( event ) );
+			break;
+
+	}
+
+}
+
+function onPointerDown( event ) {
+
+	if ( ! this.enabled ) return;
+
+	if ( ! document.pointerLockElement ) {
+
+		this.domElement.setPointerCapture( event.pointerId );
+
+	}
+
+	this.domElement.addEventListener( 'pointermove', this._onPointerMove );
+
+	this.pointerHover( this._getPointer( event ) );
+	this.pointerDown( this._getPointer( event ) );
+
+}
+
+function onPointerMove( event ) {
+
+	if ( ! this.enabled ) return;
+
+	this.pointerMove( this._getPointer( event ) );
+
+}
+
+function onPointerUp( event ) {
+
+	if ( ! this.enabled ) return;
+
+	this.domElement.releasePointerCapture( event.pointerId );
+
+	this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
+
+	this.pointerUp( this._getPointer( event ) );
+
+}
+
+function intersectObjectWithRay( object, raycaster, includeInvisible ) {
+
+	const allIntersections = raycaster.intersectObject( object, true );
+
+	for ( let i = 0; i < allIntersections.length; i ++ ) {
+
+		if ( allIntersections[ i ].object.visible || includeInvisible ) {
+
+			return allIntersections[ i ];
+
+		}
+
+	}
+
+	return false;
+
+}
+
+//
+
+// Reusable utility variables
+
+const _tempEuler = new Euler();
+const _alignVector = new Vector3( 0, 1, 0 );
+const _zeroVector = new Vector3( 0, 0, 0 );
+const _lookAtMatrix = new Matrix4();
+const _tempQuaternion2 = new Quaternion();
+const _identityQuaternion = new Quaternion();
+const _dirVector = new Vector3();
+const _tempMatrix = new Matrix4();
+
+const _unitX = new Vector3( 1, 0, 0 );
+const _unitY = new Vector3( 0, 1, 0 );
+const _unitZ = new Vector3( 0, 0, 1 );
+
+const _v1 = new Vector3();
+const _v2 = new Vector3();
+const _v3 = new Vector3();
+
+class TransformControlsGizmo extends Object3D {
+
+	constructor() {
+
+		super();
+
+		this.isTransformControlsGizmo = true;
+
+		this.type = 'TransformControlsGizmo';
+
+		// shared materials
+
+		const gizmoMaterial = new MeshBasicMaterial( {
+			depthTest: false,
+			depthWrite: false,
+			fog: false,
+			toneMapped: false,
+			transparent: true
+		} );
+
+		const gizmoLineMaterial = new LineBasicMaterial( {
+			depthTest: false,
+			depthWrite: false,
+			fog: false,
+			toneMapped: false,
+			transparent: true
+		} );
+
+		// Make unique material for each axis/color
+
+		const matInvisible = gizmoMaterial.clone();
+		matInvisible.opacity = 0.15;
+
+		const matHelper = gizmoLineMaterial.clone();
+		matHelper.opacity = 0.5;
+
+		const matRed = gizmoMaterial.clone();
+		matRed.color.setHex( 0xff0000 );
+
+		const matGreen = gizmoMaterial.clone();
+		matGreen.color.setHex( 0x00ff00 );
+
+		const matBlue = gizmoMaterial.clone();
+		matBlue.color.setHex( 0x0000ff );
+
+		const matRedTransparent = gizmoMaterial.clone();
+		matRedTransparent.color.setHex( 0xff0000 );
+		matRedTransparent.opacity = 0.5;
+
+		const matGreenTransparent = gizmoMaterial.clone();
+		matGreenTransparent.color.setHex( 0x00ff00 );
+		matGreenTransparent.opacity = 0.5;
+
+		const matBlueTransparent = gizmoMaterial.clone();
+		matBlueTransparent.color.setHex( 0x0000ff );
+		matBlueTransparent.opacity = 0.5;
+
+		const matWhiteTransparent = gizmoMaterial.clone();
+		matWhiteTransparent.opacity = 0.25;
+
+		const matYellowTransparent = gizmoMaterial.clone();
+		matYellowTransparent.color.setHex( 0xffff00 );
+		matYellowTransparent.opacity = 0.25;
+
+		const matYellow = gizmoMaterial.clone();
+		matYellow.color.setHex( 0xffff00 );
+
+		const matGray = gizmoMaterial.clone();
+		matGray.color.setHex( 0x787878 );
+
+		// reusable geometry
+
+		const arrowGeometry = new CylinderGeometry( 0, 0.04, 0.1, 12 );
+		arrowGeometry.translate( 0, 0.05, 0 );
+
+		const scaleHandleGeometry = new BoxGeometry( 0.08, 0.08, 0.08 );
+		scaleHandleGeometry.translate( 0, 0.04, 0 );
+
+		const lineGeometry = new BufferGeometry();
+		lineGeometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0,	1, 0, 0 ], 3 ) );
+
+		const lineGeometry2 = new CylinderGeometry( 0.0075, 0.0075, 0.5, 3 );
+		lineGeometry2.translate( 0, 0.25, 0 );
+
+		function CircleGeometry( radius, arc ) {
+
+			const geometry = new TorusGeometry( radius, 0.0075, 3, 64, arc * Math.PI * 2 );
+			geometry.rotateY( Math.PI / 2 );
+			geometry.rotateX( Math.PI / 2 );
+			return geometry;
+
+		}
+
+		// Special geometry for transform helper. If scaled with position vector it spans from [0,0,0] to position
+
+		function TranslateHelperGeometry() {
+
+			const geometry = new BufferGeometry();
+
+			geometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 1, 1, 1 ], 3 ) );
+
+			return geometry;
+
+		}
+
+		// Gizmo definitions - custom hierarchy definitions for setupGizmo() function
+
+		const gizmoTranslate = {
+			X: [
+				[ new Mesh( arrowGeometry, matRed ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ]],
+				[ new Mesh( arrowGeometry, matRed ), [ - 0.5, 0, 0 ], [ 0, 0, Math.PI / 2 ]],
+				[ new Mesh( lineGeometry2, matRed ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]]
+			],
+			Y: [
+				[ new Mesh( arrowGeometry, matGreen ), [ 0, 0.5, 0 ]],
+				[ new Mesh( arrowGeometry, matGreen ), [ 0, - 0.5, 0 ], [ Math.PI, 0, 0 ]],
+				[ new Mesh( lineGeometry2, matGreen ) ]
+			],
+			Z: [
+				[ new Mesh( arrowGeometry, matBlue ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ]],
+				[ new Mesh( arrowGeometry, matBlue ), [ 0, 0, - 0.5 ], [ - Math.PI / 2, 0, 0 ]],
+				[ new Mesh( lineGeometry2, matBlue ), null, [ Math.PI / 2, 0, 0 ]]
+			],
+			XYZ: [
+				[ new Mesh( new OctahedronGeometry( 0.1, 0 ), matWhiteTransparent.clone() ), [ 0, 0, 0 ]]
+			],
+			XY: [
+				[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matBlueTransparent.clone() ), [ 0.15, 0.15, 0 ]]
+			],
+			YZ: [
+				[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matRedTransparent.clone() ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]]
+			],
+			XZ: [
+				[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matGreenTransparent.clone() ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]]
+			]
+		};
+
+		const pickerTranslate = {
+			X: [
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0.3, 0, 0 ], [ 0, 0, - Math.PI / 2 ]],
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ - 0.3, 0, 0 ], [ 0, 0, Math.PI / 2 ]]
+			],
+			Y: [
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0.3, 0 ]],
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, - 0.3, 0 ], [ 0, 0, Math.PI ]]
+			],
+			Z: [
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, 0.3 ], [ Math.PI / 2, 0, 0 ]],
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, - 0.3 ], [ - Math.PI / 2, 0, 0 ]]
+			],
+			XYZ: [
+				[ new Mesh( new OctahedronGeometry( 0.2, 0 ), matInvisible ) ]
+			],
+			XY: [
+				[ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0.15, 0 ]]
+			],
+			YZ: [
+				[ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]]
+			],
+			XZ: [
+				[ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]]
+			]
+		};
+
+		const helperTranslate = {
+			START: [
+				[ new Mesh( new OctahedronGeometry( 0.01, 2 ), matHelper ), null, null, null, 'helper' ]
+			],
+			END: [
+				[ new Mesh( new OctahedronGeometry( 0.01, 2 ), matHelper ), null, null, null, 'helper' ]
+			],
+			DELTA: [
+				[ new Line( TranslateHelperGeometry(), matHelper ), null, null, null, 'helper' ]
+			],
+			X: [
+				[ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ]
+			],
+			Y: [
+				[ new Line( lineGeometry, matHelper.clone() ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ]
+			],
+			Z: [
+				[ new Line( lineGeometry, matHelper.clone() ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ]
+			]
+		};
+
+		const gizmoRotate = {
+			XYZE: [
+				[ new Mesh( CircleGeometry( 0.5, 1 ), matGray ), null, [ 0, Math.PI / 2, 0 ]]
+			],
+			X: [
+				[ new Mesh( CircleGeometry( 0.5, 0.5 ), matRed ) ]
+			],
+			Y: [
+				[ new Mesh( CircleGeometry( 0.5, 0.5 ), matGreen ), null, [ 0, 0, - Math.PI / 2 ]]
+			],
+			Z: [
+				[ new Mesh( CircleGeometry( 0.5, 0.5 ), matBlue ), null, [ 0, Math.PI / 2, 0 ]]
+			],
+			E: [
+				[ new Mesh( CircleGeometry( 0.75, 1 ), matYellowTransparent ), null, [ 0, Math.PI / 2, 0 ]]
+			]
+		};
+
+		const helperRotate = {
+			AXIS: [
+				[ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ]
+			]
+		};
+
+		const pickerRotate = {
+			XYZE: [
+				[ new Mesh( new SphereGeometry( 0.25, 10, 8 ), matInvisible ) ]
+			],
+			X: [
+				[ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ 0, - Math.PI / 2, - Math.PI / 2 ]],
+			],
+			Y: [
+				[ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ]],
+			],
+			Z: [
+				[ new Mesh( new TorusGeometry( 0.5, 0.1, 4, 24 ), matInvisible ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]],
+			],
+			E: [
+				[ new Mesh( new TorusGeometry( 0.75, 0.1, 2, 24 ), matInvisible ) ]
+			]
+		};
+
+		const gizmoScale = {
+			X: [
+				[ new Mesh( scaleHandleGeometry, matRed ), [ 0.5, 0, 0 ], [ 0, 0, - Math.PI / 2 ]],
+				[ new Mesh( lineGeometry2, matRed ), [ 0, 0, 0 ], [ 0, 0, - Math.PI / 2 ]],
+				[ new Mesh( scaleHandleGeometry, matRed ), [ - 0.5, 0, 0 ], [ 0, 0, Math.PI / 2 ]],
+			],
+			Y: [
+				[ new Mesh( scaleHandleGeometry, matGreen ), [ 0, 0.5, 0 ]],
+				[ new Mesh( lineGeometry2, matGreen ) ],
+				[ new Mesh( scaleHandleGeometry, matGreen ), [ 0, - 0.5, 0 ], [ 0, 0, Math.PI ]],
+			],
+			Z: [
+				[ new Mesh( scaleHandleGeometry, matBlue ), [ 0, 0, 0.5 ], [ Math.PI / 2, 0, 0 ]],
+				[ new Mesh( lineGeometry2, matBlue ), [ 0, 0, 0 ], [ Math.PI / 2, 0, 0 ]],
+				[ new Mesh( scaleHandleGeometry, matBlue ), [ 0, 0, - 0.5 ], [ - Math.PI / 2, 0, 0 ]]
+			],
+			XY: [
+				[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matBlueTransparent ), [ 0.15, 0.15, 0 ]]
+			],
+			YZ: [
+				[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matRedTransparent ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]]
+			],
+			XZ: [
+				[ new Mesh( new BoxGeometry( 0.15, 0.15, 0.01 ), matGreenTransparent ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]]
+			],
+			XYZ: [
+				[ new Mesh( new BoxGeometry( 0.1, 0.1, 0.1 ), matWhiteTransparent.clone() ) ],
+			]
+		};
+
+		const pickerScale = {
+			X: [
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0.3, 0, 0 ], [ 0, 0, - Math.PI / 2 ]],
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ - 0.3, 0, 0 ], [ 0, 0, Math.PI / 2 ]]
+			],
+			Y: [
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0.3, 0 ]],
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, - 0.3, 0 ], [ 0, 0, Math.PI ]]
+			],
+			Z: [
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, 0.3 ], [ Math.PI / 2, 0, 0 ]],
+				[ new Mesh( new CylinderGeometry( 0.2, 0, 0.6, 4 ), matInvisible ), [ 0, 0, - 0.3 ], [ - Math.PI / 2, 0, 0 ]]
+			],
+			XY: [
+				[ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0.15, 0 ]],
+			],
+			YZ: [
+				[ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0, 0.15, 0.15 ], [ 0, Math.PI / 2, 0 ]],
+			],
+			XZ: [
+				[ new Mesh( new BoxGeometry( 0.2, 0.2, 0.01 ), matInvisible ), [ 0.15, 0, 0.15 ], [ - Math.PI / 2, 0, 0 ]],
+			],
+			XYZ: [
+				[ new Mesh( new BoxGeometry( 0.2, 0.2, 0.2 ), matInvisible ), [ 0, 0, 0 ]],
+			]
+		};
+
+		const helperScale = {
+			X: [
+				[ new Line( lineGeometry, matHelper.clone() ), [ - 1e3, 0, 0 ], null, [ 1e6, 1, 1 ], 'helper' ]
+			],
+			Y: [
+				[ new Line( lineGeometry, matHelper.clone() ), [ 0, - 1e3, 0 ], [ 0, 0, Math.PI / 2 ], [ 1e6, 1, 1 ], 'helper' ]
+			],
+			Z: [
+				[ new Line( lineGeometry, matHelper.clone() ), [ 0, 0, - 1e3 ], [ 0, - Math.PI / 2, 0 ], [ 1e6, 1, 1 ], 'helper' ]
+			]
+		};
+
+		// Creates an Object3D with gizmos described in custom hierarchy definition.
+
+		function setupGizmo( gizmoMap ) {
+
+			const gizmo = new Object3D();
+
+			for ( const name in gizmoMap ) {
+
+				for ( let i = gizmoMap[ name ].length; i --; ) {
+
+					const object = gizmoMap[ name ][ i ][ 0 ].clone();
+					const position = gizmoMap[ name ][ i ][ 1 ];
+					const rotation = gizmoMap[ name ][ i ][ 2 ];
+					const scale = gizmoMap[ name ][ i ][ 3 ];
+					const tag = gizmoMap[ name ][ i ][ 4 ];
+
+					// name and tag properties are essential for picking and updating logic.
+					object.name = name;
+					object.tag = tag;
+
+					if ( position ) {
+
+						object.position.set( position[ 0 ], position[ 1 ], position[ 2 ] );
+
+					}
+
+					if ( rotation ) {
+
+						object.rotation.set( rotation[ 0 ], rotation[ 1 ], rotation[ 2 ] );
+
+					}
+
+					if ( scale ) {
+
+						object.scale.set( scale[ 0 ], scale[ 1 ], scale[ 2 ] );
+
+					}
+
+					object.updateMatrix();
+
+					const tempGeometry = object.geometry.clone();
+					tempGeometry.applyMatrix4( object.matrix );
+					object.geometry = tempGeometry;
+					object.renderOrder = Infinity;
+
+					object.position.set( 0, 0, 0 );
+					object.rotation.set( 0, 0, 0 );
+					object.scale.set( 1, 1, 1 );
+
+					gizmo.add( object );
+
+				}
+
+			}
+
+			return gizmo;
+
+		}
+
+		// Gizmo creation
+
+		this.gizmo = {};
+		this.picker = {};
+		this.helper = {};
+
+		this.add( this.gizmo[ 'translate' ] = setupGizmo( gizmoTranslate ) );
+		this.add( this.gizmo[ 'rotate' ] = setupGizmo( gizmoRotate ) );
+		this.add( this.gizmo[ 'scale' ] = setupGizmo( gizmoScale ) );
+		this.add( this.picker[ 'translate' ] = setupGizmo( pickerTranslate ) );
+		this.add( this.picker[ 'rotate' ] = setupGizmo( pickerRotate ) );
+		this.add( this.picker[ 'scale' ] = setupGizmo( pickerScale ) );
+		this.add( this.helper[ 'translate' ] = setupGizmo( helperTranslate ) );
+		this.add( this.helper[ 'rotate' ] = setupGizmo( helperRotate ) );
+		this.add( this.helper[ 'scale' ] = setupGizmo( helperScale ) );
+
+		// Pickers should be hidden always
+
+		this.picker[ 'translate' ].visible = false;
+		this.picker[ 'rotate' ].visible = false;
+		this.picker[ 'scale' ].visible = false;
+
+	}
+
+	// updateMatrixWorld will update transformations and appearance of individual handles
+
+	updateMatrixWorld( force ) {
+
+		const space = ( this.mode === 'scale' ) ? 'local' : this.space; // scale always oriented to local rotation
+
+		const quaternion = ( space === 'local' ) ? this.worldQuaternion : _identityQuaternion;
+
+		// Show only gizmos for current transform mode
+
+		this.gizmo[ 'translate' ].visible = this.mode === 'translate';
+		this.gizmo[ 'rotate' ].visible = this.mode === 'rotate';
+		this.gizmo[ 'scale' ].visible = this.mode === 'scale';
+
+		this.helper[ 'translate' ].visible = this.mode === 'translate';
+		this.helper[ 'rotate' ].visible = this.mode === 'rotate';
+		this.helper[ 'scale' ].visible = this.mode === 'scale';
+
+
+		let handles = [];
+		handles = handles.concat( this.picker[ this.mode ].children );
+		handles = handles.concat( this.gizmo[ this.mode ].children );
+		handles = handles.concat( this.helper[ this.mode ].children );
+
+		for ( let i = 0; i < handles.length; i ++ ) {
+
+			const handle = handles[ i ];
+
+			// hide aligned to camera
+
+			handle.visible = true;
+			handle.rotation.set( 0, 0, 0 );
+			handle.position.copy( this.worldPosition );
+
+			let factor;
+
+			if ( this.camera.isOrthographicCamera ) {
+
+				factor = ( this.camera.top - this.camera.bottom ) / this.camera.zoom;
+
+			} else {
+
+				factor = this.worldPosition.distanceTo( this.cameraPosition ) * Math.min( 1.9 * Math.tan( Math.PI * this.camera.fov / 360 ) / this.camera.zoom, 7 );
+
+			}
+
+			handle.scale.set( 1, 1, 1 ).multiplyScalar( factor * this.size / 4 );
+
+			// TODO: simplify helpers and consider decoupling from gizmo
+
+			if ( handle.tag === 'helper' ) {
+
+				handle.visible = false;
+
+				if ( handle.name === 'AXIS' ) {
+
+					handle.position.copy( this.worldPositionStart );
+					handle.visible = !! this.axis;
+
+					if ( this.axis === 'X' ) {
+
+						_tempQuaternion.setFromEuler( _tempEuler.set( 0, 0, 0 ) );
+						handle.quaternion.copy( quaternion ).multiply( _tempQuaternion );
+
+						if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) {
+
+							handle.visible = false;
+
+						}
+
+					}
+
+					if ( this.axis === 'Y' ) {
+
+						_tempQuaternion.setFromEuler( _tempEuler.set( 0, 0, Math.PI / 2 ) );
+						handle.quaternion.copy( quaternion ).multiply( _tempQuaternion );
+
+						if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) {
+
+							handle.visible = false;
+
+						}
+
+					}
+
+					if ( this.axis === 'Z' ) {
+
+						_tempQuaternion.setFromEuler( _tempEuler.set( 0, Math.PI / 2, 0 ) );
+						handle.quaternion.copy( quaternion ).multiply( _tempQuaternion );
+
+						if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) > 0.9 ) {
+
+							handle.visible = false;
+
+						}
+
+					}
+
+					if ( this.axis === 'XYZE' ) {
+
+						_tempQuaternion.setFromEuler( _tempEuler.set( 0, Math.PI / 2, 0 ) );
+						_alignVector.copy( this.rotationAxis );
+						handle.quaternion.setFromRotationMatrix( _lookAtMatrix.lookAt( _zeroVector, _alignVector, _unitY ) );
+						handle.quaternion.multiply( _tempQuaternion );
+						handle.visible = this.dragging;
+
+					}
+
+					if ( this.axis === 'E' ) {
+
+						handle.visible = false;
+
+					}
+
+
+				} else if ( handle.name === 'START' ) {
+
+					handle.position.copy( this.worldPositionStart );
+					handle.visible = this.dragging;
+
+				} else if ( handle.name === 'END' ) {
+
+					handle.position.copy( this.worldPosition );
+					handle.visible = this.dragging;
+
+				} else if ( handle.name === 'DELTA' ) {
+
+					handle.position.copy( this.worldPositionStart );
+					handle.quaternion.copy( this.worldQuaternionStart );
+					_tempVector.set( 1e-10, 1e-10, 1e-10 ).add( this.worldPositionStart ).sub( this.worldPosition ).multiplyScalar( - 1 );
+					_tempVector.applyQuaternion( this.worldQuaternionStart.clone().invert() );
+					handle.scale.copy( _tempVector );
+					handle.visible = this.dragging;
+
+				} else {
+
+					handle.quaternion.copy( quaternion );
+
+					if ( this.dragging ) {
+
+						handle.position.copy( this.worldPositionStart );
+
+					} else {
+
+						handle.position.copy( this.worldPosition );
+
+					}
+
+					if ( this.axis ) {
+
+						handle.visible = this.axis.search( handle.name ) !== - 1;
+
+					}
+
+				}
+
+				// If updating helper, skip rest of the loop
+				continue;
+
+			}
+
+			// Align handles to current local or world rotation
+
+			handle.quaternion.copy( quaternion );
+
+			if ( this.mode === 'translate' || this.mode === 'scale' ) {
+
+				// Hide translate and scale axis facing the camera
+
+				const AXIS_HIDE_THRESHOLD = 0.99;
+				const PLANE_HIDE_THRESHOLD = 0.2;
+
+				if ( handle.name === 'X' ) {
+
+					if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_THRESHOLD ) {
+
+						handle.scale.set( 1e-10, 1e-10, 1e-10 );
+						handle.visible = false;
+
+					}
+
+				}
+
+				if ( handle.name === 'Y' ) {
+
+					if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_THRESHOLD ) {
+
+						handle.scale.set( 1e-10, 1e-10, 1e-10 );
+						handle.visible = false;
+
+					}
+
+				}
+
+				if ( handle.name === 'Z' ) {
+
+					if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) > AXIS_HIDE_THRESHOLD ) {
+
+						handle.scale.set( 1e-10, 1e-10, 1e-10 );
+						handle.visible = false;
+
+					}
+
+				}
+
+				if ( handle.name === 'XY' ) {
+
+					if ( Math.abs( _alignVector.copy( _unitZ ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_THRESHOLD ) {
+
+						handle.scale.set( 1e-10, 1e-10, 1e-10 );
+						handle.visible = false;
+
+					}
+
+				}
+
+				if ( handle.name === 'YZ' ) {
+
+					if ( Math.abs( _alignVector.copy( _unitX ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_THRESHOLD ) {
+
+						handle.scale.set( 1e-10, 1e-10, 1e-10 );
+						handle.visible = false;
+
+					}
+
+				}
+
+				if ( handle.name === 'XZ' ) {
+
+					if ( Math.abs( _alignVector.copy( _unitY ).applyQuaternion( quaternion ).dot( this.eye ) ) < PLANE_HIDE_THRESHOLD ) {
+
+						handle.scale.set( 1e-10, 1e-10, 1e-10 );
+						handle.visible = false;
+
+					}
+
+				}
+
+			} else if ( this.mode === 'rotate' ) {
+
+				// Align handles to current local or world rotation
+
+				_tempQuaternion2.copy( quaternion );
+				_alignVector.copy( this.eye ).applyQuaternion( _tempQuaternion.copy( quaternion ).invert() );
+
+				if ( handle.name.search( 'E' ) !== - 1 ) {
+
+					handle.quaternion.setFromRotationMatrix( _lookAtMatrix.lookAt( this.eye, _zeroVector, _unitY ) );
+
+				}
+
+				if ( handle.name === 'X' ) {
+
+					_tempQuaternion.setFromAxisAngle( _unitX, Math.atan2( - _alignVector.y, _alignVector.z ) );
+					_tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion );
+					handle.quaternion.copy( _tempQuaternion );
+
+				}
+
+				if ( handle.name === 'Y' ) {
+
+					_tempQuaternion.setFromAxisAngle( _unitY, Math.atan2( _alignVector.x, _alignVector.z ) );
+					_tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion );
+					handle.quaternion.copy( _tempQuaternion );
+
+				}
+
+				if ( handle.name === 'Z' ) {
+
+					_tempQuaternion.setFromAxisAngle( _unitZ, Math.atan2( _alignVector.y, _alignVector.x ) );
+					_tempQuaternion.multiplyQuaternions( _tempQuaternion2, _tempQuaternion );
+					handle.quaternion.copy( _tempQuaternion );
+
+				}
+
+			}
+
+			// Hide disabled axes
+			handle.visible = handle.visible && ( handle.name.indexOf( 'X' ) === - 1 || this.showX );
+			handle.visible = handle.visible && ( handle.name.indexOf( 'Y' ) === - 1 || this.showY );
+			handle.visible = handle.visible && ( handle.name.indexOf( 'Z' ) === - 1 || this.showZ );
+			handle.visible = handle.visible && ( handle.name.indexOf( 'E' ) === - 1 || ( this.showX && this.showY && this.showZ ) );
+
+			// highlight selected axis
+
+			handle.material._color = handle.material._color || handle.material.color.clone();
+			handle.material._opacity = handle.material._opacity || handle.material.opacity;
+
+			handle.material.color.copy( handle.material._color );
+			handle.material.opacity = handle.material._opacity;
+
+			if ( this.enabled && this.axis ) {
+
+				if ( handle.name === this.axis ) {
+
+					handle.material.color.setHex( 0xffff00 );
+					handle.material.opacity = 1.0;
+
+				} else if ( this.axis.split( '' ).some( function ( a ) {
+
+					return handle.name === a;
+
+				} ) ) {
+
+					handle.material.color.setHex( 0xffff00 );
+					handle.material.opacity = 1.0;
+
+				}
+
+			}
+
+		}
+
+		super.updateMatrixWorld( force );
+
+	}
+
+}
+
+//
+
+class TransformControlsPlane extends Mesh {
+
+	constructor() {
+
+		super(
+			new PlaneGeometry( 100000, 100000, 2, 2 ),
+			new MeshBasicMaterial( { visible: false, wireframe: true, side: DoubleSide, transparent: true, opacity: 0.1, toneMapped: false } )
+		);
+
+		this.isTransformControlsPlane = true;
+
+		this.type = 'TransformControlsPlane';
+
+	}
+
+	updateMatrixWorld( force ) {
+
+		let space = this.space;
+
+		this.position.copy( this.worldPosition );
+
+		if ( this.mode === 'scale' ) space = 'local'; // scale always oriented to local rotation
+
+		_v1.copy( _unitX ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion );
+		_v2.copy( _unitY ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion );
+		_v3.copy( _unitZ ).applyQuaternion( space === 'local' ? this.worldQuaternion : _identityQuaternion );
+
+		// Align the plane for current transform mode, axis and space.
+
+		_alignVector.copy( _v2 );
+
+		switch ( this.mode ) {
+
+			case 'translate':
+			case 'scale':
+				switch ( this.axis ) {
+
+					case 'X':
+						_alignVector.copy( this.eye ).cross( _v1 );
+						_dirVector.copy( _v1 ).cross( _alignVector );
+						break;
+					case 'Y':
+						_alignVector.copy( this.eye ).cross( _v2 );
+						_dirVector.copy( _v2 ).cross( _alignVector );
+						break;
+					case 'Z':
+						_alignVector.copy( this.eye ).cross( _v3 );
+						_dirVector.copy( _v3 ).cross( _alignVector );
+						break;
+					case 'XY':
+						_dirVector.copy( _v3 );
+						break;
+					case 'YZ':
+						_dirVector.copy( _v1 );
+						break;
+					case 'XZ':
+						_alignVector.copy( _v3 );
+						_dirVector.copy( _v2 );
+						break;
+					case 'XYZ':
+					case 'E':
+						_dirVector.set( 0, 0, 0 );
+						break;
+
+				}
+
+				break;
+			case 'rotate':
+			default:
+				// special case for rotate
+				_dirVector.set( 0, 0, 0 );
+
+		}
+
+		if ( _dirVector.length() === 0 ) {
+
+			// If in rotate mode, make the plane parallel to camera
+			this.quaternion.copy( this.cameraQuaternion );
+
+		} else {
+
+			_tempMatrix.lookAt( _tempVector.set( 0, 0, 0 ), _dirVector, _alignVector );
+
+			this.quaternion.setFromRotationMatrix( _tempMatrix );
+
+		}
+
+		super.updateMatrixWorld( force );
+
+	}
+
+}
+
+export { TransformControls, TransformControlsGizmo, TransformControlsPlane };

+ 377 - 0
public/archive/static/js/jsm/csm/CSM.js

@@ -0,0 +1,377 @@
+import {
+	Vector2,
+	Vector3,
+	DirectionalLight,
+	MathUtils,
+	ShaderChunk,
+	Matrix4,
+	Box3
+} from 'three';
+import { CSMFrustum } from './CSMFrustum.js';
+import { CSMShader } from './CSMShader.js';
+
+const _cameraToLightMatrix = new Matrix4();
+const _lightSpaceFrustum = new CSMFrustum();
+const _center = new Vector3();
+const _bbox = new Box3();
+const _uniformArray = [];
+const _logArray = [];
+
+export class CSM {
+
+	constructor( data ) {
+
+		data = data || {};
+
+		this.camera = data.camera;
+		this.parent = data.parent;
+		this.cascades = data.cascades || 3;
+		this.maxFar = data.maxFar || 100000;
+		this.mode = data.mode || 'practical';
+		this.shadowMapSize = data.shadowMapSize || 2048;
+		this.shadowBias = data.shadowBias || 0.000001;
+		this.lightDirection = data.lightDirection || new Vector3( 1, - 1, 1 ).normalize();
+		this.lightIntensity = data.lightIntensity || 1;
+		this.lightNear = data.lightNear || 1;
+		this.lightFar = data.lightFar || 2000;
+		this.lightMargin = data.lightMargin || 200;
+		this.customSplitsCallback = data.customSplitsCallback;
+		this.fade = false;
+		this.mainFrustum = new CSMFrustum();
+		this.frustums = [];
+		this.breaks = [];
+
+		this.lights = [];
+		this.shaders = new Map();
+
+		this.createLights();
+		this.updateFrustums();
+		this.injectInclude();
+
+	}
+
+	createLights() {
+
+		for ( let i = 0; i < this.cascades; i ++ ) {
+
+			const light = new DirectionalLight( 0xffffff, this.lightIntensity );
+			light.castShadow = true;
+			light.shadow.mapSize.width = this.shadowMapSize;
+			light.shadow.mapSize.height = this.shadowMapSize;
+
+			light.shadow.camera.near = this.lightNear;
+			light.shadow.camera.far = this.lightFar;
+			light.shadow.bias = this.shadowBias;
+
+			this.parent.add( light );
+			this.parent.add( light.target );
+			this.lights.push( light );
+
+		}
+
+	}
+
+	initCascades() {
+
+		const camera = this.camera;
+		camera.updateProjectionMatrix();
+		this.mainFrustum.setFromProjectionMatrix( camera.projectionMatrix, this.maxFar );
+		this.mainFrustum.split( this.breaks, this.frustums );
+
+	}
+
+	updateShadowBounds() {
+
+		const frustums = this.frustums;
+		for ( let i = 0; i < frustums.length; i ++ ) {
+
+			const light = this.lights[ i ];
+			const shadowCam = light.shadow.camera;
+			const frustum = this.frustums[ i ];
+
+			// Get the two points that represent that furthest points on the frustum assuming
+			// that's either the diagonal across the far plane or the diagonal across the whole
+			// frustum itself.
+			const nearVerts = frustum.vertices.near;
+			const farVerts = frustum.vertices.far;
+			const point1 = farVerts[ 0 ];
+			let point2;
+			if ( point1.distanceTo( farVerts[ 2 ] ) > point1.distanceTo( nearVerts[ 2 ] ) ) {
+
+				point2 = farVerts[ 2 ];
+
+			} else {
+
+				point2 = nearVerts[ 2 ];
+
+			}
+
+			let squaredBBWidth = point1.distanceTo( point2 );
+			if ( this.fade ) {
+
+				// expand the shadow extents by the fade margin if fade is enabled.
+				const camera = this.camera;
+				const far = Math.max( camera.far, this.maxFar );
+				const linearDepth = frustum.vertices.far[ 0 ].z / ( far - camera.near );
+				const margin = 0.25 * Math.pow( linearDepth, 2.0 ) * ( far - camera.near );
+
+				squaredBBWidth += margin;
+
+			}
+
+			shadowCam.left = - squaredBBWidth / 2;
+			shadowCam.right = squaredBBWidth / 2;
+			shadowCam.top = squaredBBWidth / 2;
+			shadowCam.bottom = - squaredBBWidth / 2;
+			shadowCam.updateProjectionMatrix();
+
+		}
+
+	}
+
+	getBreaks() {
+
+		const camera = this.camera;
+		const far = Math.min( camera.far, this.maxFar );
+		this.breaks.length = 0;
+
+		switch ( this.mode ) {
+
+			case 'uniform':
+				uniformSplit( this.cascades, camera.near, far, this.breaks );
+				break;
+			case 'logarithmic':
+				logarithmicSplit( this.cascades, camera.near, far, this.breaks );
+				break;
+			case 'practical':
+				practicalSplit( this.cascades, camera.near, far, 0.5, this.breaks );
+				break;
+			case 'custom':
+				if ( this.customSplitsCallback === undefined ) console.error( 'CSM: Custom split scheme callback not defined.' );
+				this.customSplitsCallback( this.cascades, camera.near, far, this.breaks );
+				break;
+
+		}
+
+		function uniformSplit( amount, near, far, target ) {
+
+			for ( let i = 1; i < amount; i ++ ) {
+
+				target.push( ( near + ( far - near ) * i / amount ) / far );
+
+			}
+
+			target.push( 1 );
+
+		}
+
+		function logarithmicSplit( amount, near, far, target ) {
+
+			for ( let i = 1; i < amount; i ++ ) {
+
+				target.push( ( near * ( far / near ) ** ( i / amount ) ) / far );
+
+			}
+
+			target.push( 1 );
+
+		}
+
+		function practicalSplit( amount, near, far, lambda, target ) {
+
+			_uniformArray.length = 0;
+			_logArray.length = 0;
+			logarithmicSplit( amount, near, far, _logArray );
+			uniformSplit( amount, near, far, _uniformArray );
+
+			for ( let i = 1; i < amount; i ++ ) {
+
+				target.push( MathUtils.lerp( _uniformArray[ i - 1 ], _logArray[ i - 1 ], lambda ) );
+
+			}
+
+			target.push( 1 );
+
+		}
+
+	}
+
+	update() {
+
+		const camera = this.camera;
+		const frustums = this.frustums;
+		for ( let i = 0; i < frustums.length; i ++ ) {
+
+			const light = this.lights[ i ];
+			const shadowCam = light.shadow.camera;
+			const texelWidth = ( shadowCam.right - shadowCam.left ) / this.shadowMapSize;
+			const texelHeight = ( shadowCam.top - shadowCam.bottom ) / this.shadowMapSize;
+			light.shadow.camera.updateMatrixWorld( true );
+			_cameraToLightMatrix.multiplyMatrices( light.shadow.camera.matrixWorldInverse, camera.matrixWorld );
+			frustums[ i ].toSpace( _cameraToLightMatrix, _lightSpaceFrustum );
+
+			const nearVerts = _lightSpaceFrustum.vertices.near;
+			const farVerts = _lightSpaceFrustum.vertices.far;
+			_bbox.makeEmpty();
+			for ( let j = 0; j < 4; j ++ ) {
+
+				_bbox.expandByPoint( nearVerts[ j ] );
+				_bbox.expandByPoint( farVerts[ j ] );
+
+			}
+
+			_bbox.getCenter( _center );
+			_center.z = _bbox.max.z + this.lightMargin;
+			_center.x = Math.floor( _center.x / texelWidth ) * texelWidth;
+			_center.y = Math.floor( _center.y / texelHeight ) * texelHeight;
+			_center.applyMatrix4( light.shadow.camera.matrixWorld );
+
+			light.position.copy( _center );
+			light.target.position.copy( _center );
+
+			light.target.position.x += this.lightDirection.x;
+			light.target.position.y += this.lightDirection.y;
+			light.target.position.z += this.lightDirection.z;
+
+		}
+
+	}
+
+	injectInclude() {
+
+		ShaderChunk.lights_fragment_begin = CSMShader.lights_fragment_begin;
+		ShaderChunk.lights_pars_begin = CSMShader.lights_pars_begin;
+
+	}
+
+	setupMaterial( material ) {
+
+		material.defines = material.defines || {};
+		material.defines.USE_CSM = 1;
+		material.defines.CSM_CASCADES = this.cascades;
+
+		if ( this.fade ) {
+
+			material.defines.CSM_FADE = '';
+
+		}
+
+		const breaksVec2 = [];
+		const scope = this;
+		const shaders = this.shaders;
+
+		material.onBeforeCompile = function ( shader ) {
+
+			const far = Math.min( scope.camera.far, scope.maxFar );
+			scope.getExtendedBreaks( breaksVec2 );
+
+			shader.uniforms.CSM_cascades = { value: breaksVec2 };
+			shader.uniforms.cameraNear = { value: scope.camera.near };
+			shader.uniforms.shadowFar = { value: far };
+
+			shaders.set( material, shader );
+
+		};
+
+		shaders.set( material, null );
+
+	}
+
+	updateUniforms() {
+
+		const far = Math.min( this.camera.far, this.maxFar );
+		const shaders = this.shaders;
+
+		shaders.forEach( function ( shader, material ) {
+
+			if ( shader !== null ) {
+
+				const uniforms = shader.uniforms;
+				this.getExtendedBreaks( uniforms.CSM_cascades.value );
+				uniforms.cameraNear.value = this.camera.near;
+				uniforms.shadowFar.value = far;
+
+			}
+
+			if ( ! this.fade && 'CSM_FADE' in material.defines ) {
+
+				delete material.defines.CSM_FADE;
+				material.needsUpdate = true;
+
+			} else if ( this.fade && ! ( 'CSM_FADE' in material.defines ) ) {
+
+				material.defines.CSM_FADE = '';
+				material.needsUpdate = true;
+
+			}
+
+		}, this );
+
+	}
+
+	getExtendedBreaks( target ) {
+
+		while ( target.length < this.breaks.length ) {
+
+			target.push( new Vector2() );
+
+		}
+
+		target.length = this.breaks.length;
+
+		for ( let i = 0; i < this.cascades; i ++ ) {
+
+			const amount = this.breaks[ i ];
+			const prev = this.breaks[ i - 1 ] || 0;
+			target[ i ].x = prev;
+			target[ i ].y = amount;
+
+		}
+
+	}
+
+	updateFrustums() {
+
+		this.getBreaks();
+		this.initCascades();
+		this.updateShadowBounds();
+		this.updateUniforms();
+
+	}
+
+	remove() {
+
+		for ( let i = 0; i < this.lights.length; i ++ ) {
+
+			this.parent.remove( this.lights[ i ] );
+
+		}
+
+	}
+
+	dispose() {
+
+		const shaders = this.shaders;
+		shaders.forEach( function ( shader, material ) {
+
+			delete material.onBeforeCompile;
+			delete material.defines.USE_CSM;
+			delete material.defines.CSM_CASCADES;
+			delete material.defines.CSM_FADE;
+
+			if ( shader !== null ) {
+
+				delete shader.uniforms.CSM_cascades;
+				delete shader.uniforms.cameraNear;
+				delete shader.uniforms.shadowFar;
+
+			}
+
+			material.needsUpdate = true;
+
+		} );
+		shaders.clear();
+
+	}
+
+}

+ 152 - 0
public/archive/static/js/jsm/csm/CSMFrustum.js

@@ -0,0 +1,152 @@
+import { Vector3, Matrix4 } from 'three';
+
+const inverseProjectionMatrix = new Matrix4();
+
+class CSMFrustum {
+
+	constructor( data ) {
+
+		data = data || {};
+
+		this.vertices = {
+			near: [
+				new Vector3(),
+				new Vector3(),
+				new Vector3(),
+				new Vector3()
+			],
+			far: [
+				new Vector3(),
+				new Vector3(),
+				new Vector3(),
+				new Vector3()
+			]
+		};
+
+		if ( data.projectionMatrix !== undefined ) {
+
+			this.setFromProjectionMatrix( data.projectionMatrix, data.maxFar || 10000 );
+
+		}
+
+	}
+
+	setFromProjectionMatrix( projectionMatrix, maxFar ) {
+
+		const isOrthographic = projectionMatrix.elements[ 2 * 4 + 3 ] === 0;
+
+		inverseProjectionMatrix.copy( projectionMatrix ).invert();
+
+		// 3 --- 0  vertices.near/far order
+		// |     |
+		// 2 --- 1
+		// clip space spans from [-1, 1]
+
+		this.vertices.near[ 0 ].set( 1, 1, - 1 );
+		this.vertices.near[ 1 ].set( 1, - 1, - 1 );
+		this.vertices.near[ 2 ].set( - 1, - 1, - 1 );
+		this.vertices.near[ 3 ].set( - 1, 1, - 1 );
+		this.vertices.near.forEach( function ( v ) {
+
+			v.applyMatrix4( inverseProjectionMatrix );
+
+		} );
+
+		this.vertices.far[ 0 ].set( 1, 1, 1 );
+		this.vertices.far[ 1 ].set( 1, - 1, 1 );
+		this.vertices.far[ 2 ].set( - 1, - 1, 1 );
+		this.vertices.far[ 3 ].set( - 1, 1, 1 );
+		this.vertices.far.forEach( function ( v ) {
+
+			v.applyMatrix4( inverseProjectionMatrix );
+
+			const absZ = Math.abs( v.z );
+			if ( isOrthographic ) {
+
+				v.z *= Math.min( maxFar / absZ, 1.0 );
+
+			} else {
+
+				v.multiplyScalar( Math.min( maxFar / absZ, 1.0 ) );
+
+			}
+
+		} );
+
+		return this.vertices;
+
+	}
+
+	split( breaks, target ) {
+
+		while ( breaks.length > target.length ) {
+
+			target.push( new CSMFrustum() );
+
+		}
+
+		target.length = breaks.length;
+
+		for ( let i = 0; i < breaks.length; i ++ ) {
+
+			const cascade = target[ i ];
+
+			if ( i === 0 ) {
+
+				for ( let j = 0; j < 4; j ++ ) {
+
+					cascade.vertices.near[ j ].copy( this.vertices.near[ j ] );
+
+				}
+
+			} else {
+
+				for ( let j = 0; j < 4; j ++ ) {
+
+					cascade.vertices.near[ j ].lerpVectors( this.vertices.near[ j ], this.vertices.far[ j ], breaks[ i - 1 ] );
+
+				}
+
+			}
+
+			if ( i === breaks.length - 1 ) {
+
+				for ( let j = 0; j < 4; j ++ ) {
+
+					cascade.vertices.far[ j ].copy( this.vertices.far[ j ] );
+
+				}
+
+			} else {
+
+				for ( let j = 0; j < 4; j ++ ) {
+
+					cascade.vertices.far[ j ].lerpVectors( this.vertices.near[ j ], this.vertices.far[ j ], breaks[ i ] );
+
+				}
+
+			}
+
+		}
+
+	}
+
+	toSpace( cameraMatrix, target ) {
+
+		for ( let i = 0; i < 4; i ++ ) {
+
+			target.vertices.near[ i ]
+				.copy( this.vertices.near[ i ] )
+				.applyMatrix4( cameraMatrix );
+
+			target.vertices.far[ i ]
+				.copy( this.vertices.far[ i ] )
+				.applyMatrix4( cameraMatrix );
+
+		}
+
+	}
+
+}
+
+export { CSMFrustum };

+ 163 - 0
public/archive/static/js/jsm/csm/CSMHelper.js

@@ -0,0 +1,163 @@
+import {
+	Group,
+	Mesh,
+	LineSegments,
+	BufferGeometry,
+	LineBasicMaterial,
+	Box3Helper,
+	Box3,
+	PlaneGeometry,
+	MeshBasicMaterial,
+	BufferAttribute,
+	DoubleSide
+} from 'three';
+
+class CSMHelper extends Group {
+
+	constructor( csm ) {
+
+		super();
+		this.csm = csm;
+		this.displayFrustum = true;
+		this.displayPlanes = true;
+		this.displayShadowBounds = true;
+
+		const indices = new Uint16Array( [ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ] );
+		const positions = new Float32Array( 24 );
+		const frustumGeometry = new BufferGeometry();
+		frustumGeometry.setIndex( new BufferAttribute( indices, 1 ) );
+		frustumGeometry.setAttribute( 'position', new BufferAttribute( positions, 3, false ) );
+		const frustumLines = new LineSegments( frustumGeometry, new LineBasicMaterial() );
+		this.add( frustumLines );
+
+		this.frustumLines = frustumLines;
+		this.cascadeLines = [];
+		this.cascadePlanes = [];
+		this.shadowLines = [];
+
+	}
+
+	updateVisibility() {
+
+		const displayFrustum = this.displayFrustum;
+		const displayPlanes = this.displayPlanes;
+		const displayShadowBounds = this.displayShadowBounds;
+
+		const frustumLines = this.frustumLines;
+		const cascadeLines = this.cascadeLines;
+		const cascadePlanes = this.cascadePlanes;
+		const shadowLines = this.shadowLines;
+		for ( let i = 0, l = cascadeLines.length; i < l; i ++ ) {
+
+			const cascadeLine = cascadeLines[ i ];
+			const cascadePlane = cascadePlanes[ i ];
+			const shadowLineGroup = shadowLines[ i ];
+
+			cascadeLine.visible = displayFrustum;
+			cascadePlane.visible = displayFrustum && displayPlanes;
+			shadowLineGroup.visible = displayShadowBounds;
+
+		}
+
+		frustumLines.visible = displayFrustum;
+
+	}
+
+	update() {
+
+		const csm = this.csm;
+		const camera = csm.camera;
+		const cascades = csm.cascades;
+		const mainFrustum = csm.mainFrustum;
+		const frustums = csm.frustums;
+		const lights = csm.lights;
+
+		const frustumLines = this.frustumLines;
+		const frustumLinePositions = frustumLines.geometry.getAttribute( 'position' );
+		const cascadeLines = this.cascadeLines;
+		const cascadePlanes = this.cascadePlanes;
+		const shadowLines = this.shadowLines;
+
+		this.position.copy( camera.position );
+		this.quaternion.copy( camera.quaternion );
+		this.scale.copy( camera.scale );
+		this.updateMatrixWorld( true );
+
+		while ( cascadeLines.length > cascades ) {
+
+			this.remove( cascadeLines.pop() );
+			this.remove( cascadePlanes.pop() );
+			this.remove( shadowLines.pop() );
+
+		}
+
+		while ( cascadeLines.length < cascades ) {
+
+			const cascadeLine = new Box3Helper( new Box3(), 0xffffff );
+			const planeMat = new MeshBasicMaterial( { transparent: true, opacity: 0.1, depthWrite: false, side: DoubleSide } );
+			const cascadePlane = new Mesh( new PlaneGeometry(), planeMat );
+			const shadowLineGroup = new Group();
+			const shadowLine = new Box3Helper( new Box3(), 0xffff00 );
+			shadowLineGroup.add( shadowLine );
+
+			this.add( cascadeLine );
+			this.add( cascadePlane );
+			this.add( shadowLineGroup );
+
+			cascadeLines.push( cascadeLine );
+			cascadePlanes.push( cascadePlane );
+			shadowLines.push( shadowLineGroup );
+
+		}
+
+		for ( let i = 0; i < cascades; i ++ ) {
+
+			const frustum = frustums[ i ];
+			const light = lights[ i ];
+			const shadowCam = light.shadow.camera;
+			const farVerts = frustum.vertices.far;
+
+			const cascadeLine = cascadeLines[ i ];
+			const cascadePlane = cascadePlanes[ i ];
+			const shadowLineGroup = shadowLines[ i ];
+			const shadowLine = shadowLineGroup.children[ 0 ];
+
+			cascadeLine.box.min.copy( farVerts[ 2 ] );
+			cascadeLine.box.max.copy( farVerts[ 0 ] );
+			cascadeLine.box.max.z += 1e-4;
+
+			cascadePlane.position.addVectors( farVerts[ 0 ], farVerts[ 2 ] );
+			cascadePlane.position.multiplyScalar( 0.5 );
+			cascadePlane.scale.subVectors( farVerts[ 0 ], farVerts[ 2 ] );
+			cascadePlane.scale.z = 1e-4;
+
+			this.remove( shadowLineGroup );
+			shadowLineGroup.position.copy( shadowCam.position );
+			shadowLineGroup.quaternion.copy( shadowCam.quaternion );
+			shadowLineGroup.scale.copy( shadowCam.scale );
+			shadowLineGroup.updateMatrixWorld( true );
+			this.attach( shadowLineGroup );
+
+			shadowLine.box.min.set( shadowCam.bottom, shadowCam.left, - shadowCam.far );
+			shadowLine.box.max.set( shadowCam.top, shadowCam.right, - shadowCam.near );
+
+		}
+
+		const nearVerts = mainFrustum.vertices.near;
+		const farVerts = mainFrustum.vertices.far;
+		frustumLinePositions.setXYZ( 0, farVerts[ 0 ].x, farVerts[ 0 ].y, farVerts[ 0 ].z );
+		frustumLinePositions.setXYZ( 1, farVerts[ 3 ].x, farVerts[ 3 ].y, farVerts[ 3 ].z );
+		frustumLinePositions.setXYZ( 2, farVerts[ 2 ].x, farVerts[ 2 ].y, farVerts[ 2 ].z );
+		frustumLinePositions.setXYZ( 3, farVerts[ 1 ].x, farVerts[ 1 ].y, farVerts[ 1 ].z );
+
+		frustumLinePositions.setXYZ( 4, nearVerts[ 0 ].x, nearVerts[ 0 ].y, nearVerts[ 0 ].z );
+		frustumLinePositions.setXYZ( 5, nearVerts[ 3 ].x, nearVerts[ 3 ].y, nearVerts[ 3 ].z );
+		frustumLinePositions.setXYZ( 6, nearVerts[ 2 ].x, nearVerts[ 2 ].y, nearVerts[ 2 ].z );
+		frustumLinePositions.setXYZ( 7, nearVerts[ 1 ].x, nearVerts[ 1 ].y, nearVerts[ 1 ].z );
+		frustumLinePositions.needsUpdate = true;
+
+	}
+
+}
+
+export { CSMHelper };

+ 251 - 0
public/archive/static/js/jsm/csm/CSMShader.js

@@ -0,0 +1,251 @@
+import { ShaderChunk } from 'three';
+
+const CSMShader = {
+	lights_fragment_begin: /* glsl */`
+GeometricContext geometry;
+
+geometry.position = - vViewPosition;
+geometry.normal = normal;
+geometry.viewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );
+
+#ifdef CLEARCOAT
+
+	geometry.clearcoatNormal = clearcoatNormal;
+
+#endif
+
+IncidentLight directLight;
+
+#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct )
+
+	PointLight pointLight;
+	#if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0
+	PointLightShadow pointLightShadow;
+	#endif
+
+	#pragma unroll_loop_start
+	for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {
+
+		pointLight = pointLights[ i ];
+
+		getPointLightInfo( pointLight, geometry, directLight );
+
+		#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS )
+		pointLightShadow = pointLightShadows[ i ];
+		directLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0;
+		#endif
+
+		RE_Direct( directLight, geometry, material, reflectedLight );
+
+	}
+	#pragma unroll_loop_end
+
+#endif
+
+#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct )
+
+	SpotLight spotLight;
+	#if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0
+	SpotLightShadow spotLightShadow;
+	#endif
+
+	#pragma unroll_loop_start
+	for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {
+
+		spotLight = spotLights[ i ];
+
+		getSpotLightInfo( spotLight, geometry, directLight );
+
+		#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )
+		spotLightShadow = spotLightShadows[ i ];
+		directLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotShadowCoord[ i ] ) : 1.0;
+		#endif
+
+		RE_Direct( directLight, geometry, material, reflectedLight );
+
+	}
+	#pragma unroll_loop_end
+
+#endif
+
+#if ( NUM_DIR_LIGHTS > 0) && defined( RE_Direct ) && defined( USE_CSM ) && defined( CSM_CASCADES )
+
+	DirectionalLight directionalLight;
+	float linearDepth = (vViewPosition.z) / (shadowFar - cameraNear);
+	#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0
+	DirectionalLightShadow directionalLightShadow;
+	#endif
+
+	#if defined( USE_SHADOWMAP ) && defined( CSM_FADE )
+	vec2 cascade;
+	float cascadeCenter;
+	float closestEdge;
+	float margin;
+	float csmx;
+	float csmy;
+
+	#pragma unroll_loop_start
+	for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {
+
+		directionalLight = directionalLights[ i ];
+		getDirectionalLightInfo( directionalLight, geometry, directLight );
+
+	  	#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )
+			// NOTE: Depth gets larger away from the camera.
+			// cascade.x is closer, cascade.y is further
+			cascade = CSM_cascades[ i ];
+			cascadeCenter = ( cascade.x + cascade.y ) / 2.0;
+			closestEdge = linearDepth < cascadeCenter ? cascade.x : cascade.y;
+			margin = 0.25 * pow( closestEdge, 2.0 );
+			csmx = cascade.x - margin / 2.0;
+			csmy = cascade.y + margin / 2.0;
+			if( linearDepth >= csmx && ( linearDepth < csmy || UNROLLED_LOOP_INDEX == CSM_CASCADES - 1 ) ) {
+
+				float dist = min( linearDepth - csmx, csmy - linearDepth );
+				float ratio = clamp( dist / margin, 0.0, 1.0 );
+
+				vec3 prevColor = directLight.color;
+				directionalLightShadow = directionalLightShadows[ i ];
+				directLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;
+
+				bool shouldFadeLastCascade = UNROLLED_LOOP_INDEX == CSM_CASCADES - 1 && linearDepth > cascadeCenter;
+				directLight.color = mix( prevColor, directLight.color, shouldFadeLastCascade ? ratio : 1.0 );
+
+				ReflectedLight prevLight = reflectedLight;
+				RE_Direct( directLight, geometry, material, reflectedLight );
+
+				bool shouldBlend = UNROLLED_LOOP_INDEX != CSM_CASCADES - 1 || UNROLLED_LOOP_INDEX == CSM_CASCADES - 1 && linearDepth < cascadeCenter;
+				float blendRatio = shouldBlend ? ratio : 1.0;
+
+				reflectedLight.directDiffuse = mix( prevLight.directDiffuse, reflectedLight.directDiffuse, blendRatio );
+				reflectedLight.directSpecular = mix( prevLight.directSpecular, reflectedLight.directSpecular, blendRatio );
+				reflectedLight.indirectDiffuse = mix( prevLight.indirectDiffuse, reflectedLight.indirectDiffuse, blendRatio );
+				reflectedLight.indirectSpecular = mix( prevLight.indirectSpecular, reflectedLight.indirectSpecular, blendRatio );
+
+			}
+	  	#endif
+
+	}
+	#pragma unroll_loop_end
+	#else
+
+		#pragma unroll_loop_start
+		for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {
+
+			directionalLight = directionalLights[ i ];
+			getDirectionalLightInfo( directionalLight, geometry, directLight );
+
+			#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )
+
+			directionalLightShadow = directionalLightShadows[ i ];
+			if(linearDepth >= CSM_cascades[UNROLLED_LOOP_INDEX].x && linearDepth < CSM_cascades[UNROLLED_LOOP_INDEX].y) directLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;
+
+			if(linearDepth >= CSM_cascades[UNROLLED_LOOP_INDEX].x && (linearDepth < CSM_cascades[UNROLLED_LOOP_INDEX].y || UNROLLED_LOOP_INDEX == CSM_CASCADES - 1)) RE_Direct( directLight, geometry, material, reflectedLight );
+
+			#endif
+
+		}
+		#pragma unroll_loop_end
+
+	#endif
+
+	#if ( NUM_DIR_LIGHTS > NUM_DIR_LIGHT_SHADOWS)
+		// compute the lights not casting shadows (if any)
+
+		#pragma unroll_loop_start
+		for ( int i = NUM_DIR_LIGHT_SHADOWS; i < NUM_DIR_LIGHTS; i ++ ) {
+
+			directionalLight = directionalLights[ i ];
+
+			getDirectionalLightInfo( directionalLight, geometry, directLight );
+
+			RE_Direct( directLight, geometry, material, reflectedLight );
+
+		}
+		#pragma unroll_loop_end
+
+	#endif
+
+#endif
+
+
+#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) && !defined( USE_CSM ) && !defined( CSM_CASCADES )
+
+	DirectionalLight directionalLight;
+	#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0
+	DirectionalLightShadow directionalLightShadow;
+	#endif
+
+	#pragma unroll_loop_start
+	for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {
+
+		directionalLight = directionalLights[ i ];
+
+		getDirectionalLightInfo( directionalLight, geometry, directLight );
+
+		#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )
+		directionalLightShadow = directionalLightShadows[ i ];
+		directLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;
+		#endif
+
+		RE_Direct( directLight, geometry, material, reflectedLight );
+
+	}
+	#pragma unroll_loop_end
+
+#endif
+
+#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )
+
+	RectAreaLight rectAreaLight;
+
+	#pragma unroll_loop_start
+	for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) {
+
+		rectAreaLight = rectAreaLights[ i ];
+		RE_Direct_RectArea( rectAreaLight, geometry, material, reflectedLight );
+
+	}
+	#pragma unroll_loop_end
+
+#endif
+
+#if defined( RE_IndirectDiffuse )
+
+	vec3 iblIrradiance = vec3( 0.0 );
+
+	vec3 irradiance = getAmbientLightIrradiance( ambientLightColor );
+
+	irradiance += getLightProbeIrradiance( lightProbe, geometry.normal );
+
+	#if ( NUM_HEMI_LIGHTS > 0 )
+
+		#pragma unroll_loop_start
+		for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {
+
+			irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry.normal );
+
+		}
+		#pragma unroll_loop_end
+
+	#endif
+
+#endif
+
+#if defined( RE_IndirectSpecular )
+
+	vec3 radiance = vec3( 0.0 );
+	vec3 clearcoatRadiance = vec3( 0.0 );
+
+#endif
+`,
+	lights_pars_begin: /* glsl */`
+#if defined( USE_CSM ) && defined( CSM_CASCADES )
+uniform vec2 CSM_cascades[CSM_CASCADES];
+uniform float cameraNear;
+uniform float shadowFar;
+#endif
+	` + ShaderChunk.lights_pars_begin
+};
+
+export { CSMShader };

+ 422 - 0
public/archive/static/js/jsm/curves/CurveExtras.js

@@ -0,0 +1,422 @@
+import {
+	Curve,
+	Vector3
+} from 'three';
+
+/**
+ * A bunch of parametric curves
+ *
+ * Formulas collected from various sources
+ * http://mathworld.wolfram.com/HeartCurve.html
+ * http://en.wikipedia.org/wiki/Viviani%27s_curve
+ * http://www.mi.sanu.ac.rs/vismath/taylorapril2011/Taylor.pdf
+ * https://prideout.net/blog/old/blog/index.html@p=44.html
+ */
+
+// GrannyKnot
+
+class GrannyKnot extends Curve {
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		t = 2 * Math.PI * t;
+
+		const x = - 0.22 * Math.cos( t ) - 1.28 * Math.sin( t ) - 0.44 * Math.cos( 3 * t ) - 0.78 * Math.sin( 3 * t );
+		const y = - 0.1 * Math.cos( 2 * t ) - 0.27 * Math.sin( 2 * t ) + 0.38 * Math.cos( 4 * t ) + 0.46 * Math.sin( 4 * t );
+		const z = 0.7 * Math.cos( 3 * t ) - 0.4 * Math.sin( 3 * t );
+
+		return point.set( x, y, z ).multiplyScalar( 20 );
+
+	}
+
+}
+
+// HeartCurve
+
+class HeartCurve extends Curve {
+
+	constructor( scale = 5 ) {
+
+		super();
+
+		this.scale = scale;
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		t *= 2 * Math.PI;
+
+		const x = 16 * Math.pow( Math.sin( t ), 3 );
+		const y = 13 * Math.cos( t ) - 5 * Math.cos( 2 * t ) - 2 * Math.cos( 3 * t ) - Math.cos( 4 * t );
+		const z = 0;
+
+		return point.set( x, y, z ).multiplyScalar( this.scale );
+
+	}
+
+}
+
+// Viviani's Curve
+
+class VivianiCurve extends Curve {
+
+	constructor( scale = 70 ) {
+
+		super();
+
+		this.scale = scale;
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		t = t * 4 * Math.PI; // normalized to 0..1
+		const a = this.scale / 2;
+
+		const x = a * ( 1 + Math.cos( t ) );
+		const y = a * Math.sin( t );
+		const z = 2 * a * Math.sin( t / 2 );
+
+		return point.set( x, y, z );
+
+	}
+
+}
+
+// KnotCurve
+
+class KnotCurve extends Curve {
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		t *= 2 * Math.PI;
+
+		const R = 10;
+		const s = 50;
+
+		const x = s * Math.sin( t );
+		const y = Math.cos( t ) * ( R + s * Math.cos( t ) );
+		const z = Math.sin( t ) * ( R + s * Math.cos( t ) );
+
+		return point.set( x, y, z );
+
+	}
+
+}
+
+
+// HelixCurve
+
+class HelixCurve extends Curve {
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		const a = 30; // radius
+		const b = 150; // height
+
+		const t2 = 2 * Math.PI * t * b / 30;
+
+		const x = Math.cos( t2 ) * a;
+		const y = Math.sin( t2 ) * a;
+		const z = b * t;
+
+		return point.set( x, y, z );
+
+	}
+
+}
+
+// TrefoilKnot
+
+class TrefoilKnot extends Curve {
+
+	constructor( scale = 10 ) {
+
+		super();
+
+		this.scale = scale;
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		t *= Math.PI * 2;
+
+		const x = ( 2 + Math.cos( 3 * t ) ) * Math.cos( 2 * t );
+		const y = ( 2 + Math.cos( 3 * t ) ) * Math.sin( 2 * t );
+		const z = Math.sin( 3 * t );
+
+		return point.set( x, y, z ).multiplyScalar( this.scale );
+
+	}
+
+}
+
+// TorusKnot
+
+class TorusKnot extends Curve {
+
+	constructor( scale = 10 ) {
+
+		super();
+
+		this.scale = scale;
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		const p = 3;
+		const q = 4;
+
+		t *= Math.PI * 2;
+
+		const x = ( 2 + Math.cos( q * t ) ) * Math.cos( p * t );
+		const y = ( 2 + Math.cos( q * t ) ) * Math.sin( p * t );
+		const z = Math.sin( q * t );
+
+		return point.set( x, y, z ).multiplyScalar( this.scale );
+
+	}
+
+}
+
+// CinquefoilKnot
+
+class CinquefoilKnot extends Curve {
+
+	constructor( scale = 10 ) {
+
+		super();
+
+		this.scale = scale;
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		const p = 2;
+		const q = 5;
+
+		t *= Math.PI * 2;
+
+		const x = ( 2 + Math.cos( q * t ) ) * Math.cos( p * t );
+		const y = ( 2 + Math.cos( q * t ) ) * Math.sin( p * t );
+		const z = Math.sin( q * t );
+
+		return point.set( x, y, z ).multiplyScalar( this.scale );
+
+	}
+
+}
+
+
+// TrefoilPolynomialKnot
+
+class TrefoilPolynomialKnot extends Curve {
+
+	constructor( scale = 10 ) {
+
+		super();
+
+		this.scale = scale;
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		t = t * 4 - 2;
+
+		const x = Math.pow( t, 3 ) - 3 * t;
+		const y = Math.pow( t, 4 ) - 4 * t * t;
+		const z = 1 / 5 * Math.pow( t, 5 ) - 2 * t;
+
+		return point.set( x, y, z ).multiplyScalar( this.scale );
+
+	}
+
+}
+
+function scaleTo( x, y, t ) {
+
+	const r = y - x;
+	return t * r + x;
+
+}
+
+// FigureEightPolynomialKnot
+
+class FigureEightPolynomialKnot extends Curve {
+
+	constructor( scale = 1 ) {
+
+		super();
+
+		this.scale = scale;
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		t = scaleTo( - 4, 4, t );
+
+		const x = 2 / 5 * t * ( t * t - 7 ) * ( t * t - 10 );
+		const y = Math.pow( t, 4 ) - 13 * t * t;
+		const z = 1 / 10 * t * ( t * t - 4 ) * ( t * t - 9 ) * ( t * t - 12 );
+
+		return point.set( x, y, z ).multiplyScalar( this.scale );
+
+	}
+
+}
+
+// DecoratedTorusKnot4a
+
+class DecoratedTorusKnot4a extends Curve {
+
+	constructor( scale = 40 ) {
+
+		super();
+
+		this.scale = scale;
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		t *= Math.PI * 2;
+
+		const x = Math.cos( 2 * t ) * ( 1 + 0.6 * ( Math.cos( 5 * t ) + 0.75 * Math.cos( 10 * t ) ) );
+		const y = Math.sin( 2 * t ) * ( 1 + 0.6 * ( Math.cos( 5 * t ) + 0.75 * Math.cos( 10 * t ) ) );
+		const z = 0.35 * Math.sin( 5 * t );
+
+		return point.set( x, y, z ).multiplyScalar( this.scale );
+
+	}
+
+}
+
+// DecoratedTorusKnot4b
+
+class DecoratedTorusKnot4b extends Curve {
+
+	constructor( scale = 40 ) {
+
+		super();
+
+		this.scale = scale;
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		const fi = t * Math.PI * 2;
+
+		const x = Math.cos( 2 * fi ) * ( 1 + 0.45 * Math.cos( 3 * fi ) + 0.4 * Math.cos( 9 * fi ) );
+		const y = Math.sin( 2 * fi ) * ( 1 + 0.45 * Math.cos( 3 * fi ) + 0.4 * Math.cos( 9 * fi ) );
+		const z = 0.2 * Math.sin( 9 * fi );
+
+		return point.set( x, y, z ).multiplyScalar( this.scale );
+
+	}
+
+}
+
+
+// DecoratedTorusKnot5a
+
+class DecoratedTorusKnot5a extends Curve {
+
+	constructor( scale = 40 ) {
+
+		super();
+
+		this.scale = scale;
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		const fi = t * Math.PI * 2;
+
+		const x = Math.cos( 3 * fi ) * ( 1 + 0.3 * Math.cos( 5 * fi ) + 0.5 * Math.cos( 10 * fi ) );
+		const y = Math.sin( 3 * fi ) * ( 1 + 0.3 * Math.cos( 5 * fi ) + 0.5 * Math.cos( 10 * fi ) );
+		const z = 0.2 * Math.sin( 20 * fi );
+
+		return point.set( x, y, z ).multiplyScalar( this.scale );
+
+	}
+
+}
+
+// DecoratedTorusKnot5c
+
+class DecoratedTorusKnot5c extends Curve {
+
+	constructor( scale = 40 ) {
+
+		super();
+
+		this.scale = scale;
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		const fi = t * Math.PI * 2;
+
+		const x = Math.cos( 4 * fi ) * ( 1 + 0.5 * ( Math.cos( 5 * fi ) + 0.4 * Math.cos( 20 * fi ) ) );
+		const y = Math.sin( 4 * fi ) * ( 1 + 0.5 * ( Math.cos( 5 * fi ) + 0.4 * Math.cos( 20 * fi ) ) );
+		const z = 0.35 * Math.sin( 15 * fi );
+
+		return point.set( x, y, z ).multiplyScalar( this.scale );
+
+	}
+
+}
+
+export {
+	GrannyKnot,
+	HeartCurve,
+	VivianiCurve,
+	KnotCurve,
+	HelixCurve,
+	TrefoilKnot,
+	TorusKnot,
+	CinquefoilKnot,
+	TrefoilPolynomialKnot,
+	FigureEightPolynomialKnot,
+	DecoratedTorusKnot4a,
+	DecoratedTorusKnot4b,
+	DecoratedTorusKnot5a,
+	DecoratedTorusKnot5c
+};

+ 80 - 0
public/archive/static/js/jsm/curves/NURBSCurve.js

@@ -0,0 +1,80 @@
+import {
+	Curve,
+	Vector3,
+	Vector4
+} from 'three';
+import * as NURBSUtils from '../curves/NURBSUtils.js';
+
+/**
+ * NURBS curve object
+ *
+ * Derives from Curve, overriding getPoint and getTangent.
+ *
+ * Implementation is based on (x, y [, z=0 [, w=1]]) control points with w=weight.
+ *
+ **/
+
+class NURBSCurve extends Curve {
+
+	constructor(
+		degree,
+		knots /* array of reals */,
+		controlPoints /* array of Vector(2|3|4) */,
+		startKnot /* index in knots */,
+		endKnot /* index in knots */
+	) {
+
+		super();
+
+		this.degree = degree;
+		this.knots = knots;
+		this.controlPoints = [];
+		// Used by periodic NURBS to remove hidden spans
+		this.startKnot = startKnot || 0;
+		this.endKnot = endKnot || ( this.knots.length - 1 );
+
+		for ( let i = 0; i < controlPoints.length; ++ i ) {
+
+			// ensure Vector4 for control points
+			const point = controlPoints[ i ];
+			this.controlPoints[ i ] = new Vector4( point.x, point.y, point.z, point.w );
+
+		}
+
+	}
+
+	getPoint( t, optionalTarget = new Vector3() ) {
+
+		const point = optionalTarget;
+
+		const u = this.knots[ this.startKnot ] + t * ( this.knots[ this.endKnot ] - this.knots[ this.startKnot ] ); // linear mapping t->u
+
+		// following results in (wx, wy, wz, w) homogeneous point
+		const hpoint = NURBSUtils.calcBSplinePoint( this.degree, this.knots, this.controlPoints, u );
+
+		if ( hpoint.w !== 1.0 ) {
+
+			// project to 3D space: (wx, wy, wz, w) -> (x, y, z, 1)
+			hpoint.divideScalar( hpoint.w );
+
+		}
+
+		return point.set( hpoint.x, hpoint.y, hpoint.z );
+
+	}
+
+	getTangent( t, optionalTarget = new Vector3() ) {
+
+		const tangent = optionalTarget;
+
+		const u = this.knots[ 0 ] + t * ( this.knots[ this.knots.length - 1 ] - this.knots[ 0 ] );
+		const ders = NURBSUtils.calcNURBSDerivatives( this.degree, this.knots, this.controlPoints, u, 1 );
+		tangent.copy( ders[ 1 ] ).normalize();
+
+		return tangent;
+
+	}
+
+}
+
+export { NURBSCurve };

+ 52 - 0
public/archive/static/js/jsm/curves/NURBSSurface.js

@@ -0,0 +1,52 @@
+import {
+	Vector4
+} from 'three';
+import * as NURBSUtils from '../curves/NURBSUtils.js';
+
+/**
+ * NURBS surface object
+ *
+ * Implementation is based on (x, y [, z=0 [, w=1]]) control points with w=weight.
+ **/
+
+class NURBSSurface {
+
+	constructor( degree1, degree2, knots1, knots2 /* arrays of reals */, controlPoints /* array^2 of Vector(2|3|4) */ ) {
+
+		this.degree1 = degree1;
+		this.degree2 = degree2;
+		this.knots1 = knots1;
+		this.knots2 = knots2;
+		this.controlPoints = [];
+
+		const len1 = knots1.length - degree1 - 1;
+		const len2 = knots2.length - degree2 - 1;
+
+		// ensure Vector4 for control points
+		for ( let i = 0; i < len1; ++ i ) {
+
+			this.controlPoints[ i ] = [];
+
+			for ( let j = 0; j < len2; ++ j ) {
+
+				const point = controlPoints[ i ][ j ];
+				this.controlPoints[ i ][ j ] = new Vector4( point.x, point.y, point.z, point.w );
+
+			}
+
+		}
+
+	}
+
+	getPoint( t1, t2, target ) {
+
+		const u = this.knots1[ 0 ] + t1 * ( this.knots1[ this.knots1.length - 1 ] - this.knots1[ 0 ] ); // linear mapping t1->u
+		const v = this.knots2[ 0 ] + t2 * ( this.knots2[ this.knots2.length - 1 ] - this.knots2[ 0 ] ); // linear mapping t2->u
+
+		NURBSUtils.calcSurfacePoint( this.degree1, this.degree2, this.knots1, this.knots2, this.controlPoints, u, v, target );
+
+	}
+
+}
+
+export { NURBSSurface };

+ 487 - 0
public/archive/static/js/jsm/curves/NURBSUtils.js

@@ -0,0 +1,487 @@
+import {
+	Vector3,
+	Vector4
+} from 'three';
+
+/**
+ * NURBS utils
+ *
+ * See NURBSCurve and NURBSSurface.
+ **/
+
+
+/**************************************************************
+ *	NURBS Utils
+ **************************************************************/
+
+/*
+Finds knot vector span.
+
+p : degree
+u : parametric value
+U : knot vector
+
+returns the span
+*/
+function findSpan( p, u, U ) {
+
+	const n = U.length - p - 1;
+
+	if ( u >= U[ n ] ) {
+
+		return n - 1;
+
+	}
+
+	if ( u <= U[ p ] ) {
+
+		return p;
+
+	}
+
+	let low = p;
+	let high = n;
+	let mid = Math.floor( ( low + high ) / 2 );
+
+	while ( u < U[ mid ] || u >= U[ mid + 1 ] ) {
+
+		if ( u < U[ mid ] ) {
+
+			high = mid;
+
+		} else {
+
+			low = mid;
+
+		}
+
+		mid = Math.floor( ( low + high ) / 2 );
+
+	}
+
+	return mid;
+
+}
+
+
+/*
+Calculate basis functions. See The NURBS Book, page 70, algorithm A2.2
+
+span : span in which u lies
+u    : parametric point
+p    : degree
+U    : knot vector
+
+returns array[p+1] with basis functions values.
+*/
+function calcBasisFunctions( span, u, p, U ) {
+
+	const N = [];
+	const left = [];
+	const right = [];
+	N[ 0 ] = 1.0;
+
+	for ( let j = 1; j <= p; ++ j ) {
+
+		left[ j ] = u - U[ span + 1 - j ];
+		right[ j ] = U[ span + j ] - u;
+
+		let saved = 0.0;
+
+		for ( let r = 0; r < j; ++ r ) {
+
+			const rv = right[ r + 1 ];
+			const lv = left[ j - r ];
+			const temp = N[ r ] / ( rv + lv );
+			N[ r ] = saved + rv * temp;
+			saved = lv * temp;
+
+		}
+
+		N[ j ] = saved;
+
+	}
+
+	return N;
+
+}
+
+
+/*
+Calculate B-Spline curve points. See The NURBS Book, page 82, algorithm A3.1.
+
+p : degree of B-Spline
+U : knot vector
+P : control points (x, y, z, w)
+u : parametric point
+
+returns point for given u
+*/
+function calcBSplinePoint( p, U, P, u ) {
+
+	const span = findSpan( p, u, U );
+	const N = calcBasisFunctions( span, u, p, U );
+	const C = new Vector4( 0, 0, 0, 0 );
+
+	for ( let j = 0; j <= p; ++ j ) {
+
+		const point = P[ span - p + j ];
+		const Nj = N[ j ];
+		const wNj = point.w * Nj;
+		C.x += point.x * wNj;
+		C.y += point.y * wNj;
+		C.z += point.z * wNj;
+		C.w += point.w * Nj;
+
+	}
+
+	return C;
+
+}
+
+
+/*
+Calculate basis functions derivatives. See The NURBS Book, page 72, algorithm A2.3.
+
+span : span in which u lies
+u    : parametric point
+p    : degree
+n    : number of derivatives to calculate
+U    : knot vector
+
+returns array[n+1][p+1] with basis functions derivatives
+*/
+function calcBasisFunctionDerivatives( span, u, p, n, U ) {
+
+	const zeroArr = [];
+	for ( let i = 0; i <= p; ++ i )
+		zeroArr[ i ] = 0.0;
+
+	const ders = [];
+
+	for ( let i = 0; i <= n; ++ i )
+		ders[ i ] = zeroArr.slice( 0 );
+
+	const ndu = [];
+
+	for ( let i = 0; i <= p; ++ i )
+		ndu[ i ] = zeroArr.slice( 0 );
+
+	ndu[ 0 ][ 0 ] = 1.0;
+
+	const left = zeroArr.slice( 0 );
+	const right = zeroArr.slice( 0 );
+
+	for ( let j = 1; j <= p; ++ j ) {
+
+		left[ j ] = u - U[ span + 1 - j ];
+		right[ j ] = U[ span + j ] - u;
+
+		let saved = 0.0;
+
+		for ( let r = 0; r < j; ++ r ) {
+
+			const rv = right[ r + 1 ];
+			const lv = left[ j - r ];
+			ndu[ j ][ r ] = rv + lv;
+
+			const temp = ndu[ r ][ j - 1 ] / ndu[ j ][ r ];
+			ndu[ r ][ j ] = saved + rv * temp;
+			saved = lv * temp;
+
+		}
+
+		ndu[ j ][ j ] = saved;
+
+	}
+
+	for ( let j = 0; j <= p; ++ j ) {
+
+		ders[ 0 ][ j ] = ndu[ j ][ p ];
+
+	}
+
+	for ( let r = 0; r <= p; ++ r ) {
+
+		let s1 = 0;
+		let s2 = 1;
+
+		const a = [];
+		for ( let i = 0; i <= p; ++ i ) {
+
+			a[ i ] = zeroArr.slice( 0 );
+
+		}
+
+		a[ 0 ][ 0 ] = 1.0;
+
+		for ( let k = 1; k <= n; ++ k ) {
+
+			let d = 0.0;
+			const rk = r - k;
+			const pk = p - k;
+
+			if ( r >= k ) {
+
+				a[ s2 ][ 0 ] = a[ s1 ][ 0 ] / ndu[ pk + 1 ][ rk ];
+				d = a[ s2 ][ 0 ] * ndu[ rk ][ pk ];
+
+			}
+
+			const j1 = ( rk >= - 1 ) ? 1 : - rk;
+			const j2 = ( r - 1 <= pk ) ? k - 1 : p - r;
+
+			for ( let j = j1; j <= j2; ++ j ) {
+
+				a[ s2 ][ j ] = ( a[ s1 ][ j ] - a[ s1 ][ j - 1 ] ) / ndu[ pk + 1 ][ rk + j ];
+				d += a[ s2 ][ j ] * ndu[ rk + j ][ pk ];
+
+			}
+
+			if ( r <= pk ) {
+
+				a[ s2 ][ k ] = - a[ s1 ][ k - 1 ] / ndu[ pk + 1 ][ r ];
+				d += a[ s2 ][ k ] * ndu[ r ][ pk ];
+
+			}
+
+			ders[ k ][ r ] = d;
+
+			const j = s1;
+			s1 = s2;
+			s2 = j;
+
+		}
+
+	}
+
+	let r = p;
+
+	for ( let k = 1; k <= n; ++ k ) {
+
+		for ( let j = 0; j <= p; ++ j ) {
+
+			ders[ k ][ j ] *= r;
+
+		}
+
+		r *= p - k;
+
+	}
+
+	return ders;
+
+}
+
+
+/*
+	Calculate derivatives of a B-Spline. See The NURBS Book, page 93, algorithm A3.2.
+
+	p  : degree
+	U  : knot vector
+	P  : control points
+	u  : Parametric points
+	nd : number of derivatives
+
+	returns array[d+1] with derivatives
+	*/
+function calcBSplineDerivatives( p, U, P, u, nd ) {
+
+	const du = nd < p ? nd : p;
+	const CK = [];
+	const span = findSpan( p, u, U );
+	const nders = calcBasisFunctionDerivatives( span, u, p, du, U );
+	const Pw = [];
+
+	for ( let i = 0; i < P.length; ++ i ) {
+
+		const point = P[ i ].clone();
+		const w = point.w;
+
+		point.x *= w;
+		point.y *= w;
+		point.z *= w;
+
+		Pw[ i ] = point;
+
+	}
+
+	for ( let k = 0; k <= du; ++ k ) {
+
+		const point = Pw[ span - p ].clone().multiplyScalar( nders[ k ][ 0 ] );
+
+		for ( let j = 1; j <= p; ++ j ) {
+
+			point.add( Pw[ span - p + j ].clone().multiplyScalar( nders[ k ][ j ] ) );
+
+		}
+
+		CK[ k ] = point;
+
+	}
+
+	for ( let k = du + 1; k <= nd + 1; ++ k ) {
+
+		CK[ k ] = new Vector4( 0, 0, 0 );
+
+	}
+
+	return CK;
+
+}
+
+
+/*
+Calculate "K over I"
+
+returns k!/(i!(k-i)!)
+*/
+function calcKoverI( k, i ) {
+
+	let nom = 1;
+
+	for ( let j = 2; j <= k; ++ j ) {
+
+		nom *= j;
+
+	}
+
+	let denom = 1;
+
+	for ( let j = 2; j <= i; ++ j ) {
+
+		denom *= j;
+
+	}
+
+	for ( let j = 2; j <= k - i; ++ j ) {
+
+		denom *= j;
+
+	}
+
+	return nom / denom;
+
+}
+
+
+/*
+Calculate derivatives (0-nd) of rational curve. See The NURBS Book, page 127, algorithm A4.2.
+
+Pders : result of function calcBSplineDerivatives
+
+returns array with derivatives for rational curve.
+*/
+function calcRationalCurveDerivatives( Pders ) {
+
+	const nd = Pders.length;
+	const Aders = [];
+	const wders = [];
+
+	for ( let i = 0; i < nd; ++ i ) {
+
+		const point = Pders[ i ];
+		Aders[ i ] = new Vector3( point.x, point.y, point.z );
+		wders[ i ] = point.w;
+
+	}
+
+	const CK = [];
+
+	for ( let k = 0; k < nd; ++ k ) {
+
+		const v = Aders[ k ].clone();
+
+		for ( let i = 1; i <= k; ++ i ) {
+
+			v.sub( CK[ k - i ].clone().multiplyScalar( calcKoverI( k, i ) * wders[ i ] ) );
+
+		}
+
+		CK[ k ] = v.divideScalar( wders[ 0 ] );
+
+	}
+
+	return CK;
+
+}
+
+
+/*
+Calculate NURBS curve derivatives. See The NURBS Book, page 127, algorithm A4.2.
+
+p  : degree
+U  : knot vector
+P  : control points in homogeneous space
+u  : parametric points
+nd : number of derivatives
+
+returns array with derivatives.
+*/
+function calcNURBSDerivatives( p, U, P, u, nd ) {
+
+	const Pders = calcBSplineDerivatives( p, U, P, u, nd );
+	return calcRationalCurveDerivatives( Pders );
+
+}
+
+
+/*
+Calculate rational B-Spline surface point. See The NURBS Book, page 134, algorithm A4.3.
+
+p1, p2 : degrees of B-Spline surface
+U1, U2 : knot vectors
+P      : control points (x, y, z, w)
+u, v   : parametric values
+
+returns point for given (u, v)
+*/
+function calcSurfacePoint( p, q, U, V, P, u, v, target ) {
+
+	const uspan = findSpan( p, u, U );
+	const vspan = findSpan( q, v, V );
+	const Nu = calcBasisFunctions( uspan, u, p, U );
+	const Nv = calcBasisFunctions( vspan, v, q, V );
+	const temp = [];
+
+	for ( let l = 0; l <= q; ++ l ) {
+
+		temp[ l ] = new Vector4( 0, 0, 0, 0 );
+		for ( let k = 0; k <= p; ++ k ) {
+
+			const point = P[ uspan - p + k ][ vspan - q + l ].clone();
+			const w = point.w;
+			point.x *= w;
+			point.y *= w;
+			point.z *= w;
+			temp[ l ].add( point.multiplyScalar( Nu[ k ] ) );
+
+		}
+
+	}
+
+	const Sw = new Vector4( 0, 0, 0, 0 );
+	for ( let l = 0; l <= q; ++ l ) {
+
+		Sw.add( temp[ l ].multiplyScalar( Nv[ l ] ) );
+
+	}
+
+	Sw.divideScalar( Sw.w );
+	target.set( Sw.x, Sw.y, Sw.z );
+
+}
+
+
+
+export {
+	findSpan,
+	calcBasisFunctions,
+	calcBSplinePoint,
+	calcBasisFunctionDerivatives,
+	calcBSplineDerivatives,
+	calcKoverI,
+	calcRationalCurveDerivatives,
+	calcNURBSDerivatives,
+	calcSurfacePoint,
+};

+ 168 - 0
public/archive/static/js/jsm/effects/AnaglyphEffect.js

@@ -0,0 +1,168 @@
+import {
+	LinearFilter,
+	Matrix3,
+	Mesh,
+	NearestFilter,
+	OrthographicCamera,
+	PlaneGeometry,
+	RGBAFormat,
+	Scene,
+	ShaderMaterial,
+	StereoCamera,
+	WebGLRenderTarget
+} from 'three';
+
+class AnaglyphEffect {
+
+	constructor( renderer, width = 512, height = 512 ) {
+
+		// Dubois matrices from https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.7.6968&rep=rep1&type=pdf#page=4
+
+		this.colorMatrixLeft = new Matrix3().fromArray( [
+			0.456100, - 0.0400822, - 0.0152161,
+			0.500484, - 0.0378246, - 0.0205971,
+			0.176381, - 0.0157589, - 0.00546856
+		] );
+
+		this.colorMatrixRight = new Matrix3().fromArray( [
+			- 0.0434706, 0.378476, - 0.0721527,
+			- 0.0879388, 0.73364, - 0.112961,
+			- 0.00155529, - 0.0184503, 1.2264
+		] );
+
+		const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+
+		const _scene = new Scene();
+
+		const _stereo = new StereoCamera();
+
+		const _params = { minFilter: LinearFilter, magFilter: NearestFilter, format: RGBAFormat };
+
+		const _renderTargetL = new WebGLRenderTarget( width, height, _params );
+		const _renderTargetR = new WebGLRenderTarget( width, height, _params );
+
+		const _material = new ShaderMaterial( {
+
+			uniforms: {
+
+				'mapLeft': { value: _renderTargetL.texture },
+				'mapRight': { value: _renderTargetR.texture },
+
+				'colorMatrixLeft': { value: this.colorMatrixLeft },
+				'colorMatrixRight': { value: this.colorMatrixRight }
+
+			},
+
+			vertexShader: [
+
+				'varying vec2 vUv;',
+
+				'void main() {',
+
+				'	vUv = vec2( uv.x, uv.y );',
+				'	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
+
+				'}'
+
+			].join( '\n' ),
+
+			fragmentShader: [
+
+				'uniform sampler2D mapLeft;',
+				'uniform sampler2D mapRight;',
+				'varying vec2 vUv;',
+
+				'uniform mat3 colorMatrixLeft;',
+				'uniform mat3 colorMatrixRight;',
+
+				// These functions implement sRGB linearization and gamma correction
+
+				'float lin( float c ) {',
+				'	return c <= 0.04045 ? c * 0.0773993808 :',
+				'			pow( c * 0.9478672986 + 0.0521327014, 2.4 );',
+				'}',
+
+				'vec4 lin( vec4 c ) {',
+				'	return vec4( lin( c.r ), lin( c.g ), lin( c.b ), c.a );',
+				'}',
+
+				'float dev( float c ) {',
+				'	return c <= 0.0031308 ? c * 12.92',
+				'			: pow( c, 0.41666 ) * 1.055 - 0.055;',
+				'}',
+
+
+				'void main() {',
+
+				'	vec2 uv = vUv;',
+
+				'	vec4 colorL = lin( texture2D( mapLeft, uv ) );',
+				'	vec4 colorR = lin( texture2D( mapRight, uv ) );',
+
+				'	vec3 color = clamp(',
+				'			colorMatrixLeft * colorL.rgb +',
+				'			colorMatrixRight * colorR.rgb, 0., 1. );',
+
+				'	gl_FragColor = vec4(',
+				'			dev( color.r ), dev( color.g ), dev( color.b ),',
+				'			max( colorL.a, colorR.a ) );',
+
+				'}'
+
+			].join( '\n' )
+
+		} );
+
+		const _mesh = new Mesh( new PlaneGeometry( 2, 2 ), _material );
+		_scene.add( _mesh );
+
+		this.setSize = function ( width, height ) {
+
+			renderer.setSize( width, height );
+
+			const pixelRatio = renderer.getPixelRatio();
+
+			_renderTargetL.setSize( width * pixelRatio, height * pixelRatio );
+			_renderTargetR.setSize( width * pixelRatio, height * pixelRatio );
+
+		};
+
+		this.render = function ( scene, camera ) {
+
+			const currentRenderTarget = renderer.getRenderTarget();
+
+			if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();
+
+			if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();
+
+			_stereo.update( camera );
+
+			renderer.setRenderTarget( _renderTargetL );
+			renderer.clear();
+			renderer.render( scene, _stereo.cameraL );
+
+			renderer.setRenderTarget( _renderTargetR );
+			renderer.clear();
+			renderer.render( scene, _stereo.cameraR );
+
+			renderer.setRenderTarget( null );
+			renderer.render( _scene, _camera );
+
+			renderer.setRenderTarget( currentRenderTarget );
+
+		};
+
+		this.dispose = function () {
+
+			_renderTargetL.dispose();
+			_renderTargetR.dispose();
+			_mesh.geometry.dispose();
+			_mesh.material.dispose();
+
+		};
+
+	}
+
+}
+
+export { AnaglyphEffect };

+ 266 - 0
public/archive/static/js/jsm/effects/AsciiEffect.js

@@ -0,0 +1,266 @@
+/**
+ * Ascii generation is based on https://github.com/hassadee/jsascii/blob/master/jsascii.js
+ *
+ * 16 April 2012 - @blurspline
+ */
+
+class AsciiEffect {
+
+	constructor( renderer, charSet = ' .:-=+*#%@', options = {} ) {
+
+		// ' .,:;=|iI+hHOE#`$';
+		// darker bolder character set from https://github.com/saw/Canvas-ASCII-Art/
+		// ' .\'`^",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$'.split('');
+
+		// Some ASCII settings
+
+		const fResolution = options[ 'resolution' ] || 0.15; // Higher for more details
+		const iScale = options[ 'scale' ] || 1;
+		const bColor = options[ 'color' ] || false; // nice but slows down rendering!
+		const bAlpha = options[ 'alpha' ] || false; // Transparency
+		const bBlock = options[ 'block' ] || false; // blocked characters. like good O dos
+		const bInvert = options[ 'invert' ] || false; // black is white, white is black
+		const strResolution = options[ 'strResolution' ] || 'low';
+
+		let width, height;
+
+		const domElement = document.createElement( 'div' );
+		domElement.style.cursor = 'default';
+
+		const oAscii = document.createElement( 'table' );
+		domElement.appendChild( oAscii );
+
+		let iWidth, iHeight;
+		let oImg;
+
+		this.setSize = function ( w, h ) {
+
+			width = w;
+			height = h;
+
+			renderer.setSize( w, h );
+
+			initAsciiSize();
+
+		};
+
+
+		this.render = function ( scene, camera ) {
+
+			renderer.render( scene, camera );
+			asciifyImage( oAscii );
+
+		};
+
+		this.domElement = domElement;
+
+
+		// Throw in ascii library from https://github.com/hassadee/jsascii/blob/master/jsascii.js (MIT License)
+
+		function initAsciiSize() {
+
+			iWidth = Math.round( width * fResolution );
+			iHeight = Math.round( height * fResolution );
+
+			oCanvas.width = iWidth;
+			oCanvas.height = iHeight;
+			// oCanvas.style.display = "none";
+			// oCanvas.style.width = iWidth;
+			// oCanvas.style.height = iHeight;
+
+			oImg = renderer.domElement;
+
+			if ( oImg.style.backgroundColor ) {
+
+				oAscii.rows[ 0 ].cells[ 0 ].style.backgroundColor = oImg.style.backgroundColor;
+				oAscii.rows[ 0 ].cells[ 0 ].style.color = oImg.style.color;
+
+			}
+
+			oAscii.cellSpacing = 0;
+			oAscii.cellPadding = 0;
+
+			const oStyle = oAscii.style;
+			oStyle.display = 'inline';
+			oStyle.width = Math.round( iWidth / fResolution * iScale ) + 'px';
+			oStyle.height = Math.round( iHeight / fResolution * iScale ) + 'px';
+			oStyle.whiteSpace = 'pre';
+			oStyle.margin = '0px';
+			oStyle.padding = '0px';
+			oStyle.letterSpacing = fLetterSpacing + 'px';
+			oStyle.fontFamily = strFont;
+			oStyle.fontSize = fFontSize + 'px';
+			oStyle.lineHeight = fLineHeight + 'px';
+			oStyle.textAlign = 'left';
+			oStyle.textDecoration = 'none';
+
+		}
+
+
+		const aDefaultCharList = ( ' .,:;i1tfLCG08@' ).split( '' );
+		const aDefaultColorCharList = ( ' CGO08@' ).split( '' );
+		const strFont = 'courier new, monospace';
+
+		const oCanvasImg = renderer.domElement;
+
+		const oCanvas = document.createElement( 'canvas' );
+		if ( ! oCanvas.getContext ) {
+
+			return;
+
+		}
+
+		const oCtx = oCanvas.getContext( '2d' );
+		if ( ! oCtx.getImageData ) {
+
+			return;
+
+		}
+
+		let aCharList = ( bColor ? aDefaultColorCharList : aDefaultCharList );
+
+		if ( charSet ) aCharList = charSet;
+
+		// Setup dom
+
+		const fFontSize = ( 2 / fResolution ) * iScale;
+		const fLineHeight = ( 2 / fResolution ) * iScale;
+
+		// adjust letter-spacing for all combinations of scale and resolution to get it to fit the image width.
+
+		let fLetterSpacing = 0;
+
+		if ( strResolution == 'low' ) {
+
+			switch ( iScale ) {
+
+				case 1 : fLetterSpacing = - 1; break;
+				case 2 :
+				case 3 : fLetterSpacing = - 2.1; break;
+				case 4 : fLetterSpacing = - 3.1; break;
+				case 5 : fLetterSpacing = - 4.15; break;
+
+			}
+
+		}
+
+		if ( strResolution == 'medium' ) {
+
+			switch ( iScale ) {
+
+				case 1 : fLetterSpacing = 0; break;
+				case 2 : fLetterSpacing = - 1; break;
+				case 3 : fLetterSpacing = - 1.04; break;
+				case 4 :
+				case 5 : fLetterSpacing = - 2.1; break;
+
+			}
+
+		}
+
+		if ( strResolution == 'high' ) {
+
+			switch ( iScale ) {
+
+				case 1 :
+				case 2 : fLetterSpacing = 0; break;
+				case 3 :
+				case 4 :
+				case 5 : fLetterSpacing = - 1; break;
+
+			}
+
+		}
+
+
+		// can't get a span or div to flow like an img element, but a table works?
+
+
+		// convert img element to ascii
+
+		function asciifyImage( oAscii ) {
+
+			oCtx.clearRect( 0, 0, iWidth, iHeight );
+			oCtx.drawImage( oCanvasImg, 0, 0, iWidth, iHeight );
+			const oImgData = oCtx.getImageData( 0, 0, iWidth, iHeight ).data;
+
+			// Coloring loop starts now
+			let strChars = '';
+
+			// console.time('rendering');
+
+			for ( let y = 0; y < iHeight; y += 2 ) {
+
+				for ( let x = 0; x < iWidth; x ++ ) {
+
+					const iOffset = ( y * iWidth + x ) * 4;
+
+					const iRed = oImgData[ iOffset ];
+					const iGreen = oImgData[ iOffset + 1 ];
+					const iBlue = oImgData[ iOffset + 2 ];
+					const iAlpha = oImgData[ iOffset + 3 ];
+					let iCharIdx;
+
+					let fBrightness;
+
+					fBrightness = ( 0.3 * iRed + 0.59 * iGreen + 0.11 * iBlue ) / 255;
+					// fBrightness = (0.3*iRed + 0.5*iGreen + 0.3*iBlue) / 255;
+
+					if ( iAlpha == 0 ) {
+
+						// should calculate alpha instead, but quick hack :)
+						//fBrightness *= (iAlpha / 255);
+						fBrightness = 1;
+
+					}
+
+					iCharIdx = Math.floor( ( 1 - fBrightness ) * ( aCharList.length - 1 ) );
+
+					if ( bInvert ) {
+
+						iCharIdx = aCharList.length - iCharIdx - 1;
+
+					}
+
+					// good for debugging
+					//fBrightness = Math.floor(fBrightness * 10);
+					//strThisChar = fBrightness;
+
+					let strThisChar = aCharList[ iCharIdx ];
+
+					if ( strThisChar === undefined || strThisChar == ' ' )
+						strThisChar = '&nbsp;';
+
+					if ( bColor ) {
+
+						strChars += '<span style=\''
+							+ 'color:rgb(' + iRed + ',' + iGreen + ',' + iBlue + ');'
+							+ ( bBlock ? 'background-color:rgb(' + iRed + ',' + iGreen + ',' + iBlue + ');' : '' )
+							+ ( bAlpha ? 'opacity:' + ( iAlpha / 255 ) + ';' : '' )
+							+ '\'>' + strThisChar + '</span>';
+
+					} else {
+
+						strChars += strThisChar;
+
+					}
+
+				}
+
+				strChars += '<br/>';
+
+			}
+
+			oAscii.innerHTML = '<tr><td>' + strChars + '</td></tr>';
+
+			// console.timeEnd('rendering');
+
+			// return oAscii;
+
+		}
+
+	}
+
+}
+
+export { AsciiEffect };

+ 553 - 0
public/archive/static/js/jsm/effects/OutlineEffect.js

@@ -0,0 +1,553 @@
+import {
+	BackSide,
+	Color,
+	ShaderMaterial,
+	UniformsLib,
+	UniformsUtils
+} from 'three';
+
+/**
+ * Reference: https://en.wikipedia.org/wiki/Cel_shading
+ *
+ * API
+ *
+ * 1. Traditional
+ *
+ * const effect = new OutlineEffect( renderer );
+ *
+ * function render() {
+ *
+ * 	effect.render( scene, camera );
+ *
+ * }
+ *
+ * 2. VR compatible
+ *
+ * const effect = new OutlineEffect( renderer );
+ * let renderingOutline = false;
+ *
+ * scene.onAfterRender = function () {
+ *
+ * 	if ( renderingOutline ) return;
+ *
+ * 	renderingOutline = true;
+ *
+ * 	effect.renderOutline( scene, camera );
+ *
+ * 	renderingOutline = false;
+ *
+ * };
+ *
+ * function render() {
+ *
+ * 	renderer.render( scene, camera );
+ *
+ * }
+ *
+ * // How to set default outline parameters
+ * new OutlineEffect( renderer, {
+ * 	defaultThickness: 0.01,
+ * 	defaultColor: [ 0, 0, 0 ],
+ * 	defaultAlpha: 0.8,
+ * 	defaultKeepAlive: true // keeps outline material in cache even if material is removed from scene
+ * } );
+ *
+ * // How to set outline parameters for each material
+ * material.userData.outlineParameters = {
+ * 	thickness: 0.01,
+ * 	color: [ 0, 0, 0 ],
+ * 	alpha: 0.8,
+ * 	visible: true,
+ * 	keepAlive: true
+ * };
+ */
+
+class OutlineEffect {
+
+	constructor( renderer, parameters = {} ) {
+
+		this.enabled = true;
+
+		const defaultThickness = parameters.defaultThickness !== undefined ? parameters.defaultThickness : 0.003;
+		const defaultColor = new Color().fromArray( parameters.defaultColor !== undefined ? parameters.defaultColor : [ 0, 0, 0 ] );
+		const defaultAlpha = parameters.defaultAlpha !== undefined ? parameters.defaultAlpha : 1.0;
+		const defaultKeepAlive = parameters.defaultKeepAlive !== undefined ? parameters.defaultKeepAlive : false;
+
+		// object.material.uuid -> outlineMaterial or
+		// object.material[ n ].uuid -> outlineMaterial
+		// save at the outline material creation and release
+		// if it's unused removeThresholdCount frames
+		// unless keepAlive is true.
+		const cache = {};
+
+		const removeThresholdCount = 60;
+
+		// outlineMaterial.uuid -> object.material or
+		// outlineMaterial.uuid -> object.material[ n ]
+		// save before render and release after render.
+		const originalMaterials = {};
+
+		// object.uuid -> originalOnBeforeRender
+		// save before render and release after render.
+		const originalOnBeforeRenders = {};
+
+		//this.cache = cache;  // for debug
+
+		const uniformsOutline = {
+			outlineThickness: { value: defaultThickness },
+			outlineColor: { value: defaultColor },
+			outlineAlpha: { value: defaultAlpha }
+		};
+
+		const vertexShader = [
+			'#include <common>',
+			'#include <uv_pars_vertex>',
+			'#include <displacementmap_pars_vertex>',
+			'#include <fog_pars_vertex>',
+			'#include <morphtarget_pars_vertex>',
+			'#include <skinning_pars_vertex>',
+			'#include <logdepthbuf_pars_vertex>',
+			'#include <clipping_planes_pars_vertex>',
+
+			'uniform float outlineThickness;',
+
+			'vec4 calculateOutline( vec4 pos, vec3 normal, vec4 skinned ) {',
+			'	float thickness = outlineThickness;',
+			'	const float ratio = 1.0;', // TODO: support outline thickness ratio for each vertex
+			'	vec4 pos2 = projectionMatrix * modelViewMatrix * vec4( skinned.xyz + normal, 1.0 );',
+			// NOTE: subtract pos2 from pos because BackSide objectNormal is negative
+			'	vec4 norm = normalize( pos - pos2 );',
+			'	return pos + norm * thickness * pos.w * ratio;',
+			'}',
+
+			'void main() {',
+
+			'	#include <uv_vertex>',
+
+			'	#include <beginnormal_vertex>',
+			'	#include <morphnormal_vertex>',
+			'	#include <skinbase_vertex>',
+			'	#include <skinnormal_vertex>',
+
+			'	#include <begin_vertex>',
+			'	#include <morphtarget_vertex>',
+			'	#include <skinning_vertex>',
+			'	#include <displacementmap_vertex>',
+			'	#include <project_vertex>',
+
+			'	vec3 outlineNormal = - objectNormal;', // the outline material is always rendered with BackSide
+
+			'	gl_Position = calculateOutline( gl_Position, outlineNormal, vec4( transformed, 1.0 ) );',
+
+			'	#include <logdepthbuf_vertex>',
+			'	#include <clipping_planes_vertex>',
+			'	#include <fog_vertex>',
+
+			'}',
+
+		].join( '\n' );
+
+		const fragmentShader = [
+
+			'#include <common>',
+			'#include <fog_pars_fragment>',
+			'#include <logdepthbuf_pars_fragment>',
+			'#include <clipping_planes_pars_fragment>',
+
+			'uniform vec3 outlineColor;',
+			'uniform float outlineAlpha;',
+
+			'void main() {',
+
+			'	#include <clipping_planes_fragment>',
+			'	#include <logdepthbuf_fragment>',
+
+			'	gl_FragColor = vec4( outlineColor, outlineAlpha );',
+
+			'	#include <tonemapping_fragment>',
+			'	#include <encodings_fragment>',
+			'	#include <fog_fragment>',
+			'	#include <premultiplied_alpha_fragment>',
+
+			'}'
+
+		].join( '\n' );
+
+		function createMaterial() {
+
+			return new ShaderMaterial( {
+				type: 'OutlineEffect',
+				uniforms: UniformsUtils.merge( [
+					UniformsLib[ 'fog' ],
+					UniformsLib[ 'displacementmap' ],
+					uniformsOutline
+				] ),
+				vertexShader: vertexShader,
+				fragmentShader: fragmentShader,
+				side: BackSide
+			} );
+
+		}
+
+		function getOutlineMaterialFromCache( originalMaterial ) {
+
+			let data = cache[ originalMaterial.uuid ];
+
+			if ( data === undefined ) {
+
+				data = {
+					material: createMaterial(),
+					used: true,
+					keepAlive: defaultKeepAlive,
+					count: 0
+				};
+
+				cache[ originalMaterial.uuid ] = data;
+
+			}
+
+			data.used = true;
+
+			return data.material;
+
+		}
+
+		function getOutlineMaterial( originalMaterial ) {
+
+			const outlineMaterial = getOutlineMaterialFromCache( originalMaterial );
+
+			originalMaterials[ outlineMaterial.uuid ] = originalMaterial;
+
+			updateOutlineMaterial( outlineMaterial, originalMaterial );
+
+			return outlineMaterial;
+
+		}
+
+		function isCompatible( object ) {
+
+			const geometry = object.geometry;
+			let hasNormals = false;
+
+			if ( object.geometry !== undefined ) {
+
+				if ( geometry.isBufferGeometry ) {
+
+					hasNormals = geometry.attributes.normal !== undefined;
+
+				} else {
+
+					hasNormals = true; // the renderer always produces a normal attribute for Geometry
+
+				}
+
+			}
+
+			return ( object.isMesh === true && object.material !== undefined && hasNormals === true );
+
+		}
+
+		function setOutlineMaterial( object ) {
+
+			if ( isCompatible( object ) === false ) return;
+
+			if ( Array.isArray( object.material ) ) {
+
+				for ( let i = 0, il = object.material.length; i < il; i ++ ) {
+
+					object.material[ i ] = getOutlineMaterial( object.material[ i ] );
+
+				}
+
+			} else {
+
+				object.material = getOutlineMaterial( object.material );
+
+			}
+
+			originalOnBeforeRenders[ object.uuid ] = object.onBeforeRender;
+			object.onBeforeRender = onBeforeRender;
+
+		}
+
+		function restoreOriginalMaterial( object ) {
+
+			if ( isCompatible( object ) === false ) return;
+
+			if ( Array.isArray( object.material ) ) {
+
+				for ( let i = 0, il = object.material.length; i < il; i ++ ) {
+
+					object.material[ i ] = originalMaterials[ object.material[ i ].uuid ];
+
+				}
+
+			} else {
+
+				object.material = originalMaterials[ object.material.uuid ];
+
+			}
+
+			object.onBeforeRender = originalOnBeforeRenders[ object.uuid ];
+
+		}
+
+		function onBeforeRender( renderer, scene, camera, geometry, material ) {
+
+			const originalMaterial = originalMaterials[ material.uuid ];
+
+			// just in case
+			if ( originalMaterial === undefined ) return;
+
+			updateUniforms( material, originalMaterial );
+
+		}
+
+		function updateUniforms( material, originalMaterial ) {
+
+			const outlineParameters = originalMaterial.userData.outlineParameters;
+
+			material.uniforms.outlineAlpha.value = originalMaterial.opacity;
+
+			if ( outlineParameters !== undefined ) {
+
+				if ( outlineParameters.thickness !== undefined ) material.uniforms.outlineThickness.value = outlineParameters.thickness;
+				if ( outlineParameters.color !== undefined ) material.uniforms.outlineColor.value.fromArray( outlineParameters.color );
+				if ( outlineParameters.alpha !== undefined ) material.uniforms.outlineAlpha.value = outlineParameters.alpha;
+
+			}
+
+			if ( originalMaterial.displacementMap ) {
+
+				material.uniforms.displacementMap.value = originalMaterial.displacementMap;
+				material.uniforms.displacementScale.value = originalMaterial.displacementScale;
+				material.uniforms.displacementBias.value = originalMaterial.displacementBias;
+
+			}
+
+		}
+
+		function updateOutlineMaterial( material, originalMaterial ) {
+
+			if ( material.name === 'invisible' ) return;
+
+			const outlineParameters = originalMaterial.userData.outlineParameters;
+
+			material.fog = originalMaterial.fog;
+			material.toneMapped = originalMaterial.toneMapped;
+			material.premultipliedAlpha = originalMaterial.premultipliedAlpha;
+			material.displacementMap = originalMaterial.displacementMap;
+
+			if ( outlineParameters !== undefined ) {
+
+				if ( originalMaterial.visible === false ) {
+
+					material.visible = false;
+
+				} else {
+
+					material.visible = ( outlineParameters.visible !== undefined ) ? outlineParameters.visible : true;
+
+				}
+
+				material.transparent = ( outlineParameters.alpha !== undefined && outlineParameters.alpha < 1.0 ) ? true : originalMaterial.transparent;
+
+				if ( outlineParameters.keepAlive !== undefined ) cache[ originalMaterial.uuid ].keepAlive = outlineParameters.keepAlive;
+
+			} else {
+
+				material.transparent = originalMaterial.transparent;
+				material.visible = originalMaterial.visible;
+
+			}
+
+			if ( originalMaterial.wireframe === true || originalMaterial.depthTest === false ) material.visible = false;
+
+			if ( originalMaterial.clippingPlanes ) {
+
+				material.clipping = true;
+
+				material.clippingPlanes = originalMaterial.clippingPlanes;
+				material.clipIntersection = originalMaterial.clipIntersection;
+				material.clipShadows = originalMaterial.clipShadows;
+
+			}
+
+			material.version = originalMaterial.version; // update outline material if necessary
+
+		}
+
+		function cleanupCache() {
+
+			let keys;
+
+			// clear originialMaterials
+			keys = Object.keys( originalMaterials );
+
+			for ( let i = 0, il = keys.length; i < il; i ++ ) {
+
+				originalMaterials[ keys[ i ] ] = undefined;
+
+			}
+
+			// clear originalOnBeforeRenders
+			keys = Object.keys( originalOnBeforeRenders );
+
+			for ( let i = 0, il = keys.length; i < il; i ++ ) {
+
+				originalOnBeforeRenders[ keys[ i ] ] = undefined;
+
+			}
+
+			// remove unused outlineMaterial from cache
+			keys = Object.keys( cache );
+
+			for ( let i = 0, il = keys.length; i < il; i ++ ) {
+
+				const key = keys[ i ];
+
+				if ( cache[ key ].used === false ) {
+
+					cache[ key ].count ++;
+
+					if ( cache[ key ].keepAlive === false && cache[ key ].count > removeThresholdCount ) {
+
+						delete cache[ key ];
+
+					}
+
+				} else {
+
+					cache[ key ].used = false;
+					cache[ key ].count = 0;
+
+				}
+
+			}
+
+		}
+
+		this.render = function ( scene, camera ) {
+
+			if ( this.enabled === false ) {
+
+				renderer.render( scene, camera );
+				return;
+
+			}
+
+			const currentAutoClear = renderer.autoClear;
+			renderer.autoClear = this.autoClear;
+
+			renderer.render( scene, camera );
+
+			renderer.autoClear = currentAutoClear;
+
+			this.renderOutline( scene, camera );
+
+		};
+
+		this.renderOutline = function ( scene, camera ) {
+
+			const currentAutoClear = renderer.autoClear;
+			const currentSceneAutoUpdate = scene.matrixWorldAutoUpdate;
+			const currentSceneBackground = scene.background;
+			const currentShadowMapEnabled = renderer.shadowMap.enabled;
+
+			scene.matrixWorldAutoUpdate = false;
+			scene.background = null;
+			renderer.autoClear = false;
+			renderer.shadowMap.enabled = false;
+
+			scene.traverse( setOutlineMaterial );
+
+			renderer.render( scene, camera );
+
+			scene.traverse( restoreOriginalMaterial );
+
+			cleanupCache();
+
+			scene.matrixWorldAutoUpdate = currentSceneAutoUpdate;
+			scene.background = currentSceneBackground;
+			renderer.autoClear = currentAutoClear;
+			renderer.shadowMap.enabled = currentShadowMapEnabled;
+
+		};
+
+		/*
+		 * See #9918
+		 *
+		 * The following property copies and wrapper methods enable
+		 * OutlineEffect to be called from other *Effect, like
+		 *
+		 * effect = new StereoEffect( new OutlineEffect( renderer ) );
+		 *
+		 * function render () {
+		 *
+	 	 * 	effect.render( scene, camera );
+		 *
+		 * }
+		 */
+		this.autoClear = renderer.autoClear;
+		this.domElement = renderer.domElement;
+		this.shadowMap = renderer.shadowMap;
+
+		this.clear = function ( color, depth, stencil ) {
+
+			renderer.clear( color, depth, stencil );
+
+		};
+
+		this.getPixelRatio = function () {
+
+			return renderer.getPixelRatio();
+
+		};
+
+		this.setPixelRatio = function ( value ) {
+
+			renderer.setPixelRatio( value );
+
+		};
+
+		this.getSize = function ( target ) {
+
+			return renderer.getSize( target );
+
+		};
+
+		this.setSize = function ( width, height, updateStyle ) {
+
+			renderer.setSize( width, height, updateStyle );
+
+		};
+
+		this.setViewport = function ( x, y, width, height ) {
+
+			renderer.setViewport( x, y, width, height );
+
+		};
+
+		this.setScissor = function ( x, y, width, height ) {
+
+			renderer.setScissor( x, y, width, height );
+
+		};
+
+		this.setScissorTest = function ( boolean ) {
+
+			renderer.setScissorTest( boolean );
+
+		};
+
+		this.setRenderTarget = function ( renderTarget ) {
+
+			renderer.setRenderTarget( renderTarget );
+
+		};
+
+	}
+
+}
+
+export { OutlineEffect };

+ 116 - 0
public/archive/static/js/jsm/effects/ParallaxBarrierEffect.js

@@ -0,0 +1,116 @@
+import {
+	LinearFilter,
+	Mesh,
+	NearestFilter,
+	OrthographicCamera,
+	PlaneGeometry,
+	RGBAFormat,
+	Scene,
+	ShaderMaterial,
+	StereoCamera,
+	WebGLRenderTarget
+} from 'three';
+
+class ParallaxBarrierEffect {
+
+	constructor( renderer ) {
+
+		const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+
+		const _scene = new Scene();
+
+		const _stereo = new StereoCamera();
+
+		const _params = { minFilter: LinearFilter, magFilter: NearestFilter, format: RGBAFormat };
+
+		const _renderTargetL = new WebGLRenderTarget( 512, 512, _params );
+		const _renderTargetR = new WebGLRenderTarget( 512, 512, _params );
+
+		const _material = new ShaderMaterial( {
+
+			uniforms: {
+
+				'mapLeft': { value: _renderTargetL.texture },
+				'mapRight': { value: _renderTargetR.texture }
+
+			},
+
+			vertexShader: [
+
+				'varying vec2 vUv;',
+
+				'void main() {',
+
+				'	vUv = vec2( uv.x, uv.y );',
+				'	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
+
+				'}'
+
+			].join( '\n' ),
+
+			fragmentShader: [
+
+				'uniform sampler2D mapLeft;',
+				'uniform sampler2D mapRight;',
+				'varying vec2 vUv;',
+
+				'void main() {',
+
+				'	vec2 uv = vUv;',
+
+				'	if ( ( mod( gl_FragCoord.y, 2.0 ) ) > 1.00 ) {',
+
+				'		gl_FragColor = texture2D( mapLeft, uv );',
+
+				'	} else {',
+
+				'		gl_FragColor = texture2D( mapRight, uv );',
+
+				'	}',
+
+				'}'
+
+			].join( '\n' )
+
+		} );
+
+		const mesh = new Mesh( new PlaneGeometry( 2, 2 ), _material );
+		_scene.add( mesh );
+
+		this.setSize = function ( width, height ) {
+
+			renderer.setSize( width, height );
+
+			const pixelRatio = renderer.getPixelRatio();
+
+			_renderTargetL.setSize( width * pixelRatio, height * pixelRatio );
+			_renderTargetR.setSize( width * pixelRatio, height * pixelRatio );
+
+		};
+
+		this.render = function ( scene, camera ) {
+
+			if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();
+
+			if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();
+
+			_stereo.update( camera );
+
+			renderer.setRenderTarget( _renderTargetL );
+			renderer.clear();
+			renderer.render( scene, _stereo.cameraL );
+
+			renderer.setRenderTarget( _renderTargetR );
+			renderer.clear();
+			renderer.render( scene, _stereo.cameraR );
+
+			renderer.setRenderTarget( null );
+			renderer.render( _scene, _camera );
+
+		};
+
+	}
+
+}
+
+export { ParallaxBarrierEffect };

+ 153 - 0
public/archive/static/js/jsm/effects/PeppersGhostEffect.js

@@ -0,0 +1,153 @@
+import {
+	PerspectiveCamera,
+	Quaternion,
+	Vector3
+} from 'three';
+
+/**
+ * peppers ghost effect based on http://www.instructables.com/id/Reflective-Prism/?ALLSTEPS
+ */
+
+class PeppersGhostEffect {
+
+	constructor( renderer ) {
+
+		const scope = this;
+
+		scope.cameraDistance = 15;
+		scope.reflectFromAbove = false;
+
+		// Internals
+		let _halfWidth, _width, _height;
+
+		const _cameraF = new PerspectiveCamera(); //front
+		const _cameraB = new PerspectiveCamera(); //back
+		const _cameraL = new PerspectiveCamera(); //left
+		const _cameraR = new PerspectiveCamera(); //right
+
+		const _position = new Vector3();
+		const _quaternion = new Quaternion();
+		const _scale = new Vector3();
+
+		// Initialization
+		renderer.autoClear = false;
+
+		this.setSize = function ( width, height ) {
+
+			_halfWidth = width / 2;
+			if ( width < height ) {
+
+				_width = width / 3;
+				_height = width / 3;
+
+			} else {
+
+				_width = height / 3;
+				_height = height / 3;
+
+			}
+
+			renderer.setSize( width, height );
+
+		};
+
+		this.render = function ( scene, camera ) {
+
+			if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();
+
+			if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();
+
+			camera.matrixWorld.decompose( _position, _quaternion, _scale );
+
+			// front
+			_cameraF.position.copy( _position );
+			_cameraF.quaternion.copy( _quaternion );
+			_cameraF.translateZ( scope.cameraDistance );
+			_cameraF.lookAt( scene.position );
+
+			// back
+			_cameraB.position.copy( _position );
+			_cameraB.quaternion.copy( _quaternion );
+			_cameraB.translateZ( - ( scope.cameraDistance ) );
+			_cameraB.lookAt( scene.position );
+			_cameraB.rotation.z += 180 * ( Math.PI / 180 );
+
+			// left
+			_cameraL.position.copy( _position );
+			_cameraL.quaternion.copy( _quaternion );
+			_cameraL.translateX( - ( scope.cameraDistance ) );
+			_cameraL.lookAt( scene.position );
+			_cameraL.rotation.x += 90 * ( Math.PI / 180 );
+
+			// right
+			_cameraR.position.copy( _position );
+			_cameraR.quaternion.copy( _quaternion );
+			_cameraR.translateX( scope.cameraDistance );
+			_cameraR.lookAt( scene.position );
+			_cameraR.rotation.x += 90 * ( Math.PI / 180 );
+
+
+			renderer.clear();
+			renderer.setScissorTest( true );
+
+			renderer.setScissor( _halfWidth - ( _width / 2 ), ( _height * 2 ), _width, _height );
+			renderer.setViewport( _halfWidth - ( _width / 2 ), ( _height * 2 ), _width, _height );
+
+			if ( scope.reflectFromAbove ) {
+
+				renderer.render( scene, _cameraB );
+
+			} else {
+
+				renderer.render( scene, _cameraF );
+
+			}
+
+			renderer.setScissor( _halfWidth - ( _width / 2 ), 0, _width, _height );
+			renderer.setViewport( _halfWidth - ( _width / 2 ), 0, _width, _height );
+
+			if ( scope.reflectFromAbove ) {
+
+				renderer.render( scene, _cameraF );
+
+			} else {
+
+				renderer.render( scene, _cameraB );
+
+			}
+
+			renderer.setScissor( _halfWidth - ( _width / 2 ) - _width, _height, _width, _height );
+			renderer.setViewport( _halfWidth - ( _width / 2 ) - _width, _height, _width, _height );
+
+			if ( scope.reflectFromAbove ) {
+
+				renderer.render( scene, _cameraR );
+
+			} else {
+
+				renderer.render( scene, _cameraL );
+
+			}
+
+			renderer.setScissor( _halfWidth + ( _width / 2 ), _height, _width, _height );
+			renderer.setViewport( _halfWidth + ( _width / 2 ), _height, _width, _height );
+
+			if ( scope.reflectFromAbove ) {
+
+				renderer.render( scene, _cameraL );
+
+			} else {
+
+				renderer.render( scene, _cameraR );
+
+			}
+
+			renderer.setScissorTest( false );
+
+		};
+
+	}
+
+}
+
+export { PeppersGhostEffect };

+ 55 - 0
public/archive/static/js/jsm/effects/StereoEffect.js

@@ -0,0 +1,55 @@
+import {
+	StereoCamera,
+	Vector2
+} from 'three';
+
+class StereoEffect {
+
+	constructor( renderer ) {
+
+		const _stereo = new StereoCamera();
+		_stereo.aspect = 0.5;
+		const size = new Vector2();
+
+		this.setEyeSeparation = function ( eyeSep ) {
+
+			_stereo.eyeSep = eyeSep;
+
+		};
+
+		this.setSize = function ( width, height ) {
+
+			renderer.setSize( width, height );
+
+		};
+
+		this.render = function ( scene, camera ) {
+
+			if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();
+
+			if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();
+
+			_stereo.update( camera );
+
+			renderer.getSize( size );
+
+			if ( renderer.autoClear ) renderer.clear();
+			renderer.setScissorTest( true );
+
+			renderer.setScissor( 0, 0, size.width / 2, size.height );
+			renderer.setViewport( 0, 0, size.width / 2, size.height );
+			renderer.render( scene, _stereo.cameraL );
+
+			renderer.setScissor( size.width / 2, 0, size.width / 2, size.height );
+			renderer.setViewport( size.width / 2, 0, size.width / 2, size.height );
+			renderer.render( scene, _stereo.cameraR );
+
+			renderer.setScissorTest( false );
+
+		};
+
+	}
+
+}
+
+export { StereoEffect };

+ 52 - 0
public/archive/static/js/jsm/environments/DebugEnvironment.js

@@ -0,0 +1,52 @@
+import {
+	BackSide,
+	BoxGeometry,
+	Mesh,
+	MeshLambertMaterial,
+	MeshStandardMaterial,
+	PointLight,
+	Scene,
+} from 'three';
+
+class DebugEnvironment extends Scene {
+
+	constructor() {
+
+		super();
+
+		const geometry = new BoxGeometry();
+		geometry.deleteAttribute( 'uv' );
+		const roomMaterial = new MeshStandardMaterial( { metalness: 0, side: BackSide } );
+		const room = new Mesh( geometry, roomMaterial );
+		room.scale.setScalar( 10 );
+		this.add( room );
+
+		const mainLight = new PointLight( 0xffffff, 50, 0, 2 );
+		this.add( mainLight );
+
+		const material1 = new MeshLambertMaterial( { color: 0xff0000, emissive: 0xffffff, emissiveIntensity: 10 } );
+
+		const light1 = new Mesh( geometry, material1 );
+		light1.position.set( - 5, 2, 0 );
+		light1.scale.set( 0.1, 1, 1 );
+		this.add( light1 );
+
+		const material2 = new MeshLambertMaterial( { color: 0x00ff00, emissive: 0xffffff, emissiveIntensity: 10 } );
+
+		const light2 = new Mesh( geometry, material2 );
+		light2.position.set( 0, 5, 0 );
+		light2.scale.set( 1, 0.1, 1 );
+		this.add( light2 );
+
+		const material3 = new MeshLambertMaterial( { color: 0x0000ff, emissive: 0xffffff, emissiveIntensity: 10 } );
+
+		const light3 = new Mesh( geometry, material3 );
+		light3.position.set( 2, 1, 5 );
+		light3.scale.set( 1.5, 2, 0.1 );
+		this.add( light3 );
+
+	}
+
+}
+
+export { DebugEnvironment };

+ 121 - 0
public/archive/static/js/jsm/environments/RoomEnvironment.js

@@ -0,0 +1,121 @@
+/**
+ * https://github.com/google/model-viewer/blob/master/packages/model-viewer/src/three-components/EnvironmentScene.ts
+ */
+
+import {
+ 	BackSide,
+ 	BoxGeometry,
+ 	Mesh,
+	MeshBasicMaterial,
+ 	MeshStandardMaterial,
+ 	PointLight,
+ 	Scene,
+} from 'three';
+
+class RoomEnvironment extends Scene {
+
+	constructor() {
+
+		super();
+
+		const geometry = new BoxGeometry();
+		geometry.deleteAttribute( 'uv' );
+
+		const roomMaterial = new MeshStandardMaterial( { side: BackSide } );
+		const boxMaterial = new MeshStandardMaterial();
+
+		const mainLight = new PointLight( 0xffffff, 5.0, 28, 2 );
+		mainLight.position.set( 0.418, 16.199, 0.300 );
+		this.add( mainLight );
+
+		const room = new Mesh( geometry, roomMaterial );
+		room.position.set( - 0.757, 13.219, 0.717 );
+		room.scale.set( 31.713, 28.305, 28.591 );
+		this.add( room );
+
+		const box1 = new Mesh( geometry, boxMaterial );
+		box1.position.set( - 10.906, 2.009, 1.846 );
+		box1.rotation.set( 0, - 0.195, 0 );
+		box1.scale.set( 2.328, 7.905, 4.651 );
+		this.add( box1 );
+
+		const box2 = new Mesh( geometry, boxMaterial );
+		box2.position.set( - 5.607, - 0.754, - 0.758 );
+		box2.rotation.set( 0, 0.994, 0 );
+		box2.scale.set( 1.970, 1.534, 3.955 );
+		this.add( box2 );
+
+		const box3 = new Mesh( geometry, boxMaterial );
+		box3.position.set( 6.167, 0.857, 7.803 );
+		box3.rotation.set( 0, 0.561, 0 );
+		box3.scale.set( 3.927, 6.285, 3.687 );
+		this.add( box3 );
+
+		const box4 = new Mesh( geometry, boxMaterial );
+		box4.position.set( - 2.017, 0.018, 6.124 );
+		box4.rotation.set( 0, 0.333, 0 );
+		box4.scale.set( 2.002, 4.566, 2.064 );
+		this.add( box4 );
+
+		const box5 = new Mesh( geometry, boxMaterial );
+		box5.position.set( 2.291, - 0.756, - 2.621 );
+		box5.rotation.set( 0, - 0.286, 0 );
+		box5.scale.set( 1.546, 1.552, 1.496 );
+		this.add( box5 );
+
+		const box6 = new Mesh( geometry, boxMaterial );
+		box6.position.set( - 2.193, - 0.369, - 5.547 );
+		box6.rotation.set( 0, 0.516, 0 );
+		box6.scale.set( 3.875, 3.487, 2.986 );
+		this.add( box6 );
+
+
+		// -x right
+		const light1 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
+		light1.position.set( - 16.116, 14.37, 8.208 );
+		light1.scale.set( 0.1, 2.428, 2.739 );
+		this.add( light1 );
+
+		// -x left
+		const light2 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
+		light2.position.set( - 16.109, 18.021, - 8.207 );
+		light2.scale.set( 0.1, 2.425, 2.751 );
+		this.add( light2 );
+
+		// +x
+		const light3 = new Mesh( geometry, createAreaLightMaterial( 17 ) );
+		light3.position.set( 14.904, 12.198, - 1.832 );
+		light3.scale.set( 0.15, 4.265, 6.331 );
+		this.add( light3 );
+
+		// +z
+		const light4 = new Mesh( geometry, createAreaLightMaterial( 43 ) );
+		light4.position.set( - 0.462, 8.89, 14.520 );
+		light4.scale.set( 4.38, 5.441, 0.088 );
+		this.add( light4 );
+
+		// -z
+		const light5 = new Mesh( geometry, createAreaLightMaterial( 20 ) );
+		light5.position.set( 3.235, 11.486, - 12.541 );
+		light5.scale.set( 2.5, 2.0, 0.1 );
+		this.add( light5 );
+
+		// +y
+		const light6 = new Mesh( geometry, createAreaLightMaterial( 100 ) );
+		light6.position.set( 0.0, 20.0, 0.0 );
+		light6.scale.set( 1.0, 0.1, 1.0 );
+		this.add( light6 );
+
+	}
+
+}
+
+function createAreaLightMaterial( intensity ) {
+
+	const material = new MeshBasicMaterial();
+	material.color.setScalar( intensity );
+	return material;
+
+}
+
+export { RoomEnvironment };

+ 713 - 0
public/archive/static/js/jsm/exporters/ColladaExporter.js

@@ -0,0 +1,713 @@
+import {
+	Color,
+	DoubleSide,
+	Matrix4,
+	MeshBasicMaterial
+} from 'three';
+
+/**
+ * https://github.com/gkjohnson/collada-exporter-js
+ *
+ * Usage:
+ *  const exporter = new ColladaExporter();
+ *
+ *  const data = exporter.parse(mesh);
+ *
+ * Format Definition:
+ *  https://www.khronos.org/collada/
+ */
+
+class ColladaExporter {
+
+	parse( object, onDone, options = {} ) {
+
+		options = Object.assign( {
+			version: '1.4.1',
+			author: null,
+			textureDirectory: '',
+			upAxis: 'Y_UP',
+			unitName: null,
+			unitMeter: null,
+		}, options );
+
+		if ( options.upAxis.match( /^[XYZ]_UP$/ ) === null ) {
+
+			console.error( 'ColladaExporter: Invalid upAxis: valid values are X_UP, Y_UP or Z_UP.' );
+			return null;
+
+		}
+
+		if ( options.unitName !== null && options.unitMeter === null ) {
+
+			console.error( 'ColladaExporter: unitMeter needs to be specified if unitName is specified.' );
+			return null;
+
+		}
+
+		if ( options.unitMeter !== null && options.unitName === null ) {
+
+			console.error( 'ColladaExporter: unitName needs to be specified if unitMeter is specified.' );
+			return null;
+
+		}
+
+		if ( options.textureDirectory !== '' ) {
+
+			options.textureDirectory = `${ options.textureDirectory }/`
+				.replace( /\\/g, '/' )
+				.replace( /\/+/g, '/' );
+
+		}
+
+		const version = options.version;
+
+		if ( version !== '1.4.1' && version !== '1.5.0' ) {
+
+			console.warn( `ColladaExporter : Version ${ version } not supported for export. Only 1.4.1 and 1.5.0.` );
+			return null;
+
+		}
+
+		// Convert the urdf xml into a well-formatted, indented format
+		function format( urdf ) {
+
+			const IS_END_TAG = /^<\//;
+			const IS_SELF_CLOSING = /(\?>$)|(\/>$)/;
+			const HAS_TEXT = /<[^>]+>[^<]*<\/[^<]+>/;
+
+			const pad = ( ch, num ) => ( num > 0 ? ch + pad( ch, num - 1 ) : '' );
+
+			let tagnum = 0;
+
+			return urdf
+				.match( /(<[^>]+>[^<]+<\/[^<]+>)|(<[^>]+>)/g )
+				.map( tag => {
+
+					if ( ! HAS_TEXT.test( tag ) && ! IS_SELF_CLOSING.test( tag ) && IS_END_TAG.test( tag ) ) {
+
+						tagnum --;
+
+					}
+
+					const res = `${ pad( '  ', tagnum ) }${ tag }`;
+
+					if ( ! HAS_TEXT.test( tag ) && ! IS_SELF_CLOSING.test( tag ) && ! IS_END_TAG.test( tag ) ) {
+
+						tagnum ++;
+
+					}
+
+					return res;
+
+				} )
+				.join( '\n' );
+
+		}
+
+		// Convert an image into a png format for saving
+		function base64ToBuffer( str ) {
+
+			const b = atob( str );
+			const buf = new Uint8Array( b.length );
+
+			for ( let i = 0, l = buf.length; i < l; i ++ ) {
+
+				buf[ i ] = b.charCodeAt( i );
+
+			}
+
+			return buf;
+
+		}
+
+		let canvas, ctx;
+
+		function imageToData( image, ext ) {
+
+			canvas = canvas || document.createElement( 'canvas' );
+			ctx = ctx || canvas.getContext( '2d' );
+
+			canvas.width = image.width;
+			canvas.height = image.height;
+
+			ctx.drawImage( image, 0, 0 );
+
+			// Get the base64 encoded data
+			const base64data = canvas
+				.toDataURL( `image/${ ext }`, 1 )
+				.replace( /^data:image\/(png|jpg);base64,/, '' );
+
+			// Convert to a uint8 array
+			return base64ToBuffer( base64data );
+
+		}
+
+		// gets the attribute array. Generate a new array if the attribute is interleaved
+		const getFuncs = [ 'getX', 'getY', 'getZ', 'getW' ];
+		const tempColor = new Color();
+
+		function attrBufferToArray( attr, isColor = false ) {
+
+			if ( isColor ) {
+
+				// convert the colors to srgb before export
+				// colors are always written as floats
+				const arr = new Float32Array( attr.count * 3 );
+				for ( let i = 0, l = attr.count; i < l; i ++ ) {
+
+					tempColor
+						.fromBufferAttribute( attr, i )
+						.convertLinearToSRGB();
+
+					arr[ 3 * i + 0 ] = tempColor.r;
+					arr[ 3 * i + 1 ] = tempColor.g;
+					arr[ 3 * i + 2 ] = tempColor.b;
+
+				}
+
+				return arr;
+
+			} else if ( attr.isInterleavedBufferAttribute ) {
+
+				// use the typed array constructor to save on memory
+				const arr = new attr.array.constructor( attr.count * attr.itemSize );
+				const size = attr.itemSize;
+
+				for ( let i = 0, l = attr.count; i < l; i ++ ) {
+
+					for ( let j = 0; j < size; j ++ ) {
+
+						arr[ i * size + j ] = attr[ getFuncs[ j ] ]( i );
+
+					}
+
+				}
+
+				return arr;
+
+			} else {
+
+				return attr.array;
+
+			}
+
+		}
+
+		// Returns an array of the same type starting at the `st` index,
+		// and `ct` length
+		function subArray( arr, st, ct ) {
+
+			if ( Array.isArray( arr ) ) return arr.slice( st, st + ct );
+			else return new arr.constructor( arr.buffer, st * arr.BYTES_PER_ELEMENT, ct );
+
+		}
+
+		// Returns the string for a geometry's attribute
+		function getAttribute( attr, name, params, type, isColor = false ) {
+
+			const array = attrBufferToArray( attr, isColor );
+			const res =
+					`<source id="${ name }">` +
+
+					`<float_array id="${ name }-array" count="${ array.length }">` +
+					array.join( ' ' ) +
+					'</float_array>' +
+
+					'<technique_common>' +
+					`<accessor source="#${ name }-array" count="${ Math.floor( array.length / attr.itemSize ) }" stride="${ attr.itemSize }">` +
+
+					params.map( n => `<param name="${ n }" type="${ type }" />` ).join( '' ) +
+
+					'</accessor>' +
+					'</technique_common>' +
+					'</source>';
+
+			return res;
+
+		}
+
+		// Returns the string for a node's transform information
+		let transMat;
+		function getTransform( o ) {
+
+			// ensure the object's matrix is up to date
+			// before saving the transform
+			o.updateMatrix();
+
+			transMat = transMat || new Matrix4();
+			transMat.copy( o.matrix );
+			transMat.transpose();
+			return `<matrix>${ transMat.toArray().join( ' ' ) }</matrix>`;
+
+		}
+
+		// Process the given piece of geometry into the geometry library
+		// Returns the mesh id
+		function processGeometry( bufferGeometry ) {
+
+			let info = geometryInfo.get( bufferGeometry );
+
+			if ( ! info ) {
+
+				const meshid = `Mesh${ libraryGeometries.length + 1 }`;
+
+				const indexCount =
+					bufferGeometry.index ?
+						bufferGeometry.index.count * bufferGeometry.index.itemSize :
+						bufferGeometry.attributes.position.count;
+
+				const groups =
+					bufferGeometry.groups != null && bufferGeometry.groups.length !== 0 ?
+						bufferGeometry.groups :
+						[ { start: 0, count: indexCount, materialIndex: 0 } ];
+
+
+				const gname = bufferGeometry.name ? ` name="${ bufferGeometry.name }"` : '';
+				let gnode = `<geometry id="${ meshid }"${ gname }><mesh>`;
+
+				// define the geometry node and the vertices for the geometry
+				const posName = `${ meshid }-position`;
+				const vertName = `${ meshid }-vertices`;
+				gnode += getAttribute( bufferGeometry.attributes.position, posName, [ 'X', 'Y', 'Z' ], 'float' );
+				gnode += `<vertices id="${ vertName }"><input semantic="POSITION" source="#${ posName }" /></vertices>`;
+
+				// NOTE: We're not optimizing the attribute arrays here, so they're all the same length and
+				// can therefore share the same triangle indices. However, MeshLab seems to have trouble opening
+				// models with attributes that share an offset.
+				// MeshLab Bug#424: https://sourceforge.net/p/meshlab/bugs/424/
+
+				// serialize normals
+				let triangleInputs = `<input semantic="VERTEX" source="#${ vertName }" offset="0" />`;
+				if ( 'normal' in bufferGeometry.attributes ) {
+
+					const normName = `${ meshid }-normal`;
+					gnode += getAttribute( bufferGeometry.attributes.normal, normName, [ 'X', 'Y', 'Z' ], 'float' );
+					triangleInputs += `<input semantic="NORMAL" source="#${ normName }" offset="0" />`;
+
+				}
+
+				// serialize uvs
+				if ( 'uv' in bufferGeometry.attributes ) {
+
+					const uvName = `${ meshid }-texcoord`;
+					gnode += getAttribute( bufferGeometry.attributes.uv, uvName, [ 'S', 'T' ], 'float' );
+					triangleInputs += `<input semantic="TEXCOORD" source="#${ uvName }" offset="0" set="0" />`;
+
+				}
+
+				// serialize lightmap uvs
+				if ( 'uv2' in bufferGeometry.attributes ) {
+
+					const uvName = `${ meshid }-texcoord2`;
+					gnode += getAttribute( bufferGeometry.attributes.uv2, uvName, [ 'S', 'T' ], 'float' );
+					triangleInputs += `<input semantic="TEXCOORD" source="#${ uvName }" offset="0" set="1" />`;
+
+				}
+
+				// serialize colors
+				if ( 'color' in bufferGeometry.attributes ) {
+
+					// colors are always written as floats
+					const colName = `${ meshid }-color`;
+					gnode += getAttribute( bufferGeometry.attributes.color, colName, [ 'R', 'G', 'B' ], 'float', true );
+					triangleInputs += `<input semantic="COLOR" source="#${ colName }" offset="0" />`;
+
+				}
+
+				let indexArray = null;
+				if ( bufferGeometry.index ) {
+
+					indexArray = attrBufferToArray( bufferGeometry.index );
+
+				} else {
+
+					indexArray = new Array( indexCount );
+					for ( let i = 0, l = indexArray.length; i < l; i ++ ) indexArray[ i ] = i;
+
+				}
+
+				for ( let i = 0, l = groups.length; i < l; i ++ ) {
+
+					const group = groups[ i ];
+					const subarr = subArray( indexArray, group.start, group.count );
+					const polycount = subarr.length / 3;
+					gnode += `<triangles material="MESH_MATERIAL_${ group.materialIndex }" count="${ polycount }">`;
+					gnode += triangleInputs;
+
+					gnode += `<p>${ subarr.join( ' ' ) }</p>`;
+					gnode += '</triangles>';
+
+				}
+
+				gnode += '</mesh></geometry>';
+
+				libraryGeometries.push( gnode );
+
+				info = { meshid: meshid, bufferGeometry: bufferGeometry };
+				geometryInfo.set( bufferGeometry, info );
+
+			}
+
+			return info;
+
+		}
+
+		// Process the given texture into the image library
+		// Returns the image library
+		function processTexture( tex ) {
+
+			let texid = imageMap.get( tex );
+			if ( texid == null ) {
+
+				texid = `image-${ libraryImages.length + 1 }`;
+
+				const ext = 'png';
+				const name = tex.name || texid;
+				let imageNode = `<image id="${ texid }" name="${ name }">`;
+
+				if ( version === '1.5.0' ) {
+
+					imageNode += `<init_from><ref>${ options.textureDirectory }${ name }.${ ext }</ref></init_from>`;
+
+				} else {
+
+					// version image node 1.4.1
+					imageNode += `<init_from>${ options.textureDirectory }${ name }.${ ext }</init_from>`;
+
+				}
+
+				imageNode += '</image>';
+
+				libraryImages.push( imageNode );
+				imageMap.set( tex, texid );
+				textures.push( {
+					directory: options.textureDirectory,
+					name,
+					ext,
+					data: imageToData( tex.image, ext ),
+					original: tex
+				} );
+
+			}
+
+			return texid;
+
+		}
+
+		// Process the given material into the material and effect libraries
+		// Returns the material id
+		function processMaterial( m ) {
+
+			let matid = materialMap.get( m );
+
+			if ( matid == null ) {
+
+				matid = `Mat${ libraryEffects.length + 1 }`;
+
+				let type = 'phong';
+
+				if ( m.isMeshLambertMaterial === true ) {
+
+					type = 'lambert';
+
+				} else if ( m.isMeshBasicMaterial === true ) {
+
+					type = 'constant';
+
+					if ( m.map !== null ) {
+
+						// The Collada spec does not support diffuse texture maps with the
+						// constant shader type.
+						// mrdoob/three.js#15469
+						console.warn( 'ColladaExporter: Texture maps not supported with MeshBasicMaterial.' );
+
+					}
+
+				}
+
+				const emissive = m.emissive ? m.emissive : new Color( 0, 0, 0 );
+				const diffuse = m.color ? m.color : new Color( 0, 0, 0 );
+				const specular = m.specular ? m.specular : new Color( 1, 1, 1 );
+				const shininess = m.shininess || 0;
+				const reflectivity = m.reflectivity || 0;
+
+				emissive.convertLinearToSRGB();
+				specular.convertLinearToSRGB();
+				diffuse.convertLinearToSRGB();
+
+				// Do not export and alpha map for the reasons mentioned in issue (#13792)
+				// in three.js alpha maps are black and white, but collada expects the alpha
+				// channel to specify the transparency
+				let transparencyNode = '';
+				if ( m.transparent === true ) {
+
+					transparencyNode +=
+						'<transparent>' +
+						(
+							m.map ?
+								'<texture texture="diffuse-sampler"></texture>' :
+								'<float>1</float>'
+						) +
+						'</transparent>';
+
+					if ( m.opacity < 1 ) {
+
+						transparencyNode += `<transparency><float>${ m.opacity }</float></transparency>`;
+
+					}
+
+				}
+
+				const techniqueNode = `<technique sid="common"><${ type }>` +
+
+					'<emission>' +
+
+					(
+						m.emissiveMap ?
+							'<texture texture="emissive-sampler" texcoord="TEXCOORD" />' :
+							`<color sid="emission">${ emissive.r } ${ emissive.g } ${ emissive.b } 1</color>`
+					) +
+
+					'</emission>' +
+
+					(
+						type !== 'constant' ?
+							'<diffuse>' +
+
+						(
+							m.map ?
+								'<texture texture="diffuse-sampler" texcoord="TEXCOORD" />' :
+								`<color sid="diffuse">${ diffuse.r } ${ diffuse.g } ${ diffuse.b } 1</color>`
+						) +
+						'</diffuse>'
+							: ''
+					) +
+
+					(
+						type !== 'constant' ?
+							'<bump>' +
+
+						(
+							m.normalMap ? '<texture texture="bump-sampler" texcoord="TEXCOORD" />' : ''
+						) +
+						'</bump>'
+							: ''
+					) +
+
+					(
+						type === 'phong' ?
+							`<specular><color sid="specular">${ specular.r } ${ specular.g } ${ specular.b } 1</color></specular>` +
+
+						'<shininess>' +
+
+						(
+							m.specularMap ?
+								'<texture texture="specular-sampler" texcoord="TEXCOORD" />' :
+								`<float sid="shininess">${ shininess }</float>`
+						) +
+
+						'</shininess>'
+							: ''
+					) +
+
+					`<reflective><color>${ diffuse.r } ${ diffuse.g } ${ diffuse.b } 1</color></reflective>` +
+
+					`<reflectivity><float>${ reflectivity }</float></reflectivity>` +
+
+					transparencyNode +
+
+					`</${ type }></technique>`;
+
+				const effectnode =
+					`<effect id="${ matid }-effect">` +
+					'<profile_COMMON>' +
+
+					(
+						m.map ?
+							'<newparam sid="diffuse-surface"><surface type="2D">' +
+							`<init_from>${ processTexture( m.map ) }</init_from>` +
+							'</surface></newparam>' +
+							'<newparam sid="diffuse-sampler"><sampler2D><source>diffuse-surface</source></sampler2D></newparam>' :
+							''
+					) +
+
+					(
+						m.specularMap ?
+							'<newparam sid="specular-surface"><surface type="2D">' +
+							`<init_from>${ processTexture( m.specularMap ) }</init_from>` +
+							'</surface></newparam>' +
+							'<newparam sid="specular-sampler"><sampler2D><source>specular-surface</source></sampler2D></newparam>' :
+							''
+					) +
+
+					(
+						m.emissiveMap ?
+							'<newparam sid="emissive-surface"><surface type="2D">' +
+							`<init_from>${ processTexture( m.emissiveMap ) }</init_from>` +
+							'</surface></newparam>' +
+							'<newparam sid="emissive-sampler"><sampler2D><source>emissive-surface</source></sampler2D></newparam>' :
+							''
+					) +
+
+					(
+						m.normalMap ?
+							'<newparam sid="bump-surface"><surface type="2D">' +
+							`<init_from>${ processTexture( m.normalMap ) }</init_from>` +
+							'</surface></newparam>' +
+							'<newparam sid="bump-sampler"><sampler2D><source>bump-surface</source></sampler2D></newparam>' :
+							''
+					) +
+
+					techniqueNode +
+
+					(
+						m.side === DoubleSide ?
+							'<extra><technique profile="THREEJS"><double_sided sid="double_sided" type="int">1</double_sided></technique></extra>' :
+							''
+					) +
+
+					'</profile_COMMON>' +
+
+					'</effect>';
+
+				const materialName = m.name ? ` name="${ m.name }"` : '';
+				const materialNode = `<material id="${ matid }"${ materialName }><instance_effect url="#${ matid }-effect" /></material>`;
+
+				libraryMaterials.push( materialNode );
+				libraryEffects.push( effectnode );
+				materialMap.set( m, matid );
+
+			}
+
+			return matid;
+
+		}
+
+		// Recursively process the object into a scene
+		function processObject( o ) {
+
+			let node = `<node name="${ o.name }">`;
+
+			node += getTransform( o );
+
+			if ( o.isMesh === true && o.geometry !== null ) {
+
+				// function returns the id associated with the mesh and a "BufferGeometry" version
+				// of the geometry in case it's not a geometry.
+				const geomInfo = processGeometry( o.geometry );
+				const meshid = geomInfo.meshid;
+				const geometry = geomInfo.bufferGeometry;
+
+				// ids of the materials to bind to the geometry
+				let matids = null;
+				let matidsArray;
+
+				// get a list of materials to bind to the sub groups of the geometry.
+				// If the amount of subgroups is greater than the materials, than reuse
+				// the materials.
+				const mat = o.material || new MeshBasicMaterial();
+				const materials = Array.isArray( mat ) ? mat : [ mat ];
+
+				if ( geometry.groups.length > materials.length ) {
+
+					matidsArray = new Array( geometry.groups.length );
+
+				} else {
+
+					matidsArray = new Array( materials.length );
+
+				}
+
+				matids = matidsArray.fill().map( ( v, i ) => processMaterial( materials[ i % materials.length ] ) );
+
+				node +=
+					`<instance_geometry url="#${ meshid }">` +
+
+					(
+						matids.length > 0 ?
+							'<bind_material><technique_common>' +
+							matids.map( ( id, i ) =>
+
+								`<instance_material symbol="MESH_MATERIAL_${ i }" target="#${ id }" >` +
+
+								'<bind_vertex_input semantic="TEXCOORD" input_semantic="TEXCOORD" input_set="0" />' +
+
+								'</instance_material>'
+							).join( '' ) +
+							'</technique_common></bind_material>' :
+							''
+					) +
+
+					'</instance_geometry>';
+
+			}
+
+			o.children.forEach( c => node += processObject( c ) );
+
+			node += '</node>';
+
+			return node;
+
+		}
+
+		const geometryInfo = new WeakMap();
+		const materialMap = new WeakMap();
+		const imageMap = new WeakMap();
+		const textures = [];
+
+		const libraryImages = [];
+		const libraryGeometries = [];
+		const libraryEffects = [];
+		const libraryMaterials = [];
+		const libraryVisualScenes = processObject( object );
+
+		const specLink = version === '1.4.1' ? 'http://www.collada.org/2005/11/COLLADASchema' : 'https://www.khronos.org/collada/';
+		let dae =
+			'<?xml version="1.0" encoding="UTF-8" standalone="no" ?>' +
+			`<COLLADA xmlns="${ specLink }" version="${ version }">` +
+			'<asset>' +
+			(
+				'<contributor>' +
+				'<authoring_tool>three.js Collada Exporter</authoring_tool>' +
+				( options.author !== null ? `<author>${ options.author }</author>` : '' ) +
+				'</contributor>' +
+				`<created>${ ( new Date() ).toISOString() }</created>` +
+				`<modified>${ ( new Date() ).toISOString() }</modified>` +
+				( options.unitName !== null ? `<unit name="${ options.unitName }" meter="${ options.unitMeter }" />` : '' ) +
+				`<up_axis>${ options.upAxis }</up_axis>`
+			) +
+			'</asset>';
+
+		dae += `<library_images>${ libraryImages.join( '' ) }</library_images>`;
+
+		dae += `<library_effects>${ libraryEffects.join( '' ) }</library_effects>`;
+
+		dae += `<library_materials>${ libraryMaterials.join( '' ) }</library_materials>`;
+
+		dae += `<library_geometries>${ libraryGeometries.join( '' ) }</library_geometries>`;
+
+		dae += `<library_visual_scenes><visual_scene id="Scene" name="scene">${ libraryVisualScenes }</visual_scene></library_visual_scenes>`;
+
+		dae += '<scene><instance_visual_scene url="#Scene"/></scene>';
+
+		dae += '</COLLADA>';
+
+		const res = {
+			data: format( dae ),
+			textures
+		};
+
+		if ( typeof onDone === 'function' ) {
+
+			requestAnimationFrame( () => onDone( res ) );
+
+		}
+
+		return res;
+
+	}
+
+}
+
+
+export { ColladaExporter };

+ 225 - 0
public/archive/static/js/jsm/exporters/DRACOExporter.js

@@ -0,0 +1,225 @@
+/**
+ * Export draco compressed files from threejs geometry objects.
+ *
+ * Draco files are compressed and usually are smaller than conventional 3D file formats.
+ *
+ * The exporter receives a options object containing
+ *  - decodeSpeed, indicates how to tune the encoder regarding decode speed (0 gives better speed but worst quality)
+ *  - encodeSpeed, indicates how to tune the encoder parameters (0 gives better speed but worst quality)
+ *  - encoderMethod
+ *  - quantization, indicates the presision of each type of data stored in the draco file in the order (POSITION, NORMAL, COLOR, TEX_COORD, GENERIC)
+ *  - exportUvs
+ *  - exportNormals
+ */
+
+/* global DracoEncoderModule */
+
+class DRACOExporter {
+
+	parse( object, options = {
+		decodeSpeed: 5,
+		encodeSpeed: 5,
+		encoderMethod: DRACOExporter.MESH_EDGEBREAKER_ENCODING,
+		quantization: [ 16, 8, 8, 8, 8 ],
+		exportUvs: true,
+		exportNormals: true,
+		exportColor: false,
+	} ) {
+
+		if ( DracoEncoderModule === undefined ) {
+
+			throw new Error( 'THREE.DRACOExporter: required the draco_encoder to work.' );
+
+		}
+
+		const geometry = object.geometry;
+
+		const dracoEncoder = DracoEncoderModule();
+		const encoder = new dracoEncoder.Encoder();
+		let builder;
+		let dracoObject;
+
+		if ( object.isMesh === true ) {
+
+			builder = new dracoEncoder.MeshBuilder();
+			dracoObject = new dracoEncoder.Mesh();
+
+			const vertices = geometry.getAttribute( 'position' );
+			builder.AddFloatAttributeToMesh( dracoObject, dracoEncoder.POSITION, vertices.count, vertices.itemSize, vertices.array );
+
+			const faces = geometry.getIndex();
+
+			if ( faces !== null ) {
+
+				builder.AddFacesToMesh( dracoObject, faces.count / 3, faces.array );
+
+			} else {
+
+				const faces = new ( vertices.count > 65535 ? Uint32Array : Uint16Array )( vertices.count );
+
+				for ( let i = 0; i < faces.length; i ++ ) {
+
+					faces[ i ] = i;
+
+				}
+
+				builder.AddFacesToMesh( dracoObject, vertices.count, faces );
+
+			}
+
+			if ( options.exportNormals === true ) {
+
+				const normals = geometry.getAttribute( 'normal' );
+
+				if ( normals !== undefined ) {
+
+					builder.AddFloatAttributeToMesh( dracoObject, dracoEncoder.NORMAL, normals.count, normals.itemSize, normals.array );
+
+				}
+
+			}
+
+			if ( options.exportUvs === true ) {
+
+				const uvs = geometry.getAttribute( 'uv' );
+
+				if ( uvs !== undefined ) {
+
+					builder.AddFloatAttributeToMesh( dracoObject, dracoEncoder.TEX_COORD, uvs.count, uvs.itemSize, uvs.array );
+
+				}
+
+			}
+
+			if ( options.exportColor === true ) {
+
+				const colors = geometry.getAttribute( 'color' );
+
+				if ( colors !== undefined ) {
+
+					builder.AddFloatAttributeToMesh( dracoObject, dracoEncoder.COLOR, colors.count, colors.itemSize, colors.array );
+
+				}
+
+			}
+
+		} else if ( object.isPoints === true ) {
+
+			builder = new dracoEncoder.PointCloudBuilder();
+			dracoObject = new dracoEncoder.PointCloud();
+
+			const vertices = geometry.getAttribute( 'position' );
+			builder.AddFloatAttribute( dracoObject, dracoEncoder.POSITION, vertices.count, vertices.itemSize, vertices.array );
+
+			if ( options.exportColor === true ) {
+
+				const colors = geometry.getAttribute( 'color' );
+
+				if ( colors !== undefined ) {
+
+					builder.AddFloatAttribute( dracoObject, dracoEncoder.COLOR, colors.count, colors.itemSize, colors.array );
+
+				}
+
+			}
+
+		} else {
+
+			throw new Error( 'DRACOExporter: Unsupported object type.' );
+
+		}
+
+		//Compress using draco encoder
+
+		const encodedData = new dracoEncoder.DracoInt8Array();
+
+		//Sets the desired encoding and decoding speed for the given options from 0 (slowest speed, but the best compression) to 10 (fastest, but the worst compression).
+
+		const encodeSpeed = ( options.encodeSpeed !== undefined ) ? options.encodeSpeed : 5;
+		const decodeSpeed = ( options.decodeSpeed !== undefined ) ? options.decodeSpeed : 5;
+
+		encoder.SetSpeedOptions( encodeSpeed, decodeSpeed );
+
+		// Sets the desired encoding method for a given geometry.
+
+		if ( options.encoderMethod !== undefined ) {
+
+			encoder.SetEncodingMethod( options.encoderMethod );
+
+		}
+
+		// Sets the quantization (number of bits used to represent) compression options for a named attribute.
+		// The attribute values will be quantized in a box defined by the maximum extent of the attribute values.
+		if ( options.quantization !== undefined ) {
+
+			for ( let i = 0; i < 5; i ++ ) {
+
+				if ( options.quantization[ i ] !== undefined ) {
+
+					encoder.SetAttributeQuantization( i, options.quantization[ i ] );
+
+				}
+
+			}
+
+		}
+
+		let length;
+
+		if ( object.isMesh === true ) {
+
+			length = encoder.EncodeMeshToDracoBuffer( dracoObject, encodedData );
+
+		} else {
+
+			length = encoder.EncodePointCloudToDracoBuffer( dracoObject, true, encodedData );
+
+		}
+
+		dracoEncoder.destroy( dracoObject );
+
+		if ( length === 0 ) {
+
+			throw new Error( 'THREE.DRACOExporter: Draco encoding failed.' );
+
+		}
+
+		//Copy encoded data to buffer.
+		const outputData = new Int8Array( new ArrayBuffer( length ) );
+
+		for ( let i = 0; i < length; i ++ ) {
+
+			outputData[ i ] = encodedData.GetValue( i );
+
+		}
+
+		dracoEncoder.destroy( encodedData );
+		dracoEncoder.destroy( encoder );
+		dracoEncoder.destroy( builder );
+
+		return outputData;
+
+	}
+
+}
+
+// Encoder methods
+
+DRACOExporter.MESH_EDGEBREAKER_ENCODING = 1;
+DRACOExporter.MESH_SEQUENTIAL_ENCODING = 0;
+
+// Geometry type
+
+DRACOExporter.POINT_CLOUD = 0;
+DRACOExporter.TRIANGULAR_MESH = 1;
+
+// Attribute type
+
+DRACOExporter.INVALID = - 1;
+DRACOExporter.POSITION = 0;
+DRACOExporter.NORMAL = 1;
+DRACOExporter.COLOR = 2;
+DRACOExporter.TEX_COORD = 3;
+DRACOExporter.GENERIC = 4;
+
+export { DRACOExporter };

+ 507 - 0
public/archive/static/js/jsm/exporters/EXRExporter.js

@@ -0,0 +1,507 @@
+/**
+ * @author sciecode / https://github.com/sciecode
+ *
+ * EXR format references:
+ * 	https://www.openexr.com/documentation/openexrfilelayout.pdf
+ */
+
+import {
+	FloatType,
+	HalfFloatType,
+	RGBAFormat,
+	DataUtils,
+} from 'three';
+import * as fflate from '../libs/fflate.module.js';
+
+const textEncoder = new TextEncoder();
+
+const NO_COMPRESSION = 0;
+const ZIPS_COMPRESSION = 2;
+const ZIP_COMPRESSION = 3;
+
+class EXRExporter {
+
+	parse( renderer, renderTarget, options ) {
+
+		if ( ! supported( renderer, renderTarget ) ) return undefined;
+
+		const info = buildInfo( renderTarget, options ),
+			dataBuffer = getPixelData( renderer, renderTarget, info ),
+			rawContentBuffer = reorganizeDataBuffer( dataBuffer, info ),
+			chunks = compressData( rawContentBuffer, info );
+
+		return fillData( chunks, info );
+
+	}
+
+}
+
+function supported( renderer, renderTarget ) {
+
+	if ( ! renderer || ! renderer.isWebGLRenderer ) {
+
+		console.error( 'EXRExporter.parse: Unsupported first parameter, expected instance of WebGLRenderer.' );
+
+		return false;
+
+	}
+
+	if ( ! renderTarget || ! renderTarget.isWebGLRenderTarget ) {
+
+		console.error( 'EXRExporter.parse: Unsupported second parameter, expected instance of WebGLRenderTarget.' );
+
+		return false;
+
+	}
+
+	if ( renderTarget.texture.type !== FloatType && renderTarget.texture.type !== HalfFloatType ) {
+
+		console.error( 'EXRExporter.parse: Unsupported WebGLRenderTarget texture type.' );
+
+		return false;
+
+	}
+
+	if ( renderTarget.texture.format !== RGBAFormat ) {
+
+		console.error( 'EXRExporter.parse: Unsupported WebGLRenderTarget texture format, expected RGBAFormat.' );
+
+		return false;
+
+	}
+
+
+	return true;
+
+}
+
+function buildInfo( renderTarget, options = {} ) {
+
+	const compressionSizes = {
+		0: 1,
+		2: 1,
+		3: 16
+	};
+
+	const WIDTH = renderTarget.width,
+		HEIGHT = renderTarget.height,
+		TYPE = renderTarget.texture.type,
+		FORMAT = renderTarget.texture.format,
+		ENCODING = renderTarget.texture.encoding,
+		COMPRESSION = ( options.compression !== undefined ) ? options.compression : ZIP_COMPRESSION,
+		EXPORTER_TYPE = ( options.type !== undefined ) ? options.type : HalfFloatType,
+		OUT_TYPE = ( EXPORTER_TYPE === FloatType ) ? 2 : 1,
+		COMPRESSION_SIZE = compressionSizes[ COMPRESSION ],
+		NUM_CHANNELS = 4;
+
+	return {
+		width: WIDTH,
+		height: HEIGHT,
+		type: TYPE,
+		format: FORMAT,
+		encoding: ENCODING,
+		compression: COMPRESSION,
+		blockLines: COMPRESSION_SIZE,
+		dataType: OUT_TYPE,
+		dataSize: 2 * OUT_TYPE,
+		numBlocks: Math.ceil( HEIGHT / COMPRESSION_SIZE ),
+		numInputChannels: 4,
+		numOutputChannels: NUM_CHANNELS,
+	};
+
+}
+
+function getPixelData( renderer, rtt, info ) {
+
+	let dataBuffer;
+
+	if ( info.type === FloatType ) {
+
+		dataBuffer = new Float32Array( info.width * info.height * info.numInputChannels );
+
+	} else {
+
+		dataBuffer = new Uint16Array( info.width * info.height * info.numInputChannels );
+
+	}
+
+	renderer.readRenderTargetPixels( rtt, 0, 0, info.width, info.height, dataBuffer );
+
+	return dataBuffer;
+
+}
+
+function reorganizeDataBuffer( inBuffer, info ) {
+
+	const w = info.width,
+		h = info.height,
+		dec = { r: 0, g: 0, b: 0, a: 0 },
+		offset = { value: 0 },
+		cOffset = ( info.numOutputChannels == 4 ) ? 1 : 0,
+		getValue = ( info.type == FloatType ) ? getFloat32 : getFloat16,
+		setValue = ( info.dataType == 1 ) ? setFloat16 : setFloat32,
+		outBuffer = new Uint8Array( info.width * info.height * info.numOutputChannels * info.dataSize ),
+		dv = new DataView( outBuffer.buffer );
+
+	for ( let y = 0; y < h; ++ y ) {
+
+		for ( let x = 0; x < w; ++ x ) {
+
+			const i = y * w * 4 + x * 4;
+
+			const r = getValue( inBuffer, i );
+			const g = getValue( inBuffer, i + 1 );
+			const b = getValue( inBuffer, i + 2 );
+			const a = getValue( inBuffer, i + 3 );
+
+			const line = ( h - y - 1 ) * w * ( 3 + cOffset ) * info.dataSize;
+
+			decodeLinear( dec, r, g, b, a );
+
+			offset.value = line + x * info.dataSize;
+			setValue( dv, dec.a, offset );
+
+			offset.value = line + ( cOffset ) * w * info.dataSize + x * info.dataSize;
+			setValue( dv, dec.b, offset );
+
+			offset.value = line + ( 1 + cOffset ) * w * info.dataSize + x * info.dataSize;
+			setValue( dv, dec.g, offset );
+
+			offset.value = line + ( 2 + cOffset ) * w * info.dataSize + x * info.dataSize;
+			setValue( dv, dec.r, offset );
+
+		}
+
+	}
+
+	return outBuffer;
+
+}
+
+function compressData( inBuffer, info ) {
+
+	let compress,
+		tmpBuffer,
+		sum = 0;
+
+	const chunks = { data: new Array(), totalSize: 0 },
+		size = info.width * info.numOutputChannels * info.blockLines * info.dataSize;
+
+	switch ( info.compression ) {
+
+		case 0:
+			compress = compressNONE;
+			break;
+
+		case 2:
+		case 3:
+			compress = compressZIP;
+			break;
+
+	}
+
+	if ( info.compression !== 0 ) {
+
+		tmpBuffer = new Uint8Array( size );
+
+	}
+
+	for ( let i = 0; i < info.numBlocks; ++ i ) {
+
+		const arr = inBuffer.subarray( size * i, size * ( i + 1 ) );
+
+		const block = compress( arr, tmpBuffer );
+
+		sum += block.length;
+
+		chunks.data.push( { dataChunk: block, size: block.length } );
+
+	}
+
+	chunks.totalSize = sum;
+
+	return chunks;
+
+}
+
+function compressNONE( data ) {
+
+	return data;
+
+}
+
+function compressZIP( data, tmpBuffer ) {
+
+	//
+	// Reorder the pixel data.
+	//
+
+	let t1 = 0,
+		t2 = Math.floor( ( data.length + 1 ) / 2 ),
+		s = 0;
+
+	const stop = data.length - 1;
+
+	while ( true ) {
+
+		if ( s > stop ) break;
+		tmpBuffer[ t1 ++ ] = data[ s ++ ];
+
+		if ( s > stop ) break;
+		tmpBuffer[ t2 ++ ] = data[ s ++ ];
+
+	}
+
+	//
+	// Predictor.
+	//
+
+	let p = tmpBuffer[ 0 ];
+
+	for ( let t = 1; t < tmpBuffer.length; t ++ ) {
+
+		const d = tmpBuffer[ t ] - p + ( 128 + 256 );
+		p = tmpBuffer[ t ];
+		tmpBuffer[ t ] = d;
+
+	}
+
+	if ( typeof fflate === 'undefined' ) {
+
+		console.error( 'THREE.EXRLoader: External \`fflate.module.js\` required' );
+
+	}
+
+	const deflate = fflate.zlibSync( tmpBuffer ); // eslint-disable-line no-undef
+
+	return deflate;
+
+}
+
+function fillHeader( outBuffer, chunks, info ) {
+
+	const offset = { value: 0 };
+	const dv = new DataView( outBuffer.buffer );
+
+	setUint32( dv, 20000630, offset ); // magic
+	setUint32( dv, 2, offset ); // mask
+
+	// = HEADER =
+
+	setString( dv, 'compression', offset );
+	setString( dv, 'compression', offset );
+	setUint32( dv, 1, offset );
+	setUint8( dv, info.compression, offset );
+
+	setString( dv, 'screenWindowCenter', offset );
+	setString( dv, 'v2f', offset );
+	setUint32( dv, 8, offset );
+	setUint32( dv, 0, offset );
+	setUint32( dv, 0, offset );
+
+	setString( dv, 'screenWindowWidth', offset );
+	setString( dv, 'float', offset );
+	setUint32( dv, 4, offset );
+	setFloat32( dv, 1.0, offset );
+
+	setString( dv, 'pixelAspectRatio', offset );
+	setString( dv, 'float', offset );
+	setUint32( dv, 4, offset );
+	setFloat32( dv, 1.0, offset );
+
+	setString( dv, 'lineOrder', offset );
+	setString( dv, 'lineOrder', offset );
+	setUint32( dv, 1, offset );
+	setUint8( dv, 0, offset );
+
+	setString( dv, 'dataWindow', offset );
+	setString( dv, 'box2i', offset );
+	setUint32( dv, 16, offset );
+	setUint32( dv, 0, offset );
+	setUint32( dv, 0, offset );
+	setUint32( dv, info.width - 1, offset );
+	setUint32( dv, info.height - 1, offset );
+
+	setString( dv, 'displayWindow', offset );
+	setString( dv, 'box2i', offset );
+	setUint32( dv, 16, offset );
+	setUint32( dv, 0, offset );
+	setUint32( dv, 0, offset );
+	setUint32( dv, info.width - 1, offset );
+	setUint32( dv, info.height - 1, offset );
+
+	setString( dv, 'channels', offset );
+	setString( dv, 'chlist', offset );
+	setUint32( dv, info.numOutputChannels * 18 + 1, offset );
+
+	setString( dv, 'A', offset );
+	setUint32( dv, info.dataType, offset );
+	offset.value += 4;
+	setUint32( dv, 1, offset );
+	setUint32( dv, 1, offset );
+
+	setString( dv, 'B', offset );
+	setUint32( dv, info.dataType, offset );
+	offset.value += 4;
+	setUint32( dv, 1, offset );
+	setUint32( dv, 1, offset );
+
+	setString( dv, 'G', offset );
+	setUint32( dv, info.dataType, offset );
+	offset.value += 4;
+	setUint32( dv, 1, offset );
+	setUint32( dv, 1, offset );
+
+	setString( dv, 'R', offset );
+	setUint32( dv, info.dataType, offset );
+	offset.value += 4;
+	setUint32( dv, 1, offset );
+	setUint32( dv, 1, offset );
+
+	setUint8( dv, 0, offset );
+
+	// null-byte
+	setUint8( dv, 0, offset );
+
+	// = OFFSET TABLE =
+
+	let sum = offset.value + info.numBlocks * 8;
+
+	for ( let i = 0; i < chunks.data.length; ++ i ) {
+
+		setUint64( dv, sum, offset );
+
+		sum += chunks.data[ i ].size + 8;
+
+	}
+
+}
+
+function fillData( chunks, info ) {
+
+	const TableSize = info.numBlocks * 8,
+		HeaderSize = 259 + ( 18 * info.numOutputChannels ), // 259 + 18 * chlist
+		offset = { value: HeaderSize + TableSize },
+		outBuffer = new Uint8Array( HeaderSize + TableSize + chunks.totalSize + info.numBlocks * 8 ),
+		dv = new DataView( outBuffer.buffer );
+
+	fillHeader( outBuffer, chunks, info );
+
+	for ( let i = 0; i < chunks.data.length; ++ i ) {
+
+		const data = chunks.data[ i ].dataChunk;
+		const size = chunks.data[ i ].size;
+
+		setUint32( dv, i * info.blockLines, offset );
+		setUint32( dv, size, offset );
+
+		outBuffer.set( data, offset.value );
+		offset.value += size;
+
+	}
+
+	return outBuffer;
+
+}
+
+function decodeLinear( dec, r, g, b, a ) {
+
+	dec.r = r;
+	dec.g = g;
+	dec.b = b;
+	dec.a = a;
+
+}
+
+// function decodeSRGB( dec, r, g, b, a ) {
+
+// 	dec.r = r > 0.04045 ? Math.pow( r * 0.9478672986 + 0.0521327014, 2.4 ) : r * 0.0773993808;
+// 	dec.g = g > 0.04045 ? Math.pow( g * 0.9478672986 + 0.0521327014, 2.4 ) : g * 0.0773993808;
+// 	dec.b = b > 0.04045 ? Math.pow( b * 0.9478672986 + 0.0521327014, 2.4 ) : b * 0.0773993808;
+// 	dec.a = a;
+
+// }
+
+
+function setUint8( dv, value, offset ) {
+
+	dv.setUint8( offset.value, value );
+
+	offset.value += 1;
+
+}
+
+function setUint32( dv, value, offset ) {
+
+	dv.setUint32( offset.value, value, true );
+
+	offset.value += 4;
+
+}
+
+function setFloat16( dv, value, offset ) {
+
+	dv.setUint16( offset.value, DataUtils.toHalfFloat( value ), true );
+
+	offset.value += 2;
+
+}
+
+function setFloat32( dv, value, offset ) {
+
+	dv.setFloat32( offset.value, value, true );
+
+	offset.value += 4;
+
+}
+
+function setUint64( dv, value, offset ) {
+
+	dv.setBigUint64( offset.value, BigInt( value ), true );
+
+	offset.value += 8;
+
+}
+
+function setString( dv, string, offset ) {
+
+	const tmp = textEncoder.encode( string + '\0' );
+
+	for ( let i = 0; i < tmp.length; ++ i ) {
+
+		setUint8( dv, tmp[ i ], offset );
+
+	}
+
+}
+
+function decodeFloat16( binary ) {
+
+	const exponent = ( binary & 0x7C00 ) >> 10,
+		fraction = binary & 0x03FF;
+
+	return ( binary >> 15 ? - 1 : 1 ) * (
+		exponent ?
+			(
+				exponent === 0x1F ?
+					fraction ? NaN : Infinity :
+					Math.pow( 2, exponent - 15 ) * ( 1 + fraction / 0x400 )
+			) :
+			6.103515625e-5 * ( fraction / 0x400 )
+	);
+
+}
+
+function getFloat16( arr, i ) {
+
+	return decodeFloat16( arr[ i ] );
+
+}
+
+function getFloat32( arr, i ) {
+
+	return arr[ i ];
+
+}
+
+export { EXRExporter, NO_COMPRESSION, ZIP_COMPRESSION, ZIPS_COMPRESSION };

+ 2755 - 0
public/archive/static/js/jsm/exporters/GLTFExporter.js

@@ -0,0 +1,2755 @@
+import {
+	BufferAttribute,
+	ClampToEdgeWrapping,
+	DoubleSide,
+	InterpolateDiscrete,
+	InterpolateLinear,
+	LinearEncoding,
+	LinearFilter,
+	LinearMipmapLinearFilter,
+	LinearMipmapNearestFilter,
+	MathUtils,
+	Matrix4,
+	MirroredRepeatWrapping,
+	NearestFilter,
+	NearestMipmapLinearFilter,
+	NearestMipmapNearestFilter,
+	PropertyBinding,
+	RGBAFormat,
+	RepeatWrapping,
+	Scene,
+	Source,
+	sRGBEncoding,
+	Vector3
+} from 'three';
+
+class GLTFExporter {
+
+	constructor() {
+
+		this.pluginCallbacks = [];
+
+		this.register( function ( writer ) {
+
+			return new GLTFLightExtension( writer );
+
+		} );
+
+		this.register( function ( writer ) {
+
+			return new GLTFMaterialsUnlitExtension( writer );
+
+		} );
+
+		this.register( function ( writer ) {
+
+			return new GLTFMaterialsPBRSpecularGlossiness( writer );
+
+		} );
+
+		this.register( function ( writer ) {
+
+			return new GLTFMaterialsTransmissionExtension( writer );
+
+		} );
+
+		this.register( function ( writer ) {
+
+			return new GLTFMaterialsVolumeExtension( writer );
+
+		} );
+
+		this.register( function ( writer ) {
+
+			return new GLTFMaterialsClearcoatExtension( writer );
+
+		} );
+
+		this.register( function ( writer ) {
+
+			return new GLTFMaterialsIridescenceExtension( writer );
+
+		} );
+
+	}
+
+	register( callback ) {
+
+		if ( this.pluginCallbacks.indexOf( callback ) === - 1 ) {
+
+			this.pluginCallbacks.push( callback );
+
+		}
+
+		return this;
+
+	}
+
+	unregister( callback ) {
+
+		if ( this.pluginCallbacks.indexOf( callback ) !== - 1 ) {
+
+			this.pluginCallbacks.splice( this.pluginCallbacks.indexOf( callback ), 1 );
+
+		}
+
+		return this;
+
+	}
+
+	/**
+	 * Parse scenes and generate GLTF output
+	 * @param  {Scene or [THREE.Scenes]} input   Scene or Array of THREE.Scenes
+	 * @param  {Function} onDone  Callback on completed
+	 * @param  {Function} onError  Callback on errors
+	 * @param  {Object} options options
+	 */
+	parse( input, onDone, onError, options ) {
+
+		const writer = new GLTFWriter();
+		const plugins = [];
+
+		for ( let i = 0, il = this.pluginCallbacks.length; i < il; i ++ ) {
+
+			plugins.push( this.pluginCallbacks[ i ]( writer ) );
+
+		}
+
+		writer.setPlugins( plugins );
+		writer.write( input, onDone, options ).catch( onError );
+
+	}
+
+	parseAsync( input, options ) {
+
+		const scope = this;
+
+		return new Promise( function ( resolve, reject ) {
+
+			scope.parse( input, resolve, reject, options );
+
+		} );
+
+	}
+
+}
+
+//------------------------------------------------------------------------------
+// Constants
+//------------------------------------------------------------------------------
+
+const 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,
+
+	CLAMP_TO_EDGE: 33071,
+	MIRRORED_REPEAT: 33648,
+	REPEAT: 10497
+};
+
+const THREE_TO_WEBGL = {};
+
+THREE_TO_WEBGL[ NearestFilter ] = WEBGL_CONSTANTS.NEAREST;
+THREE_TO_WEBGL[ NearestMipmapNearestFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_NEAREST;
+THREE_TO_WEBGL[ NearestMipmapLinearFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_LINEAR;
+THREE_TO_WEBGL[ LinearFilter ] = WEBGL_CONSTANTS.LINEAR;
+THREE_TO_WEBGL[ LinearMipmapNearestFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_NEAREST;
+THREE_TO_WEBGL[ LinearMipmapLinearFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_LINEAR;
+
+THREE_TO_WEBGL[ ClampToEdgeWrapping ] = WEBGL_CONSTANTS.CLAMP_TO_EDGE;
+THREE_TO_WEBGL[ RepeatWrapping ] = WEBGL_CONSTANTS.REPEAT;
+THREE_TO_WEBGL[ MirroredRepeatWrapping ] = WEBGL_CONSTANTS.MIRRORED_REPEAT;
+
+const PATH_PROPERTIES = {
+	scale: 'scale',
+	position: 'translation',
+	quaternion: 'rotation',
+	morphTargetInfluences: 'weights'
+};
+
+// GLB constants
+// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification
+
+const GLB_HEADER_BYTES = 12;
+const GLB_HEADER_MAGIC = 0x46546C67;
+const GLB_VERSION = 2;
+
+const GLB_CHUNK_PREFIX_BYTES = 8;
+const GLB_CHUNK_TYPE_JSON = 0x4E4F534A;
+const GLB_CHUNK_TYPE_BIN = 0x004E4942;
+
+//------------------------------------------------------------------------------
+// Utility functions
+//------------------------------------------------------------------------------
+
+/**
+ * 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 ) {
+
+	return new TextEncoder().encode( text ).buffer;
+
+}
+
+/**
+ * Is identity matrix
+ *
+ * @param {Matrix4} matrix
+ * @returns {Boolean} Returns true, if parameter is identity matrix
+ */
+function isIdentityMatrix( matrix ) {
+
+	return equalArray( matrix.elements, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] );
+
+}
+
+/**
+ * Get the min and max vectors from the given attribute
+ * @param  {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 ) {
+
+	const output = {
+
+		min: new Array( attribute.itemSize ).fill( Number.POSITIVE_INFINITY ),
+		max: new Array( attribute.itemSize ).fill( Number.NEGATIVE_INFINITY )
+
+	};
+
+	for ( let i = start; i < start + count; i ++ ) {
+
+		for ( let a = 0; a < attribute.itemSize; a ++ ) {
+
+			let value;
+
+			if ( attribute.itemSize > 4 ) {
+
+				 // no support for interleaved data for itemSize > 4
+
+				value = attribute.array[ i * attribute.itemSize + a ];
+
+			} else {
+
+				if ( a === 0 ) value = attribute.getX( i );
+				else if ( a === 1 ) value = attribute.getY( i );
+				else if ( a === 2 ) value = attribute.getZ( i );
+				else if ( a === 3 ) value = attribute.getW( i );
+
+			}
+
+			output.min[ a ] = Math.min( output.min[ a ], value );
+			output.max[ a ] = Math.max( output.max[ a ], value );
+
+		}
+
+	}
+
+	return output;
+
+}
+
+/**
+ * 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 = 0 ) {
+
+	const paddedLength = getPaddedBufferSize( arrayBuffer.byteLength );
+
+	if ( paddedLength !== arrayBuffer.byteLength ) {
+
+		const array = new Uint8Array( paddedLength );
+		array.set( new Uint8Array( arrayBuffer ) );
+
+		if ( paddingByte !== 0 ) {
+
+			for ( let i = arrayBuffer.byteLength; i < paddedLength; i ++ ) {
+
+				array[ i ] = paddingByte;
+
+			}
+
+		}
+
+		return array.buffer;
+
+	}
+
+	return arrayBuffer;
+
+}
+
+function getCanvas() {
+
+	if ( typeof document === 'undefined' && typeof OffscreenCanvas !== 'undefined' ) {
+
+		return new OffscreenCanvas( 1, 1 );
+
+	}
+
+	return document.createElement( 'canvas' );
+
+}
+
+function getToBlobPromise( canvas, mimeType ) {
+
+	if ( canvas.toBlob !== undefined ) {
+
+		return new Promise( ( resolve ) => canvas.toBlob( resolve, mimeType ) );
+
+	}
+
+	let quality;
+
+	// Blink's implementation of convertToBlob seems to default to a quality level of 100%
+	// Use the Blink default quality levels of toBlob instead so that file sizes are comparable.
+	if ( mimeType === 'image/jpeg' ) {
+
+		quality = 0.92;
+
+	} else if ( mimeType === 'image/webp' ) {
+
+		quality = 0.8;
+
+	}
+
+	return canvas.convertToBlob( {
+
+		type: mimeType,
+		quality: quality
+
+	} );
+
+}
+
+/**
+ * Writer
+ */
+class GLTFWriter {
+
+	constructor() {
+
+		this.plugins = [];
+
+		this.options = {};
+		this.pending = [];
+		this.buffers = [];
+
+		this.byteOffset = 0;
+		this.buffers = [];
+		this.nodeMap = new Map();
+		this.skins = [];
+		this.extensionsUsed = {};
+
+		this.uids = new Map();
+		this.uid = 0;
+
+		this.json = {
+			asset: {
+				version: '2.0',
+				generator: 'THREE.GLTFExporter'
+			}
+		};
+
+		this.cache = {
+			meshes: new Map(),
+			attributes: new Map(),
+			attributesNormalized: new Map(),
+			materials: new Map(),
+			textures: new Map(),
+			images: new Map()
+		};
+
+	}
+
+	setPlugins( plugins ) {
+
+		this.plugins = plugins;
+
+	}
+
+	/**
+	 * Parse scenes and generate GLTF output
+	 * @param  {Scene or [THREE.Scenes]} input   Scene or Array of THREE.Scenes
+	 * @param  {Function} onDone  Callback on completed
+	 * @param  {Object} options options
+	 */
+	async write( input, onDone, options ) {
+
+		this.options = Object.assign( {}, {
+			// default options
+			binary: false,
+			trs: false,
+			onlyVisible: true,
+			truncateDrawRange: true,
+			maxTextureSize: Infinity,
+			animations: [],
+			includeCustomExtensions: false
+		}, options );
+
+		if ( this.options.animations.length > 0 ) {
+
+			// Only TRS properties, and not matrices, may be targeted by animation.
+			this.options.trs = true;
+
+		}
+
+		this.processInput( input );
+
+		await Promise.all( this.pending );
+
+		const writer = this;
+		const buffers = writer.buffers;
+		const json = writer.json;
+		options = writer.options;
+		const extensionsUsed = writer.extensionsUsed;
+
+		// Merge buffers.
+		const blob = new Blob( buffers, { type: 'application/octet-stream' } );
+
+		// Declare extensions.
+		const extensionsUsedList = Object.keys( extensionsUsed );
+
+		if ( extensionsUsedList.length > 0 ) json.extensionsUsed = extensionsUsedList;
+
+		// Update bytelength of the single buffer.
+		if ( json.buffers && json.buffers.length > 0 ) json.buffers[ 0 ].byteLength = blob.size;
+
+		if ( options.binary === true ) {
+
+			// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification
+
+			const reader = new FileReader();
+			reader.readAsArrayBuffer( blob );
+			reader.onloadend = function () {
+
+				// Binary chunk.
+				const binaryChunk = getPaddedArrayBuffer( reader.result );
+				const 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.
+				const jsonChunk = getPaddedArrayBuffer( stringToArrayBuffer( JSON.stringify( json ) ), 0x20 );
+				const 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.
+				const header = new ArrayBuffer( GLB_HEADER_BYTES );
+				const headerView = new DataView( header );
+				headerView.setUint32( 0, GLB_HEADER_MAGIC, true );
+				headerView.setUint32( 4, GLB_VERSION, true );
+				const totalByteLength = GLB_HEADER_BYTES
+					+ jsonChunkPrefix.byteLength + jsonChunk.byteLength
+					+ binaryChunkPrefix.byteLength + binaryChunk.byteLength;
+				headerView.setUint32( 8, totalByteLength, true );
+
+				const glbBlob = new Blob( [
+					header,
+					jsonChunkPrefix,
+					jsonChunk,
+					binaryChunkPrefix,
+					binaryChunk
+				], { type: 'application/octet-stream' } );
+
+				const glbReader = new FileReader();
+				glbReader.readAsArrayBuffer( glbBlob );
+				glbReader.onloadend = function () {
+
+					onDone( glbReader.result );
+
+				};
+
+			};
+
+		} else {
+
+			if ( json.buffers && json.buffers.length > 0 ) {
+
+				const reader = new FileReader();
+				reader.readAsDataURL( blob );
+				reader.onloadend = function () {
+
+					const base64data = reader.result;
+					json.buffers[ 0 ].uri = base64data;
+					onDone( json );
+
+				};
+
+			} else {
+
+				onDone( json );
+
+			}
+
+		}
+
+
+	}
+
+	/**
+	 * Serializes a userData.
+	 *
+	 * @param {THREE.Object3D|THREE.Material} object
+	 * @param {Object} objectDef
+	 */
+	serializeUserData( object, objectDef ) {
+
+		if ( Object.keys( object.userData ).length === 0 ) return;
+
+		const options = this.options;
+		const extensionsUsed = this.extensionsUsed;
+
+		try {
+
+			const json = JSON.parse( JSON.stringify( object.userData ) );
+
+			if ( options.includeCustomExtensions && json.gltfExtensions ) {
+
+				if ( objectDef.extensions === undefined ) objectDef.extensions = {};
+
+				for ( const extensionName in json.gltfExtensions ) {
+
+					objectDef.extensions[ extensionName ] = json.gltfExtensions[ extensionName ];
+					extensionsUsed[ extensionName ] = true;
+
+				}
+
+				delete json.gltfExtensions;
+
+			}
+
+			if ( Object.keys( json ).length > 0 ) objectDef.extras = json;
+
+		} catch ( error ) {
+
+			console.warn( 'THREE.GLTFExporter: userData of \'' + object.name + '\' ' +
+				'won\'t be serialized because of JSON.stringify error - ' + error.message );
+
+		}
+
+	}
+
+	/**
+	 * Returns ids for buffer attributes.
+	 * @param  {Object} object
+	 * @return {Integer}
+	 */
+	getUID( attribute, isRelativeCopy = false ) {
+
+		if ( this.uids.has( attribute ) === false ) {
+
+			const uids = new Map();
+
+			uids.set( true, this.uid ++ );
+			uids.set( false, this.uid ++ );
+
+			this.uids.set( attribute, uids );
+
+		}
+
+		const uids = this.uids.get( attribute );
+
+		return uids.get( isRelativeCopy );
+
+	}
+
+	/**
+	 * Checks if normal attribute values are normalized.
+	 *
+	 * @param {BufferAttribute} normal
+	 * @returns {Boolean}
+	 */
+	isNormalizedNormalAttribute( normal ) {
+
+		const cache = this.cache;
+
+		if ( cache.attributesNormalized.has( normal ) ) return false;
+
+		const v = new Vector3();
+
+		for ( let i = 0, il = normal.count; i < il; i ++ ) {
+
+			// 0.0005 is from glTF-validator
+			if ( Math.abs( v.fromBufferAttribute( normal, i ).length() - 1.0 ) > 0.0005 ) return false;
+
+		}
+
+		return true;
+
+	}
+
+	/**
+	 * Creates normalized normal buffer attribute.
+	 *
+	 * @param {BufferAttribute} normal
+	 * @returns {BufferAttribute}
+	 *
+	 */
+	createNormalizedNormalAttribute( normal ) {
+
+		const cache = this.cache;
+
+		if ( cache.attributesNormalized.has( normal ) )	return cache.attributesNormalized.get( normal );
+
+		const attribute = normal.clone();
+		const v = new Vector3();
+
+		for ( let i = 0, il = attribute.count; i < il; i ++ ) {
+
+			v.fromBufferAttribute( attribute, i );
+
+			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();
+
+			}
+
+			attribute.setXYZ( i, v.x, v.y, v.z );
+
+		}
+
+		cache.attributesNormalized.set( normal, attribute );
+
+		return attribute;
+
+	}
+
+	/**
+	 * Applies a texture transform, if present, to the map definition. Requires
+	 * the KHR_texture_transform extension.
+	 *
+	 * @param {Object} mapDef
+	 * @param {THREE.Texture} texture
+	 */
+	applyTextureTransform( mapDef, texture ) {
+
+		let didTransform = false;
+		const transformDef = {};
+
+		if ( texture.offset.x !== 0 || texture.offset.y !== 0 ) {
+
+			transformDef.offset = texture.offset.toArray();
+			didTransform = true;
+
+		}
+
+		if ( texture.rotation !== 0 ) {
+
+			transformDef.rotation = texture.rotation;
+			didTransform = true;
+
+		}
+
+		if ( texture.repeat.x !== 1 || texture.repeat.y !== 1 ) {
+
+			transformDef.scale = texture.repeat.toArray();
+			didTransform = true;
+
+		}
+
+		if ( didTransform ) {
+
+			mapDef.extensions = mapDef.extensions || {};
+			mapDef.extensions[ 'KHR_texture_transform' ] = transformDef;
+			this.extensionsUsed[ 'KHR_texture_transform' ] = true;
+
+		}
+
+	}
+
+	buildMetalRoughTexture( metalnessMap, roughnessMap ) {
+
+		if ( metalnessMap === roughnessMap ) return metalnessMap;
+
+		function getEncodingConversion( map ) {
+
+			if ( map.encoding === sRGBEncoding ) {
+
+				return function SRGBToLinear( c ) {
+
+					return ( c < 0.04045 ) ? c * 0.0773993808 : Math.pow( c * 0.9478672986 + 0.0521327014, 2.4 );
+
+				};
+
+			}
+
+			return function LinearToLinear( c ) {
+
+				return c;
+
+			};
+
+		}
+
+		console.warn( 'THREE.GLTFExporter: Merged metalnessMap and roughnessMap textures.' );
+
+		const metalness = metalnessMap?.image;
+		const roughness = roughnessMap?.image;
+
+		const width = Math.max( metalness?.width || 0, roughness?.width || 0 );
+		const height = Math.max( metalness?.height || 0, roughness?.height || 0 );
+
+		const canvas = getCanvas();
+		canvas.width = width;
+		canvas.height = height;
+
+		const context = canvas.getContext( '2d' );
+		context.fillStyle = '#00ffff';
+		context.fillRect( 0, 0, width, height );
+
+		const composite = context.getImageData( 0, 0, width, height );
+
+		if ( metalness ) {
+
+			context.drawImage( metalness, 0, 0, width, height );
+
+			const convert = getEncodingConversion( metalnessMap );
+			const data = context.getImageData( 0, 0, width, height ).data;
+
+			for ( let i = 2; i < data.length; i += 4 ) {
+
+				composite.data[ i ] = convert( data[ i ] / 256 ) * 256;
+
+			}
+
+		}
+
+		if ( roughness ) {
+
+			context.drawImage( roughness, 0, 0, width, height );
+
+			const convert = getEncodingConversion( roughnessMap );
+			const data = context.getImageData( 0, 0, width, height ).data;
+
+			for ( let i = 1; i < data.length; i += 4 ) {
+
+				composite.data[ i ] = convert( data[ i ] / 256 ) * 256;
+
+			}
+
+		}
+
+		context.putImageData( composite, 0, 0 );
+
+		//
+
+		const reference = metalnessMap || roughnessMap;
+
+		const texture = reference.clone();
+
+		texture.source = new Source( canvas );
+		texture.encoding = LinearEncoding;
+
+		return texture;
+
+	}
+
+	/**
+	 * Process a buffer to append to the default one.
+	 * @param  {ArrayBuffer} buffer
+	 * @return {Integer}
+	 */
+	processBuffer( buffer ) {
+
+		const json = this.json;
+		const buffers = this.buffers;
+
+		if ( ! json.buffers ) json.buffers = [ { byteLength: 0 } ];
+
+		// All buffers are merged before export.
+		buffers.push( buffer );
+
+		return 0;
+
+	}
+
+	/**
+	 * Process and generate a BufferView
+	 * @param  {BufferAttribute} attribute
+	 * @param  {number} componentType
+	 * @param  {number} start
+	 * @param  {number} count
+	 * @param  {number} target (Optional) Target usage of the BufferView
+	 * @return {Object}
+	 */
+	processBufferView( attribute, componentType, start, count, target ) {
+
+		const json = this.json;
+
+		if ( ! json.bufferViews ) json.bufferViews = [];
+
+		// Create a new dataview and dump the attribute's array into it
+
+		let componentSize;
+
+		if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) {
+
+			componentSize = 1;
+
+		} else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) {
+
+			componentSize = 2;
+
+		} else {
+
+			componentSize = 4;
+
+		}
+
+		const byteLength = getPaddedBufferSize( count * attribute.itemSize * componentSize );
+		const dataView = new DataView( new ArrayBuffer( byteLength ) );
+		let offset = 0;
+
+		for ( let i = start; i < start + count; i ++ ) {
+
+			for ( let a = 0; a < attribute.itemSize; a ++ ) {
+
+				let value;
+
+				if ( attribute.itemSize > 4 ) {
+
+					 // no support for interleaved data for itemSize > 4
+
+					value = attribute.array[ i * attribute.itemSize + a ];
+
+				} else {
+
+					if ( a === 0 ) value = attribute.getX( i );
+					else if ( a === 1 ) value = attribute.getY( i );
+					else if ( a === 2 ) value = attribute.getZ( i );
+					else if ( a === 3 ) value = attribute.getW( i );
+
+				}
+
+				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;
+
+			}
+
+		}
+
+		const bufferViewDef = {
+
+			buffer: this.processBuffer( dataView.buffer ),
+			byteOffset: this.byteOffset,
+			byteLength: byteLength
+
+		};
+
+		if ( target !== undefined ) bufferViewDef.target = target;
+
+		if ( target === WEBGL_CONSTANTS.ARRAY_BUFFER ) {
+
+			// Only define byteStride for vertex attributes.
+			bufferViewDef.byteStride = attribute.itemSize * componentSize;
+
+		}
+
+		this.byteOffset += byteLength;
+
+		json.bufferViews.push( bufferViewDef );
+
+		// @TODO Merge bufferViews where possible.
+		const output = {
+
+			id: json.bufferViews.length - 1,
+			byteLength: 0
+
+		};
+
+		return output;
+
+	}
+
+	/**
+	 * Process and generate a BufferView from an image Blob.
+	 * @param {Blob} blob
+	 * @return {Promise<Integer>}
+	 */
+	processBufferViewImage( blob ) {
+
+		const writer = this;
+		const json = writer.json;
+
+		if ( ! json.bufferViews ) json.bufferViews = [];
+
+		return new Promise( function ( resolve ) {
+
+			const reader = new FileReader();
+			reader.readAsArrayBuffer( blob );
+			reader.onloadend = function () {
+
+				const buffer = getPaddedArrayBuffer( reader.result );
+
+				const bufferViewDef = {
+					buffer: writer.processBuffer( buffer ),
+					byteOffset: writer.byteOffset,
+					byteLength: buffer.byteLength
+				};
+
+				writer.byteOffset += buffer.byteLength;
+				resolve( json.bufferViews.push( bufferViewDef ) - 1 );
+
+			};
+
+		} );
+
+	}
+
+	/**
+	 * Process attribute to generate an accessor
+	 * @param  {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|null} Index of the processed accessor on the "accessors" array
+	 */
+	processAccessor( attribute, geometry, start, count ) {
+
+		const options = this.options;
+		const json = this.json;
+
+		const types = {
+
+			1: 'SCALAR',
+			2: 'VEC2',
+			3: 'VEC3',
+			4: 'VEC4',
+			16: 'MAT4'
+
+		};
+
+		let 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 ) {
+
+			const end = start + count;
+			const 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;
+
+		const minMax = getMinMax( attribute, start, count );
+		let 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;
+
+		}
+
+		const bufferView = this.processBufferView( attribute, componentType, start, count, bufferViewTarget );
+
+		const accessorDef = {
+
+			bufferView: bufferView.id,
+			byteOffset: bufferView.byteOffset,
+			componentType: componentType,
+			count: count,
+			max: minMax.max,
+			min: minMax.min,
+			type: types[ attribute.itemSize ]
+
+		};
+
+		if ( attribute.normalized === true ) accessorDef.normalized = true;
+		if ( ! json.accessors ) json.accessors = [];
+
+		return json.accessors.push( accessorDef ) - 1;
+
+	}
+
+	/**
+	 * Process image
+	 * @param  {Image} image to process
+	 * @param  {Integer} format of the image (RGBAFormat)
+	 * @param  {Boolean} flipY before writing out the image
+	 * @param  {String} mimeType export format
+	 * @return {Integer}     Index of the processed texture in the "images" array
+	 */
+	processImage( image, format, flipY, mimeType = 'image/png' ) {
+
+		const writer = this;
+		const cache = writer.cache;
+		const json = writer.json;
+		const options = writer.options;
+		const pending = writer.pending;
+
+		if ( ! cache.images.has( image ) ) cache.images.set( image, {} );
+
+		const cachedImages = cache.images.get( image );
+
+		const key = mimeType + ':flipY/' + flipY.toString();
+
+		if ( cachedImages[ key ] !== undefined ) return cachedImages[ key ];
+
+		if ( ! json.images ) json.images = [];
+
+		const imageDef = { mimeType: mimeType };
+
+		const canvas = getCanvas();
+
+		canvas.width = Math.min( image.width, options.maxTextureSize );
+		canvas.height = Math.min( image.height, options.maxTextureSize );
+
+		const ctx = canvas.getContext( '2d' );
+
+		if ( flipY === true ) {
+
+			ctx.translate( 0, canvas.height );
+			ctx.scale( 1, - 1 );
+
+		}
+
+		if ( image.data !== undefined ) { // THREE.DataTexture
+
+			if ( format !== RGBAFormat ) {
+
+				console.error( 'GLTFExporter: Only RGBAFormat is supported.' );
+
+			}
+
+			if ( image.width > options.maxTextureSize || image.height > options.maxTextureSize ) {
+
+				console.warn( 'GLTFExporter: Image size is bigger than maxTextureSize', image );
+
+			}
+
+			const data = new Uint8ClampedArray( image.height * image.width * 4 );
+
+			for ( let i = 0; i < data.length; i += 4 ) {
+
+				data[ i + 0 ] = image.data[ i + 0 ];
+				data[ i + 1 ] = image.data[ i + 1 ];
+				data[ i + 2 ] = image.data[ i + 2 ];
+				data[ i + 3 ] = image.data[ i + 3 ];
+
+			}
+
+			ctx.putImageData( new ImageData( data, image.width, image.height ), 0, 0 );
+
+		} else {
+
+			ctx.drawImage( image, 0, 0, canvas.width, canvas.height );
+
+		}
+
+		if ( options.binary === true ) {
+
+			pending.push(
+
+				getToBlobPromise( canvas, mimeType )
+					.then( blob => writer.processBufferViewImage( blob ) )
+					.then( bufferViewIndex => {
+
+						imageDef.bufferView = bufferViewIndex;
+
+					} )
+
+			);
+
+		} else {
+
+			if ( canvas.toDataURL !== undefined ) {
+
+				imageDef.uri = canvas.toDataURL( mimeType );
+
+			} else {
+
+				pending.push(
+
+					getToBlobPromise( canvas, mimeType )
+						.then( blob => new FileReader().readAsDataURL( blob ) )
+						.then( dataURL => {
+
+							imageDef.uri = dataURL;
+
+						} )
+
+				);
+
+			}
+
+		}
+
+		const index = json.images.push( imageDef ) - 1;
+		cachedImages[ key ] = index;
+		return index;
+
+	}
+
+	/**
+	 * Process sampler
+	 * @param  {Texture} map Texture to process
+	 * @return {Integer}     Index of the processed texture in the "samplers" array
+	 */
+	processSampler( map ) {
+
+		const json = this.json;
+
+		if ( ! json.samplers ) json.samplers = [];
+
+		const samplerDef = {
+			magFilter: THREE_TO_WEBGL[ map.magFilter ],
+			minFilter: THREE_TO_WEBGL[ map.minFilter ],
+			wrapS: THREE_TO_WEBGL[ map.wrapS ],
+			wrapT: THREE_TO_WEBGL[ map.wrapT ]
+		};
+
+		return json.samplers.push( samplerDef ) - 1;
+
+	}
+
+	/**
+	 * Process texture
+	 * @param  {Texture} map Map to process
+	 * @return {Integer} Index of the processed texture in the "textures" array
+	 */
+	processTexture( map ) {
+
+		const cache = this.cache;
+		const json = this.json;
+
+		if ( cache.textures.has( map ) ) return cache.textures.get( map );
+
+		if ( ! json.textures ) json.textures = [];
+
+		let mimeType = map.userData.mimeType;
+
+		if ( mimeType === 'image/webp' ) mimeType = 'image/png';
+
+		const textureDef = {
+			sampler: this.processSampler( map ),
+			source: this.processImage( map.image, map.format, map.flipY, mimeType )
+		};
+
+		if ( map.name ) textureDef.name = map.name;
+
+		this._invokeAll( function ( ext ) {
+
+			ext.writeTexture && ext.writeTexture( map, textureDef );
+
+		} );
+
+		const index = json.textures.push( textureDef ) - 1;
+		cache.textures.set( map, index );
+		return index;
+
+	}
+
+	/**
+	 * Process material
+	 * @param  {THREE.Material} material Material to process
+	 * @return {Integer|null} Index of the processed material in the "materials" array
+	 */
+	processMaterial( material ) {
+
+		const cache = this.cache;
+		const json = this.json;
+
+		if ( cache.materials.has( material ) ) return cache.materials.get( material );
+
+		if ( material.isShaderMaterial ) {
+
+			console.warn( 'GLTFExporter: THREE.ShaderMaterial not supported.' );
+			return null;
+
+		}
+
+		if ( ! json.materials ) json.materials = [];
+
+		// @QUESTION Should we avoid including any attribute that has the default value?
+		const materialDef = {	pbrMetallicRoughness: {} };
+
+		if ( material.isMeshStandardMaterial !== true && material.isMeshBasicMaterial !== true ) {
+
+			console.warn( 'GLTFExporter: Use MeshStandardMaterial or MeshBasicMaterial for best results.' );
+
+		}
+
+		// pbrMetallicRoughness.baseColorFactor
+		const color = material.color.toArray().concat( [ material.opacity ] );
+
+		if ( ! equalArray( color, [ 1, 1, 1, 1 ] ) ) {
+
+			materialDef.pbrMetallicRoughness.baseColorFactor = color;
+
+		}
+
+		if ( material.isMeshStandardMaterial ) {
+
+			materialDef.pbrMetallicRoughness.metallicFactor = material.metalness;
+			materialDef.pbrMetallicRoughness.roughnessFactor = material.roughness;
+
+		} else {
+
+			materialDef.pbrMetallicRoughness.metallicFactor = 0.5;
+			materialDef.pbrMetallicRoughness.roughnessFactor = 0.5;
+
+		}
+
+		// pbrMetallicRoughness.metallicRoughnessTexture
+		if ( material.metalnessMap || material.roughnessMap ) {
+
+			const metalRoughTexture = this.buildMetalRoughTexture( material.metalnessMap, material.roughnessMap );
+
+			const metalRoughMapDef = { index: this.processTexture( metalRoughTexture ) };
+			this.applyTextureTransform( metalRoughMapDef, metalRoughTexture );
+			materialDef.pbrMetallicRoughness.metallicRoughnessTexture = metalRoughMapDef;
+
+		}
+
+		// pbrMetallicRoughness.baseColorTexture or pbrSpecularGlossiness diffuseTexture
+		if ( material.map ) {
+
+			const baseColorMapDef = { index: this.processTexture( material.map ) };
+			this.applyTextureTransform( baseColorMapDef, material.map );
+			materialDef.pbrMetallicRoughness.baseColorTexture = baseColorMapDef;
+
+		}
+
+		if ( material.emissive ) {
+
+			// note: emissive components are limited to stay within the 0 - 1 range to accommodate glTF spec. see #21849 and #22000.
+			const emissive = material.emissive.clone().multiplyScalar( material.emissiveIntensity );
+			const maxEmissiveComponent = Math.max( emissive.r, emissive.g, emissive.b );
+
+			if ( maxEmissiveComponent > 1 ) {
+
+				emissive.multiplyScalar( 1 / maxEmissiveComponent );
+
+				console.warn( 'THREE.GLTFExporter: Some emissive components exceed 1; emissive has been limited' );
+
+			}
+
+			if ( maxEmissiveComponent > 0 ) {
+
+				materialDef.emissiveFactor = emissive.toArray();
+
+			}
+
+			// emissiveTexture
+			if ( material.emissiveMap ) {
+
+				const emissiveMapDef = { index: this.processTexture( material.emissiveMap ) };
+				this.applyTextureTransform( emissiveMapDef, material.emissiveMap );
+				materialDef.emissiveTexture = emissiveMapDef;
+
+			}
+
+		}
+
+		// normalTexture
+		if ( material.normalMap ) {
+
+			const normalMapDef = { index: this.processTexture( material.normalMap ) };
+
+			if ( material.normalScale && material.normalScale.x !== 1 ) {
+
+				// glTF normal scale is univariate. Ignore `y`, which may be flipped.
+				// Context: https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995
+				normalMapDef.scale = material.normalScale.x;
+
+			}
+
+			this.applyTextureTransform( normalMapDef, material.normalMap );
+			materialDef.normalTexture = normalMapDef;
+
+		}
+
+		// occlusionTexture
+		if ( material.aoMap ) {
+
+			const occlusionMapDef = {
+				index: this.processTexture( material.aoMap ),
+				texCoord: 1
+			};
+
+			if ( material.aoMapIntensity !== 1.0 ) {
+
+				occlusionMapDef.strength = material.aoMapIntensity;
+
+			}
+
+			this.applyTextureTransform( occlusionMapDef, material.aoMap );
+			materialDef.occlusionTexture = occlusionMapDef;
+
+		}
+
+		// alphaMode
+		if ( material.transparent ) {
+
+			materialDef.alphaMode = 'BLEND';
+
+		} else {
+
+			if ( material.alphaTest > 0.0 ) {
+
+				materialDef.alphaMode = 'MASK';
+				materialDef.alphaCutoff = material.alphaTest;
+
+			}
+
+		}
+
+		// doubleSided
+		if ( material.side === DoubleSide ) materialDef.doubleSided = true;
+		if ( material.name !== '' ) materialDef.name = material.name;
+
+		this.serializeUserData( material, materialDef );
+
+		this._invokeAll( function ( ext ) {
+
+			ext.writeMaterial && ext.writeMaterial( material, materialDef );
+
+		} );
+
+		const index = json.materials.push( materialDef ) - 1;
+		cache.materials.set( material, index );
+		return index;
+
+	}
+
+	/**
+	 * Process mesh
+	 * @param  {THREE.Mesh} mesh Mesh to process
+	 * @return {Integer|null} Index of the processed mesh in the "meshes" array
+	 */
+	processMesh( mesh ) {
+
+		const cache = this.cache;
+		const json = this.json;
+
+		const meshCacheKeyParts = [ mesh.geometry.uuid ];
+
+		if ( Array.isArray( mesh.material ) ) {
+
+			for ( let i = 0, l = mesh.material.length; i < l; i ++ ) {
+
+				meshCacheKeyParts.push( mesh.material[ i ].uuid	);
+
+			}
+
+		} else {
+
+			meshCacheKeyParts.push( mesh.material.uuid );
+
+		}
+
+		const meshCacheKey = meshCacheKeyParts.join( ':' );
+
+		if ( cache.meshes.has( meshCacheKey ) ) return cache.meshes.get( meshCacheKey );
+
+		const geometry = mesh.geometry;
+
+		let 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 {
+
+			mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINES : WEBGL_CONSTANTS.TRIANGLES;
+
+		}
+
+		const meshDef = {};
+		const attributes = {};
+		const primitives = [];
+		const targets = [];
+
+		// Conversion between attributes names in threejs and gltf spec
+		const nameConversion = {
+			uv: 'TEXCOORD_0',
+			uv2: 'TEXCOORD_1',
+			color: 'COLOR_0',
+			skinWeight: 'WEIGHTS_0',
+			skinIndex: 'JOINTS_0'
+		};
+
+		const originalNormal = geometry.getAttribute( 'normal' );
+
+		if ( originalNormal !== undefined && ! this.isNormalizedNormalAttribute( originalNormal ) ) {
+
+			console.warn( 'THREE.GLTFExporter: Creating normalized normal attribute from the non-normalized one.' );
+
+			geometry.setAttribute( 'normal', this.createNormalizedNormalAttribute( originalNormal ) );
+
+		}
+
+		// @QUESTION Detect if .vertexColors = true?
+		// For every attribute create an accessor
+		let modifiedAttribute = null;
+
+		for ( let attributeName in geometry.attributes ) {
+
+			// Ignore morph target attributes, which are exported later.
+			if ( attributeName.slice( 0, 5 ) === 'morph' ) continue;
+
+			const attribute = geometry.attributes[ attributeName ];
+			attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase();
+
+			// Prefix all geometry attributes except the ones specifically
+			// listed in the spec; non-spec attributes are considered custom.
+			const validVertexAttributes =
+					/^(POSITION|NORMAL|TANGENT|TEXCOORD_\d+|COLOR_\d+|JOINTS_\d+|WEIGHTS_\d+)$/;
+
+			if ( ! validVertexAttributes.test( attributeName ) ) attributeName = '_' + attributeName;
+
+			if ( cache.attributes.has( this.getUID( attribute ) ) ) {
+
+				attributes[ attributeName ] = cache.attributes.get( this.getUID( attribute ) );
+				continue;
+
+			}
+
+			// JOINTS_0 must be UNSIGNED_BYTE or UNSIGNED_SHORT.
+			modifiedAttribute = null;
+			const array = attribute.array;
+
+			if ( attributeName === 'JOINTS_0' &&
+				! ( array instanceof Uint16Array ) &&
+				! ( array instanceof Uint8Array ) ) {
+
+				console.warn( 'GLTFExporter: Attribute "skinIndex" converted to type UNSIGNED_SHORT.' );
+				modifiedAttribute = new BufferAttribute( new Uint16Array( array ), attribute.itemSize, attribute.normalized );
+
+			}
+
+			const accessor = this.processAccessor( modifiedAttribute || attribute, geometry );
+
+			if ( accessor !== null ) {
+
+				attributes[ attributeName ] = accessor;
+				cache.attributes.set( this.getUID( attribute ), accessor );
+
+			}
+
+		}
+
+		if ( originalNormal !== undefined ) geometry.setAttribute( '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 ) {
+
+			const weights = [];
+			const targetNames = [];
+			const reverseDictionary = {};
+
+			if ( mesh.morphTargetDictionary !== undefined ) {
+
+				for ( const key in mesh.morphTargetDictionary ) {
+
+					reverseDictionary[ mesh.morphTargetDictionary[ key ] ] = key;
+
+				}
+
+			}
+
+			for ( let i = 0; i < mesh.morphTargetInfluences.length; ++ i ) {
+
+				const target = {};
+				let warned = false;
+
+				for ( const 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;
+
+					}
+
+					const attribute = geometry.morphAttributes[ attributeName ][ i ];
+					const gltfAttributeName = attributeName.toUpperCase();
+
+					// 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
+
+					const baseAttribute = geometry.attributes[ attributeName ];
+
+					if ( cache.attributes.has( this.getUID( attribute, true ) ) ) {
+
+						target[ gltfAttributeName ] = cache.attributes.get( this.getUID( attribute, true ) );
+						continue;
+
+					}
+
+					// Clones attribute not to override
+					const relativeAttribute = attribute.clone();
+
+					if ( ! geometry.morphTargetsRelative ) {
+
+						for ( let 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[ gltfAttributeName ] = this.processAccessor( relativeAttribute, geometry );
+					cache.attributes.set( this.getUID( baseAttribute, true ), target[ gltfAttributeName ] );
+
+				}
+
+				targets.push( target );
+
+				weights.push( mesh.morphTargetInfluences[ i ] );
+
+				if ( mesh.morphTargetDictionary !== undefined ) targetNames.push( reverseDictionary[ i ] );
+
+			}
+
+			meshDef.weights = weights;
+
+			if ( targetNames.length > 0 ) {
+
+				meshDef.extras = {};
+				meshDef.extras.targetNames = targetNames;
+
+			}
+
+		}
+
+		const isMultiMaterial = Array.isArray( mesh.material );
+
+		if ( isMultiMaterial && geometry.groups.length === 0 ) return null;
+
+		const materials = isMultiMaterial ? mesh.material : [ mesh.material ];
+		const groups = isMultiMaterial ? geometry.groups : [ { materialIndex: 0, start: undefined, count: undefined } ];
+
+		for ( let i = 0, il = groups.length; i < il; i ++ ) {
+
+			const primitive = {
+				mode: mode,
+				attributes: attributes,
+			};
+
+			this.serializeUserData( geometry, primitive );
+
+			if ( targets.length > 0 ) primitive.targets = targets;
+
+			if ( geometry.index !== null ) {
+
+				let cacheKey = this.getUID( geometry.index );
+
+				if ( groups[ i ].start !== undefined || groups[ i ].count !== undefined ) {
+
+					cacheKey += ':' + groups[ i ].start + ':' + groups[ i ].count;
+
+				}
+
+				if ( cache.attributes.has( cacheKey ) ) {
+
+					primitive.indices = cache.attributes.get( cacheKey );
+
+				} else {
+
+					primitive.indices = this.processAccessor( geometry.index, geometry, groups[ i ].start, groups[ i ].count );
+					cache.attributes.set( cacheKey, primitive.indices );
+
+				}
+
+				if ( primitive.indices === null ) delete primitive.indices;
+
+			}
+
+			const material = this.processMaterial( materials[ groups[ i ].materialIndex ] );
+
+			if ( material !== null ) primitive.material = material;
+
+			primitives.push( primitive );
+
+		}
+
+		meshDef.primitives = primitives;
+
+		if ( ! json.meshes ) json.meshes = [];
+
+		this._invokeAll( function ( ext ) {
+
+			ext.writeMesh && ext.writeMesh( mesh, meshDef );
+
+		} );
+
+		const index = json.meshes.push( meshDef ) - 1;
+		cache.meshes.set( meshCacheKey, index );
+		return index;
+
+	}
+
+	/**
+	 * Process camera
+	 * @param  {THREE.Camera} camera Camera to process
+	 * @return {Integer}      Index of the processed mesh in the "camera" array
+	 */
+	processCamera( camera ) {
+
+		const json = this.json;
+
+		if ( ! json.cameras ) json.cameras = [];
+
+		const isOrtho = camera.isOrthographicCamera;
+
+		const cameraDef = {
+			type: isOrtho ? 'orthographic' : 'perspective'
+		};
+
+		if ( isOrtho ) {
+
+			cameraDef.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 {
+
+			cameraDef.perspective = {
+				aspectRatio: camera.aspect,
+				yfov: MathUtils.degToRad( camera.fov ),
+				zfar: camera.far <= 0 ? 0.001 : camera.far,
+				znear: camera.near < 0 ? 0 : camera.near
+			};
+
+		}
+
+		// Question: Is saving "type" as name intentional?
+		if ( camera.name !== '' ) cameraDef.name = camera.type;
+
+		return json.cameras.push( cameraDef ) - 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|null}
+	 */
+	processAnimation( clip, root ) {
+
+		const json = this.json;
+		const nodeMap = this.nodeMap;
+
+		if ( ! json.animations ) json.animations = [];
+
+		clip = GLTFExporter.Utils.mergeMorphTargetTracks( clip.clone(), root );
+
+		const tracks = clip.tracks;
+		const channels = [];
+		const samplers = [];
+
+		for ( let i = 0; i < tracks.length; ++ i ) {
+
+			const track = tracks[ i ];
+			const trackBinding = PropertyBinding.parseTrackName( track.name );
+			let trackNode = PropertyBinding.findNode( root, trackBinding.nodeName );
+			const 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;
+
+			}
+
+			const inputItemSize = 1;
+			let outputItemSize = track.values.length / track.times.length;
+
+			if ( trackProperty === PATH_PROPERTIES.morphTargetInfluences ) {
+
+				outputItemSize /= trackNode.morphTargetInfluences.length;
+
+			}
+
+			let 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() === InterpolateDiscrete ) {
+
+				interpolation = 'STEP';
+
+			} else {
+
+				interpolation = 'LINEAR';
+
+			}
+
+			samplers.push( {
+				input: this.processAccessor( new BufferAttribute( track.times, inputItemSize ) ),
+				output: this.processAccessor( new BufferAttribute( track.values, outputItemSize ) ),
+				interpolation: interpolation
+			} );
+
+			channels.push( {
+				sampler: samplers.length - 1,
+				target: {
+					node: nodeMap.get( trackNode ),
+					path: trackProperty
+				}
+			} );
+
+		}
+
+		json.animations.push( {
+			name: clip.name || 'clip_' + json.animations.length,
+			samplers: samplers,
+			channels: channels
+		} );
+
+		return json.animations.length - 1;
+
+	}
+
+	/**
+	 * @param {THREE.Object3D} object
+	 * @return {number|null}
+	 */
+	 processSkin( object ) {
+
+		const json = this.json;
+		const nodeMap = this.nodeMap;
+
+		const node = json.nodes[ nodeMap.get( object ) ];
+
+		const skeleton = object.skeleton;
+
+		if ( skeleton === undefined ) return null;
+
+		const rootJoint = object.skeleton.bones[ 0 ];
+
+		if ( rootJoint === undefined ) return null;
+
+		const joints = [];
+		const inverseBindMatrices = new Float32Array( skeleton.bones.length * 16 );
+		const temporaryBoneInverse = new Matrix4();
+
+		for ( let i = 0; i < skeleton.bones.length; ++ i ) {
+
+			joints.push( nodeMap.get( skeleton.bones[ i ] ) );
+			temporaryBoneInverse.copy( skeleton.boneInverses[ i ] );
+			temporaryBoneInverse.multiply( object.bindMatrix ).toArray( inverseBindMatrices, i * 16 );
+
+		}
+
+		if ( json.skins === undefined ) json.skins = [];
+
+		json.skins.push( {
+			inverseBindMatrices: this.processAccessor( new BufferAttribute( inverseBindMatrices, 16 ) ),
+			joints: joints,
+			skeleton: nodeMap.get( rootJoint )
+		} );
+
+		const skinIndex = node.skin = json.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
+	 */
+	processNode( object ) {
+
+		const json = this.json;
+		const options = this.options;
+		const nodeMap = this.nodeMap;
+
+		if ( ! json.nodes ) json.nodes = [];
+
+		const nodeDef = {};
+
+		if ( options.trs ) {
+
+			const rotation = object.quaternion.toArray();
+			const position = object.position.toArray();
+			const scale = object.scale.toArray();
+
+			if ( ! equalArray( rotation, [ 0, 0, 0, 1 ] ) ) {
+
+				nodeDef.rotation = rotation;
+
+			}
+
+			if ( ! equalArray( position, [ 0, 0, 0 ] ) ) {
+
+				nodeDef.translation = position;
+
+			}
+
+			if ( ! equalArray( scale, [ 1, 1, 1 ] ) ) {
+
+				nodeDef.scale = scale;
+
+			}
+
+		} else {
+
+			if ( object.matrixAutoUpdate ) {
+
+				object.updateMatrix();
+
+			}
+
+			if ( isIdentityMatrix( object.matrix ) === false ) {
+
+				nodeDef.matrix = object.matrix.elements;
+
+			}
+
+		}
+
+		// We don't export empty strings name because it represents no-name in Three.js.
+		if ( object.name !== '' ) nodeDef.name = String( object.name );
+
+		this.serializeUserData( object, nodeDef );
+
+		if ( object.isMesh || object.isLine || object.isPoints ) {
+
+			const meshIndex = this.processMesh( object );
+
+			if ( meshIndex !== null ) nodeDef.mesh = meshIndex;
+
+		} else if ( object.isCamera ) {
+
+			nodeDef.camera = this.processCamera( object );
+
+		}
+
+		if ( object.isSkinnedMesh ) this.skins.push( object );
+
+		if ( object.children.length > 0 ) {
+
+			const children = [];
+
+			for ( let i = 0, l = object.children.length; i < l; i ++ ) {
+
+				const child = object.children[ i ];
+
+				if ( child.visible || options.onlyVisible === false ) {
+
+					const nodeIndex = this.processNode( child );
+
+					if ( nodeIndex !== null ) children.push( nodeIndex );
+
+				}
+
+			}
+
+			if ( children.length > 0 ) nodeDef.children = children;
+
+		}
+
+		this._invokeAll( function ( ext ) {
+
+			ext.writeNode && ext.writeNode( object, nodeDef );
+
+		} );
+
+		const nodeIndex = json.nodes.push( nodeDef ) - 1;
+		nodeMap.set( object, nodeIndex );
+		return nodeIndex;
+
+	}
+
+	/**
+	 * Process Scene
+	 * @param  {Scene} node Scene to process
+	 */
+	processScene( scene ) {
+
+		const json = this.json;
+		const options = this.options;
+
+		if ( ! json.scenes ) {
+
+			json.scenes = [];
+			json.scene = 0;
+
+		}
+
+		const sceneDef = {};
+
+		if ( scene.name !== '' ) sceneDef.name = scene.name;
+
+		json.scenes.push( sceneDef );
+
+		const nodes = [];
+
+		for ( let i = 0, l = scene.children.length; i < l; i ++ ) {
+
+			const child = scene.children[ i ];
+
+			if ( child.visible || options.onlyVisible === false ) {
+
+				const nodeIndex = this.processNode( child );
+
+				if ( nodeIndex !== null ) nodes.push( nodeIndex );
+
+			}
+
+		}
+
+		if ( nodes.length > 0 ) sceneDef.nodes = nodes;
+
+		this.serializeUserData( scene, sceneDef );
+
+	}
+
+	/**
+	 * Creates a Scene to hold a list of objects and parse it
+	 * @param  {Array} objects List of objects to process
+	 */
+	processObjects( objects ) {
+
+		const scene = new Scene();
+		scene.name = 'AuxScene';
+
+		for ( let 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 ] );
+
+		}
+
+		this.processScene( scene );
+
+	}
+
+	/**
+	 * @param {THREE.Object3D|Array<THREE.Object3D>} input
+	 */
+	processInput( input ) {
+
+		const options = this.options;
+
+		input = input instanceof Array ? input : [ input ];
+
+		this._invokeAll( function ( ext ) {
+
+			ext.beforeParse && ext.beforeParse( input );
+
+		} );
+
+		const objectsWithoutScene = [];
+
+		for ( let i = 0; i < input.length; i ++ ) {
+
+			if ( input[ i ] instanceof Scene ) {
+
+				this.processScene( input[ i ] );
+
+			} else {
+
+				objectsWithoutScene.push( input[ i ] );
+
+			}
+
+		}
+
+		if ( objectsWithoutScene.length > 0 ) this.processObjects( objectsWithoutScene );
+
+		for ( let i = 0; i < this.skins.length; ++ i ) {
+
+			this.processSkin( this.skins[ i ] );
+
+		}
+
+		for ( let i = 0; i < options.animations.length; ++ i ) {
+
+			this.processAnimation( options.animations[ i ], input[ 0 ] );
+
+		}
+
+		this._invokeAll( function ( ext ) {
+
+			ext.afterParse && ext.afterParse( input );
+
+		} );
+
+	}
+
+	_invokeAll( func ) {
+
+		for ( let i = 0, il = this.plugins.length; i < il; i ++ ) {
+
+			func( this.plugins[ i ] );
+
+		}
+
+	}
+
+}
+
+/**
+ * Punctual Lights Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual
+ */
+class GLTFLightExtension {
+
+	constructor( writer ) {
+
+		this.writer = writer;
+		this.name = 'KHR_lights_punctual';
+
+	}
+
+	writeNode( light, nodeDef ) {
+
+		if ( ! light.isLight ) return;
+
+		if ( ! light.isDirectionalLight && ! light.isPointLight && ! light.isSpotLight ) {
+
+			console.warn( 'THREE.GLTFExporter: Only directional, point, and spot lights are supported.', light );
+			return;
+
+		}
+
+		const writer = this.writer;
+		const json = writer.json;
+		const extensionsUsed = writer.extensionsUsed;
+
+		const lightDef = {};
+
+		if ( light.name ) lightDef.name = light.name;
+
+		lightDef.color = light.color.toArray();
+
+		lightDef.intensity = light.intensity;
+
+		if ( light.isDirectionalLight ) {
+
+			lightDef.type = 'directional';
+
+		} else if ( light.isPointLight ) {
+
+			lightDef.type = 'point';
+
+			if ( light.distance > 0 ) lightDef.range = light.distance;
+
+		} else if ( light.isSpotLight ) {
+
+			lightDef.type = 'spot';
+
+			if ( light.distance > 0 ) lightDef.range = light.distance;
+
+			lightDef.spot = {};
+			lightDef.spot.innerConeAngle = ( light.penumbra - 1.0 ) * light.angle * - 1.0;
+			lightDef.spot.outerConeAngle = light.angle;
+
+		}
+
+		if ( light.decay !== undefined && light.decay !== 2 ) {
+
+			console.warn( 'THREE.GLTFExporter: Light decay may be lost. glTF is physically-based, '
+				+ 'and expects light.decay=2.' );
+
+		}
+
+		if ( light.target
+				&& ( light.target.parent !== light
+				|| light.target.position.x !== 0
+				|| light.target.position.y !== 0
+				|| light.target.position.z !== - 1 ) ) {
+
+			console.warn( 'THREE.GLTFExporter: Light direction may be lost. For best results, '
+				+ 'make light.target a child of the light with position 0,0,-1.' );
+
+		}
+
+		if ( ! extensionsUsed[ this.name ] ) {
+
+			json.extensions = json.extensions || {};
+			json.extensions[ this.name ] = { lights: [] };
+			extensionsUsed[ this.name ] = true;
+
+		}
+
+		const lights = json.extensions[ this.name ].lights;
+		lights.push( lightDef );
+
+		nodeDef.extensions = nodeDef.extensions || {};
+		nodeDef.extensions[ this.name ] = { light: lights.length - 1 };
+
+	}
+
+}
+
+/**
+ * Unlit Materials Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit
+ */
+class GLTFMaterialsUnlitExtension {
+
+	constructor( writer ) {
+
+		this.writer = writer;
+		this.name = 'KHR_materials_unlit';
+
+	}
+
+	writeMaterial( material, materialDef ) {
+
+		if ( ! material.isMeshBasicMaterial ) return;
+
+		const writer = this.writer;
+		const extensionsUsed = writer.extensionsUsed;
+
+		materialDef.extensions = materialDef.extensions || {};
+		materialDef.extensions[ this.name ] = {};
+
+		extensionsUsed[ this.name ] = true;
+
+		materialDef.pbrMetallicRoughness.metallicFactor = 0.0;
+		materialDef.pbrMetallicRoughness.roughnessFactor = 0.9;
+
+	}
+
+}
+
+/**
+ * Specular-Glossiness Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Archived/KHR_materials_pbrSpecularGlossiness
+ */
+class GLTFMaterialsPBRSpecularGlossiness {
+
+	constructor( writer ) {
+
+		this.writer = writer;
+		this.name = 'KHR_materials_pbrSpecularGlossiness';
+
+	}
+
+	writeMaterial( material, materialDef ) {
+
+		if ( ! material.isGLTFSpecularGlossinessMaterial ) return;
+
+		const writer = this.writer;
+		const extensionsUsed = writer.extensionsUsed;
+
+		const extensionDef = {};
+
+		if ( materialDef.pbrMetallicRoughness.baseColorFactor ) {
+
+			extensionDef.diffuseFactor = materialDef.pbrMetallicRoughness.baseColorFactor;
+
+		}
+
+		const specularFactor = [ 1, 1, 1 ];
+		material.specular.toArray( specularFactor, 0 );
+		extensionDef.specularFactor = specularFactor;
+		extensionDef.glossinessFactor = material.glossiness;
+
+		if ( materialDef.pbrMetallicRoughness.baseColorTexture ) {
+
+			extensionDef.diffuseTexture = materialDef.pbrMetallicRoughness.baseColorTexture;
+
+		}
+
+		if ( material.specularMap ) {
+
+			const specularMapDef = { index: writer.processTexture( material.specularMap ) };
+			writer.applyTextureTransform( specularMapDef, material.specularMap );
+			extensionDef.specularGlossinessTexture = specularMapDef;
+
+		}
+
+		materialDef.extensions = materialDef.extensions || {};
+		materialDef.extensions[ this.name ] = extensionDef;
+		extensionsUsed[ this.name ] = true;
+
+	}
+
+}
+
+/**
+ * Clearcoat Materials Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat
+ */
+class GLTFMaterialsClearcoatExtension {
+
+	constructor( writer ) {
+
+		this.writer = writer;
+		this.name = 'KHR_materials_clearcoat';
+
+	}
+
+	writeMaterial( material, materialDef ) {
+
+		if ( ! material.isMeshPhysicalMaterial ) return;
+
+		const writer = this.writer;
+		const extensionsUsed = writer.extensionsUsed;
+
+		const extensionDef = {};
+
+		extensionDef.clearcoatFactor = material.clearcoat;
+
+		if ( material.clearcoatMap ) {
+
+			const clearcoatMapDef = { index: writer.processTexture( material.clearcoatMap ) };
+			writer.applyTextureTransform( clearcoatMapDef, material.clearcoatMap );
+			extensionDef.clearcoatTexture = clearcoatMapDef;
+
+		}
+
+		extensionDef.clearcoatRoughnessFactor = material.clearcoatRoughness;
+
+		if ( material.clearcoatRoughnessMap ) {
+
+			const clearcoatRoughnessMapDef = { index: writer.processTexture( material.clearcoatRoughnessMap ) };
+			writer.applyTextureTransform( clearcoatRoughnessMapDef, material.clearcoatRoughnessMap );
+			extensionDef.clearcoatRoughnessTexture = clearcoatRoughnessMapDef;
+
+		}
+
+		if ( material.clearcoatNormalMap ) {
+
+			const clearcoatNormalMapDef = { index: writer.processTexture( material.clearcoatNormalMap ) };
+			writer.applyTextureTransform( clearcoatNormalMapDef, material.clearcoatNormalMap );
+			extensionDef.clearcoatNormalTexture = clearcoatNormalMapDef;
+
+		}
+
+		materialDef.extensions = materialDef.extensions || {};
+		materialDef.extensions[ this.name ] = extensionDef;
+
+		extensionsUsed[ this.name ] = true;
+
+
+	}
+
+}
+
+/**
+ * Iridescence Materials Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_iridescence
+ */
+class GLTFMaterialsIridescenceExtension {
+
+	constructor( writer ) {
+
+		this.writer = writer;
+		this.name = 'KHR_materials_iridescence';
+
+	}
+
+	writeMaterial( material, materialDef ) {
+
+		if ( ! material.isMeshPhysicalMaterial ) return;
+
+		const writer = this.writer;
+		const extensionsUsed = writer.extensionsUsed;
+
+		const extensionDef = {};
+
+		extensionDef.iridescenceFactor = material.iridescence;
+
+		if ( material.iridescenceMap ) {
+
+			const iridescenceMapDef = { index: writer.processTexture( material.iridescenceMap ) };
+			writer.applyTextureTransform( iridescenceMapDef, material.iridescenceMap );
+			extensionDef.iridescenceTexture = iridescenceMapDef;
+
+		}
+
+		extensionDef.iridescenceIor = material.iridescenceIOR;
+		extensionDef.iridescenceThicknessMinimum = material.iridescenceThicknessRange[ 0 ];
+		extensionDef.iridescenceThicknessMaximum = material.iridescenceThicknessRange[ 1 ];
+
+		if ( material.iridescenceThicknessMap ) {
+
+			const iridescenceThicknessMapDef = { index: writer.processTexture( material.iridescenceThicknessMap ) };
+			writer.applyTextureTransform( iridescenceThicknessMapDef, material.iridescenceThicknessMap );
+			extensionDef.iridescenceThicknessTexture = iridescenceThicknessMapDef;
+
+		}
+
+		materialDef.extensions = materialDef.extensions || {};
+		materialDef.extensions[ this.name ] = extensionDef;
+
+		extensionsUsed[ this.name ] = true;
+
+	}
+
+}
+
+/**
+ * Transmission Materials Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission
+ */
+class GLTFMaterialsTransmissionExtension {
+
+	constructor( writer ) {
+
+		this.writer = writer;
+		this.name = 'KHR_materials_transmission';
+
+	}
+
+	writeMaterial( material, materialDef ) {
+
+		if ( ! material.isMeshPhysicalMaterial || material.transmission === 0 ) return;
+
+		const writer = this.writer;
+		const extensionsUsed = writer.extensionsUsed;
+
+		const extensionDef = {};
+
+		extensionDef.transmissionFactor = material.transmission;
+
+		if ( material.transmissionMap ) {
+
+			const transmissionMapDef = { index: writer.processTexture( material.transmissionMap ) };
+			writer.applyTextureTransform( transmissionMapDef, material.transmissionMap );
+			extensionDef.transmissionTexture = transmissionMapDef;
+
+		}
+
+		materialDef.extensions = materialDef.extensions || {};
+		materialDef.extensions[ this.name ] = extensionDef;
+
+		extensionsUsed[ this.name ] = true;
+
+	}
+
+}
+
+/**
+ * Materials Volume Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_volume
+ */
+class GLTFMaterialsVolumeExtension {
+
+	constructor( writer ) {
+
+		this.writer = writer;
+		this.name = 'KHR_materials_volume';
+
+	}
+
+	writeMaterial( material, materialDef ) {
+
+		if ( ! material.isMeshPhysicalMaterial || material.transmission === 0 ) return;
+
+		const writer = this.writer;
+		const extensionsUsed = writer.extensionsUsed;
+
+		const extensionDef = {};
+
+		extensionDef.thicknessFactor = material.thickness;
+
+		if ( material.thicknessMap ) {
+
+			const thicknessMapDef = { index: writer.processTexture( material.thicknessMap ) };
+			writer.applyTextureTransform( thicknessMapDef, material.thicknessMap );
+			extensionDef.thicknessTexture = thicknessMapDef;
+
+		}
+
+		extensionDef.attenuationDistance = material.attenuationDistance;
+		extensionDef.attenuationColor = material.attenuationColor.toArray();
+
+		materialDef.extensions = materialDef.extensions || {};
+		materialDef.extensions[ this.name ] = extensionDef;
+
+		extensionsUsed[ this.name ] = true;
+
+	}
+
+}
+
+/**
+ * Static utility functions
+ */
+GLTFExporter.Utils = {
+
+	insertKeyframe: function ( track, time ) {
+
+		const tolerance = 0.001; // 1ms
+		const valueSize = track.getValueSize();
+
+		const times = new track.TimeBufferType( track.times.length + 1 );
+		const values = new track.ValueBufferType( track.values.length + valueSize );
+		const interpolant = track.createInterpolant( new track.ValueBufferType( valueSize ) );
+
+		let index;
+
+		if ( track.times.length === 0 ) {
+
+			times[ 0 ] = time;
+
+			for ( let i = 0; i < valueSize; i ++ ) {
+
+				values[ i ] = 0;
+
+			}
+
+			index = 0;
+
+		} else if ( time < track.times[ 0 ] ) {
+
+			if ( Math.abs( track.times[ 0 ] - time ) < tolerance ) return 0;
+
+			times[ 0 ] = time;
+			times.set( track.times, 1 );
+
+			values.set( interpolant.evaluate( time ), 0 );
+			values.set( track.values, valueSize );
+
+			index = 0;
+
+		} else if ( time > track.times[ track.times.length - 1 ] ) {
+
+			if ( Math.abs( track.times[ track.times.length - 1 ] - time ) < tolerance ) {
+
+				return track.times.length - 1;
+
+			}
+
+			times[ times.length - 1 ] = time;
+			times.set( track.times, 0 );
+
+			values.set( track.values, 0 );
+			values.set( interpolant.evaluate( time ), track.values.length );
+
+			index = times.length - 1;
+
+		} else {
+
+			for ( let i = 0; i < track.times.length; i ++ ) {
+
+				if ( Math.abs( track.times[ i ] - time ) < tolerance ) return i;
+
+				if ( track.times[ i ] < time && track.times[ i + 1 ] > time ) {
+
+					times.set( track.times.slice( 0, i + 1 ), 0 );
+					times[ i + 1 ] = time;
+					times.set( track.times.slice( i + 1 ), i + 2 );
+
+					values.set( track.values.slice( 0, ( i + 1 ) * valueSize ), 0 );
+					values.set( interpolant.evaluate( time ), ( i + 1 ) * valueSize );
+					values.set( track.values.slice( ( i + 1 ) * valueSize ), ( i + 2 ) * valueSize );
+
+					index = i + 1;
+
+					break;
+
+				}
+
+			}
+
+		}
+
+		track.times = times;
+		track.values = values;
+
+		return index;
+
+	},
+
+	mergeMorphTargetTracks: function ( clip, root ) {
+
+		const tracks = [];
+		const mergedTracks = {};
+		const sourceTracks = clip.tracks;
+
+		for ( let i = 0; i < sourceTracks.length; ++ i ) {
+
+			let sourceTrack = sourceTracks[ i ];
+			const sourceTrackBinding = PropertyBinding.parseTrackName( sourceTrack.name );
+			const sourceTrackNode = PropertyBinding.findNode( root, sourceTrackBinding.nodeName );
+
+			if ( sourceTrackBinding.propertyName !== 'morphTargetInfluences' || sourceTrackBinding.propertyIndex === undefined ) {
+
+				// Tracks that don't affect morph targets, or that affect all morph targets together, can be left as-is.
+				tracks.push( sourceTrack );
+				continue;
+
+			}
+
+			if ( sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodDiscrete
+				&& sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodLinear ) {
+
+				if ( sourceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {
+
+					// This should never happen, because glTF morph target animations
+					// affect all targets already.
+					throw new Error( 'THREE.GLTFExporter: Cannot merge tracks with glTF CUBICSPLINE interpolation.' );
+
+				}
+
+				console.warn( 'THREE.GLTFExporter: Morph target interpolation mode not yet supported. Using LINEAR instead.' );
+
+				sourceTrack = sourceTrack.clone();
+				sourceTrack.setInterpolation( InterpolateLinear );
+
+			}
+
+			const targetCount = sourceTrackNode.morphTargetInfluences.length;
+			const targetIndex = sourceTrackNode.morphTargetDictionary[ sourceTrackBinding.propertyIndex ];
+
+			if ( targetIndex === undefined ) {
+
+				throw new Error( 'THREE.GLTFExporter: Morph target name not found: ' + sourceTrackBinding.propertyIndex );
+
+			}
+
+			let mergedTrack;
+
+			// If this is the first time we've seen this object, create a new
+			// track to store merged keyframe data for each morph target.
+			if ( mergedTracks[ sourceTrackNode.uuid ] === undefined ) {
+
+				mergedTrack = sourceTrack.clone();
+
+				const values = new mergedTrack.ValueBufferType( targetCount * mergedTrack.times.length );
+
+				for ( let j = 0; j < mergedTrack.times.length; j ++ ) {
+
+					values[ j * targetCount + targetIndex ] = mergedTrack.values[ j ];
+
+				}
+
+				// We need to take into consideration the intended target node
+				// of our original un-merged morphTarget animation.
+				mergedTrack.name = ( sourceTrackBinding.nodeName || '' ) + '.morphTargetInfluences';
+				mergedTrack.values = values;
+
+				mergedTracks[ sourceTrackNode.uuid ] = mergedTrack;
+				tracks.push( mergedTrack );
+
+				continue;
+
+			}
+
+			const sourceInterpolant = sourceTrack.createInterpolant( new sourceTrack.ValueBufferType( 1 ) );
+
+			mergedTrack = mergedTracks[ sourceTrackNode.uuid ];
+
+			// For every existing keyframe of the merged track, write a (possibly
+			// interpolated) value from the source track.
+			for ( let j = 0; j < mergedTrack.times.length; j ++ ) {
+
+				mergedTrack.values[ j * targetCount + targetIndex ] = sourceInterpolant.evaluate( mergedTrack.times[ j ] );
+
+			}
+
+			// For every existing keyframe of the source track, write a (possibly
+			// new) keyframe to the merged track. Values from the previous loop may
+			// be written again, but keyframes are de-duplicated.
+			for ( let j = 0; j < sourceTrack.times.length; j ++ ) {
+
+				const keyframeIndex = this.insertKeyframe( mergedTrack, sourceTrack.times[ j ] );
+				mergedTrack.values[ keyframeIndex * targetCount + targetIndex ] = sourceTrack.values[ j ];
+
+			}
+
+		}
+
+		clip.tracks = tracks;
+
+		return clip;
+
+	}
+
+};
+
+export { GLTFExporter };

+ 281 - 0
public/archive/static/js/jsm/exporters/KTX2Exporter.js

@@ -0,0 +1,281 @@
+import {
+	FloatType,
+	HalfFloatType,
+	UnsignedByteType,
+	RGBAFormat,
+	RGFormat,
+	RGIntegerFormat,
+	RedFormat,
+	RedIntegerFormat,
+	LinearEncoding,
+	sRGBEncoding,
+	DataTexture,
+	REVISION,
+} from 'three';
+
+import {
+	write,
+	KTX2Container,
+	KHR_DF_CHANNEL_RGBSDA_ALPHA,
+	KHR_DF_CHANNEL_RGBSDA_BLUE,
+	KHR_DF_CHANNEL_RGBSDA_GREEN,
+	KHR_DF_CHANNEL_RGBSDA_RED,
+	KHR_DF_MODEL_RGBSDA,
+	KHR_DF_PRIMARIES_BT709,
+	KHR_DF_SAMPLE_DATATYPE_FLOAT,
+	KHR_DF_SAMPLE_DATATYPE_LINEAR,
+	KHR_DF_SAMPLE_DATATYPE_SIGNED,
+	KHR_DF_TRANSFER_LINEAR,
+	KHR_DF_TRANSFER_SRGB,
+	VK_FORMAT_R16_SFLOAT,
+	VK_FORMAT_R16G16_SFLOAT,
+	VK_FORMAT_R16G16B16A16_SFLOAT,
+	VK_FORMAT_R32_SFLOAT,
+	VK_FORMAT_R32G32_SFLOAT,
+	VK_FORMAT_R32G32B32A32_SFLOAT,
+	VK_FORMAT_R8_SRGB,
+	VK_FORMAT_R8_UNORM,
+	VK_FORMAT_R8G8_SRGB,
+	VK_FORMAT_R8G8_UNORM,
+	VK_FORMAT_R8G8B8A8_SRGB,
+	VK_FORMAT_R8G8B8A8_UNORM,
+} from '../libs/ktx-parse.module.js';
+
+const VK_FORMAT_MAP = {
+
+	[ RGBAFormat ]: {
+		[ FloatType ]: {
+			[ LinearEncoding ]: VK_FORMAT_R32G32B32A32_SFLOAT,
+		},
+		[ HalfFloatType ]: {
+			[ LinearEncoding ]: VK_FORMAT_R16G16B16A16_SFLOAT,
+		},
+		[ UnsignedByteType ]: {
+			[ LinearEncoding ]: VK_FORMAT_R8G8B8A8_UNORM,
+			[ sRGBEncoding ]: VK_FORMAT_R8G8B8A8_SRGB,
+		},
+	},
+
+	[ RGFormat ]: {
+		[ FloatType ]: {
+			[ LinearEncoding ]: VK_FORMAT_R32G32_SFLOAT,
+		},
+		[ HalfFloatType ]: {
+			[ LinearEncoding ]: VK_FORMAT_R16G16_SFLOAT,
+		},
+		[ UnsignedByteType ]: {
+			[ LinearEncoding ]: VK_FORMAT_R8G8_UNORM,
+			[ sRGBEncoding ]: VK_FORMAT_R8G8_SRGB,
+		},
+	},
+
+	[ RedFormat ]: {
+		[ FloatType ]: {
+			[ LinearEncoding ]: VK_FORMAT_R32_SFLOAT,
+		},
+		[ HalfFloatType ]: {
+			[ LinearEncoding ]: VK_FORMAT_R16_SFLOAT,
+		},
+		[ UnsignedByteType ]: {
+			[ LinearEncoding ]: VK_FORMAT_R8_SRGB,
+			[ sRGBEncoding ]: VK_FORMAT_R8_UNORM,
+		},
+	},
+
+};
+
+const KHR_DF_CHANNEL_MAP = {
+
+	0: KHR_DF_CHANNEL_RGBSDA_RED,
+	1: KHR_DF_CHANNEL_RGBSDA_GREEN,
+	2: KHR_DF_CHANNEL_RGBSDA_BLUE,
+	3: KHR_DF_CHANNEL_RGBSDA_ALPHA,
+
+};
+
+const ERROR_INPUT = 'THREE.KTX2Exporter: Supported inputs are DataTexture, Data3DTexture, or WebGLRenderer and WebGLRenderTarget.';
+const ERROR_FORMAT = 'THREE.KTX2Exporter: Supported formats are RGBAFormat, RGFormat, or RedFormat.';
+const ERROR_TYPE = 'THREE.KTX2Exporter: Supported types are FloatType, HalfFloatType, or UnsignedByteType."';
+const ERROR_ENCODING = 'THREE.KTX2Exporter: Supported encodings are sRGB (UnsignedByteType only) or Linear.';
+
+export class KTX2Exporter {
+
+	parse( arg1, arg2 ) {
+
+		let texture;
+
+		if ( arg1.isDataTexture || arg1.isData3DTexture ) {
+
+			texture = arg1;
+
+		} else if ( arg1.isWebGLRenderer && arg2.isWebGLRenderTarget ) {
+
+			texture = toDataTexture( arg1, arg2 );
+
+		} else {
+
+			throw new Error( ERROR_INPUT );
+
+		}
+
+		if ( VK_FORMAT_MAP[ texture.format ] === undefined ) {
+
+			throw new Error( ERROR_FORMAT );
+
+		}
+
+		if ( VK_FORMAT_MAP[ texture.format ][ texture.type ] === undefined ) {
+
+			throw new Error( ERROR_TYPE );
+
+		}
+
+		if ( VK_FORMAT_MAP[ texture.format ][ texture.type ][ texture.encoding ] === undefined ) {
+
+			throw new Error( ERROR_ENCODING );
+
+		}
+
+		//
+
+		const array = texture.image.data;
+		const channelCount = getChannelCount( texture );
+		const container = new KTX2Container();
+
+		container.vkFormat = VK_FORMAT_MAP[ texture.format ][ texture.type ][ texture.encoding ];
+		container.typeSize = array.BYTES_PER_ELEMENT;
+		container.pixelWidth = texture.image.width;
+		container.pixelHeight = texture.image.height;
+
+		if ( texture.isData3DTexture ) {
+
+			container.pixelDepth = texture.image.depth;
+
+		}
+
+		//
+
+		const basicDesc = container.dataFormatDescriptor[ 0 ];
+
+		// TODO: After `texture.encoding` is replaced, distinguish between
+		// non-color data (unspecified model and primaries) and sRGB or Linear-sRGB colors.
+		basicDesc.colorModel = KHR_DF_MODEL_RGBSDA;
+		basicDesc.colorPrimaries = KHR_DF_PRIMARIES_BT709;
+		basicDesc.transferFunction = texture.encoding === sRGBEncoding
+			? KHR_DF_TRANSFER_SRGB
+			: KHR_DF_TRANSFER_LINEAR;
+
+		basicDesc.texelBlockDimension = [ 0, 0, 0, 0 ];
+
+		basicDesc.bytesPlane = [
+
+			container.typeSize * channelCount, 0, 0, 0, 0, 0, 0, 0,
+
+		];
+
+		for ( let i = 0; i < channelCount; ++ i ) {
+
+			let channelType = KHR_DF_CHANNEL_MAP[ i ];
+
+			if ( texture.encoding === LinearEncoding ) {
+
+				channelType |= KHR_DF_SAMPLE_DATATYPE_LINEAR;
+
+			}
+
+			if ( texture.type === FloatType || texture.type === HalfFloatType ) {
+
+				channelType |= KHR_DF_SAMPLE_DATATYPE_FLOAT;
+				channelType |= KHR_DF_SAMPLE_DATATYPE_SIGNED;
+
+			}
+
+			basicDesc.samples.push( {
+
+				channelType: channelType,
+				bitOffset: i * array.BYTES_PER_ELEMENT,
+				bitLength: array.BYTES_PER_ELEMENT * 8 - 1,
+				samplePosition: [ 0, 0, 0, 0 ],
+				sampleLower: texture.type === UnsignedByteType ? 0 : - 1,
+				sampleUpper: texture.type === UnsignedByteType ? 255 : 1,
+
+			} );
+
+		}
+
+		//
+
+		container.levels = [ {
+
+			levelData: new Uint8Array( array.buffer, array.byteOffset, array.byteLength ),
+			uncompressedByteLength: array.byteLength,
+
+		} ];
+
+		//
+
+		container.keyValue[ 'KTXwriter' ] = `three.js ${ REVISION }`;
+
+		//
+
+		return write( container, { keepWriter: true } );
+
+	}
+
+}
+
+function toDataTexture( renderer, rtt ) {
+
+	const channelCount = getChannelCount( rtt.texture );
+
+	let view;
+
+	if ( rtt.texture.type === FloatType ) {
+
+		view = new Float32Array( rtt.width * rtt.height * channelCount );
+
+	} else if ( rtt.texture.type === HalfFloatType ) {
+
+		view = new Uint16Array( rtt.width * rtt.height * channelCount );
+
+	} else if ( rtt.texture.type === UnsignedByteType ) {
+
+		view = new Uint8Array( rtt.width * rtt.height * channelCount );
+
+	} else {
+
+		throw new Error( ERROR_TYPE );
+
+	}
+
+	renderer.readRenderTargetPixels( rtt, 0, 0, rtt.width, rtt.height, view );
+
+	return new DataTexture( view, rtt.width, rtt.height, rtt.texture.format, rtt.texture.type );
+
+}
+
+function getChannelCount( texture ) {
+
+	switch ( texture.format ) {
+
+		case RGBAFormat:
+
+			return 4;
+
+		case RGFormat:
+		case RGIntegerFormat:
+
+			return 2;
+
+		case RedFormat:
+		case RedIntegerFormat:
+
+			return 1;
+
+		default:
+
+			throw new Error( ERROR_FORMAT );
+
+	}
+
+}

+ 217 - 0
public/archive/static/js/jsm/exporters/MMDExporter.js

@@ -0,0 +1,217 @@
+import {
+	Matrix4,
+	Quaternion,
+	Vector3
+} from 'three';
+import { MMDParser } from '../libs/mmdparser.module.js';
+
+/**
+ * Dependencies
+ *  - mmd-parser https://github.com/takahirox/mmd-parser
+ */
+
+class MMDExporter {
+
+	/* TODO: implement
+	// mesh -> pmd
+	this.parsePmd = function ( object ) {
+
+	};
+	*/
+
+	/* TODO: implement
+	// mesh -> pmx
+	this.parsePmx = function ( object ) {
+
+	};
+	*/
+
+	/* TODO: implement
+	// animation + skeleton -> vmd
+	this.parseVmd = function ( object ) {
+
+	};
+	*/
+
+	/*
+	 * skeleton -> vpd
+	 * Returns Shift_JIS encoded Uint8Array. Otherwise return strings.
+	 */
+	parseVpd( 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;
+
+			let a = num.toString();
+
+			if ( a.indexOf( '.' ) === - 1 ) {
+
+				a += '.';
+
+			}
+
+			a += '000000';
+
+			const index = a.indexOf( '.' );
+
+			const d = a.slice( 0, index );
+			const p = a.slice( index + 1, index + 7 );
+
+			return d + '.' + p;
+
+		}
+
+		function toStringsFromArray( array ) {
+
+			const a = [];
+
+			for ( let i = 0, il = array.length; i < il; i ++ ) {
+
+				a.push( toStringsFromNumber( array[ i ] ) );
+
+			}
+
+			return a.join( ',' );
+
+		}
+
+		skin.updateMatrixWorld( true );
+
+		const bones = skin.skeleton.bones;
+		const bones2 = getBindBones( skin );
+
+		const position = new Vector3();
+		const quaternion = new Quaternion();
+		const quaternion2 = new Quaternion();
+		const matrix = new Matrix4();
+
+		const 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 ( let i = 0, il = bones.length; i < il; i ++ ) {
+
+			const bone = bones[ i ];
+			const 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 );
+
+			const pArray = position.sub( bone2.position ).toArray();
+			const 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( '' );
+
+		const lines = array.join( '\n' );
+
+		return ( outputShiftJis === true ) ? unicodeToShiftjis( lines ) : lines;
+
+	}
+
+}
+
+// Unicode to Shift_JIS table
+let u2sTable;
+
+function unicodeToShiftjis( str ) {
+
+	if ( u2sTable === undefined ) {
+
+		const encoder = new MMDParser.CharsetEncoder(); // eslint-disable-line no-undef
+		const table = encoder.s2uTable;
+		u2sTable = {};
+
+		const keys = Object.keys( table );
+
+		for ( let i = 0, il = keys.length; i < il; i ++ ) {
+
+			let key = keys[ i ];
+
+			const value = table[ key ];
+			key = parseInt( key );
+
+			u2sTable[ value ] = key;
+
+		}
+
+	}
+
+	const array = [];
+
+	for ( let i = 0, il = str.length; i < il; i ++ ) {
+
+		const code = str.charCodeAt( i );
+
+		const value = u2sTable[ code ];
+
+		if ( value === undefined ) {
+
+			throw new Error( '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?
+	const poseSkin = skin.clone();
+	poseSkin.pose();
+	return poseSkin.skeleton.bones;
+
+}
+
+export { MMDExporter };

+ 284 - 0
public/archive/static/js/jsm/exporters/OBJExporter.js

@@ -0,0 +1,284 @@
+import {
+	Color,
+	Matrix3,
+	Vector2,
+	Vector3
+} from 'three';
+
+class OBJExporter {
+
+	parse( object ) {
+
+		let output = '';
+
+		let indexVertex = 0;
+		let indexVertexUvs = 0;
+		let indexNormals = 0;
+
+		const vertex = new Vector3();
+		const color = new Color();
+		const normal = new Vector3();
+		const uv = new Vector2();
+
+		const face = [];
+
+		function parseMesh( mesh ) {
+
+			let nbVertex = 0;
+			let nbNormals = 0;
+			let nbVertexUvs = 0;
+
+			const geometry = mesh.geometry;
+
+			const normalMatrixWorld = new Matrix3();
+
+			// shortcuts
+			const vertices = geometry.getAttribute( 'position' );
+			const normals = geometry.getAttribute( 'normal' );
+			const uvs = geometry.getAttribute( 'uv' );
+			const 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 ( let i = 0, l = vertices.count; i < l; i ++, nbVertex ++ ) {
+
+					vertex.fromBufferAttribute( vertices, i );
+
+					// transform 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 ( let i = 0, l = uvs.count; i < l; i ++, nbVertexUvs ++ ) {
+
+					uv.fromBufferAttribute( uvs, i );
+
+					// transform the uv to export format
+					output += 'vt ' + uv.x + ' ' + uv.y + '\n';
+
+				}
+
+			}
+
+			// normals
+
+			if ( normals !== undefined ) {
+
+				normalMatrixWorld.getNormalMatrix( mesh.matrixWorld );
+
+				for ( let i = 0, l = normals.count; i < l; i ++, nbNormals ++ ) {
+
+					normal.fromBufferAttribute( normals, i );
+
+					// transform the normal to world space
+					normal.applyMatrix3( normalMatrixWorld ).normalize();
+
+					// transform the normal to export format
+					output += 'vn ' + normal.x + ' ' + normal.y + ' ' + normal.z + '\n';
+
+				}
+
+			}
+
+			// faces
+
+			if ( indices !== null ) {
+
+				for ( let i = 0, l = indices.count; i < l; i += 3 ) {
+
+					for ( let m = 0; m < 3; m ++ ) {
+
+						const 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 ( let i = 0, l = vertices.count; i < l; i += 3 ) {
+
+					for ( let m = 0; m < 3; m ++ ) {
+
+						const 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';
+
+				}
+
+			}
+
+			// update index
+			indexVertex += nbVertex;
+			indexVertexUvs += nbVertexUvs;
+			indexNormals += nbNormals;
+
+		}
+
+		function parseLine( line ) {
+
+			let nbVertex = 0;
+
+			const geometry = line.geometry;
+			const type = line.type;
+
+			// shortcuts
+			const vertices = geometry.getAttribute( 'position' );
+
+			// name of the line object
+			output += 'o ' + line.name + '\n';
+
+			if ( vertices !== undefined ) {
+
+				for ( let i = 0, l = vertices.count; i < l; i ++, nbVertex ++ ) {
+
+					vertex.fromBufferAttribute( vertices, i );
+
+					// transform 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 ( let j = 1, l = vertices.count; j <= l; j ++ ) {
+
+					output += ( indexVertex + j ) + ' ';
+
+				}
+
+				output += '\n';
+
+			}
+
+			if ( type === 'LineSegments' ) {
+
+				for ( let j = 1, k = j + 1, l = vertices.count; j < l; j += 2, k = j + 1 ) {
+
+					output += 'l ' + ( indexVertex + j ) + ' ' + ( indexVertex + k ) + '\n';
+
+				}
+
+			}
+
+			// update index
+			indexVertex += nbVertex;
+
+		}
+
+		function parsePoints( points ) {
+
+			let nbVertex = 0;
+
+			const geometry = points.geometry;
+
+			const vertices = geometry.getAttribute( 'position' );
+			const colors = geometry.getAttribute( 'color' );
+
+			output += 'o ' + points.name + '\n';
+
+			if ( vertices !== undefined ) {
+
+				for ( let i = 0, l = vertices.count; i < l; i ++, nbVertex ++ ) {
+
+					vertex.fromBufferAttribute( vertices, i );
+					vertex.applyMatrix4( points.matrixWorld );
+
+					output += 'v ' + vertex.x + ' ' + vertex.y + ' ' + vertex.z;
+
+					if ( colors !== undefined ) {
+
+						color.fromBufferAttribute( colors, i ).convertLinearToSRGB();
+
+						output += ' ' + color.r + ' ' + color.g + ' ' + color.b;
+
+					}
+
+					output += '\n';
+
+				}
+
+				output += 'p ';
+
+				for ( let j = 1, l = vertices.count; j <= l; j ++ ) {
+
+					output += ( indexVertex + j ) + ' ';
+
+				}
+
+				output += '\n';
+
+			}
+
+			// update index
+			indexVertex += nbVertex;
+
+		}
+
+		object.traverse( function ( child ) {
+
+			if ( child.isMesh === true ) {
+
+				parseMesh( child );
+
+			}
+
+			if ( child.isLine === true ) {
+
+				parseLine( child );
+
+			}
+
+			if ( child.isPoints === true ) {
+
+				parsePoints( child );
+
+			}
+
+		} );
+
+		return output;
+
+	}
+
+}
+
+export { OBJExporter };

+ 521 - 0
public/archive/static/js/jsm/exporters/PLYExporter.js

@@ -0,0 +1,521 @@
+import {
+	Matrix3,
+	Vector3,
+	Color
+} from 'three';
+
+/**
+ * https://github.com/gkjohnson/ply-exporter-js
+ *
+ * Usage:
+ *  const exporter = new PLYExporter();
+ *
+ *  // second argument is a list of options
+ *  exporter.parse(mesh, data => console.log(data), { binary: true, excludeAttributes: [ 'color' ], littleEndian: true });
+ *
+ * Format Definition:
+ * http://paulbourke.net/dataformats/ply/
+ */
+
+class PLYExporter {
+
+	parse( object, onDone, options ) {
+
+		// Iterate over the valid meshes in the object
+		function traverseMeshes( cb ) {
+
+			object.traverse( function ( child ) {
+
+				if ( child.isMesh === true || child.isPoints ) {
+
+					const mesh = child;
+					const geometry = mesh.geometry;
+
+					if ( geometry.hasAttribute( 'position' ) === true ) {
+
+						cb( mesh, geometry );
+
+					}
+
+				}
+
+			} );
+
+		}
+
+		// Default options
+		const defaultOptions = {
+			binary: false,
+			excludeAttributes: [], // normal, uv, color, index
+			littleEndian: false
+		};
+
+		options = Object.assign( defaultOptions, options );
+
+		const excludeAttributes = options.excludeAttributes;
+		let includeIndices = true;
+		let includeNormals = false;
+		let includeColors = false;
+		let includeUVs = false;
+
+		// count the vertices, check which properties are used,
+		// and cache the BufferGeometry
+		let vertexCount = 0;
+		let faceCount = 0;
+
+		object.traverse( function ( child ) {
+
+			if ( child.isMesh === true ) {
+
+				const mesh = child;
+				const geometry = mesh.geometry;
+
+				const vertices = geometry.getAttribute( 'position' );
+				const normals = geometry.getAttribute( 'normal' );
+				const uvs = geometry.getAttribute( 'uv' );
+				const colors = geometry.getAttribute( 'color' );
+				const 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;
+
+			} else if ( child.isPoints ) {
+
+				const mesh = child;
+				const geometry = mesh.geometry;
+
+				const vertices = geometry.getAttribute( 'position' );
+				vertexCount += vertices.count;
+
+				includeIndices = false;
+
+			}
+
+		} );
+
+		const tempColor = new Color();
+		includeIndices = includeIndices && excludeAttributes.indexOf( 'index' ) === - 1;
+		includeNormals = includeNormals && excludeAttributes.indexOf( 'normal' ) === - 1;
+		includeColors = includeColors && excludeAttributes.indexOf( 'color' ) === - 1;
+		includeUVs = includeUVs && excludeAttributes.indexOf( 'uv' ) === - 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;
+
+		}
+
+		const indexByteCount = 4;
+
+		let header =
+			'ply\n' +
+			`format ${ options.binary ? ( options.littleEndian ? 'binary_little_endian' : '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 int vertex_index\n';
+
+		}
+
+		header += 'end_header\n';
+
+
+		// Generate attribute data
+		const vertex = new Vector3();
+		const normalMatrixWorld = new Matrix3();
+		let result = null;
+
+		if ( options.binary === true ) {
+
+			// Binary File Generation
+			const 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
+			const 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
+			const faceListLength = includeIndices ? faceCount * ( indexByteCount * 3 + 1 ) : 0;
+			const output = new DataView( new ArrayBuffer( headerBin.length + vertexListLength + faceListLength ) );
+			new Uint8Array( output.buffer ).set( headerBin, 0 );
+
+
+			let vOffset = headerBin.length;
+			let fOffset = headerBin.length + vertexListLength;
+			let writtenVertices = 0;
+			traverseMeshes( function ( mesh, geometry ) {
+
+				const vertices = geometry.getAttribute( 'position' );
+				const normals = geometry.getAttribute( 'normal' );
+				const uvs = geometry.getAttribute( 'uv' );
+				const colors = geometry.getAttribute( 'color' );
+				const indices = geometry.getIndex();
+
+				normalMatrixWorld.getNormalMatrix( mesh.matrixWorld );
+
+				for ( let i = 0, l = vertices.count; i < l; i ++ ) {
+
+					vertex.fromBufferAttribute( vertices, i );
+
+					vertex.applyMatrix4( mesh.matrixWorld );
+
+
+					// Position information
+					output.setFloat32( vOffset, vertex.x, options.littleEndian );
+					vOffset += 4;
+
+					output.setFloat32( vOffset, vertex.y, options.littleEndian );
+					vOffset += 4;
+
+					output.setFloat32( vOffset, vertex.z, options.littleEndian );
+					vOffset += 4;
+
+					// Normal information
+					if ( includeNormals === true ) {
+
+						if ( normals != null ) {
+
+							vertex.fromBufferAttribute( normals, i );
+
+							vertex.applyMatrix3( normalMatrixWorld ).normalize();
+
+							output.setFloat32( vOffset, vertex.x, options.littleEndian );
+							vOffset += 4;
+
+							output.setFloat32( vOffset, vertex.y, options.littleEndian );
+							vOffset += 4;
+
+							output.setFloat32( vOffset, vertex.z, options.littleEndian );
+							vOffset += 4;
+
+						} else {
+
+							output.setFloat32( vOffset, 0, options.littleEndian );
+							vOffset += 4;
+
+							output.setFloat32( vOffset, 0, options.littleEndian );
+							vOffset += 4;
+
+							output.setFloat32( vOffset, 0, options.littleEndian );
+							vOffset += 4;
+
+						}
+
+					}
+
+					// UV information
+					if ( includeUVs === true ) {
+
+						if ( uvs != null ) {
+
+							output.setFloat32( vOffset, uvs.getX( i ), options.littleEndian );
+							vOffset += 4;
+
+							output.setFloat32( vOffset, uvs.getY( i ), options.littleEndian );
+							vOffset += 4;
+
+						} else {
+
+							output.setFloat32( vOffset, 0, options.littleEndian );
+							vOffset += 4;
+
+							output.setFloat32( vOffset, 0, options.littleEndian );
+							vOffset += 4;
+
+						}
+
+					}
+
+					// Color information
+					if ( includeColors === true ) {
+
+						if ( colors != null ) {
+
+							tempColor
+								.fromBufferAttribute( colors, i )
+								.convertLinearToSRGB();
+
+							output.setUint8( vOffset, Math.floor( tempColor.r * 255 ) );
+							vOffset += 1;
+
+							output.setUint8( vOffset, Math.floor( tempColor.g * 255 ) );
+							vOffset += 1;
+
+							output.setUint8( vOffset, Math.floor( tempColor.b * 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
+
+					if ( indices !== null ) {
+
+						for ( let i = 0, l = indices.count; i < l; i += 3 ) {
+
+							output.setUint8( fOffset, 3 );
+							fOffset += 1;
+
+							output.setUint32( fOffset, indices.getX( i + 0 ) + writtenVertices, options.littleEndian );
+							fOffset += indexByteCount;
+
+							output.setUint32( fOffset, indices.getX( i + 1 ) + writtenVertices, options.littleEndian );
+							fOffset += indexByteCount;
+
+							output.setUint32( fOffset, indices.getX( i + 2 ) + writtenVertices, options.littleEndian );
+							fOffset += indexByteCount;
+
+						}
+
+					} else {
+
+						for ( let i = 0, l = vertices.count; i < l; i += 3 ) {
+
+							output.setUint8( fOffset, 3 );
+							fOffset += 1;
+
+							output.setUint32( fOffset, writtenVertices + i, options.littleEndian );
+							fOffset += indexByteCount;
+
+							output.setUint32( fOffset, writtenVertices + i + 1, options.littleEndian );
+							fOffset += indexByteCount;
+
+							output.setUint32( fOffset, writtenVertices + i + 2, options.littleEndian );
+							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;
+
+			} );
+
+			result = output.buffer;
+
+		} else {
+
+			// Ascii File Generation
+			// count the number of vertices
+			let writtenVertices = 0;
+			let vertexList = '';
+			let faceList = '';
+
+			traverseMeshes( function ( mesh, geometry ) {
+
+				const vertices = geometry.getAttribute( 'position' );
+				const normals = geometry.getAttribute( 'normal' );
+				const uvs = geometry.getAttribute( 'uv' );
+				const colors = geometry.getAttribute( 'color' );
+				const indices = geometry.getIndex();
+
+				normalMatrixWorld.getNormalMatrix( mesh.matrixWorld );
+
+				// form each line
+				for ( let i = 0, l = vertices.count; i < l; i ++ ) {
+
+					vertex.fromBufferAttribute( vertices, i );
+
+					vertex.applyMatrix4( mesh.matrixWorld );
+
+
+					// Position information
+					let line =
+						vertex.x + ' ' +
+						vertex.y + ' ' +
+						vertex.z;
+
+					// Normal information
+					if ( includeNormals === true ) {
+
+						if ( normals != null ) {
+
+							vertex.fromBufferAttribute( normals, i );
+
+							vertex.applyMatrix3( normalMatrixWorld ).normalize();
+
+							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 {
+
+							line += ' 0 0';
+
+						}
+
+					}
+
+					// Color information
+					if ( includeColors === true ) {
+
+						if ( colors != null ) {
+
+							tempColor
+								.fromBufferAttribute( colors, i )
+								.convertLinearToSRGB();
+
+							line += ' ' +
+								Math.floor( tempColor.r * 255 ) + ' ' +
+								Math.floor( tempColor.g * 255 ) + ' ' +
+								Math.floor( tempColor.b * 255 );
+
+						} else {
+
+							line += ' 255 255 255';
+
+						}
+
+					}
+
+					vertexList += line + '\n';
+
+				}
+
+				// Create the face list
+				if ( includeIndices === true ) {
+
+					if ( indices !== null ) {
+
+						for ( let 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 ( let 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;
+
+			} );
+
+			result = `${ header }${vertexList}${ includeIndices ? `${faceList}\n` : '\n' }`;
+
+		}
+
+		if ( typeof onDone === 'function' ) requestAnimationFrame( () => onDone( result ) );
+
+		return result;
+
+	}
+
+}
+
+export { PLYExporter };

+ 195 - 0
public/archive/static/js/jsm/exporters/STLExporter.js

@@ -0,0 +1,195 @@
+import { Vector3 } from 'three';
+
+/**
+ * Usage:
+ *  const exporter = new STLExporter();
+ *
+ *  // second argument is a list of options
+ *  const data = exporter.parse( mesh, { binary: true } );
+ *
+ */
+
+class STLExporter {
+
+	parse( scene, options = {} ) {
+
+		const binary = options.binary !== undefined ? options.binary : false;
+
+		//
+
+		const objects = [];
+		let triangles = 0;
+
+		scene.traverse( function ( object ) {
+
+			if ( object.isMesh ) {
+
+				const geometry = object.geometry;
+
+				const index = geometry.index;
+				const positionAttribute = geometry.getAttribute( 'position' );
+
+				triangles += ( index !== null ) ? ( index.count / 3 ) : ( positionAttribute.count / 3 );
+
+				objects.push( {
+					object3d: object,
+					geometry: geometry
+				} );
+
+			}
+
+		} );
+
+		let output;
+		let offset = 80; // skip header
+
+		if ( binary === true ) {
+
+			const bufferLength = triangles * 2 + triangles * 3 * 4 * 4 + 80 + 4;
+			const arrayBuffer = new ArrayBuffer( bufferLength );
+			output = new DataView( arrayBuffer );
+			output.setUint32( offset, triangles, true ); offset += 4;
+
+		} else {
+
+			output = '';
+			output += 'solid exported\n';
+
+		}
+
+		const vA = new Vector3();
+		const vB = new Vector3();
+		const vC = new Vector3();
+		const cb = new Vector3();
+		const ab = new Vector3();
+		const normal = new Vector3();
+
+		for ( let i = 0, il = objects.length; i < il; i ++ ) {
+
+			const object = objects[ i ].object3d;
+			const geometry = objects[ i ].geometry;
+
+			const index = geometry.index;
+			const positionAttribute = geometry.getAttribute( 'position' );
+
+			if ( index !== null ) {
+
+				// indexed geometry
+
+				for ( let j = 0; j < index.count; j += 3 ) {
+
+					const a = index.getX( j + 0 );
+					const b = index.getX( j + 1 );
+					const c = index.getX( j + 2 );
+
+					writeFace( a, b, c, positionAttribute, object );
+
+				}
+
+			} else {
+
+				// non-indexed geometry
+
+				for ( let j = 0; j < positionAttribute.count; j += 3 ) {
+
+					const a = j + 0;
+					const b = j + 1;
+					const c = j + 2;
+
+					writeFace( a, b, c, positionAttribute, object );
+
+				}
+
+			}
+
+		}
+
+		if ( binary === false ) {
+
+			output += 'endsolid exported\n';
+
+		}
+
+		return output;
+
+		function writeFace( a, b, c, positionAttribute, object ) {
+
+			vA.fromBufferAttribute( positionAttribute, a );
+			vB.fromBufferAttribute( positionAttribute, b );
+			vC.fromBufferAttribute( positionAttribute, c );
+
+			if ( object.isSkinnedMesh === true ) {
+
+				object.boneTransform( a, vA );
+				object.boneTransform( b, vB );
+				object.boneTransform( c, vC );
+
+			}
+
+			vA.applyMatrix4( object.matrixWorld );
+			vB.applyMatrix4( object.matrixWorld );
+			vC.applyMatrix4( object.matrixWorld );
+
+			writeNormal( vA, vB, vC );
+
+			writeVertex( vA );
+			writeVertex( vB );
+			writeVertex( vC );
+
+			if ( binary === true ) {
+
+				output.setUint16( offset, 0, true ); offset += 2;
+
+			} else {
+
+				output += '\t\tendloop\n';
+				output += '\tendfacet\n';
+
+			}
+
+		}
+
+		function writeNormal( vA, vB, vC ) {
+
+			cb.subVectors( vC, vB );
+			ab.subVectors( vA, vB );
+			cb.cross( ab ).normalize();
+
+			normal.copy( cb ).normalize();
+
+			if ( binary === true ) {
+
+				output.setFloat32( offset, normal.x, true ); offset += 4;
+				output.setFloat32( offset, normal.y, true ); offset += 4;
+				output.setFloat32( offset, normal.z, true ); offset += 4;
+
+			} else {
+
+				output += '\tfacet normal ' + normal.x + ' ' + normal.y + ' ' + normal.z + '\n';
+				output += '\t\touter loop\n';
+
+			}
+
+		}
+
+		function writeVertex( vertex ) {
+
+			if ( binary === true ) {
+
+				output.setFloat32( offset, vertex.x, true ); offset += 4;
+				output.setFloat32( offset, vertex.y, true ); offset += 4;
+				output.setFloat32( offset, vertex.z, true ); offset += 4;
+
+			} else {
+
+				output += '\t\t\tvertex ' + vertex.x + ' ' + vertex.y + ' ' + vertex.z + '\n';
+
+			}
+
+		}
+
+	}
+
+}
+
+export { STLExporter };

+ 558 - 0
public/archive/static/js/jsm/exporters/USDZExporter.js

@@ -0,0 +1,558 @@
+import {
+	DoubleSide
+} from 'three';
+
+import * as fflate from '../libs/fflate.module.js';
+
+class USDZExporter {
+
+	async parse( scene ) {
+
+		const files = {};
+		const modelFileName = 'model.usda';
+
+		// model file should be first in USDZ archive so we init it here
+		files[ modelFileName ] = null;
+
+		let output = buildHeader();
+
+		const materials = {};
+		const textures = {};
+
+		scene.traverseVisible( ( object ) => {
+
+			if ( object.isMesh ) {
+
+				if ( object.material.isMeshStandardMaterial ) {
+
+					const geometry = object.geometry;
+					const material = object.material;
+
+					const geometryFileName = 'geometries/Geometry_' + geometry.id + '.usd';
+
+					if ( ! ( geometryFileName in files ) ) {
+
+						const meshObject = buildMeshObject( geometry );
+						files[ geometryFileName ] = buildUSDFileAsString( meshObject );
+
+					}
+
+					if ( ! ( material.uuid in materials ) ) {
+
+						materials[ material.uuid ] = material;
+
+					}
+
+					output += buildXform( object, geometry, material );
+
+				} else {
+
+					console.warn( 'THREE.USDZExporter: Unsupported material type (USDZ only supports MeshStandardMaterial)', object );
+
+				}
+
+			}
+
+		} );
+
+		output += buildMaterials( materials, textures );
+
+		files[ modelFileName ] = fflate.strToU8( output );
+		output = null;
+
+		for ( const id in textures ) {
+
+			const texture = textures[ id ];
+			const color = id.split( '_' )[ 1 ];
+			const isRGBA = texture.format === 1023;
+
+			const canvas = imageToCanvas( texture.image, color );
+			const blob = await new Promise( resolve => canvas.toBlob( resolve, isRGBA ? 'image/png' : 'image/jpeg', 1 ) );
+
+			files[ `textures/Texture_${ id }.${ isRGBA ? 'png' : 'jpg' }` ] = new Uint8Array( await blob.arrayBuffer() );
+
+		}
+
+		// 64 byte alignment
+		// https://github.com/101arrowz/fflate/issues/39#issuecomment-777263109
+
+		let offset = 0;
+
+		for ( const filename in files ) {
+
+			const file = files[ filename ];
+			const headerSize = 34 + filename.length;
+
+			offset += headerSize;
+
+			const offsetMod64 = offset & 63;
+
+			if ( offsetMod64 !== 4 ) {
+
+				const padLength = 64 - offsetMod64;
+				const padding = new Uint8Array( padLength );
+
+				files[ filename ] = [ file, { extra: { 12345: padding } } ];
+
+			}
+
+			offset = file.length;
+
+		}
+
+		return fflate.zipSync( files, { level: 0 } );
+
+	}
+
+}
+
+function imageToCanvas( image, color ) {
+
+	if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
+		( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
+		( typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas ) ||
+		( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) {
+
+		const scale = 1024 / Math.max( image.width, image.height );
+
+		const canvas = document.createElement( 'canvas' );
+		canvas.width = image.width * Math.min( 1, scale );
+		canvas.height = image.height * Math.min( 1, scale );
+
+		const context = canvas.getContext( '2d' );
+		context.drawImage( image, 0, 0, canvas.width, canvas.height );
+
+		if ( color !== undefined ) {
+
+			const hex = parseInt( color, 16 );
+
+			const r = ( hex >> 16 & 255 ) / 255;
+			const g = ( hex >> 8 & 255 ) / 255;
+			const b = ( hex & 255 ) / 255;
+
+			const imagedata = context.getImageData( 0, 0, canvas.width, canvas.height );
+			const data = imagedata.data;
+
+			for ( let i = 0; i < data.length; i += 4 ) {
+
+				data[ i + 0 ] = data[ i + 0 ] * r;
+				data[ i + 1 ] = data[ i + 1 ] * g;
+				data[ i + 2 ] = data[ i + 2 ] * b;
+
+			}
+
+			context.putImageData( imagedata, 0, 0 );
+
+		}
+
+		return canvas;
+
+	}
+
+}
+
+//
+
+const PRECISION = 7;
+
+function buildHeader() {
+
+	return `#usda 1.0
+(
+    customLayerData = {
+        string creator = "Three.js USDZExporter"
+    }
+    metersPerUnit = 1
+    upAxis = "Y"
+)
+
+`;
+
+}
+
+function buildUSDFileAsString( dataToInsert ) {
+
+	let output = buildHeader();
+	output += dataToInsert;
+	return fflate.strToU8( output );
+
+}
+
+// Xform
+
+function buildXform( object, geometry, material ) {
+
+	const name = 'Object_' + object.id;
+	const transform = buildMatrix( object.matrixWorld );
+
+	if ( object.matrixWorld.determinant() < 0 ) {
+
+		console.warn( 'THREE.USDZExporter: USDZ does not support negative scales', object );
+
+	}
+
+	return `def Xform "${ name }" (
+    prepend references = @./geometries/Geometry_${ geometry.id }.usd@</Geometry>
+)
+{
+    matrix4d xformOp:transform = ${ transform }
+    uniform token[] xformOpOrder = ["xformOp:transform"]
+
+    rel material:binding = </Materials/Material_${ material.id }>
+}
+
+`;
+
+}
+
+function buildMatrix( matrix ) {
+
+	const array = matrix.elements;
+
+	return `( ${ buildMatrixRow( array, 0 ) }, ${ buildMatrixRow( array, 4 ) }, ${ buildMatrixRow( array, 8 ) }, ${ buildMatrixRow( array, 12 ) } )`;
+
+}
+
+function buildMatrixRow( array, offset ) {
+
+	return `(${ array[ offset + 0 ] }, ${ array[ offset + 1 ] }, ${ array[ offset + 2 ] }, ${ array[ offset + 3 ] })`;
+
+}
+
+// Mesh
+
+function buildMeshObject( geometry ) {
+
+	const mesh = buildMesh( geometry );
+	return `
+def "Geometry"
+{
+  ${mesh}
+}
+`;
+
+}
+
+function buildMesh( geometry ) {
+
+	const name = 'Geometry';
+	const attributes = geometry.attributes;
+	const count = attributes.position.count;
+
+	return `
+    def Mesh "${ name }"
+    {
+        int[] faceVertexCounts = [${ buildMeshVertexCount( geometry ) }]
+        int[] faceVertexIndices = [${ buildMeshVertexIndices( geometry ) }]
+        normal3f[] normals = [${ buildVector3Array( attributes.normal, count )}] (
+            interpolation = "vertex"
+        )
+        point3f[] points = [${ buildVector3Array( attributes.position, count )}]
+        float2[] primvars:st = [${ buildVector2Array( attributes.uv, count )}] (
+            interpolation = "vertex"
+        )
+        uniform token subdivisionScheme = "none"
+    }
+`;
+
+}
+
+function buildMeshVertexCount( geometry ) {
+
+	const count = geometry.index !== null ? geometry.index.count : geometry.attributes.position.count;
+
+	return Array( count / 3 ).fill( 3 ).join( ', ' );
+
+}
+
+function buildMeshVertexIndices( geometry ) {
+
+	const index = geometry.index;
+	const array = [];
+
+	if ( index !== null ) {
+
+		for ( let i = 0; i < index.count; i ++ ) {
+
+			array.push( index.getX( i ) );
+
+		}
+
+	} else {
+
+		const length = geometry.attributes.position.count;
+
+		for ( let i = 0; i < length; i ++ ) {
+
+			array.push( i );
+
+		}
+
+	}
+
+	return array.join( ', ' );
+
+}
+
+function buildVector3Array( attribute, count ) {
+
+	if ( attribute === undefined ) {
+
+		console.warn( 'USDZExporter: Normals missing.' );
+		return Array( count ).fill( '(0, 0, 0)' ).join( ', ' );
+
+	}
+
+	const array = [];
+
+	for ( let i = 0; i < attribute.count; i ++ ) {
+
+		const x = attribute.getX( i );
+		const y = attribute.getY( i );
+		const z = attribute.getZ( i );
+
+		array.push( `(${ x.toPrecision( PRECISION ) }, ${ y.toPrecision( PRECISION ) }, ${ z.toPrecision( PRECISION ) })` );
+
+	}
+
+	return array.join( ', ' );
+
+}
+
+function buildVector2Array( attribute, count ) {
+
+	if ( attribute === undefined ) {
+
+		console.warn( 'USDZExporter: UVs missing.' );
+		return Array( count ).fill( '(0, 0)' ).join( ', ' );
+
+	}
+
+	const array = [];
+
+	for ( let i = 0; i < attribute.count; i ++ ) {
+
+		const x = attribute.getX( i );
+		const y = attribute.getY( i );
+
+		array.push( `(${ x.toPrecision( PRECISION ) }, ${ 1 - y.toPrecision( PRECISION ) })` );
+
+	}
+
+	return array.join( ', ' );
+
+}
+
+// Materials
+
+function buildMaterials( materials, textures ) {
+
+	const array = [];
+
+	for ( const uuid in materials ) {
+
+		const material = materials[ uuid ];
+
+		array.push( buildMaterial( material, textures ) );
+
+	}
+
+	return `def "Materials"
+{
+${ array.join( '' ) }
+}
+
+`;
+
+}
+
+function buildMaterial( material, textures ) {
+
+	// https://graphics.pixar.com/usd/docs/UsdPreviewSurface-Proposal.html
+
+	const pad = '            ';
+	const inputs = [];
+	const samplers = [];
+
+	function buildTexture( texture, mapType, color ) {
+
+		const id = texture.id + ( color ? '_' + color.getHexString() : '' );
+		const isRGBA = texture.format === 1023;
+
+		textures[ id ] = texture;
+
+		return `
+        def Shader "Transform2d_${ mapType }" (
+            sdrMetadata = {
+                string role = "math"
+            }
+        )
+        {
+            uniform token info:id = "UsdTransform2d"
+            float2 inputs:in.connect = </Materials/Material_${ material.id }/uvReader_st.outputs:result>
+            float2 inputs:scale = ${ buildVector2( texture.repeat ) }
+            float2 inputs:translation = ${ buildVector2( texture.offset ) }
+            float2 outputs:result
+        }
+
+        def Shader "Texture_${ texture.id }_${ mapType }"
+        {
+            uniform token info:id = "UsdUVTexture"
+            asset inputs:file = @textures/Texture_${ id }.${ isRGBA ? 'png' : 'jpg' }@
+            float2 inputs:st.connect = </Materials/Material_${ material.id }/Transform2d_${ mapType }.outputs:result>
+            token inputs:wrapS = "repeat"
+            token inputs:wrapT = "repeat"
+            float outputs:r
+            float outputs:g
+            float outputs:b
+            float3 outputs:rgb
+            ${ material.transparent || material.alphaTest > 0.0 ? 'float outputs:a' : '' }
+        }`;
+
+	}
+
+
+	if ( material.side === DoubleSide ) {
+
+		console.warn( 'THREE.USDZExporter: USDZ does not support double sided materials', material );
+
+	}
+
+	if ( material.map !== null ) {
+
+		inputs.push( `${ pad }color3f inputs:diffuseColor.connect = </Materials/Material_${ material.id }/Texture_${ material.map.id }_diffuse.outputs:rgb>` );
+
+		if ( material.transparent ) {
+
+			inputs.push( `${ pad }float inputs:opacity.connect = </Materials/Material_${ material.id }/Texture_${ material.map.id }_diffuse.outputs:a>` );
+
+		} else if ( material.alphaTest > 0.0 ) {
+
+			inputs.push( `${ pad }float inputs:opacity.connect = </Materials/Material_${ material.id }/Texture_${ material.map.id }_diffuse.outputs:a>` );
+			inputs.push( `${ pad }float inputs:opacityThreshold = ${material.alphaTest}` );
+
+		}
+
+		samplers.push( buildTexture( material.map, 'diffuse', material.color ) );
+
+	} else {
+
+		inputs.push( `${ pad }color3f inputs:diffuseColor = ${ buildColor( material.color ) }` );
+
+	}
+
+	if ( material.emissiveMap !== null ) {
+
+		inputs.push( `${ pad }color3f inputs:emissiveColor.connect = </Materials/Material_${ material.id }/Texture_${ material.emissiveMap.id }_emissive.outputs:rgb>` );
+
+		samplers.push( buildTexture( material.emissiveMap, 'emissive' ) );
+
+	} else if ( material.emissive.getHex() > 0 ) {
+
+		inputs.push( `${ pad }color3f inputs:emissiveColor = ${ buildColor( material.emissive ) }` );
+
+	}
+
+	if ( material.normalMap !== null ) {
+
+		inputs.push( `${ pad }normal3f inputs:normal.connect = </Materials/Material_${ material.id }/Texture_${ material.normalMap.id }_normal.outputs:rgb>` );
+
+		samplers.push( buildTexture( material.normalMap, 'normal' ) );
+
+	}
+
+	if ( material.aoMap !== null ) {
+
+		inputs.push( `${ pad }float inputs:occlusion.connect = </Materials/Material_${ material.id }/Texture_${ material.aoMap.id }_occlusion.outputs:r>` );
+
+		samplers.push( buildTexture( material.aoMap, 'occlusion' ) );
+
+	}
+
+	if ( material.roughnessMap !== null && material.roughness === 1 ) {
+
+		inputs.push( `${ pad }float inputs:roughness.connect = </Materials/Material_${ material.id }/Texture_${ material.roughnessMap.id }_roughness.outputs:g>` );
+
+		samplers.push( buildTexture( material.roughnessMap, 'roughness' ) );
+
+	} else {
+
+		inputs.push( `${ pad }float inputs:roughness = ${ material.roughness }` );
+
+	}
+
+	if ( material.metalnessMap !== null && material.metalness === 1 ) {
+
+		inputs.push( `${ pad }float inputs:metallic.connect = </Materials/Material_${ material.id }/Texture_${ material.metalnessMap.id }_metallic.outputs:b>` );
+
+		samplers.push( buildTexture( material.metalnessMap, 'metallic' ) );
+
+	} else {
+
+		inputs.push( `${ pad }float inputs:metallic = ${ material.metalness }` );
+
+	}
+
+	if ( material.alphaMap !== null ) {
+
+		inputs.push( `${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.alphaMap.id}_opacity.outputs:r>` );
+		inputs.push( `${pad}float inputs:opacityThreshold = 0.0001` );
+
+		samplers.push( buildTexture( material.alphaMap, 'opacity' ) );
+
+	} else {
+
+		inputs.push( `${pad}float inputs:opacity = ${material.opacity}` );
+
+	}
+
+	if ( material.isMeshPhysicalMaterial ) {
+
+		inputs.push( `${ pad }float inputs:clearcoat = ${ material.clearcoat }` );
+		inputs.push( `${ pad }float inputs:clearcoatRoughness = ${ material.clearcoatRoughness }` );
+		inputs.push( `${ pad }float inputs:ior = ${ material.ior }` );
+
+	}
+
+	return `
+    def Material "Material_${ material.id }"
+    {
+        def Shader "PreviewSurface"
+        {
+            uniform token info:id = "UsdPreviewSurface"
+${ inputs.join( '\n' ) }
+            int inputs:useSpecularWorkflow = 0
+            token outputs:surface
+        }
+
+        token outputs:surface.connect = </Materials/Material_${ material.id }/PreviewSurface.outputs:surface>
+        token inputs:frame:stPrimvarName = "st"
+
+        def Shader "uvReader_st"
+        {
+            uniform token info:id = "UsdPrimvarReader_float2"
+            token inputs:varname.connect = </Materials/Material_${ material.id }.inputs:frame:stPrimvarName>
+            float2 inputs:fallback = (0.0, 0.0)
+            float2 outputs:result
+        }
+
+${ samplers.join( '\n' ) }
+
+    }
+`;
+
+}
+
+function buildColor( color ) {
+
+	return `(${ color.r }, ${ color.g }, ${ color.b })`;
+
+}
+
+function buildVector2( vector ) {
+
+	return `(${ vector.x }, ${ vector.y })`;
+
+}
+
+export { USDZExporter };

+ 69 - 0
public/archive/static/js/jsm/geometries/BoxLineGeometry.js

@@ -0,0 +1,69 @@
+import {
+	BufferGeometry,
+	Float32BufferAttribute
+} from 'three';
+
+class BoxLineGeometry extends BufferGeometry {
+
+	constructor( width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1 ) {
+
+		super();
+
+		widthSegments = Math.floor( widthSegments );
+		heightSegments = Math.floor( heightSegments );
+		depthSegments = Math.floor( depthSegments );
+
+		const widthHalf = width / 2;
+		const heightHalf = height / 2;
+		const depthHalf = depth / 2;
+
+		const segmentWidth = width / widthSegments;
+		const segmentHeight = height / heightSegments;
+		const segmentDepth = depth / depthSegments;
+
+		const vertices = [];
+
+		let x = - widthHalf;
+		let y = - heightHalf;
+		let z = - depthHalf;
+
+		for ( let i = 0; i <= widthSegments; i ++ ) {
+
+			vertices.push( x, - heightHalf, - depthHalf, x, heightHalf, - depthHalf );
+			vertices.push( x, heightHalf, - depthHalf, x, heightHalf, depthHalf );
+			vertices.push( x, heightHalf, depthHalf, x, - heightHalf, depthHalf );
+			vertices.push( x, - heightHalf, depthHalf, x, - heightHalf, - depthHalf );
+
+			x += segmentWidth;
+
+		}
+
+		for ( let i = 0; i <= heightSegments; i ++ ) {
+
+			vertices.push( - widthHalf, y, - depthHalf, widthHalf, y, - depthHalf );
+			vertices.push( widthHalf, y, - depthHalf, widthHalf, y, depthHalf );
+			vertices.push( widthHalf, y, depthHalf, - widthHalf, y, depthHalf );
+			vertices.push( - widthHalf, y, depthHalf, - widthHalf, y, - depthHalf );
+
+			y += segmentHeight;
+
+		}
+
+		for ( let i = 0; i <= depthSegments; i ++ ) {
+
+			vertices.push( - widthHalf, - heightHalf, z, - widthHalf, heightHalf, z );
+			vertices.push( - widthHalf, heightHalf, z, widthHalf, heightHalf, z );
+			vertices.push( widthHalf, heightHalf, z, widthHalf, - heightHalf, z );
+			vertices.push( widthHalf, - heightHalf, z, - widthHalf, - heightHalf, z );
+
+			z += segmentDepth;
+
+		}
+
+		this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
+
+	}
+
+}
+
+export { BoxLineGeometry };

+ 59 - 0
public/archive/static/js/jsm/geometries/ConvexGeometry.js

@@ -0,0 +1,59 @@
+import {
+	BufferGeometry,
+	Float32BufferAttribute
+} from 'three';
+import { ConvexHull } from '../math/ConvexHull.js';
+
+class ConvexGeometry extends BufferGeometry {
+
+	constructor( points = [] ) {
+
+		super();
+
+		// buffers
+
+		const vertices = [];
+		const normals = [];
+
+		if ( ConvexHull === undefined ) {
+
+			console.error( 'THREE.ConvexGeometry: ConvexGeometry relies on ConvexHull' );
+
+		}
+
+		const convexHull = new ConvexHull().setFromPoints( points );
+
+		// generate vertices and normals
+
+		const faces = convexHull.faces;
+
+		for ( let i = 0; i < faces.length; i ++ ) {
+
+			const face = faces[ i ];
+			let edge = face.edge;
+
+			// we move along a doubly-connected edge list to access all face points (see HalfEdge docs)
+
+			do {
+
+				const point = edge.head().point;
+
+				vertices.push( point.x, point.y, point.z );
+				normals.push( face.normal.x, face.normal.y, face.normal.z );
+
+				edge = edge.next;
+
+			} while ( edge !== face.edge );
+
+		}
+
+		// build geometry
+
+		this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
+		this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
+
+	}
+
+}
+
+export { ConvexGeometry };

+ 356 - 0
public/archive/static/js/jsm/geometries/DecalGeometry.js

@@ -0,0 +1,356 @@
+import {
+	BufferGeometry,
+	Float32BufferAttribute,
+	Matrix4,
+	Vector3
+} from 'three';
+
+/**
+ * You can use this geometry to create a decal mesh, that serves different kinds of purposes.
+ * e.g. adding unique details to models, performing dynamic visual environmental changes or covering seams.
+ *
+ * Constructor parameter:
+ *
+ * mesh — Any mesh object
+ * position — Position of the decal projector
+ * orientation — Orientation of the decal projector
+ * size — Size of the decal projector
+ *
+ * reference: http://blog.wolfire.com/2009/06/how-to-project-decals/
+ *
+ */
+
+class DecalGeometry extends BufferGeometry {
+
+	constructor( mesh, position, orientation, size ) {
+
+		super();
+
+		// buffers
+
+		const vertices = [];
+		const normals = [];
+		const uvs = [];
+
+		// helpers
+
+		const plane = new Vector3();
+
+		// this matrix represents the transformation of the decal projector
+
+		const projectorMatrix = new Matrix4();
+		projectorMatrix.makeRotationFromEuler( orientation );
+		projectorMatrix.setPosition( position );
+
+		const projectorMatrixInverse = new Matrix4();
+		projectorMatrixInverse.copy( projectorMatrix ).invert();
+
+		// generate buffers
+
+		generate();
+
+		// build geometry
+
+		this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
+		this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
+		this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
+
+		function generate() {
+
+			let decalVertices = [];
+
+			const vertex = new Vector3();
+			const normal = new Vector3();
+
+			// handle different geometry types
+
+			const geometry = mesh.geometry;
+
+			const positionAttribute = geometry.attributes.position;
+			const normalAttribute = geometry.attributes.normal;
+
+			// first, create an array of 'DecalVertex' objects
+			// three consecutive 'DecalVertex' objects represent a single face
+			//
+			// this data structure will be later used to perform the clipping
+
+			if ( geometry.index !== null ) {
+
+				// indexed BufferGeometry
+
+				const index = geometry.index;
+
+				for ( let i = 0; i < index.count; i ++ ) {
+
+					vertex.fromBufferAttribute( positionAttribute, index.getX( i ) );
+					normal.fromBufferAttribute( normalAttribute, index.getX( i ) );
+
+					pushDecalVertex( decalVertices, vertex, normal );
+
+				}
+
+			} else {
+
+				// non-indexed BufferGeometry
+
+				for ( let i = 0; i < positionAttribute.count; i ++ ) {
+
+					vertex.fromBufferAttribute( positionAttribute, i );
+					normal.fromBufferAttribute( normalAttribute, i );
+
+					pushDecalVertex( decalVertices, vertex, normal );
+
+				}
+
+			}
+
+			// second, clip the geometry so that it doesn't extend out from the projector
+
+			decalVertices = clipGeometry( decalVertices, plane.set( 1, 0, 0 ) );
+			decalVertices = clipGeometry( decalVertices, plane.set( - 1, 0, 0 ) );
+			decalVertices = clipGeometry( decalVertices, plane.set( 0, 1, 0 ) );
+			decalVertices = clipGeometry( decalVertices, plane.set( 0, - 1, 0 ) );
+			decalVertices = clipGeometry( decalVertices, plane.set( 0, 0, 1 ) );
+			decalVertices = clipGeometry( decalVertices, plane.set( 0, 0, - 1 ) );
+
+			// third, generate final vertices, normals and uvs
+
+			for ( let i = 0; i < decalVertices.length; i ++ ) {
+
+				const decalVertex = decalVertices[ i ];
+
+				// create texture coordinates (we are still in projector space)
+
+				uvs.push(
+					0.5 + ( decalVertex.position.x / size.x ),
+					0.5 + ( decalVertex.position.y / size.y )
+				);
+
+				// transform the vertex back to world space
+
+				decalVertex.position.applyMatrix4( projectorMatrix );
+
+				// now create vertex and normal buffer data
+
+				vertices.push( decalVertex.position.x, decalVertex.position.y, decalVertex.position.z );
+				normals.push( decalVertex.normal.x, decalVertex.normal.y, decalVertex.normal.z );
+
+			}
+
+		}
+
+		function pushDecalVertex( decalVertices, vertex, normal ) {
+
+			// transform the vertex to world space, then to projector space
+
+			vertex.applyMatrix4( mesh.matrixWorld );
+			vertex.applyMatrix4( projectorMatrixInverse );
+
+			normal.transformDirection( mesh.matrixWorld );
+
+			decalVertices.push( new DecalVertex( vertex.clone(), normal.clone() ) );
+
+		}
+
+		function clipGeometry( inVertices, plane ) {
+
+			const outVertices = [];
+
+			const s = 0.5 * Math.abs( size.dot( plane ) );
+
+			// a single iteration clips one face,
+			// which consists of three consecutive 'DecalVertex' objects
+
+			for ( let i = 0; i < inVertices.length; i += 3 ) {
+
+				let total = 0;
+				let nV1;
+				let nV2;
+				let nV3;
+				let nV4;
+
+				const d1 = inVertices[ i + 0 ].position.dot( plane ) - s;
+				const d2 = inVertices[ i + 1 ].position.dot( plane ) - s;
+				const d3 = inVertices[ i + 2 ].position.dot( plane ) - s;
+
+				const v1Out = d1 > 0;
+				const v2Out = d2 > 0;
+				const v3Out = d3 > 0;
+
+				// calculate, how many vertices of the face lie outside of the clipping plane
+
+				total = ( v1Out ? 1 : 0 ) + ( v2Out ? 1 : 0 ) + ( v3Out ? 1 : 0 );
+
+				switch ( total ) {
+
+					case 0: {
+
+						// the entire face lies inside of the plane, no clipping needed
+
+						outVertices.push( inVertices[ i ] );
+						outVertices.push( inVertices[ i + 1 ] );
+						outVertices.push( inVertices[ i + 2 ] );
+						break;
+
+					}
+
+					case 1: {
+
+						// one vertex lies outside of the plane, perform clipping
+
+						if ( v1Out ) {
+
+							nV1 = inVertices[ i + 1 ];
+							nV2 = inVertices[ i + 2 ];
+							nV3 = clip( inVertices[ i ], nV1, plane, s );
+							nV4 = clip( inVertices[ i ], nV2, plane, s );
+
+						}
+
+						if ( v2Out ) {
+
+							nV1 = inVertices[ i ];
+							nV2 = inVertices[ i + 2 ];
+							nV3 = clip( inVertices[ i + 1 ], nV1, plane, s );
+							nV4 = clip( inVertices[ i + 1 ], nV2, plane, s );
+
+							outVertices.push( nV3 );
+							outVertices.push( nV2.clone() );
+							outVertices.push( nV1.clone() );
+
+							outVertices.push( nV2.clone() );
+							outVertices.push( nV3.clone() );
+							outVertices.push( nV4 );
+							break;
+
+						}
+
+						if ( v3Out ) {
+
+							nV1 = inVertices[ i ];
+							nV2 = inVertices[ i + 1 ];
+							nV3 = clip( inVertices[ i + 2 ], nV1, plane, s );
+							nV4 = clip( inVertices[ i + 2 ], nV2, plane, s );
+
+						}
+
+						outVertices.push( nV1.clone() );
+						outVertices.push( nV2.clone() );
+						outVertices.push( nV3 );
+
+						outVertices.push( nV4 );
+						outVertices.push( nV3.clone() );
+						outVertices.push( nV2.clone() );
+
+						break;
+
+					}
+
+					case 2: {
+
+						// two vertices lies outside of the plane, perform clipping
+
+						if ( ! v1Out ) {
+
+							nV1 = inVertices[ i ].clone();
+							nV2 = clip( nV1, inVertices[ i + 1 ], plane, s );
+							nV3 = clip( nV1, inVertices[ i + 2 ], plane, s );
+							outVertices.push( nV1 );
+							outVertices.push( nV2 );
+							outVertices.push( nV3 );
+
+						}
+
+						if ( ! v2Out ) {
+
+							nV1 = inVertices[ i + 1 ].clone();
+							nV2 = clip( nV1, inVertices[ i + 2 ], plane, s );
+							nV3 = clip( nV1, inVertices[ i ], plane, s );
+							outVertices.push( nV1 );
+							outVertices.push( nV2 );
+							outVertices.push( nV3 );
+
+						}
+
+						if ( ! v3Out ) {
+
+							nV1 = inVertices[ i + 2 ].clone();
+							nV2 = clip( nV1, inVertices[ i ], plane, s );
+							nV3 = clip( nV1, inVertices[ i + 1 ], plane, s );
+							outVertices.push( nV1 );
+							outVertices.push( nV2 );
+							outVertices.push( nV3 );
+
+						}
+
+						break;
+
+					}
+
+					case 3: {
+
+						// the entire face lies outside of the plane, so let's discard the corresponding vertices
+
+						break;
+
+					}
+
+				}
+
+			}
+
+			return outVertices;
+
+		}
+
+		function clip( v0, v1, p, s ) {
+
+			const d0 = v0.position.dot( p ) - s;
+			const d1 = v1.position.dot( p ) - s;
+
+			const s0 = d0 / ( d0 - d1 );
+
+			const v = new DecalVertex(
+				new Vector3(
+					v0.position.x + s0 * ( v1.position.x - v0.position.x ),
+					v0.position.y + s0 * ( v1.position.y - v0.position.y ),
+					v0.position.z + s0 * ( v1.position.z - v0.position.z )
+				),
+				new Vector3(
+					v0.normal.x + s0 * ( v1.normal.x - v0.normal.x ),
+					v0.normal.y + s0 * ( v1.normal.y - v0.normal.y ),
+					v0.normal.z + s0 * ( v1.normal.z - v0.normal.z )
+				)
+			);
+
+			// need to clip more values (texture coordinates)? do it this way:
+			// intersectpoint.value = a.value + s * ( b.value - a.value );
+
+			return v;
+
+		}
+
+	}
+
+}
+
+// helper
+
+class DecalVertex {
+
+	constructor( position, normal ) {
+
+		this.position = position;
+		this.normal = normal;
+
+	}
+
+	clone() {
+
+		return new this.constructor( this.position.clone(), this.normal.clone() );
+
+	}
+
+}
+
+export { DecalGeometry, DecalVertex };

+ 1017 - 0
public/archive/static/js/jsm/geometries/LightningStrike.js

@@ -0,0 +1,1017 @@
+import {
+	BufferGeometry,
+	DynamicDrawUsage,
+	Float32BufferAttribute,
+	MathUtils,
+	Uint32BufferAttribute,
+	Vector3
+} from 'three';
+import { SimplexNoise } from '../math/SimplexNoise.js';
+
+/**
+ * @fileoverview LightningStrike object for creating lightning strikes and voltaic arcs.
+ *
+ *
+ * Usage
+ *
+ * var myRay = new LightningStrike( paramsObject );
+ * var myRayMesh = new THREE.Mesh( myRay, myMaterial );
+ * scene.add( myRayMesh );
+ * ...
+ * myRay.update( currentTime );
+ *
+ * The "currentTime" can vary its rate, go forwards, backwards or even jump, but it cannot be negative.
+ *
+ * You should normally leave the ray position to (0, 0, 0). You should control it by changing the sourceOffset and destOffset parameters.
+ *
+ *
+ * LightningStrike parameters
+ *
+ * The paramsObject can contain any of the following parameters.
+ *
+ * Legend:
+ * 'LightningStrike' (also called 'ray'): An independent voltaic arc with its ramifications and defined with a set of parameters.
+ * 'Subray': A ramification of the ray. It is not a LightningStrike object.
+ * 'Segment': A linear segment piece of a subray.
+ * 'Leaf segment': A ray segment which cannot be smaller.
+ *
+ *
+ * The following parameters can be changed any time and if they vary smoothly, the ray form will also change smoothly:
+ *
+ * @param {Vector3} sourceOffset The point where the ray starts.
+ *
+ * @param {Vector3} destOffset The point where the ray ends.
+ *
+ * @param {double} timeScale The rate at wich the ray form changes in time. Default: 1
+ *
+ * @param {double} roughness From 0 to 1. The higher the value, the more wrinkled is the ray. Default: 0.9
+ *
+ * @param {double} straightness From 0 to 1. The higher the value, the more straight will be a subray path. Default: 0.7
+ *
+ * @param {Vector3} up0 Ray 'up' direction at the ray starting point. Must be normalized. It should be perpendicular to the ray forward direction but it doesn't matter much.
+ *
+ * @param {Vector3} up1 Like the up0 parameter but at the end of the ray. Must be normalized.
+ *
+ * @param {double} radius0 Radius of the main ray trunk at the start point. Default: 1
+ *
+ * @param {double} radius1 Radius of the main ray trunk at the end point. Default: 1
+ *
+ * @param {double} radius0Factor The radius0 of a subray is this factor times the radius0 of its parent subray. Default: 0.5
+ *
+ * @param {double} radius1Factor The radius1 of a subray is this factor times the radius1 of its parent subray. Default: 0.2
+ *
+ * @param {minRadius} Minimum value a subray radius0 or radius1 can get. Default: 0.1
+ *
+ *
+ * The following parameters should not be changed after lightning creation. They can be changed but the ray will change its form abruptly:
+ *
+ * @param {boolean} isEternal If true the ray never extinguishes. Otherwise its life is controlled by the 'birthTime' and 'deathTime' parameters. Default: true if any of those two parameters is undefined.
+ *
+ * @param {double} birthTime The time at which the ray starts its life and begins propagating. Only if isEternal is false. Default: None.
+ *
+ * @param {double} deathTime The time at which the ray ends vanishing and its life. Only if isEternal is false. Default: None.
+ *
+ * @param {double} propagationTimeFactor From 0 to 1. Lifetime factor at which the ray ends propagating and enters the steady phase. For example, 0.1 means it is propagating 1/10 of its lifetime. Default: 0.1
+ *
+ * @param {double} vanishingTimeFactor From 0 to 1. Lifetime factor at which the ray ends the steady phase and begins vanishing. For example, 0.9 means it is vanishing 1/10 of its lifetime. Default: 0.9
+ *
+ * @param {double} subrayPeriod Subrays cycle periodically. This is their time period. Default: 4
+ *
+ * @param {double} subrayDutyCycle From 0 to 1. This is the fraction of time a subray is active. Default: 0.6
+ *
+ *
+ * These parameters cannot change after lightning creation:
+ *
+ * @param {integer} maxIterations: Greater than 0. The number of ray's leaf segments is 2**maxIterations. Default: 9
+ *
+ * @param {boolean} isStatic Set to true only for rays which won't change over time and are not attached to moving objects (Rare case). It is used to set the vertex buffers non-dynamic. You can omit calling update() for these rays.
+ *
+ * @param {integer} ramification Greater than 0. Maximum number of child subrays a subray can have. Default: 5
+ *
+ * @param {integer} maxSubrayRecursion Greater than 0. Maximum level of recursion (subray descendant generations). Default: 3
+ *
+ * @param {double} recursionProbability From 0 to 1. The lower the value, the less chance each new generation of subrays has to generate new subrays. Default: 0.6
+ *
+ * @param {boolean} generateUVs If true, the ray geometry will have uv coordinates generated. u runs along the ray, and v across its perimeter. Default: false.
+ *
+ * @param {Object} randomGenerator Set here your random number generator which will seed the SimplexNoise and other decisions during ray tree creation.
+ * It can be used to generate repeatable rays. For that, set also the noiseSeed parameter, and each ray created with that generator and seed pair will be identical in time.
+ * The randomGenerator parameter should be an object with a random() function similar to Math.random, but seedable.
+ * It must have also a getSeed() method, which returns the current seed, and a setSeed( seed ) method, which accepts as seed a fractional number from 0 to 1, as well as any other number.
+ * The default value is an internal generator for some uses and Math.random for others (It is non-repeatable even if noiseSeed is supplied)
+ *
+ * @param {double} noiseSeed Seed used to make repeatable rays (see the randomGenerator)
+ *
+ * @param {function} onDecideSubrayCreation Set this to change the callback which decides subray creation. You can look at the default callback in the code (createDefaultSubrayCreationCallbacks)for more info.
+ *
+ * @param {function} onSubrayCreation This is another callback, more simple than the previous one. It can be used to adapt the form of subrays or other parameters once a subray has been created and initialized. It is used in the examples to adapt subrays to a sphere or to a plane.
+ *
+ *
+*/
+
+class LightningStrike extends BufferGeometry {
+
+	constructor( rayParameters = {} ) {
+
+		super();
+
+		this.isLightningStrike = true;
+
+		this.type = 'LightningStrike';
+
+		// Set parameters, and set undefined parameters to default values
+		this.init( LightningStrike.copyParameters( rayParameters, rayParameters ) );
+
+		// Creates and populates the mesh
+		this.createMesh();
+
+	}
+
+	static createRandomGenerator() {
+
+		const numSeeds = 2053;
+		const seeds = [];
+
+		for ( let i = 0; i < numSeeds; i ++ ) {
+
+			seeds.push( Math.random() );
+
+		}
+
+		const generator = {
+
+			currentSeed: 0,
+
+			random: function () {
+
+				const value = seeds[ generator.currentSeed ];
+
+				generator.currentSeed = ( generator.currentSeed + 1 ) % numSeeds;
+
+				return value;
+
+			},
+
+			getSeed: function () {
+
+				return generator.currentSeed / numSeeds;
+
+			},
+
+			setSeed: function ( seed ) {
+
+				generator.currentSeed = Math.floor( seed * numSeeds ) % numSeeds;
+
+			}
+
+		};
+
+		return generator;
+
+	}
+
+	static copyParameters( dest = {}, source = {} ) {
+
+		const vecCopy = function ( v ) {
+
+			if ( source === dest ) {
+
+				return v;
+
+			} else {
+
+				return v.clone();
+
+			}
+
+		};
+
+		dest.sourceOffset = source.sourceOffset !== undefined ? vecCopy( source.sourceOffset ) : new Vector3( 0, 100, 0 ),
+		dest.destOffset = source.destOffset !== undefined ? vecCopy( source.destOffset ) : new Vector3( 0, 0, 0 ),
+
+		dest.timeScale = source.timeScale !== undefined ? source.timeScale : 1,
+		dest.roughness = source.roughness !== undefined ? source.roughness : 0.9,
+		dest.straightness = source.straightness !== undefined ? source.straightness : 0.7,
+
+		dest.up0 = source.up0 !== undefined ? vecCopy( source.up0 ) : new Vector3( 0, 0, 1 );
+		dest.up1 = source.up1 !== undefined ? vecCopy( source.up1 ) : new Vector3( 0, 0, 1 ),
+		dest.radius0 = source.radius0 !== undefined ? source.radius0 : 1,
+		dest.radius1 = source.radius1 !== undefined ? source.radius1 : 1,
+		dest.radius0Factor = source.radius0Factor !== undefined ? source.radius0Factor : 0.5,
+		dest.radius1Factor = source.radius1Factor !== undefined ? source.radius1Factor : 0.2,
+		dest.minRadius = source.minRadius !== undefined ? source.minRadius : 0.2,
+
+		// These parameters should not be changed after lightning creation. They can be changed but the ray will change its form abruptly:
+
+		dest.isEternal = source.isEternal !== undefined ? source.isEternal : ( source.birthTime === undefined || source.deathTime === undefined ),
+		dest.birthTime = source.birthTime,
+		dest.deathTime = source.deathTime,
+		dest.propagationTimeFactor = source.propagationTimeFactor !== undefined ? source.propagationTimeFactor : 0.1,
+		dest.vanishingTimeFactor = source.vanishingTimeFactor !== undefined ? source.vanishingTimeFactor : 0.9,
+		dest.subrayPeriod = source.subrayPeriod !== undefined ? source.subrayPeriod : 4,
+		dest.subrayDutyCycle = source.subrayDutyCycle !== undefined ? source.subrayDutyCycle : 0.6;
+
+		// These parameters cannot change after lightning creation:
+
+		dest.maxIterations = source.maxIterations !== undefined ? source.maxIterations : 9;
+		dest.isStatic = source.isStatic !== undefined ? source.isStatic : false;
+		dest.ramification = source.ramification !== undefined ? source.ramification : 5;
+		dest.maxSubrayRecursion = source.maxSubrayRecursion !== undefined ? source.maxSubrayRecursion : 3;
+		dest.recursionProbability = source.recursionProbability !== undefined ? source.recursionProbability : 0.6;
+		dest.generateUVs = source.generateUVs !== undefined ? source.generateUVs : false;
+		dest.randomGenerator = source.randomGenerator,
+		dest.noiseSeed = source.noiseSeed,
+		dest.onDecideSubrayCreation = source.onDecideSubrayCreation,
+		dest.onSubrayCreation = source.onSubrayCreation;
+
+		return dest;
+
+	}
+
+	update( time ) {
+
+		if ( this.isStatic ) return;
+
+		if ( this.rayParameters.isEternal || ( this.rayParameters.birthTime <= time && time <= this.rayParameters.deathTime ) ) {
+
+			this.updateMesh( time );
+
+			if ( time < this.subrays[ 0 ].endPropagationTime ) {
+
+				this.state = LightningStrike.RAY_PROPAGATING;
+
+			} else if ( time > this.subrays[ 0 ].beginVanishingTime ) {
+
+				this.state = LightningStrike.RAY_VANISHING;
+
+			} else {
+
+				this.state = LightningStrike.RAY_STEADY;
+
+			}
+
+			this.visible = true;
+
+		} else {
+
+			this.visible = false;
+
+			if ( time < this.rayParameters.birthTime ) {
+
+				this.state = LightningStrike.RAY_UNBORN;
+
+			} else {
+
+				this.state = LightningStrike.RAY_EXTINGUISHED;
+
+			}
+
+		}
+
+	}
+
+	init( rayParameters ) {
+
+		// Init all the state from the parameters
+
+		this.rayParameters = rayParameters;
+
+		// These parameters cannot change after lightning creation:
+
+		this.maxIterations = rayParameters.maxIterations !== undefined ? Math.floor( rayParameters.maxIterations ) : 9;
+		rayParameters.maxIterations = this.maxIterations;
+		this.isStatic = rayParameters.isStatic !== undefined ? rayParameters.isStatic : false;
+		rayParameters.isStatic = this.isStatic;
+		this.ramification = rayParameters.ramification !== undefined ? Math.floor( rayParameters.ramification ) : 5;
+		rayParameters.ramification = this.ramification;
+		this.maxSubrayRecursion = rayParameters.maxSubrayRecursion !== undefined ? Math.floor( rayParameters.maxSubrayRecursion ) : 3;
+		rayParameters.maxSubrayRecursion = this.maxSubrayRecursion;
+		this.recursionProbability = rayParameters.recursionProbability !== undefined ? rayParameters.recursionProbability : 0.6;
+		rayParameters.recursionProbability = this.recursionProbability;
+		this.generateUVs = rayParameters.generateUVs !== undefined ? rayParameters.generateUVs : false;
+		rayParameters.generateUVs = this.generateUVs;
+
+		// Random generator
+		if ( rayParameters.randomGenerator !== undefined ) {
+
+			this.randomGenerator = rayParameters.randomGenerator;
+			this.seedGenerator = rayParameters.randomGenerator;
+
+			if ( rayParameters.noiseSeed !== undefined ) {
+
+				this.seedGenerator.setSeed( rayParameters.noiseSeed );
+
+			}
+
+		} else {
+
+			this.randomGenerator = LightningStrike.createRandomGenerator();
+			this.seedGenerator = Math;
+
+		}
+
+		// Ray creation callbacks
+		if ( rayParameters.onDecideSubrayCreation !== undefined ) {
+
+			this.onDecideSubrayCreation = rayParameters.onDecideSubrayCreation;
+
+		} else {
+
+			this.createDefaultSubrayCreationCallbacks();
+
+			if ( rayParameters.onSubrayCreation !== undefined ) {
+
+				this.onSubrayCreation = rayParameters.onSubrayCreation;
+
+			}
+
+		}
+
+		// Internal state
+
+		this.state = LightningStrike.RAY_INITIALIZED;
+
+		this.maxSubrays = Math.ceil( 1 + Math.pow( this.ramification, Math.max( 0, this.maxSubrayRecursion - 1 ) ) );
+		rayParameters.maxSubrays = this.maxSubrays;
+
+		this.maxRaySegments = 2 * ( 1 << this.maxIterations );
+
+		this.subrays = [];
+
+		for ( let i = 0; i < this.maxSubrays; i ++ ) {
+
+			this.subrays.push( this.createSubray() );
+
+		}
+
+		this.raySegments = [];
+
+		for ( let i = 0; i < this.maxRaySegments; i ++ ) {
+
+			this.raySegments.push( this.createSegment() );
+
+		}
+
+		this.time = 0;
+		this.timeFraction = 0;
+		this.currentSegmentCallback = null;
+		this.currentCreateTriangleVertices = this.generateUVs ? this.createTriangleVerticesWithUVs : this.createTriangleVerticesWithoutUVs;
+		this.numSubrays = 0;
+		this.currentSubray = null;
+		this.currentSegmentIndex = 0;
+		this.isInitialSegment = false;
+		this.subrayProbability = 0;
+
+		this.currentVertex = 0;
+		this.currentIndex = 0;
+		this.currentCoordinate = 0;
+		this.currentUVCoordinate = 0;
+		this.vertices = null;
+		this.uvs = null;
+		this.indices = null;
+		this.positionAttribute = null;
+		this.uvsAttribute = null;
+
+		this.simplexX = new SimplexNoise( this.seedGenerator );
+		this.simplexY = new SimplexNoise( this.seedGenerator );
+		this.simplexZ = new SimplexNoise( this.seedGenerator );
+
+		// Temp vectors
+		this.forwards = new Vector3();
+		this.forwardsFill = new Vector3();
+		this.side = new Vector3();
+		this.down = new Vector3();
+		this.middlePos = new Vector3();
+		this.middleLinPos = new Vector3();
+		this.newPos = new Vector3();
+		this.vPos = new Vector3();
+		this.cross1 = new Vector3();
+
+	}
+
+	createMesh() {
+
+		const maxDrawableSegmentsPerSubRay = 1 << this.maxIterations;
+
+		const maxVerts = 3 * ( maxDrawableSegmentsPerSubRay + 1 ) * this.maxSubrays;
+		const maxIndices = 18 * maxDrawableSegmentsPerSubRay * this.maxSubrays;
+
+		this.vertices = new Float32Array( maxVerts * 3 );
+		this.indices = new Uint32Array( maxIndices );
+
+		if ( this.generateUVs ) {
+
+			this.uvs = new Float32Array( maxVerts * 2 );
+
+		}
+
+		// Populate the mesh
+		this.fillMesh( 0 );
+
+		this.setIndex( new Uint32BufferAttribute( this.indices, 1 ) );
+
+		this.positionAttribute = new Float32BufferAttribute( this.vertices, 3 );
+		this.setAttribute( 'position', this.positionAttribute );
+
+		if ( this.generateUVs ) {
+
+			this.uvsAttribute = new Float32BufferAttribute( new Float32Array( this.uvs ), 2 );
+			this.setAttribute( 'uv', this.uvsAttribute );
+
+		}
+
+		if ( ! this.isStatic ) {
+
+			this.index.usage = DynamicDrawUsage;
+			this.positionAttribute.usage = DynamicDrawUsage;
+
+			if ( this.generateUVs ) {
+
+				this.uvsAttribute.usage = DynamicDrawUsage;
+
+			}
+
+		}
+
+		// Store buffers for later modification
+		this.vertices = this.positionAttribute.array;
+		this.indices = this.index.array;
+
+		if ( this.generateUVs ) {
+
+			this.uvs = this.uvsAttribute.array;
+
+		}
+
+	}
+
+	updateMesh( time ) {
+
+		this.fillMesh( time );
+
+		this.drawRange.count = this.currentIndex;
+
+		this.index.needsUpdate = true;
+
+		this.positionAttribute.needsUpdate = true;
+
+		if ( this.generateUVs ) {
+
+			this.uvsAttribute.needsUpdate = true;
+
+		}
+
+	}
+
+	fillMesh( time ) {
+
+		const scope = this;
+
+		this.currentVertex = 0;
+		this.currentIndex = 0;
+		this.currentCoordinate = 0;
+		this.currentUVCoordinate = 0;
+
+		this.fractalRay( time, function fillVertices( segment ) {
+
+			const subray = scope.currentSubray;
+
+			if ( time < subray.birthTime ) { //&& ( ! this.rayParameters.isEternal || scope.currentSubray.recursion > 0 ) ) {
+
+				return;
+
+			} else if ( this.rayParameters.isEternal && scope.currentSubray.recursion == 0 ) {
+
+				// Eternal rays don't propagate nor vanish, but its subrays do
+
+				scope.createPrism( segment );
+
+				scope.onDecideSubrayCreation( segment, scope );
+
+			} else if ( time < subray.endPropagationTime ) {
+
+				if ( scope.timeFraction >= segment.fraction0 * subray.propagationTimeFactor ) {
+
+					// Ray propagation has arrived to this segment
+
+					scope.createPrism( segment );
+
+					scope.onDecideSubrayCreation( segment, scope );
+
+				}
+
+			} else if ( time < subray.beginVanishingTime ) {
+
+				// Ray is steady (nor propagating nor vanishing)
+
+				scope.createPrism( segment );
+
+				scope.onDecideSubrayCreation( segment, scope );
+
+			} else {
+
+				if ( scope.timeFraction <= subray.vanishingTimeFactor + segment.fraction1 * ( 1 - subray.vanishingTimeFactor ) ) {
+
+					// Segment has not yet vanished
+
+					scope.createPrism( segment );
+
+				}
+
+				scope.onDecideSubrayCreation( segment, scope );
+
+			}
+
+		} );
+
+	}
+
+	addNewSubray( /*rayParameters*/ ) {
+
+		return this.subrays[ this.numSubrays ++ ];
+
+	}
+
+	initSubray( subray, rayParameters ) {
+
+		subray.pos0.copy( rayParameters.sourceOffset );
+		subray.pos1.copy( rayParameters.destOffset );
+		subray.up0.copy( rayParameters.up0 );
+		subray.up1.copy( rayParameters.up1 );
+		subray.radius0 = rayParameters.radius0;
+		subray.radius1 = rayParameters.radius1;
+		subray.birthTime = rayParameters.birthTime;
+		subray.deathTime = rayParameters.deathTime;
+		subray.timeScale = rayParameters.timeScale;
+		subray.roughness = rayParameters.roughness;
+		subray.straightness = rayParameters.straightness;
+		subray.propagationTimeFactor = rayParameters.propagationTimeFactor;
+		subray.vanishingTimeFactor = rayParameters.vanishingTimeFactor;
+
+		subray.maxIterations = this.maxIterations;
+		subray.seed = rayParameters.noiseSeed !== undefined ? rayParameters.noiseSeed : 0;
+		subray.recursion = 0;
+
+	}
+
+	fractalRay( time, segmentCallback ) {
+
+		this.time = time;
+		this.currentSegmentCallback = segmentCallback;
+		this.numSubrays = 0;
+
+		// Add the top level subray
+		this.initSubray( this.addNewSubray(), this.rayParameters );
+
+		// Process all subrays that are being generated until consuming all of them
+		for ( let subrayIndex = 0; subrayIndex < this.numSubrays; subrayIndex ++ ) {
+
+			const subray = this.subrays[ subrayIndex ];
+			this.currentSubray = subray;
+
+			this.randomGenerator.setSeed( subray.seed );
+
+			subray.endPropagationTime = MathUtils.lerp( subray.birthTime, subray.deathTime, subray.propagationTimeFactor );
+			subray.beginVanishingTime = MathUtils.lerp( subray.deathTime, subray.birthTime, 1 - subray.vanishingTimeFactor );
+
+			const random1 = this.randomGenerator.random;
+			subray.linPos0.set( random1(), random1(), random1() ).multiplyScalar( 1000 );
+			subray.linPos1.set( random1(), random1(), random1() ).multiplyScalar( 1000 );
+
+			this.timeFraction = ( time - subray.birthTime ) / ( subray.deathTime - subray.birthTime );
+
+			this.currentSegmentIndex = 0;
+			this.isInitialSegment = true;
+
+			const segment = this.getNewSegment();
+			segment.iteration = 0;
+			segment.pos0.copy( subray.pos0 );
+			segment.pos1.copy( subray.pos1 );
+			segment.linPos0.copy( subray.linPos0 );
+			segment.linPos1.copy( subray.linPos1 );
+			segment.up0.copy( subray.up0 );
+			segment.up1.copy( subray.up1 );
+			segment.radius0 = subray.radius0;
+			segment.radius1 = subray.radius1;
+			segment.fraction0 = 0;
+			segment.fraction1 = 1;
+			segment.positionVariationFactor = 1 - subray.straightness;
+
+			this.subrayProbability = this.ramification * Math.pow( this.recursionProbability, subray.recursion ) / ( 1 << subray.maxIterations );
+
+			this.fractalRayRecursive( segment );
+
+		}
+
+		this.currentSegmentCallback = null;
+		this.currentSubray = null;
+
+	}
+
+	fractalRayRecursive( segment ) {
+
+		// Leave recursion condition
+		if ( segment.iteration >= this.currentSubray.maxIterations ) {
+
+			this.currentSegmentCallback( segment );
+
+			return;
+
+		}
+
+		// Interpolation
+		this.forwards.subVectors( segment.pos1, segment.pos0 );
+		let lForwards = this.forwards.length();
+
+		if ( lForwards < 0.000001 ) {
+
+			this.forwards.set( 0, 0, 0.01 );
+			lForwards = this.forwards.length();
+
+		}
+
+		const middleRadius = ( segment.radius0 + segment.radius1 ) * 0.5;
+		const middleFraction = ( segment.fraction0 + segment.fraction1 ) * 0.5;
+
+		const timeDimension = this.time * this.currentSubray.timeScale * Math.pow( 2, segment.iteration );
+
+		this.middlePos.lerpVectors( segment.pos0, segment.pos1, 0.5 );
+		this.middleLinPos.lerpVectors( segment.linPos0, segment.linPos1, 0.5 );
+		const p = this.middleLinPos;
+
+		// Noise
+		this.newPos.set( this.simplexX.noise4d( p.x, p.y, p.z, timeDimension ),
+			this.simplexY.noise4d( p.x, p.y, p.z, timeDimension ),
+			this.simplexZ.noise4d( p.x, p.y, p.z, timeDimension ) );
+
+		this.newPos.multiplyScalar( segment.positionVariationFactor * lForwards );
+		this.newPos.add( this.middlePos );
+
+		// Recursion
+
+		const newSegment1 = this.getNewSegment();
+		newSegment1.pos0.copy( segment.pos0 );
+		newSegment1.pos1.copy( this.newPos );
+		newSegment1.linPos0.copy( segment.linPos0 );
+		newSegment1.linPos1.copy( this.middleLinPos );
+		newSegment1.up0.copy( segment.up0 );
+		newSegment1.up1.copy( segment.up1 );
+		newSegment1.radius0 = segment.radius0;
+		newSegment1.radius1 = middleRadius;
+		newSegment1.fraction0 = segment.fraction0;
+		newSegment1.fraction1 = middleFraction;
+		newSegment1.positionVariationFactor = segment.positionVariationFactor * this.currentSubray.roughness;
+		newSegment1.iteration = segment.iteration + 1;
+
+		const newSegment2 = this.getNewSegment();
+		newSegment2.pos0.copy( this.newPos );
+		newSegment2.pos1.copy( segment.pos1 );
+		newSegment2.linPos0.copy( this.middleLinPos );
+		newSegment2.linPos1.copy( segment.linPos1 );
+		this.cross1.crossVectors( segment.up0, this.forwards.normalize() );
+		newSegment2.up0.crossVectors( this.forwards, this.cross1 ).normalize();
+		newSegment2.up1.copy( segment.up1 );
+		newSegment2.radius0 = middleRadius;
+		newSegment2.radius1 = segment.radius1;
+		newSegment2.fraction0 = middleFraction;
+		newSegment2.fraction1 = segment.fraction1;
+		newSegment2.positionVariationFactor = segment.positionVariationFactor * this.currentSubray.roughness;
+		newSegment2.iteration = segment.iteration + 1;
+
+		this.fractalRayRecursive( newSegment1 );
+
+		this.fractalRayRecursive( newSegment2 );
+
+	}
+
+	createPrism( segment ) {
+
+		// Creates one triangular prism and its vertices at the segment
+
+		this.forwardsFill.subVectors( segment.pos1, segment.pos0 ).normalize();
+
+		if ( this.isInitialSegment ) {
+
+			this.currentCreateTriangleVertices( segment.pos0, segment.up0, this.forwardsFill, segment.radius0, 0 );
+
+			this.isInitialSegment = false;
+
+		}
+
+		this.currentCreateTriangleVertices( segment.pos1, segment.up0, this.forwardsFill, segment.radius1, segment.fraction1 );
+
+		this.createPrismFaces();
+
+	}
+
+	createTriangleVerticesWithoutUVs( pos, up, forwards, radius ) {
+
+		// Create an equilateral triangle (only vertices)
+
+		this.side.crossVectors( up, forwards ).multiplyScalar( radius * LightningStrike.COS30DEG );
+		this.down.copy( up ).multiplyScalar( - radius * LightningStrike.SIN30DEG );
+
+		const p = this.vPos;
+		const v = this.vertices;
+
+		p.copy( pos ).sub( this.side ).add( this.down );
+
+		v[ this.currentCoordinate ++ ] = p.x;
+		v[ this.currentCoordinate ++ ] = p.y;
+		v[ this.currentCoordinate ++ ] = p.z;
+
+		p.copy( pos ).add( this.side ).add( this.down );
+
+		v[ this.currentCoordinate ++ ] = p.x;
+		v[ this.currentCoordinate ++ ] = p.y;
+		v[ this.currentCoordinate ++ ] = p.z;
+
+		p.copy( up ).multiplyScalar( radius ).add( pos );
+
+		v[ this.currentCoordinate ++ ] = p.x;
+		v[ this.currentCoordinate ++ ] = p.y;
+		v[ this.currentCoordinate ++ ] = p.z;
+
+		this.currentVertex += 3;
+
+	}
+
+	createTriangleVerticesWithUVs( pos, up, forwards, radius, u ) {
+
+		// Create an equilateral triangle (only vertices)
+
+		this.side.crossVectors( up, forwards ).multiplyScalar( radius * LightningStrike.COS30DEG );
+		this.down.copy( up ).multiplyScalar( - radius * LightningStrike.SIN30DEG );
+
+		const p = this.vPos;
+		const v = this.vertices;
+		const uv = this.uvs;
+
+		p.copy( pos ).sub( this.side ).add( this.down );
+
+		v[ this.currentCoordinate ++ ] = p.x;
+		v[ this.currentCoordinate ++ ] = p.y;
+		v[ this.currentCoordinate ++ ] = p.z;
+
+		uv[ this.currentUVCoordinate ++ ] = u;
+		uv[ this.currentUVCoordinate ++ ] = 0;
+
+		p.copy( pos ).add( this.side ).add( this.down );
+
+		v[ this.currentCoordinate ++ ] = p.x;
+		v[ this.currentCoordinate ++ ] = p.y;
+		v[ this.currentCoordinate ++ ] = p.z;
+
+		uv[ this.currentUVCoordinate ++ ] = u;
+		uv[ this.currentUVCoordinate ++ ] = 0.5;
+
+		p.copy( up ).multiplyScalar( radius ).add( pos );
+
+		v[ this.currentCoordinate ++ ] = p.x;
+		v[ this.currentCoordinate ++ ] = p.y;
+		v[ this.currentCoordinate ++ ] = p.z;
+
+		uv[ this.currentUVCoordinate ++ ] = u;
+		uv[ this.currentUVCoordinate ++ ] = 1;
+
+		this.currentVertex += 3;
+
+	}
+
+	createPrismFaces( vertex/*, index*/ ) {
+
+		const indices = this.indices;
+		vertex = this.currentVertex - 6;
+
+		indices[ this.currentIndex ++ ] = vertex + 1;
+		indices[ this.currentIndex ++ ] = vertex + 2;
+		indices[ this.currentIndex ++ ] = vertex + 5;
+		indices[ this.currentIndex ++ ] = vertex + 1;
+		indices[ this.currentIndex ++ ] = vertex + 5;
+		indices[ this.currentIndex ++ ] = vertex + 4;
+		indices[ this.currentIndex ++ ] = vertex + 0;
+		indices[ this.currentIndex ++ ] = vertex + 1;
+		indices[ this.currentIndex ++ ] = vertex + 4;
+		indices[ this.currentIndex ++ ] = vertex + 0;
+		indices[ this.currentIndex ++ ] = vertex + 4;
+		indices[ this.currentIndex ++ ] = vertex + 3;
+		indices[ this.currentIndex ++ ] = vertex + 2;
+		indices[ this.currentIndex ++ ] = vertex + 0;
+		indices[ this.currentIndex ++ ] = vertex + 3;
+		indices[ this.currentIndex ++ ] = vertex + 2;
+		indices[ this.currentIndex ++ ] = vertex + 3;
+		indices[ this.currentIndex ++ ] = vertex + 5;
+
+	}
+
+	createDefaultSubrayCreationCallbacks() {
+
+		const random1 = this.randomGenerator.random;
+
+		this.onDecideSubrayCreation = function ( segment, lightningStrike ) {
+
+			// Decide subrays creation at parent (sub)ray segment
+
+			const subray = lightningStrike.currentSubray;
+
+			const period = lightningStrike.rayParameters.subrayPeriod;
+			const dutyCycle = lightningStrike.rayParameters.subrayDutyCycle;
+
+			const phase0 = ( lightningStrike.rayParameters.isEternal && subray.recursion == 0 ) ? - random1() * period : MathUtils.lerp( subray.birthTime, subray.endPropagationTime, segment.fraction0 ) - random1() * period;
+
+			const phase = lightningStrike.time - phase0;
+			const currentCycle = Math.floor( phase / period );
+
+			const childSubraySeed = random1() * ( currentCycle + 1 );
+
+			const isActive = phase % period <= dutyCycle * period;
+
+			let probability = 0;
+
+			if ( isActive ) {
+
+				probability = lightningStrike.subrayProbability;
+				// Distribution test: probability *= segment.fraction0 > 0.5 && segment.fraction0 < 0.9 ? 1 / 0.4 : 0;
+
+			}
+
+			if ( subray.recursion < lightningStrike.maxSubrayRecursion && lightningStrike.numSubrays < lightningStrike.maxSubrays && random1() < probability ) {
+
+				const childSubray = lightningStrike.addNewSubray();
+
+				const parentSeed = lightningStrike.randomGenerator.getSeed();
+				childSubray.seed = childSubraySeed;
+				lightningStrike.randomGenerator.setSeed( childSubraySeed );
+
+				childSubray.recursion = subray.recursion + 1;
+				childSubray.maxIterations = Math.max( 1, subray.maxIterations - 1 );
+
+				childSubray.linPos0.set( random1(), random1(), random1() ).multiplyScalar( 1000 );
+				childSubray.linPos1.set( random1(), random1(), random1() ).multiplyScalar( 1000 );
+				childSubray.up0.copy( subray.up0 );
+				childSubray.up1.copy( subray.up1 );
+				childSubray.radius0 = segment.radius0 * lightningStrike.rayParameters.radius0Factor;
+				childSubray.radius1 = Math.min( lightningStrike.rayParameters.minRadius, segment.radius1 * lightningStrike.rayParameters.radius1Factor );
+
+				childSubray.birthTime = phase0 + ( currentCycle ) * period;
+				childSubray.deathTime = childSubray.birthTime + period * dutyCycle;
+
+				if ( ! lightningStrike.rayParameters.isEternal && subray.recursion == 0 ) {
+
+					childSubray.birthTime = Math.max( childSubray.birthTime, subray.birthTime );
+					childSubray.deathTime = Math.min( childSubray.deathTime, subray.deathTime );
+
+				}
+
+				childSubray.timeScale = subray.timeScale * 2;
+				childSubray.roughness = subray.roughness;
+				childSubray.straightness = subray.straightness;
+				childSubray.propagationTimeFactor = subray.propagationTimeFactor;
+				childSubray.vanishingTimeFactor = subray.vanishingTimeFactor;
+
+				lightningStrike.onSubrayCreation( segment, subray, childSubray, lightningStrike );
+
+				lightningStrike.randomGenerator.setSeed( parentSeed );
+
+			}
+
+		};
+
+		const vec1Pos = new Vector3();
+		const vec2Forward = new Vector3();
+		const vec3Side = new Vector3();
+		const vec4Up = new Vector3();
+
+		this.onSubrayCreation = function ( segment, parentSubray, childSubray, lightningStrike ) {
+
+			// Decide childSubray origin and destination positions (pos0 and pos1) and possibly other properties of childSubray
+
+			// Just use the default cone position generator
+			lightningStrike.subrayCylinderPosition( segment, parentSubray, childSubray, 0.5, 0.6, 0.2 );
+
+		};
+
+		this.subrayConePosition = function ( segment, parentSubray, childSubray, heightFactor, sideWidthFactor, minSideWidthFactor ) {
+
+			// Sets childSubray pos0 and pos1 in a cone
+
+			childSubray.pos0.copy( segment.pos0 );
+
+			vec1Pos.subVectors( parentSubray.pos1, parentSubray.pos0 );
+			vec2Forward.copy( vec1Pos ).normalize();
+			vec1Pos.multiplyScalar( segment.fraction0 + ( 1 - segment.fraction0 ) * ( random1() * heightFactor ) );
+			const length = vec1Pos.length();
+			vec3Side.crossVectors( parentSubray.up0, vec2Forward );
+			const angle = 2 * Math.PI * random1();
+			vec3Side.multiplyScalar( Math.cos( angle ) );
+			vec4Up.copy( parentSubray.up0 ).multiplyScalar( Math.sin( angle ) );
+
+			childSubray.pos1.copy( vec3Side ).add( vec4Up ).multiplyScalar( length * sideWidthFactor * ( minSideWidthFactor + random1() * ( 1 - minSideWidthFactor ) ) ).add( vec1Pos ).add( parentSubray.pos0 );
+
+		};
+
+		this.subrayCylinderPosition = function ( segment, parentSubray, childSubray, heightFactor, sideWidthFactor, minSideWidthFactor ) {
+
+			// Sets childSubray pos0 and pos1 in a cylinder
+
+			childSubray.pos0.copy( segment.pos0 );
+
+			vec1Pos.subVectors( parentSubray.pos1, parentSubray.pos0 );
+			vec2Forward.copy( vec1Pos ).normalize();
+			vec1Pos.multiplyScalar( segment.fraction0 + ( 1 - segment.fraction0 ) * ( ( 2 * random1() - 1 ) * heightFactor ) );
+			const length = vec1Pos.length();
+			vec3Side.crossVectors( parentSubray.up0, vec2Forward );
+			const angle = 2 * Math.PI * random1();
+			vec3Side.multiplyScalar( Math.cos( angle ) );
+			vec4Up.copy( parentSubray.up0 ).multiplyScalar( Math.sin( angle ) );
+
+			childSubray.pos1.copy( vec3Side ).add( vec4Up ).multiplyScalar( length * sideWidthFactor * ( minSideWidthFactor + random1() * ( 1 - minSideWidthFactor ) ) ).add( vec1Pos ).add( parentSubray.pos0 );
+
+		};
+
+	}
+
+	createSubray() {
+
+		return {
+
+			seed: 0,
+			maxIterations: 0,
+			recursion: 0,
+			pos0: new Vector3(),
+			pos1: new Vector3(),
+			linPos0: new Vector3(),
+			linPos1: new Vector3(),
+			up0: new Vector3(),
+			up1: new Vector3(),
+			radius0: 0,
+			radius1: 0,
+			birthTime: 0,
+			deathTime: 0,
+			timeScale: 0,
+			roughness: 0,
+			straightness: 0,
+			propagationTimeFactor: 0,
+			vanishingTimeFactor: 0,
+			endPropagationTime: 0,
+			beginVanishingTime: 0
+
+		};
+
+	}
+
+	createSegment() {
+
+		return {
+			iteration: 0,
+			pos0: new Vector3(),
+			pos1: new Vector3(),
+			linPos0: new Vector3(),
+			linPos1: new Vector3(),
+			up0: new Vector3(),
+			up1: new Vector3(),
+			radius0: 0,
+			radius1: 0,
+			fraction0: 0,
+			fraction1: 0,
+			positionVariationFactor: 0
+		};
+
+	}
+
+	getNewSegment() {
+
+		return this.raySegments[ this.currentSegmentIndex ++ ];
+
+	}
+
+	copy( source ) {
+
+		super.copy( source );
+
+		this.init( LightningStrike.copyParameters( {}, source.rayParameters ) );
+
+		return this;
+
+	}
+
+	clone() {
+
+		return new this.constructor( LightningStrike.copyParameters( {}, this.rayParameters ) );
+
+	}
+
+}
+
+// Ray states
+LightningStrike.RAY_INITIALIZED = 0;
+LightningStrike.RAY_UNBORN = 1;
+LightningStrike.RAY_PROPAGATING = 2;
+LightningStrike.RAY_STEADY = 3;
+LightningStrike.RAY_VANISHING = 4;
+LightningStrike.RAY_EXTINGUISHED = 5;
+
+LightningStrike.COS30DEG = Math.cos( 30 * Math.PI / 180 );
+LightningStrike.SIN30DEG = Math.sin( 30 * Math.PI / 180 );
+
+export { LightningStrike };

+ 254 - 0
public/archive/static/js/jsm/geometries/ParametricGeometries.js

@@ -0,0 +1,254 @@
+import {
+	Curve,
+	Vector3
+} from 'three';
+
+import { ParametricGeometry } from './ParametricGeometry.js';
+
+/**
+ * Experimenting of primitive geometry creation using Surface Parametric equations
+ */
+
+const ParametricGeometries = {
+
+	klein: function ( v, u, target ) {
+
+		u *= Math.PI;
+		v *= 2 * Math.PI;
+
+		u = u * 2;
+		let x, z;
+		if ( u < Math.PI ) {
+
+			x = 3 * Math.cos( u ) * ( 1 + Math.sin( u ) ) + ( 2 * ( 1 - Math.cos( u ) / 2 ) ) * Math.cos( u ) * Math.cos( v );
+			z = - 8 * Math.sin( u ) - 2 * ( 1 - Math.cos( u ) / 2 ) * Math.sin( u ) * Math.cos( v );
+
+		} else {
+
+			x = 3 * Math.cos( u ) * ( 1 + Math.sin( u ) ) + ( 2 * ( 1 - Math.cos( u ) / 2 ) ) * Math.cos( v + Math.PI );
+			z = - 8 * Math.sin( u );
+
+		}
+
+		const y = - 2 * ( 1 - Math.cos( u ) / 2 ) * Math.sin( v );
+
+		target.set( x, y, z );
+
+	},
+
+	plane: function ( width, height ) {
+
+		return function ( u, v, target ) {
+
+			const x = u * width;
+			const y = 0;
+			const z = v * height;
+
+			target.set( x, y, z );
+
+		};
+
+	},
+
+	mobius: function ( u, t, target ) {
+
+		// flat mobius strip
+		// http://www.wolframalpha.com/input/?i=M%C3%B6bius+strip+parametric+equations&lk=1&a=ClashPrefs_*Surface.MoebiusStrip.SurfaceProperty.ParametricEquations-
+		u = u - 0.5;
+		const v = 2 * Math.PI * t;
+
+		const a = 2;
+
+		const x = Math.cos( v ) * ( a + u * Math.cos( v / 2 ) );
+		const y = Math.sin( v ) * ( a + u * Math.cos( v / 2 ) );
+		const z = u * Math.sin( v / 2 );
+
+		target.set( x, y, z );
+
+	},
+
+	mobius3d: function ( u, t, target ) {
+
+		// volumetric mobius strip
+
+		u *= Math.PI;
+		t *= 2 * Math.PI;
+
+		u = u * 2;
+		const phi = u / 2;
+		const major = 2.25, a = 0.125, b = 0.65;
+
+		let x = a * Math.cos( t ) * Math.cos( phi ) - b * Math.sin( t ) * Math.sin( phi );
+		const z = a * Math.cos( t ) * Math.sin( phi ) + b * Math.sin( t ) * Math.cos( phi );
+		const y = ( major + x ) * Math.sin( u );
+		x = ( major + x ) * Math.cos( u );
+
+		target.set( x, y, z );
+
+	}
+
+};
+
+
+/*********************************************
+ *
+ * Parametric Replacement for TubeGeometry
+ *
+ *********************************************/
+
+ParametricGeometries.TubeGeometry = class TubeGeometry extends ParametricGeometry {
+
+	constructor( path, segments = 64, radius = 1, segmentsRadius = 8, closed = false ) {
+
+		const numpoints = segments + 1;
+
+		const frames = path.computeFrenetFrames( segments, closed ),
+			tangents = frames.tangents,
+			normals = frames.normals,
+			binormals = frames.binormals;
+
+		const position = new Vector3();
+
+		function ParametricTube( u, v, target ) {
+
+			v *= 2 * Math.PI;
+
+			const i = Math.floor( u * ( numpoints - 1 ) );
+
+			path.getPointAt( u, position );
+
+			const normal = normals[ i ];
+			const binormal = binormals[ i ];
+
+			const cx = - radius * Math.cos( v ); // TODO: Hack: Negating it so it faces outside.
+			const cy = radius * Math.sin( v );
+
+			position.x += cx * normal.x + cy * binormal.x;
+			position.y += cx * normal.y + cy * binormal.y;
+			position.z += cx * normal.z + cy * binormal.z;
+
+			target.copy( position );
+
+		}
+
+		super( ParametricTube, segments, segmentsRadius );
+
+		// proxy internals
+
+		this.tangents = tangents;
+		this.normals = normals;
+		this.binormals = binormals;
+
+		this.path = path;
+		this.segments = segments;
+		this.radius = radius;
+		this.segmentsRadius = segmentsRadius;
+		this.closed = closed;
+
+	}
+
+};
+
+
+/*********************************************
+  *
+  * Parametric Replacement for TorusKnotGeometry
+  *
+  *********************************************/
+ParametricGeometries.TorusKnotGeometry = class TorusKnotGeometry extends ParametricGeometries.TubeGeometry {
+
+	constructor( radius = 200, tube = 40, segmentsT = 64, segmentsR = 8, p = 2, q = 3 ) {
+
+		class TorusKnotCurve extends Curve {
+
+			getPoint( t, optionalTarget = new Vector3() ) {
+
+				const point = optionalTarget;
+
+				t *= Math.PI * 2;
+
+				const r = 0.5;
+
+				const x = ( 1 + r * Math.cos( q * t ) ) * Math.cos( p * t );
+				const y = ( 1 + r * Math.cos( q * t ) ) * Math.sin( p * t );
+				const z = r * Math.sin( q * t );
+
+				return point.set( x, y, z ).multiplyScalar( radius );
+
+			}
+
+		}
+
+		const segments = segmentsT;
+		const radiusSegments = segmentsR;
+		const extrudePath = new TorusKnotCurve();
+
+		super( extrudePath, segments, tube, radiusSegments, true, false );
+
+		this.radius = radius;
+		this.tube = tube;
+		this.segmentsT = segmentsT;
+		this.segmentsR = segmentsR;
+		this.p = p;
+		this.q = q;
+
+	}
+
+};
+
+/*********************************************
+  *
+  * Parametric Replacement for SphereGeometry
+  *
+  *********************************************/
+ParametricGeometries.SphereGeometry = class SphereGeometry extends ParametricGeometry {
+
+	constructor( size, u, v ) {
+
+		function sphere( u, v, target ) {
+
+			u *= Math.PI;
+			v *= 2 * Math.PI;
+
+			const x = size * Math.sin( u ) * Math.cos( v );
+			const y = size * Math.sin( u ) * Math.sin( v );
+			const z = size * Math.cos( u );
+
+			target.set( x, y, z );
+
+		}
+
+		super( sphere, u, v );
+
+	}
+
+};
+
+
+/*********************************************
+  *
+  * Parametric Replacement for PlaneGeometry
+  *
+  *********************************************/
+
+ParametricGeometries.PlaneGeometry = class PlaneGeometry extends ParametricGeometry {
+
+	constructor( width, depth, segmentsWidth, segmentsDepth ) {
+
+		function plane( u, v, target ) {
+
+			const x = u * width;
+			const y = 0;
+			const z = v * depth;
+
+			target.set( x, y, z );
+
+		}
+
+		super( plane, segmentsWidth, segmentsDepth );
+
+	}
+
+};
+
+export { ParametricGeometries };

+ 129 - 0
public/archive/static/js/jsm/geometries/ParametricGeometry.js

@@ -0,0 +1,129 @@
+/**
+ * Parametric Surfaces Geometry
+ * based on the brilliant article by @prideout https://prideout.net/blog/old/blog/index.html@p=44.html
+ */
+
+import {
+	BufferGeometry,
+	Float32BufferAttribute,
+	Vector3
+} from 'three';
+
+class ParametricGeometry extends BufferGeometry {
+
+	constructor( func = ( u, v, target ) => target.set( u, v, Math.cos( u ) * Math.sin( v ) ), slices = 8, stacks = 8 ) {
+
+		super();
+
+		this.type = 'ParametricGeometry';
+
+		this.parameters = {
+			func: func,
+			slices: slices,
+			stacks: stacks
+		};
+
+		// buffers
+
+		const indices = [];
+		const vertices = [];
+		const normals = [];
+		const uvs = [];
+
+		const EPS = 0.00001;
+
+		const normal = new Vector3();
+
+		const p0 = new Vector3(), p1 = new Vector3();
+		const pu = new Vector3(), pv = new Vector3();
+
+		// generate vertices, normals and uvs
+
+		const sliceCount = slices + 1;
+
+		for ( let i = 0; i <= stacks; i ++ ) {
+
+			const v = i / stacks;
+
+			for ( let j = 0; j <= slices; j ++ ) {
+
+				const u = j / slices;
+
+				// vertex
+
+				func( u, v, p0 );
+				vertices.push( p0.x, p0.y, p0.z );
+
+				// normal
+
+				// approximate tangent vectors via finite differences
+
+				if ( u - EPS >= 0 ) {
+
+					func( u - EPS, v, p1 );
+					pu.subVectors( p0, p1 );
+
+				} else {
+
+					func( u + EPS, v, p1 );
+					pu.subVectors( p1, p0 );
+
+				}
+
+				if ( v - EPS >= 0 ) {
+
+					func( u, v - EPS, p1 );
+					pv.subVectors( p0, p1 );
+
+				} else {
+
+					func( u, v + EPS, p1 );
+					pv.subVectors( p1, p0 );
+
+				}
+
+				// cross product of tangent vectors returns surface normal
+
+				normal.crossVectors( pu, pv ).normalize();
+				normals.push( normal.x, normal.y, normal.z );
+
+				// uv
+
+				uvs.push( u, v );
+
+			}
+
+		}
+
+		// generate indices
+
+		for ( let i = 0; i < stacks; i ++ ) {
+
+			for ( let j = 0; j < slices; j ++ ) {
+
+				const a = i * sliceCount + j;
+				const b = i * sliceCount + j + 1;
+				const c = ( i + 1 ) * sliceCount + j + 1;
+				const d = ( i + 1 ) * sliceCount + j;
+
+				// faces one and two
+
+				indices.push( a, b, d );
+				indices.push( b, c, d );
+
+			}
+
+		}
+
+		// build geometry
+
+		this.setIndex( indices );
+		this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
+		this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
+		this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
+
+	}
+
+}
+
+export { ParametricGeometry };

+ 155 - 0
public/archive/static/js/jsm/geometries/RoundedBoxGeometry.js

@@ -0,0 +1,155 @@
+import {
+	BoxGeometry,
+	Vector3
+} from 'three';
+
+const _tempNormal = new Vector3();
+
+function getUv( faceDirVector, normal, uvAxis, projectionAxis, radius, sideLength ) {
+
+	const totArcLength = 2 * Math.PI * radius / 4;
+
+	// length of the planes between the arcs on each axis
+	const centerLength = Math.max( sideLength - 2 * radius, 0 );
+	const halfArc = Math.PI / 4;
+
+	// Get the vector projected onto the Y plane
+	_tempNormal.copy( normal );
+	_tempNormal[ projectionAxis ] = 0;
+	_tempNormal.normalize();
+
+	// total amount of UV space alloted to a single arc
+	const arcUvRatio = 0.5 * totArcLength / ( totArcLength + centerLength );
+
+	// the distance along one arc the point is at
+	const arcAngleRatio = 1.0 - ( _tempNormal.angleTo( faceDirVector ) / halfArc );
+
+	if ( Math.sign( _tempNormal[ uvAxis ] ) === 1 ) {
+
+		return arcAngleRatio * arcUvRatio;
+
+	} else {
+
+		// total amount of UV space alloted to the plane between the arcs
+		const lenUv = centerLength / ( totArcLength + centerLength );
+		return lenUv + arcUvRatio + arcUvRatio * ( 1.0 - arcAngleRatio );
+
+	}
+
+}
+
+class RoundedBoxGeometry extends BoxGeometry {
+
+	constructor( width = 1, height = 1, depth = 1, segments = 2, radius = 0.1 ) {
+
+		// ensure segments is odd so we have a plane connecting the rounded corners
+		segments = segments * 2 + 1;
+
+		// ensure radius isn't bigger than shortest side
+		radius = Math.min( width / 2, height / 2, depth / 2, radius );
+
+		super( 1, 1, 1, segments, segments, segments );
+
+		// if we just have one segment we're the same as a regular box
+		if ( segments === 1 ) return;
+
+		const geometry2 = this.toNonIndexed();
+
+		this.index = null;
+		this.attributes.position = geometry2.attributes.position;
+		this.attributes.normal = geometry2.attributes.normal;
+		this.attributes.uv = geometry2.attributes.uv;
+
+		//
+
+		const position = new Vector3();
+		const normal = new Vector3();
+
+		const box = new Vector3( width, height, depth ).divideScalar( 2 ).subScalar( radius );
+
+		const positions = this.attributes.position.array;
+		const normals = this.attributes.normal.array;
+		const uvs = this.attributes.uv.array;
+
+		const faceTris = positions.length / 6;
+		const faceDirVector = new Vector3();
+		const halfSegmentSize = 0.5 / segments;
+
+		for ( let i = 0, j = 0; i < positions.length; i += 3, j += 2 ) {
+
+			position.fromArray( positions, i );
+			normal.copy( position );
+			normal.x -= Math.sign( normal.x ) * halfSegmentSize;
+			normal.y -= Math.sign( normal.y ) * halfSegmentSize;
+			normal.z -= Math.sign( normal.z ) * halfSegmentSize;
+			normal.normalize();
+
+			positions[ i + 0 ] = box.x * Math.sign( position.x ) + normal.x * radius;
+			positions[ i + 1 ] = box.y * Math.sign( position.y ) + normal.y * radius;
+			positions[ i + 2 ] = box.z * Math.sign( position.z ) + normal.z * radius;
+
+			normals[ i + 0 ] = normal.x;
+			normals[ i + 1 ] = normal.y;
+			normals[ i + 2 ] = normal.z;
+
+			const side = Math.floor( i / faceTris );
+
+			switch ( side ) {
+
+				case 0: // right
+
+					// generate UVs along Z then Y
+					faceDirVector.set( 1, 0, 0 );
+					uvs[ j + 0 ] = getUv( faceDirVector, normal, 'z', 'y', radius, depth );
+					uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'z', radius, height );
+					break;
+
+				case 1: // left
+
+					// generate UVs along Z then Y
+					faceDirVector.set( - 1, 0, 0 );
+					uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'z', 'y', radius, depth );
+					uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'z', radius, height );
+					break;
+
+				case 2: // top
+
+					// generate UVs along X then Z
+					faceDirVector.set( 0, 1, 0 );
+					uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'x', 'z', radius, width );
+					uvs[ j + 1 ] = getUv( faceDirVector, normal, 'z', 'x', radius, depth );
+					break;
+
+				case 3: // bottom
+
+					// generate UVs along X then Z
+					faceDirVector.set( 0, - 1, 0 );
+					uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'x', 'z', radius, width );
+					uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'z', 'x', radius, depth );
+					break;
+
+				case 4: // front
+
+					// generate UVs along X then Y
+					faceDirVector.set( 0, 0, 1 );
+					uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'x', 'y', radius, width );
+					uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'x', radius, height );
+					break;
+
+				case 5: // back
+
+					// generate UVs along X then Y
+					faceDirVector.set( 0, 0, - 1 );
+					uvs[ j + 0 ] = getUv( faceDirVector, normal, 'x', 'y', radius, width );
+					uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'x', radius, height );
+					break;
+
+			}
+
+		}
+
+	}
+
+}
+
+export { RoundedBoxGeometry };

+ 704 - 0
public/archive/static/js/jsm/geometries/TeapotGeometry.js

@@ -0,0 +1,704 @@
+import {
+	BufferAttribute,
+	BufferGeometry,
+	Matrix4,
+	Vector3,
+	Vector4
+} from 'three';
+
+/**
+ * Tessellates the famous Utah teapot database by Martin Newell into triangles.
+ *
+ * Parameters: 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
+ * 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
+ *
+ */
+
+class TeapotGeometry extends BufferGeometry {
+
+	constructor( size = 50, segments = 10, bottom = true, lid = true, body = true, fitLid = true, blinn = true ) {
+
+		// 32 * 4 * 4 Bezier spline patches
+		const 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
+		];
+
+		const 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
+		];
+
+		super();
+
+		// number of segments per patch
+		segments = Math.max( 2, Math.floor( segments ) );
+
+		// 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.
+		const blinnScale = 1.3;
+
+		// scale the size to be the real scaling factor
+		const maxHeight = 3.15 * ( blinn ? 1 : blinnScale );
+
+		const maxHeight2 = maxHeight / 2;
+		const trueSize = size / maxHeight2;
+
+		// Number of elements depends on what is needed. Subtract degenerate
+		// triangles at tip of bottom and lid out in advance.
+		let numTriangles = bottom ? ( 8 * segments - 4 ) * segments : 0;
+		numTriangles += lid ? ( 16 * segments - 4 ) * segments : 0;
+		numTriangles += body ? 40 * segments * segments : 0;
+
+		const indices = new Uint32Array( numTriangles * 3 );
+
+		let numVertices = bottom ? 4 : 0;
+		numVertices += lid ? 8 : 0;
+		numVertices += body ? 20 : 0;
+		numVertices *= ( segments + 1 ) * ( segments + 1 );
+
+		const vertices = new Float32Array( numVertices * 3 );
+		const normals = new Float32Array( numVertices * 3 );
+		const uvs = new Float32Array( numVertices * 2 );
+
+		// Bezier form
+		const ms = new 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 );
+
+		const g = [];
+
+		const sp = [];
+		const tp = [];
+		const dsp = [];
+		const dtp = [];
+
+		// M * G * M matrix, sort of see
+		// http://www.cs.helsinki.fi/group/goa/mallinnus/curves/surfaces.html
+		const mgm = [];
+
+		const vert = [];
+		const sdir = [];
+		const tdir = [];
+
+		const norm = new Vector3();
+
+		let tcoord;
+
+		let sval;
+		let tval;
+		let p;
+		let dsval = 0;
+		let dtval = 0;
+
+		const normOut = new Vector3();
+
+		const gmx = new Matrix4();
+		const tmtx = new Matrix4();
+
+		const vsp = new Vector4();
+		const vtp = new Vector4();
+		const vdsp = new Vector4();
+		const vdtp = new Vector4();
+
+		const vsdir = new Vector3();
+		const vtdir = new Vector3();
+
+		const 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.
+		const notDegenerate = ( vtx1, vtx2, vtx3 ) => // if any vertex matches, return false
+			! ( ( ( 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 ( let i = 0; i < 3; i ++ ) {
+
+			mgm[ i ] = new Matrix4();
+
+		}
+
+		const minPatches = body ? 0 : 20;
+		const maxPatches = bottom ? 32 : 28;
+
+		const vertPerRow = segments + 1;
+
+		let surfCount = 0;
+
+		let vertCount = 0;
+		let normCount = 0;
+		let uvCount = 0;
+
+		let indexCount = 0;
+
+		for ( let 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 ( let i = 0; i < 3; i ++ ) {
+
+					// get control patches
+					for ( let r = 0; r < 4; r ++ ) {
+
+						for ( let 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 ( let sstep = 0; sstep <= segments; sstep ++ ) {
+
+					const s = sstep / segments;
+
+					for ( let tstep = 0; tstep <= segments; tstep ++ ) {
+
+						const 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 ( let 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 ( let sstep = 0; sstep < segments; sstep ++ ) {
+
+					for ( let tstep = 0; tstep < segments; tstep ++ ) {
+
+						const v1 = surfCount * vertPerRow * vertPerRow + sstep * vertPerRow + tstep;
+						const v2 = v1 + 1;
+						const v3 = v2 + vertPerRow;
+						const v4 = v1 + vertPerRow;
+
+						// Normals and UVs cannot be shared. Without clone(), you can see the consequences
+						// of sharing if you call geometry.applyMatrix4( 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 BufferAttribute( indices, 1 ) );
+		this.setAttribute( 'position', new BufferAttribute( vertices, 3 ) );
+		this.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );
+		this.setAttribute( 'uv', new BufferAttribute( uvs, 2 ) );
+
+		this.computeBoundingSphere();
+
+	}
+
+}
+
+export { TeapotGeometry };

+ 57 - 0
public/archive/static/js/jsm/geometries/TextGeometry.js

@@ -0,0 +1,57 @@
+/**
+ * Text = 3D Text
+ *
+ * parameters = {
+ *  font: <THREE.Font>, // font
+ *
+ *  size: <float>, // size of the text
+ *  height: <float>, // thickness to extrude text
+ *  curveSegments: <int>, // number of points on the curves
+ *
+ *  bevelEnabled: <bool>, // turn on bevel
+ *  bevelThickness: <float>, // how deep into text bevel goes
+ *  bevelSize: <float>, // how far from text outline (including bevelOffset) is bevel
+ *  bevelOffset: <float> // how far from text outline does bevel start
+ * }
+ */
+
+import {
+	ExtrudeGeometry
+} from 'three';
+
+class TextGeometry extends ExtrudeGeometry {
+
+	constructor( text, parameters = {} ) {
+
+		const font = parameters.font;
+
+		if ( font === undefined ) {
+
+			super(); // generate default extrude geometry
+
+		} else {
+
+			const shapes = font.generateShapes( text, parameters.size );
+
+			// translate parameters to ExtrudeGeometry API
+
+			parameters.depth = parameters.height !== undefined ? parameters.height : 50;
+
+			// defaults
+
+			if ( parameters.bevelThickness === undefined ) parameters.bevelThickness = 10;
+			if ( parameters.bevelSize === undefined ) parameters.bevelSize = 8;
+			if ( parameters.bevelEnabled === undefined ) parameters.bevelEnabled = false;
+
+			super( shapes, parameters );
+
+		}
+
+		this.type = 'TextGeometry';
+
+	}
+
+}
+
+
+export { TextGeometry };

+ 130 - 0
public/archive/static/js/jsm/helpers/LightProbeHelper.js

@@ -0,0 +1,130 @@
+import {
+	Mesh,
+	ShaderMaterial,
+	SphereGeometry
+} from 'three';
+
+class LightProbeHelper extends Mesh {
+
+	constructor( lightProbe, size ) {
+
+		const material = new ShaderMaterial( {
+
+			type: 'LightProbeHelperMaterial',
+
+			uniforms: {
+
+				sh: { value: lightProbe.sh.coefficients }, // by reference
+
+				intensity: { value: lightProbe.intensity }
+
+			},
+
+			vertexShader: [
+
+				'varying vec3 vNormal;',
+
+				'void main() {',
+
+				'	vNormal = normalize( normalMatrix * normal );',
+
+				'	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
+
+				'}',
+
+			].join( '\n' ),
+
+			fragmentShader: [
+
+				'#define RECIPROCAL_PI 0.318309886',
+
+				'vec3 inverseTransformDirection( in vec3 normal, in mat4 matrix ) {',
+
+				'	// matrix is assumed to be orthogonal',
+
+				'	return normalize( ( vec4( normal, 0.0 ) * matrix ).xyz );',
+
+				'}',
+
+				'// source: https://graphics.stanford.edu/papers/envmap/envmap.pdf',
+				'vec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {',
+
+				'	// normal is assumed to have unit length',
+
+				'	float x = normal.x, y = normal.y, z = normal.z;',
+
+				'	// band 0',
+				'	vec3 result = shCoefficients[ 0 ] * 0.886227;',
+
+				'	// band 1',
+				'	result += shCoefficients[ 1 ] * 2.0 * 0.511664 * y;',
+				'	result += shCoefficients[ 2 ] * 2.0 * 0.511664 * z;',
+				'	result += shCoefficients[ 3 ] * 2.0 * 0.511664 * x;',
+
+				'	// band 2',
+				'	result += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y;',
+				'	result += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z;',
+				'	result += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 );',
+				'	result += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z;',
+				'	result += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y );',
+
+				'	return result;',
+
+				'}',
+
+				'uniform vec3 sh[ 9 ]; // sh coefficients',
+
+				'uniform float intensity; // light probe intensity',
+
+				'varying vec3 vNormal;',
+
+				'void main() {',
+
+				'	vec3 normal = normalize( vNormal );',
+
+				'	vec3 worldNormal = inverseTransformDirection( normal, viewMatrix );',
+
+				'	vec3 irradiance = shGetIrradianceAt( worldNormal, sh );',
+
+				'	vec3 outgoingLight = RECIPROCAL_PI * irradiance * intensity;',
+
+				'	gl_FragColor = linearToOutputTexel( vec4( outgoingLight, 1.0 ) );',
+
+				'}'
+
+			].join( '\n' )
+
+		} );
+
+		const geometry = new SphereGeometry( 1, 32, 16 );
+
+		super( geometry, material );
+
+		this.lightProbe = lightProbe;
+		this.size = size;
+		this.type = 'LightProbeHelper';
+
+		this.onBeforeRender();
+
+	}
+
+	dispose() {
+
+		this.geometry.dispose();
+		this.material.dispose();
+
+	}
+
+	onBeforeRender() {
+
+		this.position.copy( this.lightProbe.position );
+
+		this.scale.set( 1, 1, 1 ).multiplyScalar( this.size );
+
+		this.material.uniforms.intensity.value = this.lightProbe.intensity;
+
+	}
+
+}
+
+export { LightProbeHelper };

+ 58 - 0
public/archive/static/js/jsm/helpers/OctreeHelper.js

@@ -0,0 +1,58 @@
+import {
+	LineSegments,
+	BufferGeometry,
+	Float32BufferAttribute,
+	LineBasicMaterial
+} from 'three';
+
+class OctreeHelper extends LineSegments {
+
+	constructor( octree, color = 0xffff00 ) {
+
+		const vertices = [];
+
+		function traverse( tree ) {
+
+			for ( let i = 0; i < tree.length; i ++ ) {
+
+				const min = tree[ i ].box.min;
+				const max = tree[ i ].box.max;
+
+				vertices.push( max.x, max.y, max.z ); vertices.push( min.x, max.y, max.z ); // 0, 1
+				vertices.push( min.x, max.y, max.z ); vertices.push( min.x, min.y, max.z ); // 1, 2
+				vertices.push( min.x, min.y, max.z ); vertices.push( max.x, min.y, max.z ); // 2, 3
+				vertices.push( max.x, min.y, max.z ); vertices.push( max.x, max.y, max.z ); // 3, 0
+
+				vertices.push( max.x, max.y, min.z ); vertices.push( min.x, max.y, min.z ); // 4, 5
+				vertices.push( min.x, max.y, min.z ); vertices.push( min.x, min.y, min.z ); // 5, 6
+				vertices.push( min.x, min.y, min.z ); vertices.push( max.x, min.y, min.z ); // 6, 7
+				vertices.push( max.x, min.y, min.z ); vertices.push( max.x, max.y, min.z ); // 7, 4
+
+				vertices.push( max.x, max.y, max.z ); vertices.push( max.x, max.y, min.z ); // 0, 4
+				vertices.push( min.x, max.y, max.z ); vertices.push( min.x, max.y, min.z ); // 1, 5
+				vertices.push( min.x, min.y, max.z ); vertices.push( min.x, min.y, min.z ); // 2, 6
+				vertices.push( max.x, min.y, max.z ); vertices.push( max.x, min.y, min.z ); // 3, 7
+
+				traverse( tree[ i ].subTrees );
+
+			}
+
+		}
+
+		traverse( octree.subTrees );
+
+		const geometry = new BufferGeometry();
+		geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
+
+		super( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );
+
+		this.octree = octree;
+		this.color = color;
+
+		this.type = 'OctreeHelper';
+
+	}
+
+}
+
+export { OctreeHelper };

+ 109 - 0
public/archive/static/js/jsm/helpers/PositionalAudioHelper.js

@@ -0,0 +1,109 @@
+import {
+	BufferGeometry,
+	BufferAttribute,
+	LineBasicMaterial,
+	Line,
+	MathUtils
+} from 'three';
+
+class PositionalAudioHelper extends Line {
+
+	constructor( audio, range = 1, divisionsInnerAngle = 16, divisionsOuterAngle = 2 ) {
+
+		const geometry = new BufferGeometry();
+		const divisions = divisionsInnerAngle + divisionsOuterAngle * 2;
+		const positions = new Float32Array( ( divisions * 3 + 3 ) * 3 );
+		geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
+
+		const materialInnerAngle = new LineBasicMaterial( { color: 0x00ff00 } );
+		const materialOuterAngle = new LineBasicMaterial( { color: 0xffff00 } );
+
+		super( geometry, [ materialOuterAngle, materialInnerAngle ] );
+
+		this.audio = audio;
+		this.range = range;
+		this.divisionsInnerAngle = divisionsInnerAngle;
+		this.divisionsOuterAngle = divisionsOuterAngle;
+		this.type = 'PositionalAudioHelper';
+
+		this.update();
+
+	}
+
+	update() {
+
+		const audio = this.audio;
+		const range = this.range;
+		const divisionsInnerAngle = this.divisionsInnerAngle;
+		const divisionsOuterAngle = this.divisionsOuterAngle;
+
+		const coneInnerAngle = MathUtils.degToRad( audio.panner.coneInnerAngle );
+		const coneOuterAngle = MathUtils.degToRad( audio.panner.coneOuterAngle );
+
+		const halfConeInnerAngle = coneInnerAngle / 2;
+		const halfConeOuterAngle = coneOuterAngle / 2;
+
+		let start = 0;
+		let count = 0;
+		let i;
+		let stride;
+
+		const geometry = this.geometry;
+		const positionAttribute = geometry.attributes.position;
+
+		geometry.clearGroups();
+
+		//
+
+		function generateSegment( from, to, divisions, materialIndex ) {
+
+			const step = ( to - from ) / divisions;
+
+			positionAttribute.setXYZ( start, 0, 0, 0 );
+			count ++;
+
+			for ( i = from; i < to; i += step ) {
+
+				stride = start + count;
+
+				positionAttribute.setXYZ( stride, Math.sin( i ) * range, 0, Math.cos( i ) * range );
+				positionAttribute.setXYZ( stride + 1, Math.sin( Math.min( i + step, to ) ) * range, 0, Math.cos( Math.min( i + step, to ) ) * range );
+				positionAttribute.setXYZ( stride + 2, 0, 0, 0 );
+
+				count += 3;
+
+			}
+
+			geometry.addGroup( start, count, materialIndex );
+
+			start += count;
+			count = 0;
+
+		}
+
+		//
+
+		generateSegment( - halfConeOuterAngle, - halfConeInnerAngle, divisionsOuterAngle, 0 );
+		generateSegment( - halfConeInnerAngle, halfConeInnerAngle, divisionsInnerAngle, 1 );
+		generateSegment( halfConeInnerAngle, halfConeOuterAngle, divisionsOuterAngle, 0 );
+
+		//
+
+		positionAttribute.needsUpdate = true;
+
+		if ( coneInnerAngle === coneOuterAngle ) this.material[ 0 ].visible = false;
+
+	}
+
+	dispose() {
+
+		this.geometry.dispose();
+		this.material[ 0 ].dispose();
+		this.material[ 1 ].dispose();
+
+	}
+
+}
+
+
+export { PositionalAudioHelper };

+ 85 - 0
public/archive/static/js/jsm/helpers/RectAreaLightHelper.js

@@ -0,0 +1,85 @@
+import {
+	BackSide,
+	BufferGeometry,
+	Float32BufferAttribute,
+	Line,
+	LineBasicMaterial,
+	Mesh,
+	MeshBasicMaterial
+} from 'three';
+
+/**
+ *  This helper must be added as a child of the light
+ */
+
+class RectAreaLightHelper extends Line {
+
+	constructor( light, color ) {
+
+		const positions = [ 1, 1, 0, - 1, 1, 0, - 1, - 1, 0, 1, - 1, 0, 1, 1, 0 ];
+
+		const geometry = new BufferGeometry();
+		geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
+		geometry.computeBoundingSphere();
+
+		const material = new LineBasicMaterial( { fog: false } );
+
+		super( geometry, material );
+
+		this.light = light;
+		this.color = color; // optional hardwired color for the helper
+		this.type = 'RectAreaLightHelper';
+
+		//
+
+		const positions2 = [ 1, 1, 0, - 1, 1, 0, - 1, - 1, 0, 1, 1, 0, - 1, - 1, 0, 1, - 1, 0 ];
+
+		const geometry2 = new BufferGeometry();
+		geometry2.setAttribute( 'position', new Float32BufferAttribute( positions2, 3 ) );
+		geometry2.computeBoundingSphere();
+
+		this.add( new Mesh( geometry2, new MeshBasicMaterial( { side: BackSide, fog: false } ) ) );
+
+	}
+
+	updateMatrixWorld() {
+
+		this.scale.set( 0.5 * this.light.width, 0.5 * this.light.height, 1 );
+
+		if ( this.color !== undefined ) {
+
+			this.material.color.set( this.color );
+			this.children[ 0 ].material.color.set( this.color );
+
+		} else {
+
+			this.material.color.copy( this.light.color ).multiplyScalar( this.light.intensity );
+
+			// prevent hue shift
+			const c = this.material.color;
+			const max = Math.max( c.r, c.g, c.b );
+			if ( max > 1 ) c.multiplyScalar( 1 / max );
+
+			this.children[ 0 ].material.color.copy( this.material.color );
+
+		}
+
+		// ignore world scale on light
+		this.matrixWorld.extractRotation( this.light.matrixWorld ).scale( this.scale ).copyPosition( this.light.matrixWorld );
+
+		this.children[ 0 ].matrixWorld.copy( this.matrixWorld );
+
+	}
+
+	dispose() {
+
+		this.geometry.dispose();
+		this.material.dispose();
+		this.children[ 0 ].geometry.dispose();
+		this.children[ 0 ].material.dispose();
+
+	}
+
+}
+
+export { RectAreaLightHelper };

+ 90 - 0
public/archive/static/js/jsm/helpers/VertexNormalsHelper.js

@@ -0,0 +1,90 @@
+import {
+	BufferGeometry,
+	Float32BufferAttribute,
+	LineSegments,
+	LineBasicMaterial,
+	Matrix3,
+	Vector3
+} from 'three';
+
+const _v1 = new Vector3();
+const _v2 = new Vector3();
+const _normalMatrix = new Matrix3();
+
+class VertexNormalsHelper extends LineSegments {
+
+	constructor( object, size = 1, color = 0xff0000 ) {
+
+		const geometry = new BufferGeometry();
+
+		const nNormals = object.geometry.attributes.normal.count;
+		const positions = new Float32BufferAttribute( nNormals * 2 * 3, 3 );
+
+		geometry.setAttribute( 'position', positions );
+
+		super( geometry, new LineBasicMaterial( { color, toneMapped: false } ) );
+
+		this.object = object;
+		this.size = size;
+		this.type = 'VertexNormalsHelper';
+
+		//
+
+		this.matrixAutoUpdate = false;
+
+		this.update();
+
+	}
+
+	update() {
+
+		this.object.updateMatrixWorld( true );
+
+		_normalMatrix.getNormalMatrix( this.object.matrixWorld );
+
+		const matrixWorld = this.object.matrixWorld;
+
+		const position = this.geometry.attributes.position;
+
+		//
+
+		const objGeometry = this.object.geometry;
+
+		if ( objGeometry ) {
+
+			const objPos = objGeometry.attributes.position;
+
+			const objNorm = objGeometry.attributes.normal;
+
+			let idx = 0;
+
+			// for simplicity, ignore index and drawcalls, and render every normal
+
+			for ( let j = 0, jl = objPos.count; j < jl; j ++ ) {
+
+				_v1.fromBufferAttribute( objPos, j ).applyMatrix4( matrixWorld );
+
+				_v2.fromBufferAttribute( objNorm, j );
+
+				_v2.applyMatrix3( _normalMatrix ).normalize().multiplyScalar( this.size ).add( _v1 );
+
+				position.setXYZ( idx, _v1.x, _v1.y, _v1.z );
+
+				idx = idx + 1;
+
+				position.setXYZ( idx, _v2.x, _v2.y, _v2.z );
+
+				idx = idx + 1;
+
+			}
+
+		}
+
+		position.needsUpdate = true;
+
+	}
+
+}
+
+
+export { VertexNormalsHelper };

+ 81 - 0
public/archive/static/js/jsm/helpers/VertexTangentsHelper.js

@@ -0,0 +1,81 @@
+import {
+	BufferGeometry,
+	Float32BufferAttribute,
+	LineSegments,
+	LineBasicMaterial,
+	Vector3
+} from 'three';
+
+const _v1 = new Vector3();
+const _v2 = new Vector3();
+
+class VertexTangentsHelper extends LineSegments {
+
+	constructor( object, size = 1, color = 0x00ffff ) {
+
+		const geometry = new BufferGeometry();
+
+		const nTangents = object.geometry.attributes.tangent.count;
+		const positions = new Float32BufferAttribute( nTangents * 2 * 3, 3 );
+
+		geometry.setAttribute( 'position', positions );
+
+		super( geometry, new LineBasicMaterial( { color, toneMapped: false } ) );
+
+		this.object = object;
+		this.size = size;
+		this.type = 'VertexTangentsHelper';
+
+		//
+
+		this.matrixAutoUpdate = false;
+
+		this.update();
+
+	}
+
+	update() {
+
+		this.object.updateMatrixWorld( true );
+
+		const matrixWorld = this.object.matrixWorld;
+
+		const position = this.geometry.attributes.position;
+
+		//
+
+		const objGeometry = this.object.geometry;
+
+		const objPos = objGeometry.attributes.position;
+
+		const objTan = objGeometry.attributes.tangent;
+
+		let idx = 0;
+
+		// for simplicity, ignore index and drawcalls, and render every tangent
+
+		for ( let j = 0, jl = objPos.count; j < jl; j ++ ) {
+
+			_v1.fromBufferAttribute( objPos, j ).applyMatrix4( matrixWorld );
+
+			_v2.fromBufferAttribute( objTan, j );
+
+			_v2.transformDirection( matrixWorld ).multiplyScalar( this.size ).add( _v1 );
+
+			position.setXYZ( idx, _v1.x, _v1.y, _v1.z );
+
+			idx = idx + 1;
+
+			position.setXYZ( idx, _v2.x, _v2.y, _v2.z );
+
+			idx = idx + 1;
+
+		}
+
+		position.needsUpdate = true;
+
+	}
+
+}
+
+export { VertexTangentsHelper };

+ 295 - 0
public/archive/static/js/jsm/helpers/ViewHelper.js

@@ -0,0 +1,295 @@
+import * as THREE from 'three';
+
+const vpTemp = new THREE.Vector4();
+
+class ViewHelper extends THREE.Object3D {
+
+	constructor( editorCamera, dom ) {
+
+		super();
+
+		this.isViewHelper = true;
+
+		this.animating = false;
+		this.controls = null;
+
+		const color1 = new THREE.Color( '#ff3653' );
+		const color2 = new THREE.Color( '#8adb00' );
+		const color3 = new THREE.Color( '#2c8fff' );
+
+		const interactiveObjects = [];
+		const raycaster = new THREE.Raycaster();
+		const mouse = new THREE.Vector2();
+		const dummy = new THREE.Object3D();
+
+		const camera = new THREE.OrthographicCamera( - 2, 2, 2, - 2, 0, 4 );
+		camera.position.set( 0, 0, 2 );
+
+		const geometry = new THREE.BoxGeometry( 0.8, 0.05, 0.05 ).translate( 0.4, 0, 0 );
+
+		const xAxis = new THREE.Mesh( geometry, getAxisMaterial( color1 ) );
+		const yAxis = new THREE.Mesh( geometry, getAxisMaterial( color2 ) );
+		const zAxis = new THREE.Mesh( geometry, getAxisMaterial( color3 ) );
+
+		yAxis.rotation.z = Math.PI / 2;
+		zAxis.rotation.y = - Math.PI / 2;
+
+		this.add( xAxis );
+		this.add( zAxis );
+		this.add( yAxis );
+
+		const posXAxisHelper = new THREE.Sprite( getSpriteMaterial( color1, 'X' ) );
+		posXAxisHelper.userData.type = 'posX';
+		const posYAxisHelper = new THREE.Sprite( getSpriteMaterial( color2, 'Y' ) );
+		posYAxisHelper.userData.type = 'posY';
+		const posZAxisHelper = new THREE.Sprite( getSpriteMaterial( color3, 'Z' ) );
+		posZAxisHelper.userData.type = 'posZ';
+		const negXAxisHelper = new THREE.Sprite( getSpriteMaterial( color1 ) );
+		negXAxisHelper.userData.type = 'negX';
+		const negYAxisHelper = new THREE.Sprite( getSpriteMaterial( color2 ) );
+		negYAxisHelper.userData.type = 'negY';
+		const negZAxisHelper = new THREE.Sprite( getSpriteMaterial( color3 ) );
+		negZAxisHelper.userData.type = 'negZ';
+
+		posXAxisHelper.position.x = 1;
+		posYAxisHelper.position.y = 1;
+		posZAxisHelper.position.z = 1;
+		negXAxisHelper.position.x = - 1;
+		negXAxisHelper.scale.setScalar( 0.8 );
+		negYAxisHelper.position.y = - 1;
+		negYAxisHelper.scale.setScalar( 0.8 );
+		negZAxisHelper.position.z = - 1;
+		negZAxisHelper.scale.setScalar( 0.8 );
+
+		this.add( posXAxisHelper );
+		this.add( posYAxisHelper );
+		this.add( posZAxisHelper );
+		this.add( negXAxisHelper );
+		this.add( negYAxisHelper );
+		this.add( negZAxisHelper );
+
+		interactiveObjects.push( posXAxisHelper );
+		interactiveObjects.push( posYAxisHelper );
+		interactiveObjects.push( posZAxisHelper );
+		interactiveObjects.push( negXAxisHelper );
+		interactiveObjects.push( negYAxisHelper );
+		interactiveObjects.push( negZAxisHelper );
+
+		const point = new THREE.Vector3();
+		const dim = 128;
+		const turnRate = 2 * Math.PI; // turn rate in angles per second
+
+		this.render = function ( renderer ) {
+
+			this.quaternion.copy( editorCamera.quaternion ).invert();
+			this.updateMatrixWorld();
+
+			point.set( 0, 0, 1 );
+			point.applyQuaternion( editorCamera.quaternion );
+
+			if ( point.x >= 0 ) {
+
+				posXAxisHelper.material.opacity = 1;
+				negXAxisHelper.material.opacity = 0.5;
+
+			} else {
+
+				posXAxisHelper.material.opacity = 0.5;
+				negXAxisHelper.material.opacity = 1;
+
+			}
+
+			if ( point.y >= 0 ) {
+
+				posYAxisHelper.material.opacity = 1;
+				negYAxisHelper.material.opacity = 0.5;
+
+			} else {
+
+				posYAxisHelper.material.opacity = 0.5;
+				negYAxisHelper.material.opacity = 1;
+
+			}
+
+			if ( point.z >= 0 ) {
+
+				posZAxisHelper.material.opacity = 1;
+				negZAxisHelper.material.opacity = 0.5;
+
+			} else {
+
+				posZAxisHelper.material.opacity = 0.5;
+				negZAxisHelper.material.opacity = 1;
+
+			}
+
+			//
+
+			const x = dom.offsetWidth - dim;
+
+			renderer.clearDepth();
+
+			renderer.getViewport( vpTemp );
+			renderer.setViewport( x, 0, dim, dim );
+
+			renderer.render( this, camera );
+
+			renderer.setViewport( vpTemp.x, vpTemp.y, vpTemp.z, vpTemp.w );
+
+		};
+
+		const targetPosition = new THREE.Vector3();
+		const targetQuaternion = new THREE.Quaternion();
+
+		const q1 = new THREE.Quaternion();
+		const q2 = new THREE.Quaternion();
+		let radius = 0;
+
+		this.handleClick = function ( event ) {
+
+			if ( this.animating === true ) return false;
+
+			const rect = dom.getBoundingClientRect();
+			const offsetX = rect.left + ( dom.offsetWidth - dim );
+			const offsetY = rect.top + ( dom.offsetHeight - dim );
+			mouse.x = ( ( event.clientX - offsetX ) / ( rect.width - offsetX ) ) * 2 - 1;
+			mouse.y = - ( ( event.clientY - offsetY ) / ( rect.bottom - offsetY ) ) * 2 + 1;
+
+			raycaster.setFromCamera( mouse, camera );
+
+			const intersects = raycaster.intersectObjects( interactiveObjects );
+
+			if ( intersects.length > 0 ) {
+
+				const intersection = intersects[ 0 ];
+				const object = intersection.object;
+
+				prepareAnimationData( object, this.controls.center );
+
+				this.animating = true;
+
+				return true;
+
+			} else {
+
+				return false;
+
+			}
+
+		};
+
+		this.update = function ( delta ) {
+
+			const step = delta * turnRate;
+			const focusPoint = this.controls.center;
+
+			// animate position by doing a slerp and then scaling the position on the unit sphere
+
+			q1.rotateTowards( q2, step );
+			editorCamera.position.set( 0, 0, 1 ).applyQuaternion( q1 ).multiplyScalar( radius ).add( focusPoint );
+
+			// animate orientation
+
+			editorCamera.quaternion.rotateTowards( targetQuaternion, step );
+
+			if ( q1.angleTo( q2 ) === 0 ) {
+
+				this.animating = false;
+
+			}
+
+		};
+
+		function prepareAnimationData( object, focusPoint ) {
+
+			switch ( object.userData.type ) {
+
+				case 'posX':
+					targetPosition.set( 1, 0, 0 );
+					targetQuaternion.setFromEuler( new THREE.Euler( 0, Math.PI * 0.5, 0 ) );
+					break;
+
+				case 'posY':
+					targetPosition.set( 0, 1, 0 );
+					targetQuaternion.setFromEuler( new THREE.Euler( - Math.PI * 0.5, 0, 0 ) );
+					break;
+
+				case 'posZ':
+					targetPosition.set( 0, 0, 1 );
+					targetQuaternion.setFromEuler( new THREE.Euler() );
+					break;
+
+				case 'negX':
+					targetPosition.set( - 1, 0, 0 );
+					targetQuaternion.setFromEuler( new THREE.Euler( 0, - Math.PI * 0.5, 0 ) );
+					break;
+
+				case 'negY':
+					targetPosition.set( 0, - 1, 0 );
+					targetQuaternion.setFromEuler( new THREE.Euler( Math.PI * 0.5, 0, 0 ) );
+					break;
+
+				case 'negZ':
+					targetPosition.set( 0, 0, - 1 );
+					targetQuaternion.setFromEuler( new THREE.Euler( 0, Math.PI, 0 ) );
+					break;
+
+				default:
+					console.error( 'ViewHelper: Invalid axis.' );
+
+			}
+
+			//
+
+			radius = editorCamera.position.distanceTo( focusPoint );
+			targetPosition.multiplyScalar( radius ).add( focusPoint );
+
+			dummy.position.copy( focusPoint );
+
+			dummy.lookAt( editorCamera.position );
+			q1.copy( dummy.quaternion );
+
+			dummy.lookAt( targetPosition );
+			q2.copy( dummy.quaternion );
+
+		}
+
+		function getAxisMaterial( color ) {
+
+			return new THREE.MeshBasicMaterial( { color: color, toneMapped: false } );
+
+		}
+
+		function getSpriteMaterial( color, text = null ) {
+
+			const canvas = document.createElement( 'canvas' );
+			canvas.width = 64;
+			canvas.height = 64;
+
+			const context = canvas.getContext( '2d' );
+			context.beginPath();
+			context.arc( 32, 32, 16, 0, 2 * Math.PI );
+			context.closePath();
+			context.fillStyle = color.getStyle();
+			context.fill();
+
+			if ( text !== null ) {
+
+				context.font = '24px Arial';
+				context.textAlign = 'center';
+				context.fillStyle = '#000000';
+				context.fillText( text, 32, 41 );
+
+			}
+
+			const texture = new THREE.CanvasTexture( canvas );
+
+			return new THREE.SpriteMaterial( { map: texture, toneMapped: false } );
+
+		}
+
+	}
+
+}
+
+export { ViewHelper };

+ 553 - 0
public/archive/static/js/jsm/interactive/HTMLMesh.js

@@ -0,0 +1,553 @@
+import {
+	CanvasTexture,
+	LinearFilter,
+	Mesh,
+	MeshBasicMaterial,
+	PlaneGeometry,
+	sRGBEncoding,
+	Color
+} from 'three';
+
+class HTMLMesh extends Mesh {
+
+	constructor( dom ) {
+
+		const texture = new HTMLTexture( dom );
+
+		const geometry = new PlaneGeometry( texture.image.width * 0.001, texture.image.height * 0.001 );
+		const material = new MeshBasicMaterial( { map: texture, toneMapped: false, transparent: true } );
+
+		super( geometry, material );
+
+		function onEvent( event ) {
+
+			material.map.dispatchDOMEvent( event );
+
+		}
+
+		this.addEventListener( 'mousedown', onEvent );
+		this.addEventListener( 'mousemove', onEvent );
+		this.addEventListener( 'mouseup', onEvent );
+		this.addEventListener( 'click', onEvent );
+
+		this.dispose = function () {
+
+			geometry.dispose();
+			material.dispose();
+
+			material.map.dispose();
+
+			this.removeEventListener( 'mousedown', onEvent );
+			this.removeEventListener( 'mousemove', onEvent );
+			this.removeEventListener( 'mouseup', onEvent );
+			this.removeEventListener( 'click', onEvent );
+
+		};
+
+	}
+
+}
+
+class HTMLTexture extends CanvasTexture {
+
+	constructor( dom ) {
+
+		super( html2canvas( dom ) );
+
+		this.dom = dom;
+
+		this.anisotropy = 16;
+		this.encoding = sRGBEncoding;
+		this.minFilter = LinearFilter;
+		this.magFilter = LinearFilter;
+
+		// Create an observer on the DOM, and run html2canvas update in the next loop
+		const observer = new MutationObserver( () => {
+
+			if ( ! this.scheduleUpdate ) {
+
+				// ideally should use xr.requestAnimationFrame, here setTimeout to avoid passing the renderer
+				this.scheduleUpdate = setTimeout( () => this.update(), 16 );
+
+			}
+
+		} );
+
+		const config = { attributes: true, childList: true, subtree: true, characterData: true };
+		observer.observe( dom, config );
+
+		this.observer = observer;
+
+	}
+
+	dispatchDOMEvent( event ) {
+
+		if ( event.data ) {
+
+			htmlevent( this.dom, event.type, event.data.x, event.data.y );
+
+		}
+
+	}
+
+	update() {
+
+		this.image = html2canvas( this.dom );
+		this.needsUpdate = true;
+
+		this.scheduleUpdate = null;
+
+	}
+
+	dispose() {
+
+		if ( this.observer ) {
+
+			this.observer.disconnect();
+
+		}
+
+		this.scheduleUpdate = clearTimeout( this.scheduleUpdate );
+
+		super.dispose();
+
+	}
+
+}
+
+
+//
+
+const canvases = new WeakMap();
+
+function html2canvas( element ) {
+
+	const range = document.createRange();
+	const color = new Color();
+
+	function Clipper( context ) {
+
+		const clips = [];
+		let isClipping = false;
+
+		function doClip() {
+
+			if ( isClipping ) {
+
+				isClipping = false;
+				context.restore();
+
+			}
+
+			if ( clips.length === 0 ) return;
+
+			let minX = - Infinity, minY = - Infinity;
+			let maxX = Infinity, maxY = Infinity;
+
+			for ( let i = 0; i < clips.length; i ++ ) {
+
+				const 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 !== '' ) {
+
+			if ( style.textTransform === 'uppercase' ) {
+
+				string = string.toUpperCase();
+
+			}
+
+			context.font = style.fontWeight + ' ' + style.fontSize + ' ' + style.fontFamily;
+			context.textBaseline = 'top';
+			context.fillStyle = style.color;
+			context.fillText( string, x, y + parseFloat( style.fontSize ) * 0.1 );
+
+		}
+
+	}
+
+	function buildRectPath( x, y, w, h, r ) {
+
+		if ( w < 2 * r ) r = w / 2;
+		if ( h < 2 * r ) r = h / 2;
+
+		context.beginPath();
+		context.moveTo( x + r, y );
+		context.arcTo( x + w, y, x + w, y + h, r );
+		context.arcTo( x + w, y + h, x, y + h, r );
+		context.arcTo( x, y + h, x, y, r );
+		context.arcTo( x, y, x + w, y, r );
+		context.closePath();
+
+	}
+
+	function drawBorder( style, which, x, y, width, height ) {
+
+		const borderWidth = style[ which + 'Width' ];
+		const borderStyle = style[ which + 'Style' ];
+		const borderColor = style[ which + 'Color' ];
+
+		if ( borderWidth !== '0px' && borderStyle !== 'none' && borderColor !== 'transparent' && borderColor !== 'rgba(0, 0, 0, 0)' ) {
+
+			context.strokeStyle = borderColor;
+			context.lineWidth = parseFloat( borderWidth );
+			context.beginPath();
+			context.moveTo( x, y );
+			context.lineTo( x + width, y + height );
+			context.stroke();
+
+		}
+
+	}
+
+	function drawElement( element, style ) {
+
+		let x = 0, y = 0, width = 0, height = 0;
+
+		if ( element.nodeType === Node.TEXT_NODE ) {
+
+			// text
+
+			range.selectNode( element );
+
+			const 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.nodeType === Node.COMMENT_NODE ) {
+
+			return;
+
+		} else if ( element instanceof HTMLCanvasElement ) {
+
+			// Canvas element
+			if ( element.style.display === 'none' ) return;
+
+			context.save();
+			const dpr = window.devicePixelRatio;
+			context.scale( 1 / dpr, 1 / dpr );
+			context.drawImage( element, 0, 0 );
+			context.restore();
+
+		} else {
+
+			if ( element.style.display === 'none' ) return;
+
+			const 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 );
+
+			// Get the border of the element used for fill and border
+
+			buildRectPath( x, y, width, height, parseFloat( style.borderRadius ) );
+
+			const backgroundColor = style.backgroundColor;
+
+			if ( backgroundColor !== 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)' ) {
+
+				context.fillStyle = backgroundColor;
+				context.fill();
+
+			}
+
+			// If all the borders match then stroke the round rectangle
+
+			const borders = [ 'borderTop', 'borderLeft', 'borderBottom', 'borderRight' ];
+
+			let match = true;
+			let prevBorder = null;
+
+			for ( const border of borders ) {
+
+				if ( prevBorder !== null ) {
+
+					match = ( style[ border + 'Width' ] === style[ prevBorder + 'Width' ] ) &&
+					( style[ border + 'Color' ] === style[ prevBorder + 'Color' ] ) &&
+					( style[ border + 'Style' ] === style[ prevBorder + 'Style' ] );
+
+				}
+
+				if ( match === false ) break;
+
+				prevBorder = border;
+
+			}
+
+			if ( match === true ) {
+
+				// They all match so stroke the rectangle from before allows for border-radius
+
+				const width = parseFloat( style.borderTopWidth );
+
+				if ( style.borderTopWidth !== '0px' && style.borderTopStyle !== 'none' && style.borderTopColor !== 'transparent' && style.borderTopColor !== 'rgba(0, 0, 0, 0)' ) {
+
+					context.strokeStyle = style.borderTopColor;
+					context.lineWidth = width;
+					context.stroke();
+
+				}
+
+			} else {
+
+				// Otherwise draw individual borders
+
+				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 instanceof HTMLInputElement ) {
+
+				let accentColor = style.accentColor;
+
+				if ( accentColor === undefined || accentColor === 'auto' ) accentColor = style.color;
+
+				color.set( accentColor );
+
+				const luminance = Math.sqrt( 0.299 * ( color.r ** 2 ) + 0.587 * ( color.g ** 2 ) + 0.114 * ( color.b ** 2 ) );
+				const accentTextColor = luminance < 0.5 ? 'white' : '#111111';
+
+				if ( element.type === 'radio' ) {
+
+					buildRectPath( x, y, width, height, height );
+
+					context.fillStyle = 'white';
+					context.strokeStyle = accentColor;
+					context.lineWidth = 1;
+					context.fill();
+					context.stroke();
+
+					if ( element.checked ) {
+
+						buildRectPath( x + 2, y + 2, width - 4, height - 4, height );
+
+						context.fillStyle = accentColor;
+						context.strokeStyle = accentTextColor;
+						context.lineWidth = 2;
+						context.fill();
+						context.stroke();
+
+					}
+
+				}
+
+				if ( element.type === 'checkbox' ) {
+
+					buildRectPath( x, y, width, height, 2 );
+
+					context.fillStyle = element.checked ? accentColor : 'white';
+					context.strokeStyle = element.checked ? accentTextColor : accentColor;
+					context.lineWidth = 1;
+					context.stroke();
+					context.fill();
+
+					if ( element.checked ) {
+
+						const currentTextAlign = context.textAlign;
+
+						context.textAlign = 'center';
+
+						const properties = {
+							color: accentTextColor,
+							fontFamily: style.fontFamily,
+							fontSize: height + 'px',
+							fontWeight: 'bold'
+						};
+
+						drawText( properties, x + ( width / 2 ), y, '✔' );
+
+						context.textAlign = currentTextAlign;
+
+					}
+
+				}
+
+				if ( element.type === 'range' ) {
+
+					const [ min, max, value ] = [ 'min', 'max', 'value' ].map( property => parseFloat( element[ property ] ) );
+					const position = ( ( value - min ) / ( max - min ) ) * ( width - height );
+
+					buildRectPath( x, y + ( height / 4 ), width, height / 2, height / 4 );
+					context.fillStyle = accentTextColor;
+					context.strokeStyle = accentColor;
+					context.lineWidth = 1;
+					context.fill();
+					context.stroke();
+
+					buildRectPath( x, y + ( height / 4 ), position + ( height / 2 ), height / 2, height / 4 );
+					context.fillStyle = accentColor;
+					context.fill();
+
+					buildRectPath( x + position, y, height, height, height / 2 );
+					context.fillStyle = accentColor;
+					context.fill();
+
+				}
+
+				if ( element.type === 'color' || element.type === 'text' || element.type === 'number' ) {
+
+					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 );
+		*/
+
+		const isClipping = style.overflow === 'auto' || style.overflow === 'hidden';
+
+		if ( isClipping ) clipper.add( { x: x, y: y, width: width, height: height } );
+
+		for ( let i = 0; i < element.childNodes.length; i ++ ) {
+
+			drawElement( element.childNodes[ i ], style );
+
+		}
+
+		if ( isClipping ) clipper.remove();
+
+	}
+
+	const offset = element.getBoundingClientRect();
+
+	let canvas;
+
+	if ( canvases.has( element ) ) {
+
+		canvas = canvases.get( element );
+
+	} else {
+
+		canvas = document.createElement( 'canvas' );
+		canvas.width = offset.width;
+		canvas.height = offset.height;
+
+	}
+
+	const context = canvas.getContext( '2d'/*, { alpha: false }*/ );
+
+	const clipper = new Clipper( context );
+
+	// console.time( 'drawElement' );
+
+	drawElement( element );
+
+	// console.timeEnd( 'drawElement' );
+
+	return canvas;
+
+}
+
+function htmlevent( element, event, x, y ) {
+
+	const mouseEventInit = {
+		clientX: ( x * element.offsetWidth ) + element.offsetLeft,
+		clientY: ( y * element.offsetHeight ) + element.offsetTop,
+		view: element.ownerDocument.defaultView
+	};
+
+	window.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
+
+	const rect = element.getBoundingClientRect();
+
+	x = x * rect.width + rect.left;
+	y = y * rect.height + rect.top;
+
+	function traverse( element ) {
+
+		if ( element.nodeType !== Node.TEXT_NODE && element.nodeType !== Node.COMMENT_NODE ) {
+
+			const rect = element.getBoundingClientRect();
+
+			if ( x > rect.left && x < rect.right && y > rect.top && y < rect.bottom ) {
+
+				element.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
+
+				if ( element instanceof HTMLInputElement && element.type === 'range' && ( event === 'mousedown' || event === 'click' ) ) {
+
+					const [ min, max ] = [ 'min', 'max' ].map( property => parseFloat( element[ property ] ) );
+
+					const width = rect.width;
+					const offsetX = x - rect.x;
+					const proportion = offsetX / width;
+					element.value = min + ( max - min ) * proportion;
+					element.dispatchEvent( new InputEvent( 'input', { bubbles: true } ) );
+
+				}
+
+			}
+
+			for ( let i = 0; i < element.childNodes.length; i ++ ) {
+
+				traverse( element.childNodes[ i ] );
+
+			}
+
+		}
+
+	}
+
+	traverse( element );
+
+}
+
+export { HTMLMesh };

Some files were not shown because too many files changed in this diff