xzw 2 rokov pred
rodič
commit
4bc1c117d4
84 zmenil súbory, kde vykonal 42162 pridanie a 51 odobranie
  1. 16 31
      src/Potree.js
  2. 2 2
      src/Potree_update_visibility.js
  3. 1 1
      src/TextSprite.js
  4. 139 0
      src/custom/materials/DepthBasicMaterial.js
  5. 401 0
      src/custom/materials/ModelTextureMaterial.js
  6. 64 0
      src/custom/modules/CameraAnimation/CamAniEditor.js
  7. 650 0
      src/custom/modules/CameraAnimation/CameraAnimation.js
  8. 252 0
      src/custom/modules/Particles/ParticleEditor.js
  9. 342 0
      src/custom/modules/clipModel/Clip.js
  10. 410 0
      src/custom/modules/datasetAlignment/Alignment.js
  11. 619 0
      src/custom/modules/mergeModel/MergeEditor.js
  12. 1263 0
      src/custom/modules/panoEdit/panoEditor.js
  13. 314 0
      src/custom/modules/panos/DepthImageSampler.js
  14. 3173 0
      src/custom/modules/panos/Images360.js
  15. 762 0
      src/custom/modules/panos/Panorama.js
  16. 87 0
      src/custom/modules/panos/Panorama1.js
  17. 1207 0
      src/custom/modules/panos/tile/PanoRenderer.js
  18. 176 0
      src/custom/modules/panos/tile/QualityManager.js
  19. 545 0
      src/custom/modules/panos/tile/TileDownloader.js
  20. 395 0
      src/custom/modules/panos/tile/TilePrioritizer.js
  21. 168 0
      src/custom/modules/panos/tile/TileTree.js
  22. 283 0
      src/custom/modules/panos/tile/TileUtils.js
  23. 652 0
      src/custom/modules/route/RouteGuider.js
  24. 879 0
      src/custom/modules/siteModel/BuildingBox.js
  25. 1281 0
      src/custom/modules/siteModel/SiteModel.js
  26. 7 10
      src/custom/note.txt
  27. 121 0
      src/custom/objects/Axis.js
  28. 141 0
      src/custom/objects/InfiniteGridHelper.js
  29. 83 0
      src/custom/objects/Label.js
  30. 350 0
      src/custom/objects/Magnifier.js
  31. 274 0
      src/custom/objects/Reticule.js
  32. 166 0
      src/custom/objects/Sprite.js
  33. 161 0
      src/custom/objects/Tag.js
  34. 177 0
      src/custom/objects/TextSprite.js
  35. 36 0
      src/custom/objects/fireParticle/Tween.js
  36. 475 0
      src/custom/objects/fireParticle/explode/ExplodeParticle.js
  37. 57 0
      src/custom/objects/fireParticle/explode/Particle.js
  38. 18 0
      src/custom/objects/fireParticle/explode/Util.js
  39. 4 0
      src/custom/objects/fireParticle/explode/const.js
  40. 40 0
      src/custom/objects/fireParticle/explode/shader.js
  41. 274 0
      src/custom/objects/fireParticle/fire/FireParticle.js
  42. 63 0
      src/custom/objects/fireParticle/fire/shader.js
  43. 57 0
      src/custom/objects/fireParticle/smoke/Particle.js
  44. 594 0
      src/custom/objects/fireParticle/smoke/SmokeParticle.js
  45. 44 0
      src/custom/objects/fireParticle/smoke/shader.js
  46. 296 0
      src/custom/objects/tool/Compass.js
  47. 229 0
      src/custom/objects/tool/CurveCtrl.js
  48. 78 0
      src/custom/objects/tool/HandleSprite.js
  49. 154 0
      src/custom/objects/tool/HandleSvg.js
  50. 1245 0
      src/custom/objects/tool/Measure.js
  51. 647 0
      src/custom/objects/tool/MeasuringTool.js
  52. 94 0
      src/custom/objects/tool/TagTool.js
  53. 1981 0
      src/custom/objects/tool/TransformControls.js
  54. 723 0
      src/custom/objects/tool/ctrlPolygon.js
  55. 347 0
      src/custom/objects/tool/mapClipBox.js
  56. 453 0
      src/custom/settings.js
  57. 911 0
      src/custom/start.js
  58. 320 0
      src/custom/utils/Common.js
  59. 115 0
      src/custom/utils/CursorDeal.js
  60. 401 0
      src/custom/utils/DrawUtil.js
  61. 910 0
      src/custom/utils/ExtendTransformationTool.js
  62. 46 0
      src/custom/utils/History.js
  63. 315 0
      src/custom/utils/MathLight.js
  64. 193 0
      src/custom/utils/SplitScreen.js
  65. 342 0
      src/custom/utils/SplitScreen4Views.js
  66. 393 0
      src/custom/utils/UnitConvert.js
  67. 367 0
      src/custom/utils/browser.js
  68. 19 0
      src/custom/utils/cameraLight.js
  69. 112 0
      src/custom/utils/file.js
  70. 608 0
      src/custom/utils/math.js
  71. 489 0
      src/custom/utils/request.js
  72. 879 0
      src/custom/utils/searchRings.js
  73. 486 0
      src/custom/utils/transitions.js
  74. 5160 0
      src/custom/viewer/ExtendViewer - 原始.js
  75. 3796 0
      src/custom/viewer/ExtendViewer.js
  76. 91 0
      src/custom/viewer/Viewport.js
  77. 777 0
      src/custom/viewer/map/Map.js
  78. 649 0
      src/custom/viewer/map/MapViewer.js
  79. 361 0
      src/custom/viewer/viewerBase.js
  80. 1 1
      src/navigation/FirstPersonControls.js
  81. 6 4
      src/navigation/FirstPersonControlsNew.js
  82. 1 1
      src/utils/TransformationTool.js
  83. 943 0
      src/utils/TransformationToolNew.js
  84. 1 1
      src/viewer/View.js

+ 16 - 31
src/Potree.js

@@ -3,8 +3,7 @@
   export * from "./custom/start.js";
   export {settings, config} from './custom/settings.js'  
    
-export * from "./Actions.js";
-export * from "./AnimationPath.js";
+export * from "./Actions.js"; 
 export * from "./Annotation.js";
 export * from "./defines.js";
 export * from "./Enum.js";
@@ -12,17 +11,15 @@ export * from "./EventDispatcher.js";
 export * from "./Features.js";
 export * from "./KeyCodes.js";
 export * from "./LRU.js";
-export * from "./PointCloudEptGeometry.js";
-export * from "./PointCloudOctree.js";
-												//export * from "./custom/ExtendPointCloudOctree.js";
+export * from "./PointCloudEptGeometry.js"; 
+export * from "./ExtendPointCloudOctree.js";
 export * from "./PointCloudOctreeGeometry.js";
 export * from "./PointCloudTree.js";
 export * from "./Points.js";
-// export * from "./Potree_update_visibility.js";
-												//export * from "./custom/ExtendPotree_update_visibility.js";
+ 
 export * from "./PotreeRenderer.js";
 export * from "./ProfileRequest.js";
-export * from "./TextSprite.js";
+export * from "./custom/objects/TextSprite.js";
 export * from "./utils.js";
 export * from "./Version.js";
 export * from "./WorkerPool.js";
@@ -46,19 +43,13 @@ export * from "./loader/ept/ZstandardLoader.js";
 export * from "./loader/PointAttributes.js";
 export * from "./loader/ShapefileLoader.js";
 export * from "./loader/GeoPackageLoader.js";
-
-												// export * from "./objects/tool/Box3Helper.js";
-												// export * from "./objects/tool/ClippingTool.js";
-												// export * from "./objects/tool/ClipVolume.js";
+ 
 export * from "./utils/Box3Helper.js";
 export * from "./utils/ClippingTool.js";
 export * from "./utils/ClipVolume.js";
 export * from "./utils/GeoTIFF.js";
-												// export * from "./objects/tool/Measure.js";
-												// export * from "./objects/tool/MeasuringTool.js";
-
-export * from "./utils/Measure.js";
-export * from "./utils/MeasuringTool.js";
+												 
+ 
 export * from "./utils/Message.js";
 export * from "./utils/PointCloudSM.js";
 												// export * from "./objects/tool/PolygonClipVolume.js";
@@ -75,26 +66,21 @@ export * from "./utils/Profile.js";
 export * from "./utils/ProfileTool.js";
 export * from "./utils/ScreenBoxSelectTool.js";
 export * from "./utils/SpotLightHelper.js";
-export * from "./utils/TransformationTool.js";
+export * from "./utils/TransformationToolNew.js";
 export * from "./utils/Volume.js";
-export * from "./utils/VolumeTool.js";
-export * from "./utils/Compass.js";
-    export * from "./custom/viewer/ExtendViewer.js";
-											 
-export * from "./viewer/Scene.js";
+export * from "./utils/VolumeTool.js"; 
+export * from "./custom/viewer/ExtendViewer.js"; 
+export * from "./viewer/ExtendScene.js";
 export * from "./viewer/HierarchicalSlider.js";
 
 export * from "./modules/OrientedImages/OrientedImages.js";
-export * from "./modules/Images360/Images360.js";
-export * from "./modules/CameraAnimation/CameraAnimation.js";
+export * from "./custom/modules/panos/Images360.js";
+export * from "./custom/modules/CameraAnimation/CameraAnimation.js";
 
 export * from "./modules/loader/2.0/OctreeLoader.js";
 
 export {OrbitControls} from "./navigation/OrbitControls.js";
-export {FirstPersonControls} from "./navigation/FirstPersonControls.js";
-export {EarthControls} from "./navigation/EarthControls.js";
-export {DeviceOrientationControls} from "./navigation/DeviceOrientationControls.js";
-export {VRControls} from "./navigation/VRControls.js";
+export {FirstPersonControls} from "./navigation/FirstPersonControls.js"; 
 	
 
     
@@ -106,8 +92,7 @@ import {LRU} from "./LRU.js";
 import {OctreeLoader} from "./modules/loader/2.0/OctreeLoader.js";
 import {POCLoader} from "./loader/POCLoader.js";
 import {EptLoader} from "./loader/EptLoader.js";
-import {ExtendPointCloudOctree} from "./ExtendPointCloudOctree.js";
-												//import {ExtendPointCloudOctree} from "./custom/ExtendPointCloudOctree.js";
+import {ExtendPointCloudOctree} from "./ExtendPointCloudOctree.js"; 
 import {WorkerPool} from "./WorkerPool.js";
 
 

+ 2 - 2
src/Potree_update_visibility.js

@@ -3,7 +3,7 @@ import * as THREE from "../libs/three.js/build/three.module.js";
 import {ClipTask, ClipMethod} from "./defines.js";
 import {Box3Helper} from "./utils/Box3Helper.js";
 
-export function updatePointClouds(pointclouds, camera, renderer){
+export function updatePointClouds(pointclouds, camera, renderer){//old 
 
 	for (let pointcloud of pointclouds) {
 		let start = performance.now();
@@ -34,7 +34,7 @@ export function updatePointClouds(pointclouds, camera, renderer){
 
 
 
-export function updateVisibilityStructures(pointclouds, camera, renderer) {
+export function updateVisibilityStructures(pointclouds, camera, renderer) {//old
 	let frustums = [];
 	let camObjPositions = [];
 	let priorityQueue = new BinaryHeap(function (x) { return 1 / x.weight; });

+ 1 - 1
src/TextSprite.js

@@ -6,7 +6,7 @@
 
 import * as THREE from "../libs/three.js/build/three.module.js";
 
-export class TextSprite extends THREE.Object3D{
+export class TextSprite extends THREE.Object3D{//old
 	
 	constructor(text){
 		super();

+ 139 - 0
src/custom/materials/DepthBasicMaterial.js

@@ -0,0 +1,139 @@
+
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+import {Shaders} from "../../../build/shaders/shaders.js";
+import {Features} from "../../Features.js";
+
+ 
+export default class DepthBasicMaterial extends THREE.ShaderMaterial{ 
+    constructor(o={}){
+        let {width, height} = viewer.renderer.getSize(new THREE.Vector2());
+        
+        let uniforms = {
+			resolution:    { type: 'v2',  value: new THREE.Vector2(width, height ) },
+            viewportOffset: { type: 'v2',  value: new THREE.Vector2(0, 0 ) }, //left, top    
+			nearPlane:     { type: 'f', 	value: 0.1 },
+			farPlane:      { type: 'f', 	value: 10000 }, 
+			depthTexture:   { type: 't', 	value: null }, 
+			opacity:        { type: 'f',	value: 1  },
+			map:             { type: 't', 	value: o.map }, 
+            baseColor:     {type:'v3',      value: o.color ?  new THREE.Color(o.color) :  new THREE.Color("#ffffff")},
+            backColor:     {type:'v3',      value: o.backColor ?  new THREE.Color(o.backColor) :  new THREE.Color("#ddd")},
+            clipDistance :     { type: 'f', 	value:o.clipDistance || 4}, //消失距离
+            occlusionDistance :     { type: 'f', 	value: o.occlusionDistance || 1 }, //变为backColor距离
+            maxClipFactor :  { type: 'f', 	value: o.maxClipFactor || 1 },  //0-1
+      
+
+		}  
+        
+        let defines = {};
+        
+        let useDepth = o.useDepth && Features.EXT_DEPTH.isSupported()/*  && Potree.settings.matUseDepth */
+        if(useDepth )defines.useDepth = ''
+        if(o.map)defines.use_map = '' 
+        super({ 
+            uniforms,
+            vertexShader: Shaders['depthBasic.vs'],   
+            fragmentShader: Shaders['depthBasic.fs'],
+            depthWrite: !1,
+            depthTest: !1,
+            transparent: o.transparent == void 0 ?  true : o.transparent,
+            side: o.side || 0 /* THREE.DoubleSide */,
+            defines, 
+        })
+        if(o.opacity != void 0){
+            this.opacity = o.opacity
+        }
+       
+        if(useDepth) this.useDepth_ = true
+        
+        
+      
+        
+        if(this.useDepth){  
+        
+            let setSize = (e)=>{//如果出现横条状的异常,往往是viewportOffset出错 
+                let viewport = e.viewport
+                let viewportOffset = viewport.offset || new THREE.Vector2() 
+                this.uniforms.resolution.value.copy(viewport.resolution2) 
+                this.uniforms.viewportOffset.value.copy(viewportOffset)
+                
+                //console.log('depth '+viewportOffset.toArray())
+            }
+            
+            let viewport = viewer.mainViewport;
+                 
+            setSize( {viewport} )
+            
+            viewer.addEventListener('resize',(e)=>{ 
+                if(!this.useDepth || !e.viewport || e.viewport.camera.isPerspectiveCamera){//地图不需要
+                    setSize(e) 
+                } 
+            })  
+        
+        
+            
+            /* viewer.addEventListener('camera_changed', (e)=>{
+                if(e.viewport.name != 'mapViewport') this.updateDepthParams(e) 
+            }) */ 
+         
+            viewer.addEventListener("render.begin", (e)=>{//before render  如果有大于两个viewport的话,不同viewport用不同的depthTex
+                if(e.viewport.camera.isPerspectiveCamera) this.updateDepthParams(e)
+            })
+            
+            this.updateDepthParams()
+        }
+         
+        
+        //点云变化时要一直触发updateDepthParams??
+        //viewer.once("render.pass.end",this.updateDepthParams.bind(this))
+    }
+    
+    updateDepthParams(e={}){//主要用于点云遮住mesh
+        if(this.useDepth){ 
+            var viewport = e.viewport || viewer.mainViewport;
+            var camera = viewport.camera;
+            /* if(Potree.settings.displayMode == 'showPanos' && viewer.images360.currentPano.depthTex){
+                this.uniforms.depthTexture.value = viewer.images360.currentPano.depthTex
+            }else{ */
+                this.uniforms.depthTexture.value = viewer.getPRenderer().getRtEDL(viewport).depthTexture   //其实只赋值一次就行
+            //}
+            this.uniforms.nearPlane.value = camera.near;
+            this.uniforms.farPlane.value = camera.far;
+            
+        }            
+    } 
+    set map(map){
+        this.uniforms.map.value = map; 
+    }
+    
+    get useDepth(){
+        return this.useDepth_
+    } 
+    
+    set useDepth(value){//如果不支持 EXT_DEPTH 的话会失效
+        if(this.useDepth_ != value){
+            if(value && Features.EXT_DEPTH.isSupported()){
+                this.defines.useDepth = ''
+                this.updateDepthParams()
+            }else{
+                delete this.defines.useDepth
+            }
+            this.useDepth_ = value
+            this.needsUpdate = true
+        }
+    } 
+    
+    
+    get opacity(){
+        return this.uniforms.opacity.value
+    }
+    set opacity(o){
+        this.uniforms && (this.uniforms.opacity.value = o)
+    }
+    
+    /* dispose(){ 
+        super.dispose()
+        viewer.depthBasic
+    } */
+    
+}

+ 401 - 0
src/custom/materials/ModelTextureMaterial.js

@@ -0,0 +1,401 @@
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+import Common from '../utils/Common.js'
+import {Features} from "../../Features.js";
+                                      
+
+const prefixVertex ="precision highp float;\nprecision highp int;\n\nuniform mat4 modelMatrix;\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat4 viewMatrix;\nuniform mat3 normalMatrix;\nuniform vec3 cameraPosition;\nattribute vec3 position;\nattribute vec3 normal;\nattribute vec2 uv;\n"
+const prefixFragment ="precision highp float;\nprecision highp int;\n\nuniform mat4 viewMatrix;\nuniform vec3 cameraPosition;\n"
+ 
+let shader = {
+	 
+		uniforms: { 
+			
+			opacity: {
+				type: "f",
+				// value: 1
+			},
+			progress: {
+				type: "f",
+				value: 0
+			},
+			
+			pano0Map: {
+				type: "t",
+				value: null
+			}, 
+            pano1Map: {
+				type: "t",
+				value: null
+			}, 
+            depthMap0: {
+				type: "t",
+				value: null
+			},
+            depthMap1: {
+				type: "t",
+				value: null
+			}, 
+			pano0Position: {
+				type: "v3",
+				value: new THREE.Vector3
+			},
+			pano0Matrix: {
+				type: "m4",
+				value: new THREE.Matrix4
+			},
+			
+			pano1Position: {
+				type: "v3",
+				value: new THREE.Vector3
+			},
+			pano1Matrix: {
+				type: "m4",
+				value: new THREE.Matrix4
+            },
+			/* pano1Matrix2: {
+				type: "m4",
+				value: new THREE.Matrix4
+            },
+            */
+            
+            inverseProjectionMatrix: {
+                value: new THREE.Matrix4
+            },  
+            /* projectionMatrix:{//需要再写一遍吗
+                value: new THREE.Matrix4
+            }, */
+            viewport: {
+                value: new THREE.Vector4
+            },
+            //如     {x: 0, y: 0, z: 428, w: 969}  xy应该是offset, zw是宽高 
+            cameraHeight0: {
+				type: "f",
+				value: 1
+			},
+            cameraHeight1: {
+				type: "f",
+				value: 1
+			},
+            
+        },
+       
+        vertexShader: prefixVertex + `
+
+            uniform vec3 pano0Position;
+            uniform mat4 pano0Matrix;
+            
+            uniform vec3 pano1Position;
+            uniform mat4 pano1Matrix;
+            //uniform mat4 pano1Matrix2;
+
+           
+            varying vec2 vUv; 
+            varying vec3 vWorldPosition0;
+            varying vec3 vWorldPosition1;
+            varying vec3 vWorldPosition12;
+            
+            vec3 transformAxis( vec3 direction ) //navvis->4dkk
+            {
+                float y = direction.y;
+                direction.y = direction.z;
+                direction.z = -y;
+                return  direction;
+            }
+             
+            
+            void main() {
+            
+                vUv = uv;
+                vec4 worldPosition = modelMatrix * vec4(position, 1.0);
+                
+                
+            
+                vec3 positionLocalToPanoCenter0 = worldPosition.xyz - pano0Position;
+                vWorldPosition0 = (vec4(positionLocalToPanoCenter0, 1.0) * pano0Matrix).xyz;
+                vWorldPosition0.x *= -1.0;
+                vWorldPosition0 = transformAxis(vWorldPosition0);
+                
+                vec3 positionLocalToPanoCenter1 = worldPosition.xyz - pano1Position;
+                vWorldPosition1 = (vec4(positionLocalToPanoCenter1, 1.0) * pano1Matrix).xyz;
+                vWorldPosition1.x *= -1.0;
+                vWorldPosition1 = transformAxis(vWorldPosition1);
+                
+                /* 
+                vec3 positionLocalToPanoCenter12 = worldPosition.xyz - pano1Position;
+                vWorldPosition12 = (vec4(positionLocalToPanoCenter12, 1.0) * pano1Matrix2).xyz;
+                vWorldPosition12.x *= -1.0;
+                vWorldPosition12 = transformAxis(vWorldPosition12);
+                 */
+                
+                
+                
+                gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
+            
+            }
+
+        `,
+        fragmentShader: prefixFragment + `
+             
+            #define PI 3.141592653 
+            
+             
+            uniform float modelAlpha;
+            uniform float opacity;
+            uniform float progress;
+            uniform int blackout;
+            uniform vec3 pano0Position;
+            uniform vec3 pano1Position;
+            uniform float maxDistance;
+            uniform float minDistance;
+            uniform float minOpa;
+            
+            uniform float cameraHeight0;
+            uniform float cameraHeight1;
+
+          
+       
+            /* uniform sampler2D pano0Map;
+            uniform sampler2D pano1Map;    */   
+            uniform samplerCube pano0Map;
+            uniform samplerCube pano1Map;
+          
+            
+            varying vec2 vUv; 
+            varying vec3 vWorldPosition0;
+            varying vec3 vWorldPosition1;
+            //varying vec3 vWorldPosition12;
+          
+            /* vec2 getSamplerCoord( vec3 direction ) 
+            {
+                direction = normalize(direction);
+                float tx=atan(direction.x,-direction.y)/(PI*2.0)+0.5;
+                float ty=acos(direction.z)/PI;
+
+                return vec2(tx,ty);
+            } */
+
+            vec2 getSamplerCoord2( vec3 direction ) 
+            { 
+                direction = normalize(direction);
+                float tx=atan(direction.x,direction.z)/(PI*2.0)+0.5;
+                float ty=acos(direction.y)/PI;
+
+                return vec2(tx,ty); 
+            }
+            
+            #extension GL_EXT_frag_depth : enable
+            #if defined(GL_EXT_frag_depth) && defined(hasDepthTex)  
+                uniform sampler2D depthMap0;
+                uniform sampler2D depthMap1;
+                uniform mat4 inverseProjectionMatrix;
+                uniform mat4 projectionMatrix;
+                uniform vec4 viewport; 
+            
+                vec2 getDepth(vec3 dir, sampler2D depthMap, float height, vec4 eyePos){
+                    vec2 depthValue = vec2(0.0, 0.0);
+                    vec2 uv2 = getSamplerCoord2(/* vWorldPosition12 */dir.xyz);  //暂时只用基于目标漫游点的方向
+                    uv2.x -= 0.25;    //全景图和Cube的水平采样起始坐标相差90度,这里矫正 0.25 个采样偏移
+                    vec4 depth = texture2D(depthMap, uv2);
+                    //float distance = depth.r + 256. * (depth.g + 256. * depth.b);
+                    //distance *= 255. * .001;           // distance is now in meters
+                    
+                    //更改
+                    float distance = (depth.g + depth.r / 256.) * 255.;  //为什么要乘以255 
+                    
+                    if(distance == 0.0){//漫游点底部识别不到的区域,给一个地板高度 
+                         if(uv2.y > 0.75) distance = height / dir.y; 
+                         else distance = 100000.0;//给个超级远的值
+                    } 
+                    
+                    if(distance == 0.0)distance = 100000.0;//给个超级远的值
+                    
+                    depthValue.x = distance;
+                    
+                   // return  r[1] + r[0] / 256  
+                    distance += .1;          // add a safety margin
+
+                    vec4 eyePos2 = vec4(normalize(eyePos.xyz) * distance, 1.);
+                    vec4 clipPos2 = projectionMatrix * eyePos2;
+                    vec4 ndcPos2 = clipPos2 * 1. / clipPos2.w;
+
+                    
+                    depthValue.y = 0.5 * ((gl_DepthRange.far - gl_DepthRange.near) * ndcPos2.z
+                            + gl_DepthRange.near + gl_DepthRange.far); 
+                    return depthValue;      
+                }
+                //注:未加载好的话,depth为0,导致第一次漫游过去的时候许多mesh会立刻被遮挡,所以要确保加载完
+            #endif
+            
+            void main()
+            {
+                vec3 vWorldPosition0N = normalize(vWorldPosition0);
+                vec3 vWorldPosition1N = normalize(vWorldPosition1);
+                /* vec2 samplerCoord0 = getSamplerCoord(vWorldPosition0.xyz);
+                vec2 samplerCoord1 = getSamplerCoord(vWorldPosition1.xyz);  
+                vec4 colorFromPano0=texture2D(pano0Map,samplerCoord0);
+                vec4 colorFromPano1=texture2D(pano1Map,samplerCoord1); */
+                
+                vec4 colorFromPano0 = vec4(0.0,0.0,0.0,0.0);
+                if(progress < 1.0){//通常是1
+                    colorFromPano0=textureCube(pano0Map,vWorldPosition0N.xyz);
+                }
+                vec4 colorFromPano1=textureCube(pano1Map,vWorldPosition1N.xyz);
+ 
+                gl_FragColor=mix(colorFromPano0,colorFromPano1,progress);
+              
+              
+                
+              
+                //深度图修改深度
+              
+                #if defined(GL_EXT_frag_depth) && defined(hasDepthTex)  
+                    vec4 ndcPos;
+                    ndcPos.xy = ((2.0 * gl_FragCoord.xy) - (2.0 * viewport.xy)) / (viewport.zw) - 1.;
+                    ndcPos.z = (2.0 * gl_FragCoord.z - gl_DepthRange.near - gl_DepthRange.far) /
+                        (gl_DepthRange.far - gl_DepthRange.near);
+                    ndcPos.w = 1.0;
+
+                    vec4 clipPos = ndcPos / gl_FragCoord.w;
+                    vec4 eyePos = inverseProjectionMatrix * clipPos;
+                    vec2 depth0 = vec2(0.0,0.0); 
+                    if(progress < 1.0){
+                        depth0 = getDepth(vWorldPosition0N, depthMap0, cameraHeight0, eyePos);
+                    }
+                    vec2 depth1 = getDepth(vWorldPosition1N, depthMap1, cameraHeight1, eyePos);
+                    
+                    /* if(progress < 1.0 && depth1.x == 0.0 && depth0.x > 0.0){
+                        gl_FragDepthEXT = depth0.y; 
+                    }else{ */
+                        gl_FragDepthEXT = mix(depth0.y,depth1.y,progress);
+                    //}
+                    
+                    
+
+                #endif
+
+                
+            }
+        `
+    }
+            
+            
+
+export default class ModelTextureMaterial extends THREE.RawShaderMaterial { 
+	constructor( ){ 
+    
+        let defines = {}
+         
+       
+        
+        super({
+            fragmentShader: shader.fragmentShader,
+			vertexShader: shader.vertexShader,
+			uniforms: THREE.UniformsUtils.clone(shader.uniforms),
+            side:THREE.DoubleSide,
+			name: "ModelTextureMaterial",
+            defines
+        })
+    
+        
+            
+        let setSize = (e)=>{ 
+            let viewport = e.viewport
+            let viewportOffset = viewport.offset || new Vector2()  
+            let resolution = viewport.resolution2 
+            this.uniforms.viewport.value.set(viewportOffset.x, viewportOffset.y, resolution.x, resolution.y) 
+        }
+        let viewport = viewer.mainViewport;
+         
+        setSize({viewport})
+
+
+        viewer.addEventListener('resize',(e)=>{
+            setSize(e)     
+        }) 
+        
+        
+        //var supportExtDepth = !!Features.EXT_DEPTH.isSupported()  
+        {
+         
+            //add
+            
+            viewer.addEventListener('camera_changed', (e)=>{
+                //this.uniforms.projectionMatrix.value.copy(e.camera.projectionMatrix) 
+                e.camera && this.uniforms.inverseProjectionMatrix.value.copy(e.camera.projectionMatrixInverse)
+            })   
+
+        } 
+
+        
+		//-------------------------------------
+	}
+
+	/**
+	 * 
+	 * @param {Panorama} pano0 
+	 * @param {Panorama} pano1 
+	 * @param {boolean} flag 
+     
+     更新全景图的材质uniforms 
+     
+	 */
+     
+      
+     
+	setProjectedPanos(pano0, pano1, progressValue ){
+        
+ 		progressValue!=void 0 && (this.uniforms.progress.value = progressValue);
+		//pano0.ensureSkyboxReadyForRender();
+        
+        
+        if(pano0){
+            this.uniforms.pano0Map.value = pano0.getSkyboxTexture();//pano0.texture
+            this.uniforms.pano0Position.value.copy(pano0.position)
+            this.uniforms.pano0Matrix.value.copy(pano0.panoMatrix/* pano0.mesh.matrixWorld */ );
+            this.uniforms.cameraHeight0.value = pano0.floorPosition.distanceTo(pano0.position)
+            
+            //pano1.ensureSkyboxReadyForRender();
+        }
+        
+		
+		this.uniforms.pano1Map.value = pano1.getSkyboxTexture()//pano1.texture;
+		this.uniforms.pano1Position.value.copy(pano1.position)
+		this.uniforms.pano1Matrix.value.copy(pano1.panoMatrix /* pano1.mesh.matrixWorld */ );
+        this.uniforms.cameraHeight1.value = pano1.floorPosition.distanceTo(pano1.position)
+        this.pano0 = pano0
+        this.pano1 = pano1
+        
+        this.updateDepthTex(pano0)  
+        this.updateDepthTex(pano1)
+        
+        
+        //console.log('setProjectedPanos', pano0&&pano0.id, pano1&&pano1.id)
+        this.needsUpdate = true;
+ 	}
+    
+    
+    
+    updateDepthTex(pano){
+        if( !Potree.settings.useDepthTex || !pano || !pano.depthTex || pano!=this.pano0 && pano!=this.pano1)return
+        //console.log('updateDepthTex', pano.id,  this.pano0 && this.pano0.id,  this.pano1 && this.pano1.id)
+        this.uniforms.depthMap0.value = this.pano0 && this.pano0.depthTex 
+        this.uniforms.depthMap1.value = this.pano1 && this.pano1.depthTex 
+        this.updateDepthTexEnable()
+    }
+    
+    updateDepthTexEnable(){
+        let hasDepthTex = this.pano0 && this.pano1 && this.pano0.pointcloud.hasDepthTex && this.pano1.pointcloud.hasDepthTex  //暂时不知道一个有图一个没图怎么写所以
+        
+        Common.addOrRemoveDefine(this, 'hasDepthTex', hasDepthTex?'add':'remove' )
+        
+        
+    }
+    
+    /* EnableDepthTex(){//开启DepthTex
+        if(this.defines['hasDepthTex']){
+            return 
+        }
+        this.defines['hasDepthTex'] = ''
+        this.needsUpdate = true;
+    } */
+}

+ 64 - 0
src/custom/modules/CameraAnimation/CamAniEditor.js

@@ -0,0 +1,64 @@
+import * as THREE from "../../../../libs/three.js/build/three.module.js"; 
+import {CameraAnimation} from './CameraAnimation.js'
+
+
+
+let CamAniEditor = {
+    
+    
+     
+    
+    createAnimation(data){
+        let animation = new CameraAnimation(viewer);
+        if(data) { 
+            animation.name = data.name;
+            animation.duration = data.duration;
+            animation.useDurSlice = data.useDurSlice
+             
+            for(const cpdata of data.points){ 
+                /* const position = Potree.Utils.datasetPosTransform({ fromDataset: true, position: cpdata.position, datasetId: Potree.settings.originDatasetId })
+                const target = Potree.Utils.datasetPosTransform({ fromDataset: true, position: cpdata.target, datasetId: Potree.settings.originDatasetId })
+                 */
+                const position = new THREE.Vector3().copy(cpdata.position)
+                const target = new THREE.Vector3().copy(cpdata.target)  
+                const duration = cpdata.time
+                const cp = animation.createControlPoint(null, {position, target, duration}); 
+            }
+        }
+        
+        animation.changeCallback()
+        viewer.scene.addCameraAnimation(animation);
+         
+        return animation
+        
+    },
+    
+    
+    removeAnimation(animation){
+        animation.dispatchEvent('dispose')
+        viewer.scene.removeCameraAnimation(animation)
+    }
+    
+    
+    
+     
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+}
+
+
+export default CamAniEditor

+ 650 - 0
src/custom/modules/CameraAnimation/CameraAnimation.js

@@ -0,0 +1,650 @@
+
+import * as THREE from "../../../../libs/three.js/build/three.module.js"; 
+import { Utils } from "../../../utils.js";
+import math  from "../../utils/math.js";
+import {LineDraw} from "../../utils/DrawUtil.js"; 
+import CurveCtrl from  "../../objects/tool/CurveCtrl.js";
+import HandleSvg from  "../../objects/tool/HandleSvg.js";
+import {/* transitions,*/ easing,  lerp} from '../../utils/transitions.js'
+ 
+const colors = {
+    position: 'red', 
+    target : 'blue'
+}
+ 
+
+let lineMats 
+const getLineMat = function(name){
+    if(!lineMats){
+        lineMats = {
+            position: LineDraw.createFatLineMat({
+                color: colors.position,  
+                lineWidth: 3   
+            }),
+            target : LineDraw.createFatLineMat({
+                color: colors.target,  
+                lineWidth: 3  
+            }),
+            frustum:  LineDraw.createFatLineMat({
+                color: colors.position,  
+                lineWidth: 2   
+            }),
+            aimAtTarget: new THREE.LineBasicMaterial({color:colors.target})
+        }
+    }
+    return lineMats[name]
+}
+
+ 
+
+
+export class CameraAnimation extends THREE.EventDispatcher{
+
+	constructor(viewer){
+		super();
+		
+		this.viewer = viewer;
+
+		this.selectedElement = null;
+
+		//this.controlPoints = [];
+
+		this.uuid = THREE.Math.generateUUID();
+
+		this.node = new THREE.Object3D();
+		this.node.name = "camera animation";
+		this.viewer.scene.scene.add(this.node);
+
+		this.frustum = this.createFrustum();
+		this.node.add(this.frustum);
+       
+
+		this.name = "Camera Animation";
+		
+		// "centripetal", "chordal", "catmullrom"
+		this.curveType = "centripetal" 
+		this.visible = true;
+
+        this.targets = [];  
+		this.createPath();
+        this.duration = 5; 
+		this.percent = 0;
+        this.currentIndex = 0
+        this.durations = []
+        this.quaternions = [];
+        
+        if(!Potree.settings.isTest){
+            this.setVisible(false)
+        }
+        
+        
+        
+        this.addEventListener('dispose', ()=>{  
+            this.dispose()
+        })
+        
+        
+        this.targetLines = new THREE.Object3D
+        this.node.add(this.targetLines)
+        
+        
+        
+        
+        
+	}
+
+	static defaultFromView(viewer){
+		const animation = new CameraAnimation(viewer);
+
+		const camera = viewer.scene.getActiveCamera();
+		const target = viewer.scene.view.getPivot();
+
+		const cpCenter = new THREE.Vector3(
+			0.3 * camera.position.x + 0.7 * target.x,
+			0.3 * camera.position.y + 0.7 * target.y,
+			0.3 * camera.position.z + 0.7 * target.z,
+		);
+
+		const targetCenter = new THREE.Vector3(
+			0.05 * camera.position.x + 0.95 * target.x,
+			0.05 * camera.position.y + 0.95 * target.y,
+			0.05 * camera.position.z + 0.95 * target.z,
+		);
+
+		const r = 2//camera.position.distanceTo(target) * 0.3;
+
+		//const dir = target.clone().sub(camera.position).normalize();
+		const angle = Utils.computeAzimuth(camera.position, target);
+
+		const n = 5;
+		for(let i = 0; i < n; i++){
+			let u = 1.5 * Math.PI * (i / n) + angle;
+
+			const dx = r * Math.cos(u);
+			const dy = r * Math.sin(u);
+
+			const cpPos = new THREE.Vector3(
+				cpCenter.x + dx,
+				cpCenter.y + dy,
+				cpCenter.z,
+			)
+
+			const targetPos = new THREE.Vector3(
+				targetCenter.x + dx * 0.1,
+				targetCenter.y + dy * 0.1,
+				targetCenter.z,
+            )
+
+			animation.createControlPoint(null,{position:cpPos, target:targetPos});
+			 
+             
+		}
+
+        animation.changeCallback()
+		return animation;
+	}
+
+	 
+
+	createControlPoint(index, posInfo ){ 
+        const length = this.posCurve.points.length
+        const position = new THREE.Vector3
+        const target = new THREE.Vector3
+        
+		if(index == void 0 ){
+			index = length;
+		}
+        
+        if(!posInfo){
+        
+            if(length >= 2 && index === 0){ 
+                const dir = new THREE.Vector3().subVectors(this.posCurve.points[0], this.posCurve.points[1] ) 
+                position.copy(this.posCurve.points[0]).add(dir);
+                
+                const tDir = new THREE.Vector3().subVectors(this.targets[0].position, this.targets[1].position ) 
+                target.copy(this.targets[0].position).add(dir);
+                 
+            }else if(length >= 2 && index === length){ 
+                const dir = new THREE.Vector3().subVectors(this.posCurve.points[length-1], this.posCurve.points[length-2] ) 
+                position.copy(this.posCurve.points[length-2]).add(dir);
+                 
+                const tDir = new THREE.Vector3().subVectors(this.targets[length-1].position, this.targets[length-2].position ) 
+                target.copy(this.targets[length-2].position).add(dir);
+                 
+            }else if(length >= 2){ 
+                position.copy(this.posCurve.points[index-1].clone().add(this.posCurve.points[index]).multiplyScalar(0.5));
+                target.copy(this.targets[length-1].position.clone().add(this.targets[length]).multiplyScalar(0.5));
+            }
+        }else{
+            position.copy(posInfo.position)
+            target.copy(posInfo.target)
+             
+        }
+            
+        this.posCurve.addPoint(position, index/* , true */)
+        //this.targetCurve.addPoint(target, index/* , true */) 
+        let targetSvg = new HandleSvg(target, colors.target)
+        targetSvg.visible = this.visible 
+        this.targets = [...this.targets.slice(0,index), targetSvg, ...this.targets.slice(index,length)]
+        if(this.useDurSlice){//不使用全局的duration,而是分段的
+            this.durations = [...this.durations.slice(0,index), posInfo.duration, ...this.durations.slice(index,length)]
+        }
+        
+        
+		this.dispatchEvent({
+			type: "controlpoint_added", 
+            index
+		});
+        
+        {
+            let targetLine = LineDraw.createLine([position,target] ,{mat: getLineMat('aimAtTarget')})
+            this.targetLines.children = [...this.targetLines.children.slice(0,index), targetLine, ...this.targetLines.children.slice(index,length)]
+            
+            
+            
+            this.targets[index].addEventListener('dragged', (e)=>{ 
+                this.updatePathCallback()
+                this.dragPointCallback(e)
+            })
+        } 
+	}
+
+	 
+    dragPointCallback(e){
+        
+        let index = e.index
+        if(e.index == void 0){
+            index = this.targets.indexOf(e.target)
+        }
+        LineDraw.moveLine(this.targetLines.children[index], [this.posCurve.points[index], this.targets[index].position] )
+         
+        this.updateFrustum()
+    } 
+     
+    updatePathCallback(){
+        {
+            this.quaternions = [];
+            let length = this.posCurve.points.length;
+            for(let i=0; i<length; i++){
+                let quaternion = math.getQuaFromPosAim(this.posCurve.points[i], this.targets[i].position) 
+                this.quaternions.push( quaternion) 
+            }
+        }
+        this.reMapCurvePercent()
+    } 
+     
+     
+    removeControlPoint(index){
+        this.posCurve.removePoint(index)
+        //this.targetCurve.removePoint(index)
+        this.targets[index].dispose();
+        this.targets.splice(index, 1)
+         
+        this.dispatchEvent({
+			type: "controlpoint_removed",
+			index
+		});
+        this.targetLines.remove(this.targetLines.children[index])
+        if(this.useDurSlice){
+            this.durations.splice(index, 1)
+        }
+        
+    }
+
+
+
+	createPath(){
+        this.posCurve = new CurveCtrl([],getLineMat('position'), colors.position, 'posCurve'); 
+        //this.targetCurve = new CurveCtrl([], getLineMat('target'), colors.target, 'targetCurve', {noLine:true}); 
+        this.posCurve.needsPercent = true
+        this.node.add(this.posCurve)
+        //this.node.add(this.targetCurve)
+        
+        this.posCurve.addEventListener('dragCurvePoint', this.dragPointCallback.bind(this))
+        this.posCurve.addEventListener('updatePath', this.updatePathCallback.bind(this))
+            
+         
+        
+        
+	}
+
+
+     
+
+	createFrustum(){
+
+		const f = 0.3;
+
+		const positions = [
+			new THREE.Vector3( 0,  0,  0),
+			new THREE.Vector3(-f, -f, +1),
+
+			new THREE.Vector3( 0,  0,  0),
+			new THREE.Vector3( f, -f, +1),
+
+			new THREE.Vector3( 0,  0,  0),
+			new THREE.Vector3( f,  f, +1),
+
+			new THREE.Vector3( 0,  0,  0),
+			new THREE.Vector3(-f,  f, +1),
+
+			new THREE.Vector3(-f, -f, +1),
+			new THREE.Vector3( f, -f, +1),
+
+			new THREE.Vector3( f, -f, +1),
+			new THREE.Vector3( f,  f, +1),
+
+			new THREE.Vector3( f,  f, +1),
+			new THREE.Vector3(-f,  f, +1),
+
+			new THREE.Vector3(-f,  f, +1),
+			new THREE.Vector3(-f, -f, +1),
+		]
+        
+        positions.forEach(e=>e.z *= -1)  //因为得到的rotation是camera的,作用在物体上要反向,所以这里反向一下
+        
+		//geometry.computeBoundingSphere();//? 
+		const line = LineDraw.createFatLine( positions, {material:getLineMat('frustum')})
+         
+        //line.scale.set(20, 20, 20); 
+        line.visible = false
+		return line;
+	}
+
+
+    reMapCurvePercent(){  //因在不同点在相同位置旋转,由于间隔仅和位置距离相关,导致时间间隔为0,采取重新调整间隔的策略。
+        var length = this.posCurve.points.length
+        
+        if(length<2){
+            return this.newPointsPercents = []
+        }
+        var newPercents = [0];
+        
+        
+        
+        if(this.useDurSlice){ //已经设定好了每一段的duration的话
+            let sums = [0]
+            let sum = 0, last
+            for(let i=0;i<length-1;i++){ //去掉最后一个duration,因为已到终点
+                let duration = this.durations[i];
+                sum += duration;   
+                last = duration;
+                sums.push(sum) 
+            }
+            for(let i=1;i<length;i++){ 
+                newPercents.push(sum == 0 ? i/length : sums[i] / sum)
+            } 
+             
+        }else{
+            
+            const maxSpaceDur = this.duration / length  //每两点之间修改间隔时间后,最大时间
+            const durPerRad = 0.8     //每弧度应该占用的时间
+            const minSpaceDur =  Math.min(0.8, maxSpaceDur)//每两点之间修改间隔时间后,最小时间
+            const maxAngleSpaceDur = THREE.Math.clamp(durPerRad * Math.PI, minSpaceDur, maxSpaceDur )// 最大可能差距是180度
+            
+            
+            
+            var percents = this.posCurve.pointsPercent; 
+            
+            
+            
+            for(let i=1;i<length;i++){
+                let diff = (percents[i] - percents[i-1]) * this.duration  //间隔时间
+                let percent 
+                let curMin = minSpaceDur
+                if(diff < maxAngleSpaceDur){ //若小于最大旋转时间
+                    let rad = this.quaternions[i].angleTo(this.quaternions[i-1]) 
+                    curMin = THREE.Math.clamp(rad * durPerRad, minSpaceDur, maxSpaceDur)
+     
+                }  
+
+                diff = Math.max(diff, curMin) 
+                percent = newPercents[i-1] + (diff / this.duration) //得到新的percent
+                
+                
+                newPercents.push(percent) 
+            }
+            
+            let maxPercent = newPercents[length-1]  //最后一个,若扩充过时间,就会>1
+
+
+            if( !math.closeTo(maxPercent, 1)){
+                let scale = 1 / maxPercent //需要压缩的比例 <1  这一步会让实际得到的间隔更小 
+                newPercents = newPercents.map(e=> e*=scale ) 
+            }
+                
+            
+        }
+        
+        this.newPointsPercents = newPercents;
+        //console.log(newPercents)
+    }
+
+
+	at(originPercent, delta, transitionRatio){
+		 
+        originPercent = THREE.Math.clamp(originPercent, 0, 1)
+        //修改第一层:起始时间
+        let percent = originPercent;
+        /* const easePercent = 0.3; //缓动占比  //如果能在所有从静止到运动的中间加缓动就好了呀:lastPos * 0.9 + currentPos *  0.1 ?
+        if(originPercent < easePercent){
+            console.log('easeIn')
+            percent = easing.easeInSine(originPercent, 0, easePercent, easePercent) //currentTime, startY, wholeY, duration   选了一个衔接时接近斜率1的缓动函数
+        }else if(originPercent > 1-easePercent){ 
+            console.log('easeOut')
+            percent = easing.easeOutSine(originPercent-(1-easePercent), 1-easePercent, easePercent, easePercent)
+        } */
+        
+        
+		
+	 
+        let quaternion
+        
+        if(percent < 1){
+            //修改第二层:使用每个点的重定位的 newPointsPercents
+            
+            this.currentIndex = this.newPointsPercents.findIndex(e=> e>percent ) - 1   
+            //假设每个节点的百分比是精确的,那么:
+            let curIndexPercent = this.newPointsPercents[this.currentIndex];
+            let nextIndexPercent = this.newPointsPercents[this.currentIndex+1];
+            let progress = (percent - curIndexPercent) / (nextIndexPercent - curIndexPercent)//在这两个节点间的百分比
+            
+            //投影到原本的 posCurve.pointsPercent上:
+            let curIndexOriPercent = this.posCurve.pointsPercent[this.currentIndex] 
+            let nextIndexOriPercent = this.posCurve.pointsPercent[this.currentIndex+1] 
+            percent = curIndexOriPercent + (nextIndexOriPercent - curIndexOriPercent) * progress
+            
+            let endQuaternion = this.quaternions[this.currentIndex+1]
+            let startQuaternion = this.quaternions[this.currentIndex]       
+            quaternion = (new THREE.Quaternion()).copy(startQuaternion) 
+            lerp.quaternion(quaternion, endQuaternion)(progress) 
+            
+            
+        }else{
+             
+            
+            this.currentIndex = this.posCurve.points.length - 1;
+            
+            quaternion = math.getQuaFromPosAim(this.posCurve.points[this.currentIndex], this.targets[this.currentIndex].position)
+        }
+        
+        
+        const position = this.posCurve.getPointAt(percent); // 需要this.posCurve.points.length>1 否则报错
+        
+        
+        //console.log(this.currentIndex, originPercent)
+        //缓动:
+        var aimQua, aimPos;
+        if(delta != void 0 ){
+            if(Potree.settings.tourTestCameraMove){
+                aimQua = this.frustum.quaternion.clone();
+                aimPos = this.frustum.position.clone();
+            }else{
+                var camera = viewer.scene.getActiveCamera();
+                aimQua = camera.quaternion.clone();
+                aimPos = camera.position.clone();
+            }
+            
+            transitionRatio = transitionRatio || 1 / Potree.settings.cameraAniSmoothRatio//渐变系数,越小缓动程度越高,越平滑
+            transitionRatio *= delta * 60 //假设标准帧率为60fps,当帧率低时(delta大时)要降低缓动
+            //console.log(transitionRatio, delta) //画面ui变化会使delta变大
+            transitionRatio = THREE.Math.clamp(transitionRatio, 0, 1);
+            lerp.quaternion(aimQua, quaternion)(transitionRatio) //每次只改变一点点  
+            lerp.vector(aimPos, position)(transitionRatio)
+  
+        }else{
+            aimQua = quaternion;  aimPos = position
+        }
+        
+        
+        
+        let rotation = new THREE.Euler().setFromQuaternion(aimQua )
+  
+ 
+        const frame = {
+			position: aimPos,  
+            rotation
+		};
+
+
+		return frame;
+	}
+
+	set(percent){
+		this.percent = percent;
+	}
+
+	 
+
+	setVisible(visible){
+		this.node.visible = visible;
+ 
+        this.posCurve.visible = visible
+        this.targets.forEach(e=>e.visible = visible ) 
+
+		this.visible = visible;
+	}
+
+	setDuration(duration){
+        if(duration != this.duration){
+            this.duration = duration;
+            if(this.quaternions.length == this.posCurve.points.length)this.reMapCurvePercent()
+            
+        } 
+	}
+
+	getDuration(duration){
+		return this.duration;
+	}
+
+	play(startOptions={}){
+        if(this.onUpdate){
+            return console.error('已经开始播放')
+        }
+       
+        
+        
+        let startPercent = 0, currentIndex = 0  
+        if(startOptions.percent != void 0 ){
+            startPercent = startOptions.percent
+        }else if(startOptions.index){
+            currentIndex = index
+            //startPercent = index/(this.posCurve.points.length-1)
+            
+            startPercent = this.posCurve.pointsPercent[index]
+        }
+        
+        
+		//const tStart = performance.now();
+        
+        
+
+
+		const duration = this.duration;
+
+		this.originalyVisible = this.visible;
+		Potree.settings.tourTestCameraMove || this.setVisible(false);
+        
+        
+        let tStart, startTransitionRatio = 0.2
+        let startDelay = 1/startTransitionRatio / 20 ;//因为缓动所以延迟开始,前面前都是at(0),使过渡到开始点位(但是依旧不能准确停在起始点,因为缓动是乘百分比有残留。所以直接平滑衔接到开始后的位置)
+        let hasPlayedTime = 0
+        
+        let finishDelay = Potree.settings.cameraAniSmoothRatio / 60 * 3//结束后还需要多久时间才能大致达到缓动的最终目标
+        let hasStoppedTime = 0
+
+		this.onUpdate = (e) => {
+            if(this.posCurve.points.length<2){
+                if(this.posCurve.points.length == 1){
+                    viewer.scene.view.position.copy(this.posCurve.points[0]);
+                    viewer.scene.view.rotation = new THREE.Euler().setFromQuaternion(this.quaternions[0])
+                }
+                this.pause()
+                return 
+            }
+             
+            let percent, transitionRatio
+            
+            if(tStart){ 
+                let tNow = performance.now(); 
+                let elapsed = (tNow - tStart) / 1000;
+                percent = elapsed / duration + startPercent;
+            }else{//从当前位置过渡到开始位置 
+                percent = 0
+                hasPlayedTime += e.delta
+                transitionRatio = startTransitionRatio;
+                //console.log('延迟开始') 
+                if(hasPlayedTime > startDelay){
+                    tStart = performance.now(); 
+                }
+                
+            }
+             
+			this.set(percent);
+             
+			const frame = this.at(percent, e.delta, transitionRatio);
+            
+            if(currentIndex != this.currentIndex){
+                currentIndex = this.currentIndex
+                console.log('updateCurrentIndex', currentIndex)
+                this.dispatchEvent({type:'updateCurrentIndex', currentIndex  })
+            }
+            
+            
+            
+            if(!Potree.settings.tourTestCameraMove){
+                viewer.scene.view.position.copy(frame.position);
+                //viewer.scene.view.lookAt(frame.target);
+                viewer.scene.view.rotation = frame.rotation; 
+            }
+
+            this.updateFrustum(frame)
+
+            
+            
+            
+			if(percent >= 1){ 
+                if(hasStoppedTime > finishDelay){
+                    this.pause()
+                }else{ 
+                    hasStoppedTime += e.delta
+                    //console.log('延迟结束') 
+                }
+                
+			}
+             
+            
+            
+		};
+
+		this.viewer.addEventListener("update", this.onUpdate);
+         
+         
+	}
+
+    pause(){ 
+        this.setVisible(this.originalyVisible); 
+        this.viewer.removeEventListener("update", this.onUpdate);
+        this.dispatchEvent('playDone')
+        this.onUpdate = null
+    }
+
+
+
+    updateFrustum(frame){  
+        const frustum = this.frustum;
+        if(this.posCurve.points.length>1){
+            frustum.visible = true
+        }else{
+            frustum.visible = false;
+            return
+        }
+        
+        frame = frame || this.at(this.percent);
+        frustum.position.copy(frame.position);
+        //frustum.lookAt(...frame.target.toArray());
+        frustum.rotation.copy(frame.rotation)
+    }
+
+    changeCallback(){
+        this.posCurve.update()
+        //this.targetCurve.update()
+        this.targets.forEach(e=>e.update()) 
+        this.updateFrustum()
+    }
+
+    dispose(){//add
+        this.posCurve.dispose()
+        //this.targetCurve.dispatchEvent({type:'dispose'}) 
+        this.targets.forEach(e=>e.dispose())
+        this.durations = []
+        this.node.parent.remove(this.node);
+    }
+}
+
+
+//scene.removeCameraAnimation
+
+
+//修改:不使用targetCurve作为target曲线,因为播放时posCuve的节点和targetCurve并没有对应,且使用target的曲线会使角度变化大的情况过渡生硬。
+// 改完旋转了。但是位置也有问题。速度完全和路程相关,当在同一位置设置多点时,这段的总时长为0. (是否要设置最小时长?不过也做不到 - -)

+ 252 - 0
src/custom/modules/Particles/ParticleEditor.js

@@ -0,0 +1,252 @@
+
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import FireParticle from '../../objects/fireParticle/fire/FireParticle.js'
+import SmokeParticle from '../../objects/fireParticle/smoke/SmokeParticle.js'
+import ExplodeParticle from '../../objects/fireParticle/explode/ExplodeParticle.js'
+import CurveCtrl from '../../objects/tool/CurveCtrl.js'
+import {LineDraw} from "../../utils/DrawUtil.js";
+import Common from '../../utils/Common.js' 
+import DepthBasicMaterial from "../../materials/DepthBasicMaterial.js";
+
+
+
+const colors = {
+    'fire+smoke':0xffffff,
+    'smoke': 0xffffff,
+    'explode':0xffffff,
+}
+
+
+let depthMatPrefix = {
+    clipDistance : 100, occlusionDistance:60, /* 变为backColor距离 */ 
+    maxClipFactor:0.5, backColor:"#777"  ,
+    useDepth:true, transparent: !0,
+}
+let lineMats; 
+let getLineMat = function(type){
+    if(!lineMats){
+        lineMats = {
+            'fire+smoke':LineDraw.createFatLineMat($.extend(depthMatPrefix,{  
+                color: colors['fire+smoke'],  
+                lineWidth: 2 
+            })),
+            'smoke' :LineDraw.createFatLineMat($.extend(depthMatPrefix,{  
+                color: colors['smoke'],  
+                lineWidth: 2   
+            })),
+            'explode' :LineDraw.createFatLineMat($.extend(depthMatPrefix,{  
+                color: colors['explode'],  
+                lineWidth: 2 
+            })),           
+        }        
+    }
+    return lineMats[type]
+}
+
+let handleMats
+let getHandleMat = function(type){ 
+    if(!handleMats){
+        let texLoader = new THREE.TextureLoader()
+        
+        handleMats = {   
+            "fire+smoke" :   new DepthBasicMaterial($.extend(depthMatPrefix,{  
+                map: texLoader.load(Potree.resourcePath+'/textures/icon-fire.png' ), 
+                color:  colors['fire+smoke'],                
+            })),
+            "smoke" :   new DepthBasicMaterial($.extend(depthMatPrefix,{     
+                map: texLoader.load(Potree.resourcePath+'/textures/icon-smoke.png' ),  
+                color:  colors['smoke'],              
+            })),
+            "explode" :   new DepthBasicMaterial($.extend(depthMatPrefix,{   
+                map: texLoader.load(Potree.resourcePath+'/textures/icon-explode.png' ), 
+                color:  colors['explode'],      
+            })), 
+        } 
+    }
+    return handleMats[type]
+}
+
+let ParticleEditor = {
+    
+    bus: THREE.EventDispatcher,
+    particleGroup : new THREE.Object3D ,
+    curveGroup:new THREE.Object3D ,
+    init:function(){
+        this.particleGroup.name = 'particles'
+        viewer.scene.scene.add( this.particleGroup );
+        
+        
+        this.curveGroup.name = 'particles-curves'
+        viewer.scene.scene.add( this.curveGroup );
+        
+        
+    },
+    addParticle : function(prop={}){
+        
+         
+        let particle
+        if(prop.type == 'fire'){ 
+            particle = new FireParticle(prop) 
+            
+        }else if(prop.type == 'smoke'){
+            particle = new SmokeParticle(prop) 
+            
+        }else if(prop.type == 'explode'){
+            particle = new ExplodeParticle(prop) 
+        }
+        
+        this.particleGroup.add(particle)
+         
+        
+        
+        return particle
+    }
+    ,
+    removeParticle(particle){
+        //particle.dispatchEvent('delete')
+        particle.dispose();
+        this.particleGroup.remove(particle)
+        particle.curve.dispose()
+    }
+    ,
+    update(delta){
+        this.particleGroup.children.forEach(e=>e.update(delta))
+    }
+    ,
+    
+    startInsertion(type = 'fire', prop={}){  //viewer.modules.ParticleEditor.startInsertion()
+        let deferred = $.Deferred();
+        let particles = [];
+        
+        let finish = (ifDone)=>{ 
+            if(ifDone){
+                deferred.resolve(particles) 
+            }
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "remove",  name:"addSth"
+            }); 
+            viewer.removeEventListener('global_click', click) 
+            this.bus.removeEventListener('cancel_insertions',cancel)
+        }
+        
+        let curve = new CurveCtrl([], getLineMat(type), colors[type], type+'_curve', {handleMat:getHandleMat(type)} )
+        this.curveGroup.add(curve)
+        prop.curve = curve
+        prop.type = type 
+        //console.log('创建curve',type,curve.uuid)
+        
+        let cancel = ()=>{
+            console.log('cancel_insertions', curve.uuid )
+            curve.dispose();
+            finish(false)
+        }
+        this.bus.dispatchEvent('cancel_insertions')//删除旧的
+        this.bus.addEventListener('cancel_insertions',cancel)
+        
+        var click = (e)=>{  
+            if(e.button === THREE.MOUSE.RIGHT){  
+                if(curve.points.length>=1){ //if(type.includes('fire') || type.includes('smoke')  ){ 
+                      
+                    particles = this.createFromData(prop) 
+                    finish(true)                        
+                } 
+                return  
+            }
+            
+             
+            var I = e.intersect && (e.intersect.orthoIntersect || e.intersect.location)
+            if(!I)return
+            
+            curve.addPoint(I, null, true) 
+            
+            if(type == 'explode'){ 
+                particles = this.createFromData(prop)   
+                
+                finish(true)
+            }
+            
+            return {stopContinue:true}//防止继续执行别的侦听,如flytopano
+        }
+     
+        
+        viewer.addEventListener('global_click', click, 10)//add importance:10
+        viewer.dispatchEvent({
+            type : "CursorChange", action : "add",  name:"addSth"
+        });
+        
+        return deferred.promise()
+    },
+    
+    
+    
+    createFromData(prop){
+        const type = prop.type;
+        var particles = []
+        let curve = prop.curve;
+        if(!curve){
+            curve = new CurveCtrl(prop.points, getLineMat(type), colors[type], type+'_curve', {handleMat:getHandleMat(type)} )
+            this.curveGroup.add(curve)
+        }
+        
+        if(type.includes('fire') || type.includes('smoke')  ){
+            if(type.includes('fire')){
+                var fire = this.addParticle({
+                    type : 'fire',
+                    positions : curve.points,
+                    curve,
+                    radius : prop.radius,
+                    height: prop.height,
+                    strength : prop.strength,
+                })
+                particles.push(fire)
+            }
+            if(type.includes('smoke')){ 
+                var smoke = this.addParticle({
+                    type : 'smoke', 
+                    positions : curve.points,
+                    curve,
+                    positionStyle : 'sphere' , 
+                    strength : prop.smokeStrength,
+                    radius: prop.smokeRadius,
+                    height: prop.smokeHeight,
+                })
+                particles.push(smoke)
+            }
+             
+            
+        }else if(type == 'explode'){
+            var explode = this.addParticle({
+                type : 'explode', 
+                position : curve.points[0],
+                strength: prop.strength,
+                radius : prop.radius,
+                particleSpaceTime: prop.particleSpaceTime,
+                curve,
+                delayStartTime:prop.delayStartTime,
+            })
+            particles.push(explode)
+        }    
+
+        var geoNeedsUpdate 
+        curve.addEventListener('dragCurvePoint',()=>{ 
+            geoNeedsUpdate = true 
+            Common.intervalTool.isWaiting('particlePointChange', ()=>{ //延时update,防止卡顿  
+                if(geoNeedsUpdate){ 
+                    particles.forEach(e=>e.updateGeometry())  
+                    geoNeedsUpdate = false 
+                    curve.dispatchEvent('sendUpdatePoints')
+                    return true  
+                }
+            }, 400)  
+        })
+        
+        
+        
+        
+        return particles
+    }
+}
+
+
+
+export default ParticleEditor

+ 342 - 0
src/custom/modules/clipModel/Clip.js

@@ -0,0 +1,342 @@
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import {BoxVolume} from '../../../utils/Volume.js'
+import {  ClipTask, ClipMethod} from "../../../defines.js"
+import {mapClipBox} from '../../objects/tool/mapClipBox.js'
+import Common from '../../utils/Common.js'
+import math from '../../utils/math.js' 
+import {Images360} from '../panos/Images360.js'  
+
+const defaultBoxWidth = 6;  //navvis:  10
+                            //navvis position: si {x: 0, y: 0, z: 0}
+   
+var Clip = {
+    bus : new THREE.EventDispatcher,
+    selectedDatasets : [],    
+    changeCallback(force){ 
+        if(Potree.settings.isOfficial){  
+            Common.intervalTool.isWaiting('clipSelectedDatasets', ()=>{ //延时update,防止卡顿
+                let pointclouds = this.getIntersectPointcloud() 
+                if(force || Common.getDifferenceSet(pointclouds,this.selectedDatasets).length){  
+                    this.selectedDatasets = pointclouds 
+                    //console.error('clipSelectedDatasets',selectedDatasets)
+                    this.bus.dispatchEvent({type:'updateSelectedDatasets', selectedDatasets:pointclouds.map(e=>e.dataset_id) })
+                    force = false
+                    return true 
+                } 
+            },  300)  
+        } 
+    },
+
+    enter:function(){
+        this.previousView = { 
+			position: viewer.images360.position,
+			target: viewer.scene.view.getPivot(),
+            displayMode : Potree.settings.displayMode,
+            //---
+            ifShowMarker : Potree.settings.ifShowMarker,
+            
+		} 
+
+        let pointcloud = this.getPointcloud() 
+        let bound = pointcloud.bound //只选取其中一个数据集的bound,而非整体,是因为担心两个数据集中间有空隙,于是刚好落在没有点云的地方。
+        let boundSize = bound.getSize(new THREE.Vector3())
+        let target = this.getTarget(bound.getCenter(new THREE.Vector3())); //navvis的位置xy是用相机位置 this.ViewService.mainView.getCamera().position  我觉得也可以用第一个漫游点的,或者最接近bound中心的漫游点
+        let scale = new THREE.Vector3(defaultBoxWidth,defaultBoxWidth, boundSize.z)//z和navvis一样
+        
+        let eyeDir = viewer.scene.view.direction.clone().setZ(0/* -boundSize.z/3 */).multiplyScalar(-defaultBoxWidth)  //为了使所在楼层不变,不修改z
+
+        //let eyeDir = scale.clone().setZ(boundSize.z/3).multiplyScalar(1.3) 
+        let position = new THREE.Vector3().addVectors(target, eyeDir)
+        
+        Potree.settings.displayMode = 'showPointCloud'
+        viewer.setView({
+            position ,
+            target,
+            duration:300,
+            callback:function(){ 
+            }
+        })
+        viewer.setControls(viewer.orbitControls);
+        viewer.setLimitFar(false)
+         
+        
+        
+        {
+            this.box = new BoxVolume({
+                clip:true
+                
+            })  
+            this.box.name = "ClipBox"; 
+            this.box.position.copy(target)
+            this.box.scale.copy(scale)
+            //带动mapBox
+            this.box.addEventListener('position_changed',e=>{
+                this.mapBox.center.setX(this.box.position.x)
+                this.mapBox.center.setY(this.box.position.y)
+                this.mapBox.updatePoints() 
+                this.changeCallback()
+            })
+            this.box.addEventListener('scale_changed',e=>{
+                var scale = this.box.scale 
+                this.mapBox.updatePoints(scale)
+                this.changeCallback()
+            })
+            this.box.addEventListener('orientation_changed',e=>{
+                this.mapBox.angle = this.box.rotation.z
+                this.mapBox.rotateBar.rotation.z = this.mapBox.angle
+                this.mapBox.updatePoints()
+                this.changeCallback()
+            })
+            viewer.scene.addVolume(this.box);
+            
+        }
+        
+        {//map
+            let boxRotateBack = ()=>{//不知道是不是这么写。 因为可能z的旋转不一定都在z 
+                this.box.rotation.x = 0;
+                this.box.rotation.y = 0;
+            }
+            this.mapBox = new mapClipBox(target, scale)
+            viewer.mapViewer.scene.add(this.mapBox)
+            //带动box
+            this.mapBox.addEventListener('repos',e=>{
+                this.box.position.setX(this.mapBox.center.x)
+                this.box.position.setY(this.mapBox.center.y)
+                boxRotateBack()
+                this.changeCallback()
+            })
+            this.mapBox.addEventListener('dragChange',e=>{
+                var scale = this.mapBox.getScale() 
+                this.box.scale.setX(scale.x)
+                this.box.scale.setY(scale.y)
+                this.box.position.setX(this.mapBox.center.x)
+                this.box.position.setY(this.mapBox.center.y)
+                boxRotateBack()
+                this.changeCallback()
+            })
+            this.mapBox.addEventListener('rotate',e=>{ 
+                this.box.rotation.z = this.mapBox.angle 
+                boxRotateBack()
+                this.changeCallback()
+            })
+        }
+        
+        
+        
+        
+        {
+            viewer.setClipTask(ClipTask["SHOW_INSIDE"])
+            //viewer.setClipMethod(ClipMethod["INSIDE_ANY"])  
+        }
+        
+        Potree.settings.unableNavigate = true
+        Potree.settings.ifShowMarker = false
+        viewer.updateVisible(viewer.measuringTool.scene, 'clipModel', false)   
+        //viewer.updateVisible(viewer.mapViewer.cursor, 'clipModel', false)//隐藏地图游标
+        viewer.inputHandler.toggleSelection(this.box);
+        viewer.inputHandler.fixSelection = true
+        viewer.transformationTool.frame.material.color.set(Potree.config.clip.color)//navvis 15899953 
+        viewer.setPointStandardMat(true) 
+        
+        {
+            this.events = {
+                flyToPos : (e)=>{ 
+                    let dis = 2
+                    let target = e.position
+                    //position = new THREE.Vector3().subVectors(target, this.scene.view.direction)
+                    
+                    //永远朝向框的中心
+                    /* let dir = new THREE.Vector3().subVectors(this.box.position, e.position).normalize()
+                    position = new THREE.Vector3().subVectors(target, dir) */
+                    
+                  
+                    target = this.box.position
+                    position = e.position
+                    //为了方便缩放操作,直接使用box中心作为target
+                    
+                    
+                    let duration = 1000
+                    viewer.scene.view.setView({position,  duration,  target})
+                } 
+            }
+            
+            this.bus.addEventListener('flyToPos',this.events.flyToPos) 
+        }
+        this.editing = true
+        
+        setTimeout(()=>{this.changeCallback(true)},1)
+    },
+    
+    leave:function(){
+        viewer.inputHandler.fixSelection = false
+        viewer.scene.removeVolume(this.box);
+        
+        this.mapBox.dispose()
+        viewer.setControls(viewer.fpControls);
+        
+        Potree.settings.unableNavigate = false
+        Potree.settings.ifShowMarker = this.previousView.ifShowMarker
+        viewer.updateVisible(viewer.measuringTool.scene, 'clipModel', true)  
+        //viewer.updateVisible(viewer.mapViewer.cursor, 'clipModel', true) 
+        viewer.setView(this.previousView)
+        viewer.setLimitFar(true)
+        viewer.setPointStandardMat(false) 
+        
+        
+        {
+            this.bus.removeEventListener('flyToPos',this.events.flyToPos) 
+            this.events = null 
+        }
+        this.editing = false
+    },
+    
+
+
+    getPointcloud:function(){ //找一个离当前最近的点云,且最好有漫游点
+        let pointclouds = viewer.scene.pointclouds.filter(e=>e.panos.length>0)
+        if(pointclouds.length == 0)pointclouds = viewer.scene.pointclouds;
+
+
+        let result = Common.sortByScore(pointclouds,[],[e=>{
+            let center = e.bound.getCenter(new THREE.Vector3)
+            let size = e.bound.getSize(new THREE.Vector3).length() / 2 
+            let posToCenter = viewer.images360.position.distanceTo(center)
+            return size / posToCenter 
+        }])
+        
+        return result[0].item
+    },
+    
+
+    getTarget:function(boundCenter){//box位置。要找一个有点云的地方。方案1相机位置, 方案2接近相机的漫游点, 方案3接近中心的漫游点。选择方案2,因最大概率有点云
+        var target = new THREE.Vector3()
+        var cameraPos = viewer.images360.position;
+        var pano = Common.find(viewer.images360.panos , [], [Images360.sortFunctions.floorDisSquaredToPoint(cameraPos)]);
+        if(pano){
+            target.copy(pano.position) 
+            target.setZ(boundCenter.z)
+        }else{
+            target.copy(boundCenter)
+        }
+        
+        return target
+    },
+    /* switchMap:function(state){
+        
+        
+    }, */
+    
+    download:function( ){
+        
+        if(this.getIntersectPointcloud().length == 0){
+            return null
+        }
+        
+        
+        var visiPointclouds = viewer.scene.pointclouds.filter(e=> viewer.getObjVisiByReason(e, 'datasetSelection'))
+        let data = {   
+            transformation_matrix: visiPointclouds.map((cloud)=>{
+                let data = {
+                    id: cloud.dataset_id, 
+                    matrix : this.getTransformationMatrix(cloud).elements, 
+                    modelMatrix:(new THREE.Matrix4).copy(cloud.transformMatrix).transpose().elements
+                }  
+                return data
+            }) ,
+            aabb: "b-0.5 -0.5 -0.5 0.5 0.5 0.5" //剪裁空间( 所有点在乘上这个矩阵后, 还能落在 1 * 1 * 1的box内的点就是所裁剪的
+           
+        }
+        
+        return data
+        //https://testlaser.4dkankan.com/indoor/t-ia44BhY/api/pointcloud/crop
+    },
+    
+    
+    
+    downloadNoCrop(){//不剪裁  下载整个点云
+        
+        var visiPointclouds = viewer.scene.pointclouds.filter(e=> viewer.getObjVisiByReason(e, 'datasetSelection'))
+        let data = {   
+            transformation_matrix: visiPointclouds.map((cloud)=>{
+                let data = {
+                    id: cloud.dataset_id, 
+                    matrix : new THREE.Matrix4().elements, //固定值
+                    modelMatrix:(new THREE.Matrix4).copy(cloud.transformMatrix).transpose().elements
+                }  
+                return data
+            }) ,
+            aabb: "b-12742000 -12742000 -12742000 12742000 12742000 12742000" //固定剪裁空间 
+           
+        }
+        console.log(data)
+        return data
+         
+        
+        
+    },
+    
+    
+    getTransformationMatrix:function(pointcloud) {//剪裁矩阵
+        var invMatrix = new THREE.Matrix4().getInverse(this.box.matrixWorld) 
+        return (new THREE.Matrix4).multiplyMatrices(invMatrix, pointcloud.transformMatrix).transpose()
+    },
+
+
+    getIntersectPointcloud(){ 
+        var boxBound = new THREE.Box3(
+            new THREE.Vector3(-0.5,-0.5,-0.5), new THREE.Vector3(0.5,0.5,0.5),
+        ).applyMatrix4(this.box.matrixWorld)    //large boundingbox
+        
+       /*  var boxTightPoints = this.box.children[0].geometry.vertices.map(e=>e.clone().applyMatrix4(this.matrixWorld)) 
+            console.log(boxTightPoints) 
+        */
+        
+        let boxMatrixInverse = new THREE.Matrix4().copy(this.box.matrixWorld).invert();
+
+        let boxPoints = [
+            new THREE.Vector3(boxBound.min.x, boxBound.min.y,0),
+            new THREE.Vector3(boxBound.max.x, boxBound.min.y,0),
+            new THREE.Vector3(boxBound.max.x, boxBound.max.y,0),
+            new THREE.Vector3(boxBound.min.x, boxBound.max.y,0)
+        ]
+
+        var intersect = (pointcloud)=>{
+        
+            if(!pointcloud.bound.intersectsBox(boxBound))return false
+            //判断box和点云的tight bound是否相交(因为box可以任意旋转,且实在找不到三维中的立方体相交的函数,所以直接用boxBound) 
+            var points = pointcloud.getUnrotBoundPoint('all') 
+            let rings = math.getPolygonsMixedRings([points.slice(0,4), boxPoints] , true) 
+            //console.log(pointcloud.dataset_id, pointcloud.name, rings.length) 
+            if(rings.length > 1 )return false
+
+            {//再用frustum和数据集的sphere相交试试,能排除一些错误
+                let a = Potree.Utils.isInsideBox(points,  boxMatrixInverse) 
+                if(!a){
+                    console.log('没能经过isInsideBox测试')
+                }
+                return a
+            }
+            return true 
+            
+        }
+
+        
+
+
+        return viewer.scene.pointclouds.filter(e=>intersect(e)) 
+        
+       
+
+    }
+    
+    
+   
+    /* 
+    裁剪点云时,2D界面显示全部平面图,按楼层切换显示。 
+     */
+    
+}
+
+
+
+export {Clip} 

+ 410 - 0
src/custom/modules/datasetAlignment/Alignment.js

@@ -0,0 +1,410 @@
+
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import SplitScreen4Views from "../../utils/SplitScreen4Views.js"
+import math from "../../utils/math.js"
+import History from "../../utils/History.js"
+
+var Alignment = {
+    SplitScreen: SplitScreen4Views, 
+    handleState:null,  //操作状态 'translate'|'rotate'
+    bus: new THREE.EventDispatcher(), 
+    
+    history : new History({ 
+        callback: (data)=>{ 
+            data.forEach(item=>{
+                Alignment.applyTemp(item)  
+            }) 
+        } 
+    }),
+
+    prepareRecord : true, 
+    
+    writeToHistory(pointclouds){ 
+        if(!this.prepareRecord)return;
+        this.prepareRecord = false
+        
+        let content = this.getTemp(pointclouds)
+        this.history.writeIn(content)
+    },
+    
+    
+    /* undo(){//撤销一步 
+        let last = this.history.pop();
+        last && last.forEach(item=>{
+            this.applyTemp(item)  
+        })
+        
+    },  */ 
+    
+    applyTemp(item){ 
+        var pointcloud = viewer.scene.pointclouds.find(p=>p.dataset_id+p.name == item.sid)
+            pointcloud.orientationUser = item.orientationUser
+            pointcloud.translateUser = item.translateUser
+            this.setMatrix( pointcloud )
+    },
+    getTemp(pointclouds){//记录最近一次保存后的状态,便于恢复
+        pointclouds = pointclouds || viewer.scene.pointclouds
+        return pointclouds.map(e=>{
+            return {
+                sid : e.dataset_id+e.name,
+                orientationUser : e.orientationUser,
+                translateUser : e.translateUser.clone(),
+            }
+        } )
+    },
+    
+    
+    
+    init:function(){ 
+        let rotateInfo   
+        
+        viewer.fpControls.addEventListener("transformPointcloud",(e)=>{ 
+            if(e.pointclouds[0].dataset_id == Potree.settings.originDatasetId){//禁止手动移动初始数据集
+                return this.bus.dispatchEvent('forbitMoveOriginDataset') 
+            }
+            
+            
+                                                              
+             
+            
+            this.writeToHistory( e.pointclouds ) 
+              
+        
+            if(this.handleState == 'translate'){
+                e.pointclouds.forEach(cloud=>Alignment.translate(cloud, e.moveVec))
+                
+                
+            }else if(this.handleState == 'rotate'){
+                if(Potree.settings.editType == 'pano'){  
+                    //旋转中心是intersectStart的版本
+                    /* let center = e.intersectStart //旋转中心是mousedown的位置
+                    if(e.intersect.equals(center))return
+                    if(!rotateInfo){  
+                        rotateInfo = {
+                            orientationUser : e.pointclouds[0].orientationUser,
+                            //vecStart : e.moveVec, // 首次移动向量 只有八个方向,精度太小,所以延迟 
+                            pointclouds: e.pointclouds
+                        } 
+                        this.bus.dispatchEvent({type:'rotateStart', startPoint:center})
+                        return
+                    }else if(!rotateInfo.vecStart){  
+                        let vec = new THREE.Vector3().subVectors(e.intersect, center).setZ(0)
+                        if(vec.length() * e.camera.zoom >  30){  //在屏幕上距离初始点有一定距离后开始算
+                            //console.log('moveVec',vec)
+                            rotateInfo.vecStart = vec
+                        } */ 
+                   
+                     
+                    let center = e.pointclouds[0].translateUser //旋转中心是第一个点云的位置  
+                    if(e.intersect.equals(center))return
+                    if(!rotateInfo){  
+                        rotateInfo = {
+                            orientationUser : e.pointclouds[0].orientationUser,
+                            vecStart : new THREE.Vector3().subVectors(e.intersectStart, center).setZ(0), 
+                            pointclouds: e.pointclouds
+                        }  
+                    }else{ 
+                    
+                        let vec = new THREE.Vector3().subVectors(e.intersect, center).setZ(0)
+                        let angle = math.getAngle(rotateInfo.vecStart,vec,'z')   
+                        let diffAngle = rotateInfo.orientationUser + angle - rotateInfo.pointclouds[0].orientationUser
+                        
+                        rotateInfo.pointclouds.forEach(cloud=>{ 
+                            
+                           /*  let centerNoTranfrom = Potree.Utils.datasetPosTransform({ toDataset: true, pointcloud:cloud, position: center }) //中心点在数据集中的位置
+                            Alignment.rotate(cloud, null, diffAngle)
+                             
+                            let centerNow = Potree.Utils.datasetPosTransform({ fromDataset: true, pointcloud:cloud, position: centerNoTranfrom }) //中心点的现在位置
+                            let shift = new THREE.Vector3().subVectors( center, centerNow); //偏移量
+                            Alignment.translate(cloud,shift)   //使center还保留在原位
+                            //let centerNow1 = Potree.Utils.datasetPosTransform({ fromDataset: true, pointcloud:rotateInfo.pointcloud, position: centerNoTranfrom }) //中心点的现在位置
+                                 */ 
+                                 
+                            Alignment.rotateAround(center, cloud, null, diffAngle)    
+                                
+                                 
+                        }) 
+                        
+                        
+                    }
+                    //this.bus.dispatchEvent({type:'rotate', endPoint: e.intersect})
+                    
+                }else{ 
+                    let center = e.pointclouds[0].translateUser //移动到的位置就是中心
+                    if(e.intersect.equals(center))return
+                    if(!rotateInfo){  
+                        rotateInfo = {
+                            orientationUser : e.pointclouds[0].orientationUser,
+                            vecStart : new THREE.Vector3().subVectors(e.intersectStart, center).setZ(0),
+                            //pointclouds: e.pointclouds
+                            pointcloud: e.pointclouds[0]
+                        }  
+                    }else{ 
+                        let vec = new THREE.Vector3().subVectors(e.intersect, center).setZ(0)
+                        let angle = math.getAngle(rotateInfo.vecStart,vec,'z')   
+                        let diffAngle = rotateInfo.orientationUser + angle - rotateInfo.pointcloud.orientationUser
+                        Alignment.rotate(rotateInfo.pointcloud, null, diffAngle)
+                    }
+                }    
+            } 
+        })
+        
+        
+        viewer.fpControls.addEventListener("end",(e)=>{ 
+            rotateInfo = null 
+            this.prepareRecord = true
+        })
+        
+        viewer.inputHandler.addEventListener('keydown',e=>{ 
+            if(e.keyCode == 90 && e.event.ctrlKey){//Z
+                this.history.undo()
+            } 
+        })  
+		 
+        
+        // cursor:
+        
+        let updateCursor = (e)=>{ 
+            if(e.drag || !this.editing)return  //仅在鼠标不按下时更新:
+            
+            let handleState = Alignment.handleState
+            
+            if(e.hoverViewport.alignment && handleState && e.hoverViewport.alignment[handleState]){
+                if(handleState == 'translate'){
+                    if( e.intersect && e.intersect.location ){ 
+                        viewer.dispatchEvent({
+                            type : "CursorChange", action : "add",  name:"movePointcloud"
+                        })
+                    }else{
+                        viewer.dispatchEvent({
+                            type : "CursorChange", action : "remove",  name:"movePointcloud"
+                        })
+                    }
+                }else if(handleState == 'rotate'){ 
+                    if( e.intersect && e.intersect.location ){ 
+                        viewer.dispatchEvent({
+                            type : "CursorChange", action : "add",  name:"rotatePointcloud"
+                        })
+                    }else{
+                        viewer.dispatchEvent({
+                            type : "CursorChange", action : "remove",  name:"rotatePointcloud"
+                        })
+                    }
+                }  
+            }else{
+                //清空:
+                viewer.dispatchEvent({
+                    type : "CursorChange", action : "remove",  name:"movePointcloud" 
+                })
+                viewer.dispatchEvent({
+                    type : "CursorChange", action : "remove",  name:"rotatePointcloud" 
+                })
+            }                
+        }
+        
+         
+        viewer.addEventListener('global_mousemove',updateCursor)  
+        viewer.addEventListener('global_drop',updateCursor)//拖拽结束  
+       
+            
+        
+        
+        viewer.addEventListener('updateModelBound', (e)=>{
+            if(this.editing){
+                this.SplitScreen.updateCameraOutOfModel() 
+            } 
+        })
+        
+        
+    },
+    
+    
+    setMatrix :  function(pointcloud){
+        var vec1 = pointcloud.position     //position为数据集内部的偏移,在navvis中对应的是dataset.pointCloudSceneNode的children[0].position
+        var vec2 = pointcloud.translateUser
+        var angle = pointcloud.orientationUser
+        var pos1Matrix = new THREE.Matrix4().setPosition(vec1);//先移动到点云本身应该在的初始位置(在4dkk里和其他应用中都是在这个位置的,也能和漫游点对应上)
+        var rotMatrix = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0,0,1), angle)//再旋转 
+        var pos2Matrix = new THREE.Matrix4().setPosition(vec2);//最后是平移
+         
+        var matrix = new THREE.Matrix4().multiplyMatrices(pos2Matrix, rotMatrix);
+        pointcloud.transformMatrix = matrix.clone();//为该数据集的变化矩阵。 对应navvis的m2w_
+        pointcloud.transformInvMatrix.copy(matrix).invert()
+        pointcloud.rotateMatrix = rotMatrix
+        pointcloud.rotateInvMatrix.copy(rotMatrix).invert()
+        
+        
+        pointcloud.panos.forEach(e=>e.transformByPointcloud())
+        
+        
+        matrix = new THREE.Matrix4().multiplyMatrices(matrix, pos1Matrix);
+        
+        
+        
+        pointcloud.matrix = matrix;
+        //pointcloud.matrixWorldNeedsUpdate = true //更新matrixWorld (非计算,直接赋值)
+        pointcloud.updateMatrixWorld(true)
+        
+        if(this.editing){
+            Alignment.changeCallBack && Alignment.changeCallBack();
+        } 
+        
+        if(pointcloud.spriteNodeRoot){
+            pointcloud.spriteNodeRoot.matrixWorld.copy(pointcloud.matrixWorld)//.multiplyMatrices(pointcloud.matrixWorld, pointcloud.matrixWorld);	
+        } 
+
+        viewer.updateModelBound();
+        //pointcloud.updateBound()
+        pointcloud.getPanosBound()  
+        
+
+    },
+    
+    
+    rotateAround(center, pointcloud, deg, angle){//绕center点转动
+        var angle = angle != void 0 ? angle : THREE.Math.degToRad(deg) 
+        let vec1 = new THREE.Vector3().subVectors(pointcloud.translateUser, center);
+        let rotMatrix = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0,0,1), angle)
+        let vec2 = vec1.clone().applyMatrix4(rotMatrix) //将到旋转中心的偏差也转动
+        let vec3 = new THREE.Vector3().subVectors(vec2,vec1); //这个就是多出来的一步translateUser
+        this.rotate(pointcloud, deg, angle) 
+        this.translate(pointcloud, vec3)
+        //绕点转动就是比普通转动多一步移动到相对center的某个位置。 1 初始点云移动到自己的position; 2 移动一个vec1  3绕原点旋转 4再移动一个原本的translateUser。 绘制出来后发现移动量就是第二步vec旋转后的偏移
+    },
+    
+    
+    rotate:function(pointcloud, deg, angle){//绕各自中心转动(各自的position)   假设点云位移position后0,0,0就是它的中心了(根据navvis观察这样做是绕同一个点旋转的)
+        var angle = angle != void 0 ? angle : THREE.Math.degToRad(deg)   //正逆负顺
+        pointcloud.orientationUser += angle
+        Alignment.setMatrix(pointcloud)
+    },
+    translate:function(pointcloud, vec){
+        pointcloud.translateUser.add(vec)
+        Alignment.setMatrix(pointcloud)
+    },
+    
+    
+    
+    
+    enter:function(){
+        //this.saveTemp()  
+        this.originData = this.getTemp() 
+        
+        this.SplitScreen.split({alignment:true})
+         
+        viewer.images360.panos.forEach(pano=>{
+            viewer.updateVisible(pano.mapMarker, 'split4Screens', false)
+        }) 
+        
+        viewer.viewports.find(e=>e.name == 'mapViewport').alignment = {rotate:true,translate:true};
+        viewer.viewports.find(e=>e.name == 'right').alignment = {translate:true, translateVec:new THREE.Vector3(0,0,1)}; //只能上下移动
+        viewer.viewports.find(e=>e.name == 'back').alignment = {translate:true, translateVec:new THREE.Vector3(0,0,1)}; //只能上下移动
+        
+        
+        this.editing = true
+        
+        viewer.updateFpVisiDatasets()
+        
+        
+        
+    },
+    leave:function(){
+        this.switchHandle(null)
+        
+        /* this.originData.forEach(e=>{//恢复
+            var pointcloud = viewer.scene.pointclouds.find(p=>p.dataset_id == e.id)
+            this.translate(pointcloud, new THREE.Vector3().subVectors(e.translateUser , pointcloud.translateUser))
+            this.rotate(pointcloud, null, e.orientationUser - pointcloud.orientationUser)
+        }) */
+        this.originData.forEach(e=>{//恢复
+            this.applyTemp(e)
+        })
+        
+        
+        this.SplitScreen.recover()
+        viewer.images360.panos.forEach(pano=>{
+            viewer.updateVisible(pano.mapMarker, 'split4Screens', true)
+        }) 
+        this.editing = false
+        this.history.clear() 
+        viewer.updateFpVisiDatasets()
+        viewer.dispatchEvent({
+            type : "CursorChange", action : "remove",  name:"movePointcloud" 
+        })
+        viewer.dispatchEvent({
+            type : "CursorChange", action : "remove",  name:"rotatePointcloud" 
+        })
+    } 
+    
+    ,
+    switchHandle:function(state){
+        this.handleState = state
+        //清空:
+        viewer.dispatchEvent({
+            type : "CursorChange", action : "remove",  name:"movePointcloud" 
+        })
+        viewer.dispatchEvent({
+            type : "CursorChange", action : "remove",  name:"rotatePointcloud" 
+        })
+        
+        this.bus.dispatchEvent({type:'switchHandle' , state })
+        
+        
+
+    },
+    
+    
+    save: function(){//保存所有数据集的位置和旋转
+        let callback = ()=>{//保存成功后
+            this.originData = this.getTemp()   //this.saveTemp();
+            //需要修改 测量线的position。漫游点已经实时修改了
+            
+            viewer.scene.measurements.forEach(e=>e.transformByPointcloud())
+            viewer.images360.updateCube(viewer.bound)
+        }
+        
+        var data = viewer.scene.pointclouds.map(e=>{
+            let pos = viewer.transform.lonlatToLocal.inverse(e.translateUser.clone())
+            
+            
+            return {
+                id: e.dataset_id,
+                orientation : e.orientationUser,
+                location:[pos.x, pos.y, pos.z],
+                //transformMatrix: e.transformMatrix.elements,
+            }
+        })
+        //data = JSON.stringify(data)
+        
+        //test: 退出后保留结果
+        if(!Potree.settings.isOfficial){
+            callback() 
+        }
+        
+        return {data, callback}
+    }
+        
+    
+}
+
+
+                                  
+                        
+                            
+                                       
+           
+      
+  
+ 
+/* 
+
+关于控制点:
+
+两个控制点只能打在同一个数据集上。传输这两个点的4dkk中的本地坐标和经纬度,后台算出该数据集的旋转平移,
+然后其他数据集绕该数据集旋转,并保持相对位置不变。
+
+
+ */
+ 
+
+export {Alignment} 

+ 619 - 0
src/custom/modules/mergeModel/MergeEditor.js

@@ -0,0 +1,619 @@
+ 
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import cameraLight from '../../utils/cameraLight.js' 
+
+import math from "../../utils/math.js"
+import Common from '../../utils/Common.js' 
+import {LineDraw, MeshDraw} from "../../utils/DrawUtil.js"; 
+import {transitions, easing, lerp} from '../../utils/transitions.js'
+import SplitScreen from "../../utils/SplitScreen.js";
+import InfiniteGridHelper from '../../objects/InfiniteGridHelper.js'
+import Compass from "../../objects/tool/Compass.js";
+import {TransformControls} from "../../objects/tool/TransformControls.js";
+import History from "../../utils/History.js"
+
+ 
+const texLoader = new THREE.TextureLoader() 
+      texLoader.crossOrigin = "anonymous" 
+  
+const edgeStrengths = {
+    pointcloud: 4,
+    glb: 100
+}
+const viewportProps = [{
+    left:0,
+    bottom:0,
+    width: 0.5,height:1,
+    name : 'top',    
+    axis:["x","y"],
+    direction : new THREE.Vector3(0,0,-1), //镜头朝向 
+    active: true,
+    //相机位置在z轴正向
+    limitBound: new THREE.Box3(new THREE.Vector3(-Infinity,-Infinity, 1),new THREE.Vector3(Infinity,Infinity,5000)), //在地面以上
+    margin:{x:50, y:150} ,
+},
+{
+    left:0.5,
+    bottom:0,
+    width: 0.5,height:1,
+    name : 'right', 
+    axis:["y","z"],
+    direction : new THREE.Vector3(1,0,0), 
+    active: true,
+    //相机位置在x轴负向  右下角屏
+    viewContainsPoints:[new THREE.Vector3(0,0,0)],
+    margin:{x:300, y:250} ,
+} ]
+ 
+ 
+ 
+let MergeEditor = {
+    bus:new THREE.EventDispatcher(), 
+     
+    
+    SplitScreen : new SplitScreen(),
+    
+    init(){  
+        { 
+            let ground = this.ground = new InfiniteGridHelper(1, 10000, new THREE.Color('#fff'), 10000, 0.2, 0.3)
+            viewer.scene.scene.add(ground) 
+            //再加两条线否则在正侧边看不到
+            let line1 = LineDraw.createLine([new THREE.Vector3(-10000, 0, 0),new THREE.Vector3(10000, 0, 0) ], {color:'#666', dontAlwaysSeen:true})
+            let line2 = LineDraw.createLine([new THREE.Vector3(0, -10000, 0),new THREE.Vector3(0, 10000, 0) ], {mat:line1.material})
+            ground.renderOrder = Potree.config.renderOrders.model + 1//line1.renderOrder + 1  //要比模型低,否则模型透明时效果不对
+            ground.add(line1)
+            ground.add(line2)
+             
+            ground.material.polygonOffset = true //多边形偏移(视觉上没有移动模型位置),防止闪烁
+            ground.material.polygonOffsetFactor = 100 //多边形偏移因子
+			ground.material.polygonOffsetUnits = 10 //多边形偏移单位  
+            ground.material.depthWrite = false            
+            //ground.material.depthTest = false
+            line1.material.polygonOffset = true
+            line1.material.polygonOffsetFactor = 130  
+			line1.material.polygonOffsetUnits = 10  
+            line1.material.depthWrite = false   
+            //见笔记:透明物体的材质设置
+        }
+        
+        
+        {
+            
+            this.transformControls = new TransformControls(viewer.mainViewport.camera, viewer.renderArea,{
+                dontHideWhenFaceCamera: true,
+            });
+            //this.transformControls.space = 'local'//为了在当前方向上平移
+            this.transformControls.setSize(1.5)
+            viewer.scene.scene.add(this.transformControls)
+            this.transformControls._gizmo.hideAxis = {rotate:['e']}
+            this.transformControls.setRotateMethod(2)
+            
+            
+            //右屏
+            this.transformControls2 = new TransformControls(viewer.mainViewport.camera, viewer.renderArea,{ 
+                dontHideWhenFaceCamera: true,
+            }); 
+            this.transformControls.setSize(1.5)
+            viewer.scene.scene.add(this.transformControls2) 
+            viewer.setObjectLayers(this.transformControls2, 'layer2' )  
+            
+            let mouseDown = (e)=>{
+                 
+                viewer.outlinePass.edgeStrength = 0//暂时消失线
+                 
+            }
+            let mouseUp = (e)=>{
+                 
+                this.updateEdgeStrength()
+                 
+            }
+            this.transformControls.addEventListener('mouseDown',mouseDown)
+            this.transformControls2.addEventListener('mouseDown',mouseDown)
+            this.transformControls.addEventListener('mouseUp',mouseUp)
+            this.transformControls2.addEventListener('mouseUp',mouseUp)
+            
+            
+        }
+        
+        
+        {
+            
+            this.secondCompass = new Compass(null)
+            
+        }
+        
+        viewer.setControls(viewer.orbitControls)
+        //viewer.mainViewport.view.fixZWhenPan = true
+        viewer.orbitControls.constantlyForward = true
+        
+        
+        viewer.addEventListener('global_single_click',(e)=>{
+            if(
+                this.noNeedSelection  //如模型查看页
+                || viewer.scene.cameraAnimations.some(c=>c.onUpdate) //正在播放
+                || e.drag && e.drag.notPressMouse   //在加测量线
+                || viewer.mainViewport.view.isFlying() //有其他校准
+                || this.split           //分屏中
+                || e.clickElement //触发别的点击事件,如测量时click marker  /* && e.clickElement != e.intersect.object */
+            ){
+                return
+            }
+            
+            if(e.intersect){
+                let object = e.intersect.object || e.intersect.pointcloud
+                let objects = this.getAllObjects()
+                if(objects.includes(object)){ 
+                    this.selectModel(object) 
+                }else{
+                    //if(!viewer.inputHandler.selection[0]){//正在平移和旋转,不允许取消
+                        this.selectModel(null)
+                    //}
+                }                    
+            }else{
+                //if(!viewer.inputHandler.selection[0]){
+                    this.selectModel(null)
+                //}                
+            }
+        })  
+        
+        viewer.inputHandler.addEventListener('keydown', (e)=>{
+            if((e.event.key).toLowerCase() == "h" ){ 
+                this.fadeOutlineAuto = !this.fadeOutlineAuto
+                this.showModelOutline(this.selected,!!this.selected)
+            } 
+        })
+        viewer.ssaaRenderPass.enabled = false
+        viewer.outlinePass.enabled = true
+        //Potree.settings.intersectWhenHover = false
+        //viewer.updateVisible(viewer.reticule, 'force', false)
+        
+        viewer.mainViewport.camera.near = 0.05; // too small will result in z-fighting
+        
+        viewer.addEventListener('updateModelBound', (e)=>{
+            if(this.split){ 
+                this.SplitScreen.updateCameraOutOfModel(/* this.selected && [this.selected] */) 
+            } 
+        })
+        
+        
+        {//校准页面拖拽
+            //左右屏都可以拖拽模型,旋转只能左屏
+            let dragInfo  
+            let drag = (e)=>{ 
+                if(this.split &&  this.selected && this.transformState && (e.dragViewport.name == 'top' || this.transformState == 'translate')   ){ 
+                    if(e.type == 'global_mousedown' ){ //开始
+                        //if((e.intersect.object || e.intersect.pointcloud) == this.selected){
+                        if(e.intersect.pointclouds.includes(this.selected) || e.intersect.allElements.some(e=>e.object == this.selected)){
+                            
+                            dragInfo = {}   
+                            //if(this.selected.isPointcloud){ 
+                                viewer.outlinePass.edgeStrength = 0//暂时消失线
+                            //} 
+                        }  
+                    }
+                         
+                    if(e.type == 'global_drag' && dragInfo  ){ 
+                        if(this.transformState == 'translate'){ 
+                        
+                            let moveVec = Potree.Utils.getOrthoCameraMoveVec(e.drag.pointerDelta, e.dragViewport.camera )//最近一次移动向量
+                            this.selected.position.add(moveVec)
+                            
+                            this.selected.dispatchEvent("position_changed")
+                        }else if(this.transformState == 'rotate'){
+                            
+                            let vec = new THREE.Vector3().subVectors(e.intersect.orthoIntersect || e.intersect.location, this.selected.boundCenter).setZ(0)
+                            if(dragInfo.lastVec == void 0){//global_mousedown
+                                dragInfo.lastVec = vec 
+                                return
+                            }
+                            let angle = math.getAngle(dragInfo.lastVec, vec, 'z')   
+                            dragInfo.lastVec = vec
+                            
+                            //this.selected.rotation.z += angle //局部
+                            
+                            
+                            /* object.quaternion.copy( .setFromAxisAngle( new THREE.Vector3(0,0,1), angle ) );
+                            object.quaternion.multiply( quaternionStart ).normalize(); */
+                            let diffQua = new THREE.Quaternion().setFromAxisAngle( new THREE.Vector3(0,0,1), angle )
+                            this.selected.quaternion.premultiply(diffQua) //世界
+                            
+                            
+                            this.selected.dispatchEvent("rotation_changed")
+                        }
+                        
+                        return {stopContinue:true}
+                    } 
+                }
+                
+            }
+            
+            viewer.addEventListener('global_mousedown',  drag) 
+            viewer.addEventListener('global_drag', drag, 10)
+            viewer.addEventListener('global_mousemove', (e)=>{
+                if(this.split && this.transformState && !e.drag && (e.hoverViewport.name == 'top' ||  this.transformState == 'translate')){
+                    
+                    /* if(this.lastHoverViewport != e.hoverViewport){
+                        this.lastHoverViewport = e.hoverViewport
+                        this.transformControls.view = e.hoverViewport.view
+                        this.transformControls.camera = e.hoverViewport.camera
+                        this.transformControls.hideAxis( this.transformState, e.hoverViewport.name == 'top' ? [z] : [x,y]);
+                    } */
+                    
+                    
+                    
+                    
+                    let mouseover =  e.intersect.pointclouds.includes(this.selected) || e.intersect.allElements.some(e=>e.object == this.selected)
+                    //let mouseover = (e.intersect.object || e.intersect.pointcloud) == this.selected
+                    if(mouseover){
+                        if(this.transformState == 'translate'){
+                            viewer.dispatchEvent({
+                                type : "CursorChange", action : "add",  name:"movePointcloud" 
+                            }) 
+                        }else{
+                            viewer.dispatchEvent({
+                                type : "CursorChange", action : "add",  name:"rotatePointcloud" 
+                            }) 
+                        }
+                    }else{
+                        this.clearTranCursor()
+                    }
+                    
+                } 
+            })
+            
+            viewer.addEventListener('global_drop', (e)=>{
+                dragInfo = null
+                this.clearTranCursor()
+                  
+                this.updateEdgeStrength()
+                  
+                
+            })
+        
+        }
+    },
+    
+    
+    clearTranCursor(){
+        viewer.dispatchEvent({
+            type : "CursorChange", action : "remove",  name:"movePointcloud" 
+        })
+        viewer.dispatchEvent({
+            type : "CursorChange", action : "remove",  name:"rotatePointcloud" 
+        })
+    },
+    
+    enterSplit(){ 
+        this.split = true
+        if(this.selected) this.SplitScreen.focusCenter = this.selected.boundCenter //旋转中心。注意 boundCenter不能直接赋值,否则改变后focusCenter也要改
+        else this.SplitScreen.focusCenter = null
+        
+        this.SplitScreen.splitStart(viewportProps)
+        
+        this.beforeSplit = {
+            pointDensity: Potree.settings.pointDensity,
+        }
+        Potree.settings.pointDensity = 'fourViewports' //强制降低点云质量
+        /* viewer.scene.pointclouds.forEach(e=>{
+            e.material.activeAttributeName = "color"
+            e.material.useFilterByNormal = true  
+        }) */ 
+        //取消outline,改点云颜色为和outline一样的颜色
+        /* if(this.selected && this.selected.isPointcloud){ 
+            this.showModelOutline(this.selected, false) 
+            this.selected.material.activeAttributeName = "color"
+            this.selected.material.color = viewer.outlinePass.visibleEdgeColor 
+        } */
+        
+        
+        viewer.setControls(viewer.fpControls)  
+        viewer.viewports.find(e=>e.name == 'right').rotateSide = true 
+        viewer.viewports.find(e=>e.name == 'top').alignment = true
+       
+         
+        viewer.viewports[1].layersAdd('layer2') 
+        viewer.viewports[0].layersAdd('layer1') 
+        viewer.setObjectLayers(this.transformControls, 'layer1' ) 
+        this.transformControls.view = viewer.viewports[0].view
+        this.transformControls.camera = viewer.viewports[0].camera
+        this.transformControls._gizmo.hideAxis = {translate:['z'], rotate:['x','y','z'] }
+        this.transformControls2.view = viewer.viewports[1].view
+        this.transformControls2.camera = viewer.viewports[1].camera
+        this.transformControls2._gizmo.hideAxis = {translate:['x','y'], rotate:['x','y','z'] }
+        
+
+        this.secondCompass.changeViewport(viewer.viewports[0])
+        this.secondCompass.setDomPos()
+        this.secondCompass.setDisplay(true)   
+        viewer.compass.changeViewport(viewer.viewports[1])
+        viewer.compass.setDomPos()
+        
+    },
+    
+    leaveSplit(){
+        this.split = false
+        this.SplitScreen.unSplit()    
+        viewer.setControls(viewer.orbitControls)
+        
+        Potree.settings.pointDensity = this.beforeSplit.pointDensity
+        /* if(this.selected && this.selected.isPointcloud){ 
+            this.showModelOutline(this.selected, true) 
+            this.selected.material.activeAttributeName = "rgba"  
+        } */
+        this.transformControls.camera = viewer.viewports[0].camera
+        this.transformControls.view = viewer.viewports[0].view 
+        this.transformControls._gizmo.hideAxis = {rotate:['e']}
+        viewer.setObjectLayers(this.transformControls, 'sceneObjects' )  //恢复
+        
+        
+        viewer.compass.changeViewport(viewer.viewports[0]) //恢复 
+        viewer.compass.setDomPos()
+        this.secondCompass.setDisplay(false)
+        
+         
+    },
+    
+    rotateSideCamera(angle){
+        this.SplitScreen.rotateSideCamera(viewer.viewports.find(e=>e.name == 'right'), angle)
+    },
+    
+    setTransformState(state){//校准时
+        this.transformState = state  
+        this.clearTranCursor()       
+    },
+    //---------------------------
+    
+    /* writeToHistory(content){ 
+        if(!this.prepareRecord)return;
+        this.prepareRecord = false
+        this.history.push(content)
+    }, */
+    
+    //---------------------------
+    
+    getAllObjects(){
+        return viewer.objs.children.concat(viewer.scene.pointclouds)
+    },
+    
+    getModel(id){
+        let models = this.getAllObjects()
+        return models.find(e=>e.dataset_id == id)
+    },
+    removeModel(model){
+        if(this.selected == model) this.selectModel(null)
+        let dispose = (e)=>{
+            e.geometry && e.geometry.dispose() 
+            e.material && e.material.dispose()
+        }
+        if(model.isPointcloud){
+            dispose(model)
+            viewer.scene.removePointCloud(model)
+        }else{
+            model.traverse(e=>{
+                dispose(e) 
+            })
+            viewer.objs.remove(model)
+        }   
+        
+        
+        
+    },
+    
+    selectModel(model, state=true, fitBound, by2d){
+        if(!model) {
+            model = this.selected
+            state = false
+        }
+         
+        if(state){
+            if(this.selected){
+                if(this.selected == model) return
+                else{
+                    let transToolAttached = !!this.transformControls.object
+                    this.selectModel(this.selected, false, fitBound, by2d)
+                    transToolAttached && this.transformControls.attach(model)
+                }
+            }
+            this.selected = model
+             
+            MergeEditor.focusOn(model, 500, !!fitBound)     //通过在场景里点击模型的话,不focus
+              
+           
+            this.showModelOutline(model)
+            
+            
+            
+            this.updateEdgeStrength()
+            
+            //console.log('selectModel', model)
+            
+        }else{
+            if(this.selected != model)return //model本来就没选中,不需要处理(防止2d先选中新的再取消旧的)
+            this.showModelOutline(model, false)
+             
+            this.selected = null
+            this.transformControls.detach()        //viewer.transformObject(null);
+            //console.log('selectModel', null)
+        } 
+        
+        
+        
+        if(!by2d && model){
+            model.dispatchEvent({type:'changeSelect', selected : state})
+        }
+         
+    },
+    
+    
+    showModelOutline(model, state){ 
+        if(this.fadeOutlineAuto){ 
+            if(state === false){
+                viewer.outlinePass.selectedObjects = []
+                clearTimeout(this.timer)
+                return
+            }
+            
+            viewer.outlinePass.selectedObjects = [model]
+            if(this.timer){
+                clearTimeout(this.timer)
+            }
+            
+            this.timer = setTimeout(()=>{
+                viewer.outlinePass.selectedObjects = []
+            }, 1000)
+        }else{
+            if(state === false){
+                viewer.outlinePass.selectedObjects = []
+            }else{
+                viewer.outlinePass.selectedObjects = [model]
+            }
+        }
+    },
+    
+    updateEdgeStrength(){
+        if(!this.selected)return
+        if(this.selected.isPointcloud){
+            viewer.outlinePass.edgeStrength = edgeStrengths.pointcloud// / this.selected.material.opacity  
+        }else{
+            viewer.outlinePass.edgeStrength = edgeStrengths.glb
+        }  
+    },
+    focusOn(objects, duration = 400, fitBound=true, dontLookUp){  
+        if(!(objects instanceof Array)){
+            objects = [objects]
+        }
+        let boundingBox = new THREE.Box3
+        objects.forEach(object=>{
+            boundingBox.union(object.boundingBox.clone().applyMatrix4(object.matrixWorld))
+        })
+         
+        if(fitBound){
+            viewer.focusOnObject({boundingBox}, 'boundingBox', duration, {dontLookUp, dontChangeCamDir:true}) 
+        }else{ 
+        
+            /* 
+            let position = viewer.inputHandler.intersect ? viewer.inputHandler.intersect.location : boundingBox.getCenter(new THREE.Vector3)
+            position && viewer.focusOnObject({position},  'point', duration, {dontChangePos: true})
+             */
+            let position = viewer.inputHandler.intersect ? viewer.inputHandler.intersect.location : boundingBox.getCenter(new THREE.Vector3)
+            if(!position)return
+            
+            
+            /* let targetOld = viewer.mainViewport.view.getPivot()
+            
+            let projected1 = targetOld.clone().project(viewer.mainViewport.camera);
+            let projected2 = position.clone().project(viewer.mainViewport.camera); //使用其z
+			let targetNew = projected1.clone().setZ(projected2.z).unproject(viewer.mainViewport.camera); 
+            viewer.mainViewport.view.lookAt(targetNew) */
+            
+            
+            viewer.mainViewport.view.radius = viewer.mainViewport.camera.position.distanceTo(position)
+            //为了不改画面,不调节方向了,只能调调radius,一定程度将target靠近model
+        }  
+    },
+    
+    
+    moveBoundCenterTo(model,pos){ //使boundCenter在所要的位置 
+        let diff = new THREE.Vector3().subVectors(pos, model.boundCenter) 
+        model.position.add(diff); 
+    },
+    
+    getBoundCenter(model){
+        if(!model.boundCenter) model.boundCenter = new THREE.Vector3
+        model.boundingBox.getCenter(model.boundCenter).applyMatrix4(model.matrixWorld) 
+    },
+    
+    setModelBtmHeight(model, z ){  
+        //无论模型怎么缩放、旋转,都使最低点为z
+        if(z == void 0) z = model.btmHeight; //维持离地高度
+        else model.btmHeight = z;
+        
+        model.updateMatrixWorld()
+        let boundingBox2 = model.boundingBox.clone().applyMatrix4(model.matrixWorld)
+        let size = boundingBox2.getSize(new THREE.Vector3);
+        let center = boundingBox2.getCenter(new THREE.Vector3);
+        let hopeZ = z + size.z / 2  
+        //model.position.z = z + size.z / 2 - center.z 
+        model.position.z += (hopeZ - center.z)
+    },
+    
+    computeBtmHeight(model){ //位移之后重新计算btmHeight
+        model.updateMatrixWorld()
+        let boundingBox2 = model.boundingBox.clone().applyMatrix4(model.matrixWorld)
+        let size = boundingBox2.getSize(new THREE.Vector3);
+        let center = boundingBox2.getCenter(new THREE.Vector3);
+        model.btmHeight = center.z - size.z / 2 
+    },
+    
+    
+    maintainBoundXY(model){ //在旋转和缩放后,立即执行这个函数,使boundCenter保持原位
+         
+        model.updateMatrixWorld()
+        let center1 = model.boundCenter.clone();//还未更新的 
+        this.getBoundCenter(model)//更新
+        let center2 = model.boundCenter.clone();
+        let diff = new THREE.Vector2().subVectors(center1,center2); 
+        model.position.x += diff.x;
+        model.position.y += diff.y;
+        model.boundCenter.copy(center1)
+    }, 
+     
+    
+    
+    maintainBoundCenter(model){
+        model.updateMatrixWorld()
+        let center1 = model.boundCenter.clone();//还未更新的 
+        this.getBoundCenter(model)//更新
+        let center2 = model.boundCenter.clone();
+        let diff = new THREE.Vector3().subVectors(center1,center2); 
+        model.position.add(diff)
+        model.boundCenter.copy(center1)
+    },
+    
+    modelTransformCallback(model){
+        
+        model.updateMatrixWorld() 
+        if(model.matrixWorld.equals(model.lastMatrixWorld))return
+        viewer.scene.measurements.forEach(measure=>{
+            let changed
+            measure.points_datasets.forEach((dataset_id,i)=>{
+                if(dataset_id == model.dataset_id){
+                    changed = true
+                    measure.points[i] = Potree.Utils.datasetPosTransform({fromDataset:true,datasetId:dataset_id, position:measure.dataset_points[i].clone()})
+                    measure.updateMarker(measure.markers[i], measure.points[i])
+                   
+                }
+            })
+            if(changed){//仿transformByPointcloud
+                measure.getPoint2dInfo(measure.points)
+                measure.update() 
+                measure.setSelected(false)//隐藏edgelabel  
+            }
+        })
+        
+          
+     
+        
+        model.lastMatrixWorld = model.matrixWorld.clone()
+    }
+}   
+    
+    
+    
+ 
+    
+    
+export default MergeEditor  
+    
+    
+/*  
+
+note:
+
+要注意getHoveredElements只在getIntersect时才使interactables包含加载的model, 也就是model上不能有使之成为interactables的事件,否则在鼠标hover到模型上开始转动的一瞬间很卡。
+
+
+
+
+
+*/ 

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1263 - 0
src/custom/modules/panoEdit/panoEditor.js


+ 314 - 0
src/custom/modules/panos/DepthImageSampler.js

@@ -0,0 +1,314 @@
+
+
+import math from "../../utils/math.js";
+
+class DepthImageSampler {
+    
+    constructor(){ 
+        var canvas = document.createElement("canvas");
+        this.canvas = canvas
+        this.context = canvas.getContext("2d")  
+        
+        
+        /* document.getElementsByTagName('body')[0].appendChild(canvas);
+        canvas.style.position = 'fixed';
+        canvas.style.width = '1024px';
+        canvas.style.top = canvas.style.left = 0
+        canvas.style['z-index'] = 100
+         */
+        
+    }
+ 
+     
+    changeImg(img){
+        if(this.img == img)return
+        this.canvas.width = img.width 
+        this.canvas.height = img.height 
+        this.context.drawImage(img, 0, 0)
+        this.img = img
+    } 
+    
+     
+    getDepth(UVx, UVy) {//根据图片像素获取深度值 
+        var x = Math.round(UVx * (this.canvas.width - 1))
+          , y = Math.round(UVy * (this.canvas.height - 1));
+        if (!(x < 0 || y < 0 || x >= this.width || y >= this.height)) {
+            var r = this.context.getImageData(x, y, 1, 1).data;
+            //console.log('color', r, x,y)
+            return r[1] + r[0] / 256
+        }
+    }   
+    
+    
+    sample( intersect, currentPano, onlyPos ) {//通过和skybox的intersect得到真实的intersect的位置
+        if(!intersect)return
+        let location = new THREE.Vector3
+        let normal
+        currentPano = currentPano || viewer.images360.currentPano
+         
+        if(currentPano != this.currentPano){
+            if(!currentPano.depthTex/*  || !currentPano.depthTex.image  */) return //未加载
+            this.changeImg(currentPano.depthTex.image)
+            this.currentPano = currentPano
+        }
+        
+        let origin = currentPano.position
+        let dir = intersect.dir || new THREE.Vector3().subVectors(intersect.point, origin).normalize()
+        //var uv = intersect.uv 
+        //let dirInPano = math.getNormalDir(dir, currentPano)//转化为考虑漫游点旋转的方向
+        
+        let dirInPano = dir.clone().applyMatrix4(currentPano.panoMatrix2Inverse).normalize(); //转化为考虑漫游点旋转的方向
+        let uv = math.getUVfromDir(dirInPano)//转化为uv
+        
+        let distance = this.getDepth(uv.x, uv.y);
+        //console.log('depth', depth,  uv.y)
+        if (!distance){
+            if(uv.y > 0.75){//漫游点底部识别不到的区域,给一个地板高度
+                //let height = origin.distanceTo(currentPano.floorPosition); 
+                const margin =  0.1
+                distance = (currentPano.floorPosition.z - origin.z - margin) / dir.z
+                location.copy(dir).multiplyScalar(distance).add(origin);
+                let normal = new THREE.Vector3(0,0,1)
+                
+                return {location, normal, distance} 
+            }
+            else return !1;  //应该是天空或模型外 , 因为很少有漫游点的地方还拍不到地板
+        }  
+        
+        //this.mainDepth = depth
+       
+        location.copy(dir).multiplyScalar(distance).add(origin);
+        
+        if(!onlyPos){
+           
+           var pL = this.getNearbyPoint(origin, uv, -1, 0)
+          , pR = this.getNearbyPoint(origin, uv,  1, 0)
+          , pB = this.getNearbyPoint(origin, uv,  0, -1)
+          , pT = this.getNearbyPoint(origin, uv,  0, 1);
+           
+            normal = this.planeFit(dir,location, pL,pR,pB,pT  )  
+        }
+ 
+          
+        /* if(normal.x != normal.x ){ 
+            console.log('NAN',  normal)
+            var pL = this.getNearbyPoint(origin, uv, -1, 0)
+          , pR = this.getNearbyPoint(origin, uv,  1, 0)
+          , pB = this.getNearbyPoint(origin, uv,  0, -1)
+          , pT = this.getNearbyPoint(origin, uv,  0, 1);
+           
+        } */
+        
+        //console.log('normal',normal)
+          
+        return {location, normal,  distance} 
+    }
+    
+    
+    getNearbyPoint(   origin, uv, x, y) { //获取附近的若干像素距离的点
+        let uv2 = uv.clone()  
+        uv2.x += x/(this.canvas.width-1); 
+        uv2.x = this.clampUV(uv2.x)
+         
+        uv2.y += y/(this.canvas.height-1);
+        uv2.y = this.clampUV(uv2.y)
+        
+        /* if(uv2.x < 0 || uv2.y < 0 || uv2.x > 1 || uv2.y > 1){
+            console.log('will nan')
+        } */
+        
+        let dir = math.getDirFromUV(uv2)//从uv获取到方向
+        dir.applyMatrix4(viewer.images360.currentPano.panoMatrix2)
+        let depth = this.getDepth(uv2.x, uv2.y);
+        /* if(Math.abs(depth - this.mainDepth) > 0.3){
+            console.log('Math.abs(depth - this.mainDepth) > 0.3')
+        } */
+        
+        //let dir = new THREE.Vector3().subVectors(intersect.point, origin).normalize()
+        let position = new THREE.Vector3().copy(dir).multiplyScalar(depth).add(origin);
+        
+        //console.log('getNearbyPoint', uv2, depth, dir, position )
+        
+        return position
+    } 
+
+    clampUV(v){
+        return (v + 1) % 1;   //  使输出在 0-1
+    }
+    
+    planeFit(dir, position, pL,pR,pB,pT ) {//求平均法线
+        let normal = new THREE.Vector3
+         
+        
+        let plane = new THREE.Plane; 
+        function addNormal(p1, p2) {//根据临接的四个点,分别求法线,然后法线相加能得到平均法线
+            if(!p1 || !p2)return
+            plane.setFromCoplanarPoints(position, p1, p2)
+            
+            //console.log('normalSub', plane.normal)
+            
+            normal.addScaledVector(plane.normal,  dir.dot(plane.normal) < 0 ? 1 : -1)//根据面的朝向判断加还是减
+        }
+        addNormal(pL, pB) 
+        addNormal(pL, pT) 
+        addNormal(pR, pB) 
+        addNormal(pR, pT) 
+         
+        if(0 !== normal.x || 0 !== normal.y || 0 !== normal.z){ 
+            normal.normalize()
+            //console.log(normal)
+            return normal
+        }
+         
+         
+        /* 四个面拼成一个菱形 */        
+          
+    } 
+      
+    
+    /* makeUvToPosMap(intersect, matrix1, vec1, vec2) {
+        var o = intersect.object.geometry
+          , a = o.attributes.position.array
+          , s = new THREE.Vector3(a[3 * intersect.face.a],a[3 * intersect.face.a + 1],a[3 * intersect.face.a + 2]).applyMatrix4(intersect.object.matrixWorld)
+          , c = new THREE.Vector3(a[3 * intersect.face.b],a[3 * intersect.face.b + 1],a[3 * intersect.face.b + 2]).applyMatrix4(intersect.object.matrixWorld)
+          , l = new THREE.Vector3(a[3 * intersect.face.c],a[3 * intersect.face.c + 1],a[3 * intersect.face.c + 2]).applyMatrix4(intersect.object.matrixWorld);
+        vec1.subVectors(s, c),
+        vec2.subVectors(l, c);
+        var u = o.attributes.uv.array
+          , d = new THREE.Vector2(u[2 * intersect.face.a],u[2 * intersect.face.a + 1])
+          , p = new THREE.Vector2(u[2 * intersect.face.b],u[2 * intersect.face.b + 1])
+          , h = new THREE.Vector2(u[2 * intersect.face.c],u[2 * intersect.face.c + 1])
+          , f = d.sub(p)
+          , g = h.sub(p);
+        matrix1.set(f.x, g.x, 0, f.y, g.y, 0, 0, 0, 1),
+        matrix1.getInverse(matrix1)
+    } */
+    
+
+  
+    /* getNearbyPoint(  point, origin, uv, o, a, s, x, y) {
+        var add = new THREE.Vector3(x, y , 0 ) 
+          , depth = this.getDepth(uv.x + add.x, uv.y + add.y  );
+           
+        if (void 0 !== depth) {
+            var f = add.applyMatrix3(o);
+            return (new THREE.Vector3).addScaledVector(a, f.x).addScaledVector(s, f.y).add(point).sub(origin).normalize().multiplyScalar(depth).add(origin)
+        }
+    } */
+    
+    
+    
+}
+
+/* 
+    注:
+    
+    当前测试的图不太对,三个通道都一样了,所以几乎是整数的depth。法线也几乎都朝向相机
+
+    由于有时候获取intersect需要知道是哪个点云,所以还是不能用这个。如加测量线。
+
+ */
+
+export default DepthImageSampler
+
+
+    /* var i = n(4)
+      , r = function() {
+        function t(t) {
+            
+        }
+        return t.prototype.getDepth = function(t, e) {
+            var n = Math.round(t)
+              , i = Math.round(e);
+            if (!(n < 0 || i < 0 || n >= this.width || i >= this.height)) {
+                var r = this.context.getImageData(n, i, 1, 1).data;
+                return r[1] + r[0] / 256
+            }
+        }
+        ,
+        Object.defineProperty(t.prototype, "width", {
+            get: function() {
+                return this.context.canvas.width
+            },
+            enumerable: !0,
+            configurable: !0
+        }),
+        Object.defineProperty(t.prototype, "height", {
+            get: function() {
+                return this.context.canvas.height
+            },
+            enumerable: !0,
+            configurable: !0
+        }),
+        t
+    }();
+    e.CanvasDepthImage = r;
+    var o = function() {
+        function t() {}
+        return t.sample = function(e, n, r, o, a) {
+            var s = n.uv
+              , c = s.x * (e.width - 1)
+              , l = (1 - s.y) * (e.height - 1)
+              , u = e.getDepth(c, l);
+            if (!u)
+                return !1;
+            o.copy(n.point).sub(r).normalize().multiplyScalar(u).add(r);
+            var d = new i.Matrix3
+              , p = new i.Vector3
+              , h = new i.Vector3;
+            t.makeUvToPosMap(n, d, p, h);
+            var f = this.getNearbyPoint(e, n.point, r, s, d, p, h, -1, 0)
+              , g = this.getNearbyPoint(e, n.point, r, s, d, p, h, 1, 0)
+              , m = this.getNearbyPoint(e, n.point, r, s, d, p, h, 0, -1)
+              , v = this.getNearbyPoint(e, n.point, r, s, d, p, h, 0, 1);
+            return this.planeFit(o, r, f, g, m, v, a)
+        }
+        ,
+        t.makeUvToPosMap = function(t, e, n, r) {
+            var o = t.object.geometry
+              , a = o.attributes.position.array
+              , s = new i.Vector3(a[3 * t.face.a],a[3 * t.face.a + 1],a[3 * t.face.a + 2]).applyMatrix4(t.object.matrixWorld)
+              , c = new i.Vector3(a[3 * t.face.b],a[3 * t.face.b + 1],a[3 * t.face.b + 2]).applyMatrix4(t.object.matrixWorld)
+              , l = new i.Vector3(a[3 * t.face.c],a[3 * t.face.c + 1],a[3 * t.face.c + 2]).applyMatrix4(t.object.matrixWorld);
+            n.subVectors(s, c),
+            r.subVectors(l, c);
+            var u = o.attributes.uv.array
+              , d = new i.Vector2(u[2 * t.face.a],u[2 * t.face.a + 1])
+              , p = new i.Vector2(u[2 * t.face.b],u[2 * t.face.b + 1])
+              , h = new i.Vector2(u[2 * t.face.c],u[2 * t.face.c + 1])
+              , f = d.sub(p)
+              , g = h.sub(p);
+            e.set(f.x, g.x, 0, f.y, g.y, 0, 0, 0, 1),
+            e.getInverse(e)
+        }
+        ,
+        t.getNearbyPoint = function(t, e, n, r, o, a, s, c, l) {
+            var u = new i.Vector3(c / (t.width - 1),l / (t.height - 1))
+              , d = (r.x + u.x) * (t.width - 1)
+              , p = (1 - (r.y + u.y)) * (t.height - 1)
+              , h = t.getDepth(d, p);
+            if (void 0 !== h) {
+                var f = u.applyMatrix3(o);
+                return (new i.Vector3).addScaledVector(a, f.x).addScaledVector(s, f.y).add(e).sub(n).normalize().multiplyScalar(h).add(n)
+            }
+        }
+        ,
+        t.planeFit = function(t, e, n, r, o, a, s) {
+            s.set(0, 0, 0);
+            var c = t.clone().sub(e)
+              , l = new i.Plane;
+            function u(e, n) {
+                e && n && (l.setFromCoplanarPoints(t, e, n),
+                s.addScaledVector(l.normal, c.dot(l.normal) < 0 ? 1 : -1))
+            }
+            return u(n, o),
+            u(n, a),
+            u(r, o),
+            u(r, a),
+            (0 !== s.x || 0 !== s.y || 0 !== s.z) && (s.normalize(),
+            !0)
+        }
+        ,
+        t
+    }();
+    */

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 3173 - 0
src/custom/modules/panos/Images360.js


+ 762 - 0
src/custom/modules/panos/Panorama.js

@@ -0,0 +1,762 @@
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import {transitions, easing, lerp} from '../../utils/transitions.js'
+import TileUtils from './tile/TileUtils.js' 
+                                                                                   
+import math from '../../utils/math.js' 
+import {TextSprite} from '../../objects/TextSprite.js'
+import DepthBasicMaterial from "../../materials/DepthBasicMaterial.js"; 
+
+let { PanoRendererEvents, PanoramaEvents, PanoSizeClass} = Potree.defines
+var texLoader = new THREE.TextureLoader()
+
+
+                                                                         
+                                         
+
+const labelProp = {
+    sizeInfo: {minSize : 200 ,  maxSize : 250,   nearBound : 0.8, farBound : 10},
+    backgroundColor:{r: 255, g: 255, b: 255, a: 0.4 },
+    textColor:{r: 0, g: 0, b: 0, a: 1 }, 
+    borderRadius: 15,
+    renderOrder:10,
+    useDepth:true, 
+    clipDistance: 30, maxClipFactor:0.3, occlusionDistance:3, 
+}
+const labelProp2 = {
+    //sizeInfo: {minSize : 200 ,  maxSize : 250,   nearBound : 0.8, farBound : 10},
+    backgroundColor:{r: 255, g: 255, b: 255, a: 0 },
+    textColor:{r:255 , g: 255, b: 255, a: 1 }, 
+    textBorderColor:{r:30 , g:30, b: 30, a: 1 }, 
+    textBorderThick:3,
+    dontFixOrient:true,
+    renderOrder:10,
+    fontsize:30,
+}
+
+let standardMarkerMat 
+let markerTex
+let getMarerMat = function(){
+    if(!markerTex) {
+        markerTex = {
+            default:texLoader.load( Potree.resourcePath+'/textures/marker.png' ),
+            ring:texLoader.load( Potree.resourcePath+'/textures/marker2.png' )
+        }
+        markerTex.default.anisotropy = 4 // 各向异性过滤 .防止倾斜模糊
+        markerTex.ring.anisotropy = 4  
+        //有可能被点云遮住吗。 
+     
+    }
+    return  new DepthBasicMaterial({opacity:0.7, side: THREE.DoubleSide , map:markerTex.default ,transparent:true, 
+        clipDistance: 2,  occlusionDistance:1,  //不能设置太短,因为过渡时深度不准确
+        //depthTest: !!Potree.settings.useDepthTex,
+        useDepth:  !!Potree.settings.useDepthTex
+        //改为DepthBasicMaterial是因为原Basic的材质过渡时会先隐藏后出现
+    })    
+}
+//显示全景图时marker没有被遮挡,如果需要,要换成depthBasicMaterial  或者直接把skybox的深度修改(拿到深度贴图后更如此)
+let planeGeo = new THREE.PlaneBufferGeometry(0.2,0.2);
+
+
+
+let sg = new THREE.SphereGeometry(0.1, 8, 8);
+let smHovered = new THREE.MeshBasicMaterial({/* side: THREE.BackSide,  */color: 0xff0000});
+
+let sm = new THREE.MeshBasicMaterial({/* side: THREE.BackSide */});
+
+
+
+var rot90 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,0,1),  Math.PI/2 ); //使用的是刚好适合全景图的,给cube贴图需要转90°
+//var rot90 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1,0,0),  -Math.PI/2 ); //4dkk->navvis
+//var rot901 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0), -Math.PI/2 ); //整张球幕图要旋转下
+//rot90 = new THREE.Quaternion().multiplyQuaternions(  rot901, rot90)   
+var old = null;
+/* 
+转成四维看看的axis:
+var a = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,0,1), THREE.Math.degToRad(-90))  因为四维的要绕y转90
+这里的quaternion.multiply(a);
+ 
+先乘再换顺序 w : q.w,  x:q.x , y:-q.z, z:q.y
+
+ */
+
+//暂时直接用4dkkconsole输出的数据 
+class Panorama extends THREE.EventDispatcher{
+
+	constructor(o,  images360){//file, time, longitude, latitude, altitude, course, pitch, roll
+        super()
+        this.id = o.id; //唯一标识
+        this.images360 = images360
+        this.visible = true  //for viewer updateVisible
+        this.enabled = true//是否可以走
+        this.addEventListener('isVisible',(e)=>{//是否显示该点的mesh(不显示也能走)
+            //console.log('pano isVisible', this.id, e.visible) 
+            viewer.updateVisible(this.marker, 'panoVisi', e.visible)
+            Potree.settings.showPanoMesh && (this.mesh.visible = e.visible)
+            if(e.reason == 'screenshot' || e.visible){
+                this.label && (this.label.visible = e.visible)//截图时隐藏下
+            }
+            viewer.updateVisible(this.label2, 'panoVisi', e.visible)
+        })
+        /*  
+        漫游点可见性:旧
+            level       reason                           类型
+            2(最高)buildingChange(不在此楼层)        unvisible   
+            1       modeIsShowPanos(漫游模式)          visible    //不记得为什么加这个了,所以重写
+            0       pointcloudVisi(隐藏了数据集)       unvisible
+         */
+         
+         /* 
+        漫游点可见性:新
+            level       reason                           类型
+            2(最高)buildingChange(不在此楼层)        unvisible   
+            1       ifShowMarker(marker显示开关)       unvisible 
+            0       pointcloudVisi(隐藏了数据集)       unvisible
+         */ 
+         
+        
+        if(Potree.settings.editType == 'pano'){//漫游点拼合编辑
+            this.uuid = o.uuid  //因为有多个数据集 所以会重复
+            this.index = o.index  //下标, 用于visibles
+            this.pointcloud = viewer.scene.pointclouds.find(e=>e.panoUuid == o.uuid) 
+            this.pointcloud.panos.push(this)
+            this.sid = this.pointcloud.dataset_id + '|' + this.uuid  //不会更改的标记  用于entity.panos里的标记
+            
+            
+            this.panosData = o
+            
+            //数据中原本的位置朝向
+            this.dataPosition = new THREE.Vector3().copy(o.pose.translation) 
+            this.dataQuaternion = new THREE.Quaternion().copy(o.pose.rotation) 
+            this.dataRotation = new THREE.Euler().setFromQuaternion(this.dataQuaternion) 
+            
+            
+            //因为位置朝向随着点云位置改变,所以直接运用到点云上,这里清零
+            this.originPosition = new THREE.Vector3()   //{x: 0, y: 0, z: 0}
+            this.quaternion = new THREE.Quaternion()  //{w: 0, x: 0, y: 0, z: 1}
+            //this.quaternion4dkk = math.convertVisionQuaternion(this.quaternion)//4dkk内使用的quaternion 
+            this.visibles = o.visibles 
+            this.rtkState = o.has_rtk ? o.use_rtk : null 
+            
+            const height = 1.4; //相机高度
+            this.originFloorPosition = this.originPosition.clone()
+            this.originFloorPosition.z -= height
+            
+            
+            /* this.originPosition = new THREE.Vector3().copy(o.pose.translation)  //{x: 0, y: 0, z: 0}
+            this.quaternion = new THREE.Quaternion().copy(o.pose.rotation) //{w: 0, x: 0, y: 0, z: 1}
+            //this.quaternion4dkk = math.convertVisionQuaternion(this.quaternion)//4dkk内使用的quaternion 
+            this.visibles = o.visibles 
+            this.pointcloud = viewer.scene.pointclouds.find(e=>e.dataset_id == o.uuid) 
+            this.pointcloud.panos.push(this)
+            
+            const height = 1.5; //相机高度
+            this.originFloorPosition = this.originPosition.clone()
+            this.originFloorPosition.z -= height
+             */
+            
+            
+        }else{
+            this.originPosition = new THREE.Vector3().fromArray(o.dataset_location) 
+            this.originFloorPosition = new THREE.Vector3().fromArray(o.dataset_floor_location)
+            
+            this.originID = parseInt(o.file_id)//"file_id":"00022"对应是原本的4dkk的id --来自vision.txt
+             
+            this.pointcloud = viewer.scene.pointclouds.find(e=>e.dataset_id == o.dataset_id) || viewer.scene.pointclouds[0]
+            this.pointcloud.panos.push(this)
+            
+            //this.sid = this.pointcloud.sceneCode + '|' + this.originID  //不会更改的标记
+            this.sid = this.pointcloud.dataset_id + '|' + this.originID  //不会更改的标记
+            //全景图和Cube的水平采样起始坐标相差90度 
+            
+
+            /* if(from4dkk){
+                var qua = o.dataset_orientation 
+                
+                var quaternion = new THREE.Quaternion().fromArray(qua)
+                    quaternion = new THREE.Quaternion().multiplyQuaternions(quaternion,  rot901);//整张球幕图要旋转下  因为在4dkk里转过,还原。如果是tiles的不用
+                this.quaternion = new THREE.Quaternion(quaternion.x, -quaternion.z, quaternion.y, quaternion.w) //转化坐标
+                    
+            }else{ */
+            
+                
+                var qua = o.dataset_orientation 
+                qua = [qua[1], qua[2], qua[3], qua[0]] 
+                this.quaternion = new THREE.Quaternion().fromArray(qua)
+                this.quaternion4dkk = math.convertVisionQuaternion(this.quaternion)//4dkk内使用的quaternion 
+                this.quaternion2 = this.quaternion.clone()
+                this.quaternion = new THREE.Quaternion().multiplyQuaternions(this.quaternion,  rot90);//全景图和Cube的水平采样起始坐标相差90度,cubeTex转90度
+                
+                this.rotation4dkk = new THREE.Euler().setFromQuaternion(this.quaternion4dkk)
+                
+             //}
+             
+             
+                //this.quaternion1 = Potree.Utils.QuaternionFactory.fromArray(o.dataset_orientation)
+                //同quaternion
+
+          
+            //let xy = this.transform.forward([this.longitude, this.latitude]);  
+            this.file = `https://4dkk.4dage.com/images/images${Potree.settings.number}/pan/high/${this.id}.jpg`
+            
+             
+            
+        }
+        this.rotation = new THREE.Euler().setFromQuaternion(this.quaternion) 
+        this.build()
+        this.transformByPointcloud() //初始化位移
+        
+        {//tile
+            this.minimumTiledPanoLoaded = !1;
+            this.highestPartialTileRenderOpCompleted = 0;
+            this.highestFullTileRenderOpCompleted = 0;
+            this.shouldRedrawOnBaseLoaded = !1;
+            this.resolutionPromise = {}
+            this.tiledPanoRenderTarget = null;
+            this.zoomed = !1;
+            
+            
+            
+           
+            images360.panoRenderer.addEventListener(PanoRendererEvents.TileRenderSuccess, this.onTileRendered.bind(this));
+            images360.panoRenderer.addEventListener(PanoRendererEvents.PanoRenderComplete, this.onPanoRendered.bind(this));
+            images360.panoRenderer.addEventListener(PanoRendererEvents.TileRenderFailure, this.onTileRenderFail.bind(this));
+            images360.panoRenderer.addEventListener(PanoRendererEvents.UploadAttemptedForAllTiles, this.onUploadAttemptedForAllTiles.bind(this));
+            
+        }
+        
+        
+        
+        this.addEventListener('hoverOn', (e)=>{//from Map
+            if(!e.byMainView){ 
+                this.hoverOn(e) 
+            } 
+        })
+        
+        this.addEventListener('hoverOff', (e)=>{
+            if(!e.byMainView){
+                this.hoverOff(e) 
+            } 
+        })
+	}
+    
+
+
+    setEnable(enable){//是否可以走
+        viewer.updateVisible(this, 'isEnabled', enable) //令所有marker不可见
+
+        this.enabled = enable 
+        //如果当前在全景模式且在这个点,需要切换显示吗? 目前用不到 
+    }
+
+ 
+    loadDepthImg(){ 
+        if(!this.pointcloud.hasDepthTex || this.depthTex || this.depthTexLoading)return
+        this.depthTexLoading = true
+        let src = Potree.settings.number == 'SS-t-7DUfWAUZ3V' ?  `${Potree.scriptPath}/data/${Potree.settings.number}/depthMap/${this.originID}.png`
+                : `https://laser-oss.4dkankan.com/${Potree.settings.webSite}/${this.pointcloud.sceneCode}/data/${this.pointcloud.sceneCode}/depthmap/${this.originID}.png`
+        let texture = texLoader.load( src, ()=>{
+            this.depthTex = texture
+            this.images360.dispatchEvent({type:'loadedDepthImg', pano:this, loaded:true})
+            this.depthTexLoading = false
+        },(e)=>{//error
+            console.error('loadDepthImg失败, 数据集sceneCode'+ this.pointcloud.sceneCode,  this.id )
+            this.pointcloud.hasDepthTex = false
+            this.images360.dispatchEvent({type:'loadedDepthImg', pano:this, })
+        });
+        texture.wrapS = THREE.RepeatWrapping;
+        texture.flipY = false 
+        texture.magFilter = THREE.LinearFilter
+        texture.minFilter = THREE.LinearFilter
+	}
+ 
+    
+    build(){
+          
+        /* let mesh = new THREE.Mesh(sg, sm); 
+        mesh.scale.set(1, 1, 1);
+        mesh.material.transparent = true;
+        mesh.material.opacity = 0.75;
+        mesh.pano = this;
+        mesh.name = 'panoSphere'
+        mesh.addEventListener('mouseover',(e)=>{
+            mesh.material = smHovered
+        })
+        mesh.addEventListener('mouseleave',(e)=>{
+            mesh.material = sm
+        })
+        mesh.addEventListener('click',(e)=>{
+            this.images360.focusPano(this)
+        }) 
+        this.mesh = mesh;
+        if(!Potree.settings.showPanoMesh) mesh.visible = false
+        this.images360.node.add(mesh)
+        */
+        
+        { // orientation
+            //var {course, pitch, roll} = this;
+            //mesh.quaternion.copy(this.quaternion) 
+             
+            //add 
+            //var quaternion = new THREE.Quaternion().multiplyQuaternions(this.quaternion,  rot901);//改  为球目全
+            //quaternion.premultiply(rot90)
+            this.panoMatrix = new THREE.Matrix4().makeRotationFromQuaternion(this.quaternion) 
+            this.oriPanoMatrix = this.panoMatrix.clone()
+            
+            if(this.quaternion2)this.oriPanoMatrix2 = new THREE.Matrix4().makeRotationFromQuaternion(this.quaternion2) 
+        
+            
+            //console.log(this.quaternion)
+            //this.quaternion = quaternion
+        } 
+        
+        
+         
+        let marker = new THREE.Mesh(planeGeo, getMarerMat() ) 
+            marker.name = 'marker_'+this.id
+            marker.up.set(0,0,1)
+            marker.lookAt(marker.up) 
+            marker.scale.set(2,2,2) 
+        this.addEventListener('changeMarkerTex',(e)=>{
+            marker.material.map = markerTex[e.name]  
+        })    
+             
+        this.marker = marker 
+        if(Potree.settings.editType == 'pano'){
+            viewer.updateVisible(marker, 'panoEdit', false, 4)
+        }
+        
+        this.images360.node.add(marker)
+        Potree.settings.isTest && this.createTextLabel()
+        this.createTextLabel2() 
+        
+        /* let mouseover = (e)=>{ 
+            if(!e.byMap){
+                pano.mapMarker.material = panoMarkerMats.selected
+                if(!e.byMainView) pano.dispatchEvent({type: "hoverOn", byMap:true})
+                this.needRender = true    
+            }
+        }
+        
+        let mouseleave = (e)=>{
+            if(!e.byMap){
+                pano.mapMarker.material = panoMarkerMats.default
+                if(!e.byMainView) pano.dispatchEvent({type: "hoverOff", byMap:true})
+                this.needRender = true
+            }
+        } */
+         
+        
+        marker.addEventListener('mouseover', this.hoverOn.bind(this));  
+        marker.addEventListener('mouseleave', this.hoverOff.bind(this)); 
+    }
+    
+    
+    
+    
+    transformByPointcloud(){
+        
+        let position = this.originPosition.clone().applyMatrix4(this.pointcloud.transformMatrix);//也可以用datasetPosTransform算
+        let floorPosition = this.originFloorPosition.clone().applyMatrix4(this.pointcloud.transformMatrix);
+        this.setPosition(position, floorPosition) 
+        this.panoMatrix = new THREE.Matrix4().multiplyMatrices(this.pointcloud.rotateMatrix, this.oriPanoMatrix  ) 
+        //this.panoMatrix2 =  Potree.Utils.datasetRotTransform({fromDataset:true, pointcloud:this.pointcloud,  matrix:this.oriPanoMatrix, getMatrix:true}) //和上一行结果一样
+        //quaternion也变下
+        if(this.oriPanoMatrix2){ 
+            this.panoMatrix2 = new THREE.Matrix4().multiplyMatrices(this.pointcloud.rotateMatrix, this.oriPanoMatrix2  )//供DepthImageSampler使用 
+            this.panoMatrix2Inverse = this.panoMatrix2.clone().invert(); 
+        }        
+        this.dispatchEvent('rePos')
+    }
+    
+    setPosition(position, floorPosition){
+        this.position = position
+        this.floorPosition = floorPosition
+        //this.mesh.position.copy(this.position)
+        this.marker.position.copy(this.floorPosition) 
+        this.marker.position.z+=0.04//会被点云遮住
+        if(this.label){ 
+            if(Potree.settings.editType == 'pano'){
+                this.label.position.copy(this.position)
+            }else{
+                this.label.position.copy(this.floorPosition)
+            } 
+            this.label.position.z+=0.14
+            this.label.update()
+        }
+        
+        if(this.label2){
+            if(Potree.settings.editType == 'pano'){
+                this.label2.position.copy(this.position)
+            }else{
+                this.label2.position.copy(this.floorPosition)
+            }
+            this.label2.position.copy(this.marker.position)
+            this.label2.update()
+        }
+          
+    }
+    
+    
+    
+    
+    
+    
+    
+    
+    hoverOn(e={}) { 
+        //console.log("hoverOn  " + this.id  )
+        transitions.start(lerp.property(this.marker.material, "opacity", 1), 250)  
+		if(!e.byMap) this.dispatchEvent({type:'hoverOn', byMainView:true})
+        if(!e.byImages360) this.images360.dispatchEvent({type:'markerHover', hovered:true, pano:this})
+    }
+
+ 
+
+
+    hoverOff(e={}){
+        //console.log("hoverOff  " + this.id  )
+        transitions.start(lerp.property(this.marker.material, "opacity", 0.5), 250) 
+        if(!e.byMap) this.dispatchEvent({type:'hoverOff',  byMainView:true})
+        if(!e.byImages360) this.images360.dispatchEvent({type:'markerHover', hovered:false, pano:this})
+    }
+    
+    
+    
+    setZoomed(zoomed){
+        this.zoomed = zoomed;
+        Potree.settings.displayMode == 'showPanos' && this.updateSkyboxForZoomLevel(); //放大后换成zoomTarget贴图
+        viewer.dispatchEvent({type:'panoSetZoom', zoomed})
+        
+    }
+    
+    
+    enter(){ 
+        this.setZoomed(!1),
+        viewer.dispatchEvent({type:PanoramaEvents.Enter,  oldPano:old, newPano:this  }  )
+        old = this 
+        //console.log("enter pano "+ this.id)
+    } 
+
+    exit(){
+        /* if(this.tiled)
+        { */
+            this.clearWaitDeferreds();
+            this.minimumTiledPanoLoaded = !1;
+            this.tiledPanoRenderTarget = null;
+            this.setZoomed(!1);
+            this.images360.panoRenderer.deactivateTiledPano(this);
+            this.highestPartialTileRenderOpCompleted = 0;
+            this.highestFullTileRenderOpCompleted = 0;
+        /*}
+         else
+        {
+            this.solidSkybox.dispose();
+            this.solidSkybox.loaded = !1;
+            this.solidSkybox.version = 0;
+        } */
+        
+        //console.log("exit pano "+ this.id)
+        
+        viewer.dispatchEvent({type:PanoramaEvents.Exit, pano:this}); 
+    }
+    
+    
+    updateSkyboxForZoomLevel(){
+        if(this.minimumTiledPanoLoaded){
+            this.images360.updateProjectedPanos();
+        }
+         
+    }
+    
+    getSkyboxTexture(){
+     
+        if(this.minimumTiledPanoLoaded)
+        {
+            if(this.zoomed && this.images360.qualityManager.maxRenderTargetSize > this.images360.qualityManager.maxNavPanoSize)//change 如果放大后和不放大都是2k就不用这个
+            {
+                return this.images360.panoRenderer.zoomRenderTarget.texture;   
+            }
+            else
+            {
+                
+                this.tiledPanoRenderTarget.texture.mapping = THREE.UVMapping//add
+                return this.tiledPanoRenderTarget.texture;
+            }
+        }
+        else
+        {
+            return null;
+        }
+         
+    }
+    
+   
+    
+    isLoaded(e){ 
+        if (e && "string" == typeof e)
+            console.error("Wrong panoSize given to Panorama.isLoaded(); a tiled pano uses PanoSizeClass"); 
+        return !!this.minimumTiledPanoLoaded && (!e || this.highestFullTileRenderOpCompleted >= e)//改:原本是:this.highestPartialTileRenderOpCompleted >= e, 希望这代表全部加载完
+     
+    }
+
+    getWaitDeferred(size){//获取不同size的tile贴图的promiss 
+        var t = this.resolutionPromise[this.id];
+        t || (t = {}, this.resolutionPromise[this.id] = t);
+        var i = t[size];
+        return i || (i = {
+            deferred: $.Deferred(),
+            active: !1
+        },
+        t[size] = i),
+        i
+    }
+    
+    clearWaitDeferreds(){
+        var e = this.resolutionPromise[this.id];
+        e || (e = {},
+        this.resolutionPromise[this.id] = e);
+        for (var t in e)
+            if (e.hasOwnProperty(t)) {
+                var i = e[t];
+                i.active = !1,
+                i.deferred = $.Deferred()
+            }
+    }
+    resetWaitDeferred(e){
+        var t = this.getWaitDeferred(e);
+        t.active = !1;
+        t.deferred = $.Deferred();
+    }
+    onTileRendered(ev){  
+        ev.id === this.id && this.dispatchEvent({
+            type:PanoramaEvents.TileLoaded, 
+            size:ev.panoSize, index:ev.tileIndex, count:ev.totalTiles
+        });
+    }
+
+    onPanoRendered(ev) { 
+        if(ev.id === this.id)  
+        {
+            this.minimumTiledPanoLoaded = !0;
+            this.updateSkyboxForZoomLevel();//更新贴图 setProjected
+            ev.panoSize > this.highestPartialTileRenderOpCompleted && (this.highestPartialTileRenderOpCompleted = ev.panoSize);//应该是更新最高获取到的Partial size
+            ev.updateFullComplete && ev.panoSize > this.highestFullTileRenderOpCompleted && (this.highestFullTileRenderOpCompleted = ev.panoSize); //应该是更新最高获取到的Full size
+            //this.dispatchEvent("load", ev.panoSize);
+            viewer.ifAllLoaded( this);
+            this.dispatchEvent({type:PanoramaEvents.LoadComplete, size:ev.panoSize, count:ev.totalTiles});
+        }
+    }
+ 
+    onTileRenderFail(ev) { 
+        ev.id === this.id && this.dispatchEvent({type:PanoramaEvents.LoadFailed   });
+    }
+    onUploadAttemptedForAllTiles(ev) { 
+        if (ev.id === this.id) {        
+            var n = this.images360.qualityManager.getPanoSize(PanoSizeClass.BASE);
+            if(ev.panoSize === n && this.shouldRedrawOnBaseLoaded) //shouldRedrawOnBaseLoaded一直是false。在4dkk里只有初始点在quickstart后变为true。
+            {
+                this.shouldRedrawOnBaseLoaded = !1;
+                this.panoRenderer.resetRenderStatus(this.id, !0, !1);
+                this.panoRenderer.renderPanoTiles(this.id, null, !0, !0);
+            }
+        }
+    }
+
+    
+    
+    createTextLabel(){
+        this.removeTextLabel()
+        this.label = new TextSprite(Object.assign({},
+           labelProp, {text: this.id }) //{text: `id:${this.id}, dataset:${this.pointcloud.name}, 4dkkId:${this.originID}`}
+        ); 
+        this.images360.node.add(this.label);
+        this.floorPosition && this.label.position.copy(this.floorPosition)
+    }
+    
+    createTextLabel2(){  
+                          
+                                                                                           
+                                                            
+                                                       
+                                                         
+                              
+                               
+                           
+                        
+          
+        this.label2 = new TextSprite(Object.assign({},
+           labelProp2, {text: /* this.originID  */   parseInt(this.id)+1   }) //{text: `id:${this.id}, dataset:${this.pointcloud.name}, 4dkkId:${this.originID}`}
+        ); 
+        this.images360.node.add(this.label2);
+        this.floorPosition && this.label2.position.copy(this.floorPosition)
+        let s = 0.4
+        this.label2.scale.set(s,s,s)
+        viewer.updateVisible(this.label2, 'notDisplay', false)
+    }
+    
+    removeTextLabel(){
+        if(this.label){ 
+            this.label.parent.remove(this.label);
+        }
+    }
+    
+    dispose(){
+        
+        let i = viewer.images360.panos.indexOf(this);
+        if(i==-1)return
+        
+        this.marker.parent.remove(this.marker)
+        
+        
+        this.removeTextLabel()
+        if(this.depthTex) this.depthTex.dispose()
+        viewer.images360.panos.splice(i,1);
+        
+        this.dispatchEvent('dispose')
+        //删除tile贴图、depthTex等以后再写
+    }
+    
+};
+
+
+ 
+
+Panorama.prototype.loadTiledPano = function() {
+    //var downloads = []  , t = [];
+    var downloaded = {}  , eventAdded = {}, latestPartialRequest = {}; //每个pano对应一组这些
+         
+    return function(size, dirs, fov, o, a, download) {
+        var dir = dirs.datasetsLocal.find(e=>e.datasetId == this.pointcloud.dataset_id).direction;
+        //var dir = dirs
+         
+        
+        null !== o && void 0 !== o || (o = !0),
+        null !== a && void 0 !== a || (a = !0);
+        var l = this.getWaitDeferred(size)
+          , c = l.deferred
+          , h = null
+          , u = null; 
+        fov && ("number" == typeof fov ? h = fov : (h = fov.hFov, u = fov.vFov))  
+        
+        if (!this.isLoaded(size)) {
+            //console.log('loadTiledPano', this.id, size, fov)
+            if (!l.active) {
+                l.active = !0 
+                let name = this.id + ":" + size
+                downloaded[name] = downloaded[name] || []
+                /* 
+                this.downloaded = downloaded
+                this.latestPartialRequest = latestPartialRequest 
+                 */
+                latestPartialRequest[name] = null
+                     
+                if (fov) {
+                    let tileArr = []//add 
+                    var d = TileUtils.matchingTilesInDirection(this, size, dir, h, u, tileArr);
+                    
+                    latestPartialRequest[name] = tileArr
+                    downloaded[name].forEach((e)=>{
+                         let item = latestPartialRequest[name].find(a=>e.faceTileIndex == a.faceTileIndex && e.face == a.face)  
+                         if(item){
+                             item.loaded = true
+                         }
+                    })
+                    if(!latestPartialRequest[name].some(e=>!e.loaded)){//所需要的全部加载成功
+                        //let total = TileUtils.getTileCountForSize(size)
+                        //this.onPanoRendered(this.id, size, total, !0);
+                        c.resolve(size/* , total */);
+                        this.resetWaitDeferred(size)
+                        //console.log('该部分早已经加载好了'+size, this.id)
+                        latestPartialRequest[name] = null
+                    }
+                     
+                    //console.log("Loading partial pano: " + this.id + " with " + d + " tiles")
+                }
+                if(!eventAdded[this.id]) {
+                    eventAdded[this.id] = !0 
+                    
+                    this.addEventListener(PanoramaEvents.LoadComplete, function(ev/* e, t */) {//本次任务全部加载完毕 
+                        
+                        //console.warn('点位(可能部分)下载完成 ', 'id:'+this.id,  'size:'+ev.size ) 
+                        
+                        var i = this.getWaitDeferred(ev.size).deferred;//"pending"为还未完成
+                        i && "pending" === i.state() && this.highestPartialTileRenderOpCompleted >= ev.size && (i.resolve(ev.size, ev.count),
+                        this.resetWaitDeferred(ev.size))//恢复active为false
+                    }.bind(this)) 
+                    
+                    this.addEventListener(PanoramaEvents.LoadFailed, function(ev) {
+                        var t = this.getWaitDeferred(e).deferred;
+                        t && "pending" === t.state() && this.highestPartialTileRenderOpCompleted >= ev.t && (t.reject(ev.t),
+                        this.resetWaitDeferred(ev.t))//恢复active为false
+                    }.bind(this)) 
+                    
+                    this.addEventListener(PanoramaEvents.TileLoaded, function(ev/* t, i, n */) {//每张加载完时
+                        
+                        //console.log('tileLoaded', 'id:'+this.id,  'size:'+ev.size, 'tileIndex:'+ev.index )
+                        let tileIndex = ev.index
+                        let total = ev.count
+                        let size = ev.size
+                        let name = this.id + ":" + size 
+                        downloaded[name] = downloaded[name] || [] //不是所有的加载都是从loadTiledPano获取的所以会有未定义的情况
+                        
+                        let {faceTileIndex,face} = TileUtils.getTileLocation(size, tileIndex, {}) 
+                        downloaded[name].push({faceTileIndex,face})    
+                        var r = this.getWaitDeferred(size).deferred;
+                        if (r && "pending" === r.state()) { 
+                            r.notify(size, tileIndex, total);
+                            if(latestPartialRequest[name]){
+                                let item = latestPartialRequest[name].find(e=>e.faceTileIndex == faceTileIndex && e.face == face)    
+                                item && (item.loaded = true ) 
+                                
+                                if(!latestPartialRequest[name].some(e=>!e.loaded)){//所需要的局部tiles全部加载成功
+                                    this.onPanoRendered(this.id, size, total, !0); //onPanoRendered还会触发 PanoramaEvents.LoadComplete   
+                                    r.resolve(size, total);
+                                    this.resetWaitDeferred(size)
+                                    //console.log('该部分加载好了'+size, this.id)
+                                    latestPartialRequest[name] = null
+                                }
+                                
+                            } 
+                        } 
+
+
+    
+                        
+                        /* var r = this.getWaitDeferred(ev.size).deferred;
+                        if (r && "pending" === r.state()) {
+                            r.notify(ev.size, ev.index, ev.count);
+                             
+                            var o = downloads[this.id + ":" + ev.size];
+                            if(o){//如果有规定下载哪些tile,只需要下载这些tile则LoadComplete
+                                o.tileCount++ 
+                                
+                                if(o.tileCount === o.targetTileCount){//达到下载目标数
+                                    this.onPanoRendered(this.id, ev.size, ev.count, !0);
+                                    r.resolve(ev.size, ev.count);
+                                    this.resetWaitDeferred(ev.size)
+                                }
+                            }
+                        } */
+                    }.bind(this))
+                }
+            }
+            this.images360.tileDownloader.clearForceQueue(),
+            this.images360.tileDownloader.forceQueueTilesForPano(this, size, dir, h, u, download) 
+            this.tiledPanoRenderTarget = this.images360.panoRenderer.activateTiledPano(this, this.images360.qualityManager.getMaxNavPanoSize(), o) 
+            this.images360.panoRenderer.renderPanoTiles(this.id, dirs, a)
+        }else{
+            //console.log('早已经全加载好了' +size, this.id)
+            c.resolve(size)
+        }
+        return c.promise()
+    }
+}()
+
+
+/* 
+
+    经观察发现,navvis的也存在的问题是点云和全景有微小的偏差,导致远处的热点在全景和点云上看位置差别感大,比如一个在路上一个在天空上。
+
+
+ */
+export default Panorama

+ 87 - 0
src/custom/modules/panos/Panorama1.js

@@ -0,0 +1,87 @@
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+
+
+var texLoader = new THREE.TextureLoader()
+
+
+
+let standardMarkerMat = new THREE.MeshBasicMaterial({side: THREE.DoubleSide , map:texLoader.load('../resources/textures/marker.png') ,transparent:true})
+let panoHeight = 2.3;
+let planeGeo = new THREE.PlaneBufferGeometry(1,1);
+
+
+
+let sg = new THREE.SphereGeometry(1, 8, 8);
+
+
+let sm = new THREE.MeshBasicMaterial({side: THREE.BackSide});
+var rot90 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0), Math.PI/2); //这里旋转还是基于原先的坐标轴,因为是先有图片的旋转后有skybox整体的旋转。但是角度不是负的是正的。
+
+
+
+
+
+
+
+
+
+class Panorama{
+
+	constructor(o){//file, time, longitude, latitude, altitude, course, pitch, roll
+		this.file = o.file;
+		this.time = o.time;
+		this.longitude = o.long;
+		this.latitude = o.lat;
+		this.altitude = o.alt;
+		this.course = o.course;
+		this.pitch = o.pitch;
+		this.roll = o.roll;
+        this.transform = o.transform
+		this.mesh = null;
+        
+        
+        let xy = this.transform.forward([this.longitude, this.latitude]);  
+        this.position = new THREE.Vector3(xy[0], xy[1], o.alt) 
+        this.build()
+	}
+    
+    
+    build(){
+         
+
+        let mesh = new THREE.Mesh(sg, sm);
+        mesh.position.copy(this.position)
+        mesh.scale.set(1, 1, 1);
+        mesh.material.transparent = true;
+        mesh.material.opacity = 0.75;
+        mesh.pano = this;
+
+        { // orientation
+            var {course, pitch, roll} = this;
+            mesh.rotation.set(
+                THREE.Math.degToRad(+roll + 90),
+                THREE.Math.degToRad(-pitch),
+                THREE.Math.degToRad(-course + 90),
+                "ZYX"
+            );
+            //add 
+            var quaternion = new THREE.Quaternion().multiplyQuaternions(mesh.quaternion,  rot90);//改  为球目全
+            this.panoMatrix = new THREE.Matrix4().makeRotationFromQuaternion(quaternion) 
+        
+        } 
+        this.mesh = mesh;
+        
+        
+         
+        let marker = new THREE.Mesh(planeGeo, standardMarkerMat)
+            
+            marker.position.copy(mesh.position);
+              
+            marker.position.z -= panoHeight
+            marker.scale.set(2,2,2) 
+            
+        this.marker = marker    
+    }
+};
+
+export default Panorama

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1207 - 0
src/custom/modules/panos/tile/PanoRenderer.js


+ 176 - 0
src/custom/modules/panos/tile/QualityManager.js

@@ -0,0 +1,176 @@
+
+import * as THREE from "../../../../../libs/three.js/build/three.module.js";
+ 
+import browser from '../../../utils/browser.js' 
+import {settings,config} from '../../../settings.js'
+ 
+
+
+let {ModelManagerEvents,PanoSizeClass} = Potree.defines
+
+
+export default class QualityManager {
+    constructor(e, t, i) {
+      
+        this.maxNavPanoSize = -1;
+        this.maxZoomPanoSize = -1;
+        this.devicePixelDensity = e;
+        this.deviceScreenSize = t;
+        this.clientBandwidth = i;
+        this.panoSizeClassMap = {};
+        this.useHighResolutionPanos = !0;  //是否能够使用2k及以上图
+        this.useUltraHighResolutionPanos = !1;
+        this.modelHasUltraHighPanos = !1;
+        this.qualityManager = this;
+        
+        this.maxRenderTargetSize = browser.isMobile() ? 2048 : 4096  //add
+        this.init()
+    }
+
+    init(e ) {
+        //var metadata = store.getters['scene/metadata'] ;//有时候请求不到
+        //if(metadata.sceneSource == 11 || metadata.sceneScheme == 12){
+        /* if(config.tileClass == '1k'){
+            this.useHighResolutionPanos = false    //xzw add 只加载1k
+        } */
+         
+        
+        this.buildPanoSizeClassMap(this.devicePixelDensity, this.deviceScreenSize, this.clientBandwidth);
+        this.ultraHighSize = this.getPanoSize(PanoSizeClass.ULTRAHIGH);
+        this.highSize = this.getPanoSize(PanoSizeClass.HIGH);
+        this.standardSize = this.getPanoSize(PanoSizeClass.STANDARD);
+        this.baseSize = this.getPanoSize(PanoSizeClass.BASE);
+        config.tiling.maxZoomPanoQuality && this.ultraHighSize <= config.tiling.maxZoomPanoQuality && (config.tiling.allowUltraHighResolution = !0);
+        this.highQualityThreshold = browser.valueFromHash("threshold2k", config.windowHeightHighQualityThreshold);
+        this.updateMaximums();
+        //e.on(ModelManagerEvents.ActiveModelChanged, this.onModelChanged.bind(this));
+    }
+
+    updateFromModel(e) {
+        //this.updateHighResolutionSettings(e) 
+        this.updateUltraHighResolutionSettings(e)
+    }
+
+    /* updateHighResolutionSettings(e) {
+        this.useHighResolutionPanos = !0 
+        this.updateMaximums()
+    } */
+
+    updateUltraHighResolutionSettings(e) {
+        if (config.tiling.allowUltraHighResolution && this.modelHasUltraHighPanos) {
+            this.useUltraHighResolutionPanos = !0;
+        } else {
+            this.useUltraHighResolutionPanos = !1;
+        }
+        this.updateMaximums();
+    }
+
+    enableUltraHighQualityMode() {
+        this.modelHasUltraHighPanos = !0;
+        this.updateUltraHighResolutionSettings(null);
+    }
+
+    ultraHighQualityModeEnabled() {
+        return this.modelHasUltraHighPanos
+    }
+
+    onModelChanged(e) {
+        this.updateFromModel(e.model),
+            this.updateMaximums()
+    }
+
+    updateMaximums() {
+        this.maxNavPanoSize = config.tiling.maxNavPanoQuality || this.detectMaxNavPanoSize(),
+        this.maxZoomPanoSize = config.tiling.maxZoomPanoQuality || this.detectMaxZoomPanoSize(),
+        this.maxZoomPanoSize < this.maxNavPanoSize && (this.maxNavPanoSize = this.maxZoomPanoSize)
+    }
+
+    buildPanoSizeClassMap() {
+        this.panoSizeClassMap[PanoSizeClass.BASE] = 512,
+            this.panoSizeClassMap[PanoSizeClass.STANDARD] = 1024,
+            this.panoSizeClassMap[PanoSizeClass.HIGH] = 2048,
+            this.panoSizeClassMap[PanoSizeClass.ULTRAHIGH] = 4096
+    }
+
+    getPanoSize(e) {
+        return this.panoSizeClassMap[e]
+    }
+
+    getMaxPossiblePanoSize() {
+        return this.getPanoSize(PanoSizeClass.ULTRAHIGH)
+    }
+
+    getMaxPanoSize() {
+        return this.maxZoomPanoSize
+    }
+
+    getMaxNavPanoSize() {
+        return this.maxNavPanoSize
+    }
+
+    getMaxZoomPanoSize() {
+        return this.maxZoomPanoSize
+    }
+
+    detectMaxNavPanoSizeClass() {
+        //return this.useHighResolutionPanos ? browser.isMobile() ? PanoSizeClass.STANDARD : window.innerHeight < this.highQualityThreshold ? PanoSizeClass.STANDARD : PanoSizeClass.HIGH : PanoSizeClass.STANDARD
+       /*  if(config.name == 'decor'){
+            return PanoSizeClass.STANDARD
+        }
+        return PanoSizeClass.HIGH  */
+        switch(Potree.settings.navTileClass){  
+            case '1k':
+                return PanoSizeClass.STANDARD;
+                break;
+            case '2k':             
+            default:
+                return PanoSizeClass.HIGH;
+        }
+        
+        
+    }
+
+    detectMaxNavPanoSize() {
+        var e = this.detectMaxNavPanoSizeClass();
+        return this.getPanoSize(e)
+    }
+
+    detectMaxZoomPanoSize() { //获取当前zoomRenderTarget应下载的最高级别
+    
+        //若是有三个级别,每次只需要加载到当前的zoomLevel;而两级时因为有zoomed来判断是使用基本贴图还是zoomRenderTarget,所以只需要返回最大的即可
+            
+        if(this.zoomLevelResolution){//有三个级别
+            if(this.zoomLevelResolution == '4k' && this.useUltraHighResolutionPanos){
+                return this.getPanoSize(PanoSizeClass.ULTRAHIGH);
+            }else if(this.zoomLevelResolution == '1k' || !this.useHighResolutionPanos){
+                return this.getPanoSize(PanoSizeClass.STANDARD);
+            }else{
+                return this.getPanoSize(PanoSizeClass.HIGH);
+            }
+        }else{
+            if (this.useHighResolutionPanos) {
+                /* if (browser.isMobile()) {//手机版如果要2k的将这里去掉
+                    if (settings.tiling.mobileHighQualityOverride) {
+                        return this.getPanoSize(PanoSizeClass.HIGH);
+                    } else {
+                        return this.getPanoSize(PanoSizeClass.STANDARD);
+                    }
+                } else  */if (this.useUltraHighResolutionPanos ) {
+                    return this.getPanoSize(PanoSizeClass.ULTRAHIGH);
+                } else {
+                    return this.getPanoSize(PanoSizeClass.HIGH);
+                }
+            } else {
+                return this.getPanoSize(PanoSizeClass.STANDARD);
+            }
+            
+            
+        }
+        
+    }
+    
+    
+    
+    
+    
+}

+ 545 - 0
src/custom/modules/panos/tile/TileDownloader.js

@@ -0,0 +1,545 @@
+import * as THREE from "../../../../../libs/three.js/build/three.module.js";
+ 
+import TilePrioritizer from './TilePrioritizer.js'
+import TileUtils from './TileUtils.js' 
+ 
+ 
+import {settings, config} from '../../../settings.js' 
+import {
+    http
+} from '../../../utils/request.js' 
+
+let {TileDownloaderEvents, DownloadStatus} = Potree.defines
+
+
+window.downloaded = {}
+window.startdownloads = [];
+
+
+
+
+class TileDownloader extends THREE.EventDispatcher{
+    constructor( ) {
+        super()
+        this.panos = null;
+        this.retryMinimumTime = 1e4;
+        this.panoLoadCallbacks = {};
+        this.downloadDescriptors = {};
+        this.priorityQueue = [];
+        this.forceQueue = [];
+        this.activeDownloads = [];
+        this.tilePrioritizer = null;
+        this.refreshInterval = null;
+        this.processPriorityQueue = !1;
+        this.concurrentDownloads = 6;//e.concurrentDownloads || 1;
+        this.downloadTestResults = {};
+
+        this.freeze = Object.freeze({
+            Testing: 1,
+            Success: 2,
+            Fail: 3
+        });
+        
+                                                                                  
+         
+        viewer.addEventListener('pageVisible', (e)=>{//不可见时不refreshUpdateInterval 
+            //console.log('visibilitychange:', state)
+            viewer.updateVisible(this,  'pageVisible', e.v) 
+            this.judgeStart() 
+        }) 
+        
+        this.visible = true //add   借用viewer.updateVisible来判断是否start
+        if(Potree.settings.useDepthTex){
+            this.judgeStart()  //开始下载depthTex
+        }else{ 
+            viewer.updateVisible(this,'showPanos', false ) //默认visible = false
+        }
+        
+    }
+
+    setPanoData(e, t /* , i */) {
+        this.panos = e,
+        this.imagePanos = t 
+          //  this.panoGroupId = i
+    }
+  
+    start() { 
+        this.downloadCubeTex = true 
+        if(!Potree.settings.useDepthTex){
+            viewer.updateVisible(this,'showPanos', true )
+            this.judgeStart()            
+        }else{
+            this.refreshInterval || this.judgeStart()
+        }
+    }
+
+    stop() {
+        this.downloadCubeTex = false
+        if(!Potree.settings.useDepthTex){
+            viewer.updateVisible(this,'showPanos', false )
+            this.judgeStart()
+        } 
+    }
+
+    judgeStart(){//add
+        if(this.visible){
+            //console.log('judgeStart true')
+            this.started = true 
+            this.refreshUpdateInterval(0)
+        }else{
+            //console.log('judgeStart false')
+            this.started = false
+            window.clearTimeout(this.refreshInterval)
+        }
+        
+    }
+
+    refreshUpdateInterval(e) {
+        e || (e = 0),
+            this.refreshInterval = window.setTimeout(function() {
+                    var e = this.update();
+                    e ? this.refreshUpdateInterval(TileDownloader.ACTIVE_REFRESH_DELAY) : this.refreshUpdateInterval(TileDownloader.IDLE_REFRESH_DELAY)
+                }
+                .bind(this), e)
+    }
+
+    update() { 
+        if(this.downloadCubeTex){ //可以下载贴图
+            var e = this.forceQueue.length > 0;
+            this.processQueueForDownloading(this.forceQueue);
+            if (this.processPriorityQueue) {
+                this.queuePrioritizedTilesForPanos(this.panos);
+                this.priorityQueue.length > 0 && (e = !0);
+                this.processQueueForDownloading(this.priorityQueue);
+            }
+            return e 
+        }else{//仅下载depthTex
+            this.tilePrioritizer.filterDepthTex(this.panos)
+        }
+        
+    }
+
+    
+
+    queuePrioritizedTilesForPanos(e) {
+        this.tilePrioritizer && (this.clearQueue(this.priorityQueue),
+            this.tilePrioritizer.filterAndPrioritize(this.priorityQueue, e, this),
+            this.clearFromQueue(this.priorityQueue, DownloadStatus.None, !0),  //去除state为DownloadStatus.None的(可能是去除已经在下载的)
+            this.setStatusOrRemoveForAllDescriptors(this.priorityQueue, DownloadStatus.Queued))
+    }
+
+    clearQueue(e) {//停止下载并清空
+        this.setStatusForAllDescriptors(e, DownloadStatus.None),
+            e.length = 0
+    }
+    
+    clearForceQueue() {
+        this.clearQueue(this.forceQueue)
+    }
+    
+    clearFromQueue(e, t, i) {
+        for (var n = 0; n < e.length; n++) {
+            var r = e[n];
+            r && (t === r.status && !i || t !== r.status && i) && (e[n] = null)
+        }
+    }
+
+    setStatusForAllDescriptors(e, t) {
+        for (var i = 0; i < e.length; i++) {
+            var n = e[i];
+            n && (n.status = t)
+        }
+    }
+
+    setStatusOrRemoveForAllDescriptors(e, t) {
+        for (var i = 0; i < e.length; i++) {
+            var n = e[i];
+            n && (n.status !== t ? n.status = t : e[i] = null)
+        }
+    }
+
+    getTileDownloadDescriptors(pano, size) {//获取该pano的该size的全部的tile的descriptor
+        var i = this.getAllTileDownloadDescriptorsForPano(pano),
+            n = i[size];
+        return n || (n = this.buildDownloadDescriptorArray(size),//创建的全部是空的
+                i[size] = n,
+                this.initTileDownloadDescriptors(n, pano, size)),//绑定到该pano size
+            n
+    }
+
+    getAllTileDownloadDescriptorsForPano(pano) {//新建空Descriptors
+        var t = this.downloadDescriptors[pano.id];
+        return t || (t = {},
+                this.downloadDescriptors[pano.id] = t),
+            t
+    }
+
+    processQueueForDownloading(e, t) {//执行下载任务
+        this.cleanupActiveDownloads();
+        if (this.activeDownloads.length < this.concurrentDownloads || t) {
+            var i = t ? e.length : this.concurrentDownloads - this.activeDownloads.length;
+
+            for (var n = 0, r = 0; n < i && e.length > 0; r++) {
+                var o = e.shift();
+                
+                
+                if(o){
+                    //add 为了防止1024的在512前下载完,这里强行等待512下载完毕再开始下载
+                    if(o.panoSize > 512 && !this.isPanoDownloaded(o.pano, 512) ){
+                        //console.log('512的还没下载好呢!')
+                        e.push(o)
+                        break;//一般512的都是连续下载的,所以后面就都不是512了直接中断 
+                    } 
+                    
+                    this.startDownload(o)
+                    n++
+                }
+                
+            }
+        }
+    } 
+    
+    
+    
+    
+    testDownload(panoSize, tileSize, callback) {
+        var n = this.downloadTestResults[panoSize];
+        if (n)
+            return void(n === this.freeze.Success ? callback(!0) : n === this.freeze.Fail && callback(!1));
+        this.downloadTestResults[panoSize] = this.freeze.Testing;
+        var r = this.panos[0],
+            o = this.getTileUrl({pano:r, panoSize, tileSize, tileIndex:0}   /* r.id, panoSize, tileSize, 0 */),
+            a = function(t) {
+                this.downloadTestResults[panoSize] = this.freeze.Success,
+                    callback(!0)
+            }
+            .bind(this),
+            s = function() {
+                this.downloadTestResults[panoSize] = this.freeze.Fail,
+                    callback(!1)
+            }
+            .bind(this);
+        this.loadImage(o, 0, a, s)
+    }
+
+    startDownload(e) {//开始下载啦
+        //console.log('startDownload')
+        
+        startdownloads.push(e) 
+        e.local2SrcFailed = this.local2SrcFailed
+        e.status = DownloadStatus.Downloading;
+        var t = this.getTileUrl(e/* e.pano.id, e.panoSize, e.tileSize, e.tileIndex, e.pano.alignmentType */);//xzw add alignmentType
+        if(!t)return;
+        this.activeDownloads.push(e);
+        this.loadImage(t, TileDownloader.DOWNLOAD_RETRIES, this.downloadComplete.bind(this, e), this.downloadFailed.bind(this, e))
+    }
+
+    downloadFailed(e, t) {
+        //add 
+        if(Potree.settings.isLocal2 && !e.local2SrcFailed){//为了兼容旧的数据src,如果新src没加载成功,就加载旧的
+            e.local2SrcFailed = this.local2SrcFailed = true
+            //this.startDownload(e)//重新下载
+            var t = this.getTileUrl(e); 
+            this.loadImage(t, TileDownloader.DOWNLOAD_RETRIES, this.downloadComplete.bind(this, e), this.downloadFailed.bind(this, e))
+ 
+        }
+    }
+
+    downloadComplete(e, t) {//下载成功时
+        //if (e.panoGroupId === this.panoGroupId) {
+            var i = this.getPanoLoadCallbacks(e.pano, e.panoSize);
+            e.status = DownloadStatus.Downloaded,
+                i && i.onProgress && i.onProgress(e.pano, e.panoSize);
+            var n = {
+                panoId: e.pano.id,
+                image: t,
+                tileSize: e.tileSize,
+                panoSize: e.panoSize,
+                tileIndex: e.tileIndex,
+                faceTileIndex: e.faceTileIndex,
+                totalTiles: e.totalTiles,
+                face: e.face,
+                tileX: e.tileX,
+                tileY: e.tileY,
+                direction: e.direction
+            };
+            
+            downloaded[e.pano.id] || (downloaded[e.pano.id]={512:[],1024:[],2048:[]})
+            downloaded[e.pano.id][e.panoSize] || (downloaded[e.pano.id][e.panoSize] = [])
+            downloaded[e.pano.id][e.panoSize].push(e)
+            if(e.panoSize != 512 && downloaded[e.pano.id][512].length<6){
+                console.warn('没下完')
+            }
+            
+            
+            
+            e.image = t,
+            this.dispatchEvent({type:TileDownloaderEvents.TileDownloadSuccess, desc:n} ) 
+            this.isPanoDownloaded(e.pano, e.panoSize) && (n = {
+                    panoId: e.pano.id,
+                    tileSize: e.tileSize,
+                    panoSize: e.panoSize
+                },
+                this.dispatchEvent({type:TileDownloaderEvents.PanoDownloadComplete, desc:n}),
+                i && i.onLoad && i.onLoad(e.pano, e.panoSize))
+        //}
+    }
+
+
+    isPanoDownloaded(e, t) {
+        var i = this.getTileDownloadDescriptors(e, t);
+        if (i.length <= 0)
+            return !1;
+        for (var n = 0; n < i.length; n++) {
+            var r = i[n];
+            if (r.status !== DownloadStatus.Downloaded)
+                return !1
+        }
+        return !0
+    }
+
+    setPanoLoadCallbacks(e, t, i, n, r) {
+        var o = e.id + ":" + this.qualityManager.getPanoSize(t);
+        this.panoLoadCallbacks[o] = {
+            onLoad: i,
+            onFail: n,
+            onProgress: r
+        }
+    }
+
+    getPanoLoadCallbacks(e, t) {
+        var i = e.id + ":" + t;
+        return this.panoLoadCallbacks[i]
+    }
+
+    buildDownloadDescriptorArray(e) {
+        for (var t = TileUtils.getTileCountForSize(e), i = [], n = 0; n < t; n++) {
+            var r = this.buildDownloadDescriptor();
+            i.push(r)
+        }
+        return i
+    }
+
+    buildDownloadDescriptor() {//Descriptor!
+        var e = {
+            panoGroupId: null,
+            pano: null,
+            panoSize: -1,
+            tileSize: -1,
+            tileIndex: -1,
+            totalTiles: -1,
+            faceTileIndex: -1,
+            status: DownloadStatus.None,
+            url: null,
+            image: null,
+            direction: new THREE.Vector3, //该tile在cube中的方向
+            face: -1,
+            cubeFace: -1,
+            tileX: -1,
+            tileY: -1
+        };
+        return e
+    }
+
+    initTileDownloadDescriptors(e, t, i) {
+        for (var n = 0; n < e.length; n++) {
+            var r = e[n];
+            this.initTileDownloadDescriptor(r, t, i, n)
+        }
+    }
+
+    initTileDownloadDescriptor(desc, pano, size, index) {
+        var r = size >= TileUtils.TILE_SIZE ? TileUtils.TILE_SIZE : size;
+        desc.face = TileUtils.getFaceForTile(size, index);//根据顺序得到的face的index
+        desc.cubeFace = TileUtils.mapFaceToCubemapFace(desc.face);//为了贴图而转化的face index
+        //desc.panoGroupId = this.panoGroupId;//就是场景号
+        desc.pano = pano;
+        desc.panoSize = size;
+        desc.tileSize = r;      //瓦片图size 512
+        desc.tileIndex = index;
+        desc.totalTiles = TileUtils.getTileCountForSize(size);
+        desc.status = DownloadStatus.None;
+        desc.image = null;
+        TileUtils.getTileLocation(desc.panoSize, desc.tileIndex, desc);//得到该tile在这个face中的具体位置(tileX等)
+        TileUtils.getTileVector(desc.panoSize, desc.tileSize, desc.cubeFace, desc.tileX, desc.tileY, TileUtils.LocationOnTile.Center, 0, desc.direction);
+    }
+
+    
+
+    getTiles(d, sceneNum, useV4url){
+        if(Potree.settings.isLocal2 && !this.local2SrcFailed || useV4url){//新的地址  scene_view_data/场景码/images/tiles
+            return `${Potree.settings.urls.prefix3}/scene_view_data/${sceneNum}/images/${d}`    
+        }
+        
+        return `${Potree.settings.urls.prefix3}/images/images${sceneNum}/${d}`    
+    }
+
+    loadImage(e, t, i, n) {
+        //自己修改了ajax,把getImage改成了loadImg
+        http.loadImage(e, t).then(function(e) {
+            i(e)
+        }).fail(n)
+    }
+}
+TileDownloader.prototype.forceQueueTilesForPano = function() {//根据条件开始加载tile
+    var e = [],
+        t = [];
+    return function(pano, size, dir, hFov, vFov, download) { 
+        e.length = 0;
+        for (var u = this.getTileDownloadDescriptors(pano, size), d = 0; d < u.length; d++) {
+            var p = u[d];
+            p.status !== DownloadStatus.None && p.status !== DownloadStatus.Queued || e.push(p)
+        }
+        if (dir && e.length > 0) {
+            TilePrioritizer.sortPanoTiles(e, pano, dir) //按最佳方向排序e
+            t.length = 0 
+            TileUtils.matchingTilesInDirection(pano, size, dir, hFov, vFov, t);//得到在符合视野标准的集合t
+            
+            
+            
+            for (var f = 0, g = function(e) {
+                return e.face === m.face && e.faceTileIndex === m.faceTileIndex
+            }; f < e.length;) {  //过滤掉不符合角度要求的
+                var m = e[f],
+                    v = t.findIndex(g);
+                v < 0 ? e.splice(f, 1) : f++
+            }
+        }
+        for (var A = 0; A < e.length; A++){
+            this.forceQueue.push(e[A]);         //装载
+        }
+        /* if(e.length){
+            console.log(e)
+        } */
+        
+        this.setStatusForAllDescriptors(this.forceQueue, DownloadStatus.ForceQueued);
+        this.clearFromQueue(this.priorityQueue, DownloadStatus.ForceQueued, !1);
+        download && this.processQueueForDownloading(this.forceQueue, !0);
+    }
+}()
+
+TileDownloader.prototype.cleanupActiveDownloads = function() {
+    var e = [];
+    return function() {
+        e.length = 0;
+        for (var t = 0; t < this.activeDownloads.length; t++) {
+            var i = this.activeDownloads[t];
+            i.status !== DownloadStatus.Downloaded && i.status !== DownloadStatus.Failed && e.push(i)
+        }
+        this.activeDownloads.length = 0,
+        this.activeDownloads.push.apply(this.activeDownloads, e)
+    }
+}()
+
+TileDownloader.prototype.getTileUrl = function() {
+    var e = {
+            256: "256",
+            512: "512",
+            1024: "1k",
+            2048: "2k",
+            4096: "4k"
+        },
+        t = {
+            face: -1,
+            faceTileIndex: -1,
+            tileX: -1,
+            tileY: -1
+        };
+    
+    return function(o={}  ) {  
+        var id = o.pano.originID, ////////
+            panoSize = o.panoSize,
+            tileSize = o.tileSize,
+            tileIndex = o.tileIndex,
+            sceneCode = o.pano.pointcloud.sceneCode,
+            useV4url = Potree.settings.testV4url /* && o.pano.pointcloud.useV4url */     //v4的全景图等路径不一样  
+        var metadata = {sceneScheme:10}  
+        
+        
+        TileUtils.getTileLocation(panoSize, tileIndex, t);
+        var s = Math.floor(panoSize / tileSize),
+            l = s * s,
+            h = Math.floor(tileIndex / l),
+            u = "",
+            d = '',  g = '';
+        
+        
+        
+        if(Potree.settings.isLocal){//原始规则
+            //1 === config.tiling.customCompression && (u = "_" + config.tiling["q" + e[panoSize]]);
+            //1 === o.tiling.customCompression && (u = "_" + o.tiling["q" + e[n]]);
+            d = "tiles/" + id + "/" + e[panoSize] + u + "_face" + h + "_" + t.tileX + "_" + t.tileY + ".jpg" 
+            d =  this.getTiles(d, sceneCode, useV4url);
+            g = "?"  
+          
+        }else{//阿里云oss的规则   if (metadata.sceneScheme == 10) 
+            
+            d = 'tiles/4k/' + id + '_skybox' + h + '.jpg?x-oss-process=';
+            if (e[panoSize] == '512') {
+                d += 'image/resize,h_512';
+            } else {
+                //4k的图,移动端是1k,pc端是2k,放大才是4k
+                if (e[panoSize] == '1k' || e[panoSize] == '2k') {  //https://4dkk.4dage.com/images/imagesx4iqYDG3/tiles/4k/122_skybox0.jpg?x-oss-process=image/resize,m_lfit,w_1024/crop,w_512,h_512,x_511,y_0
+                    d += 'image/resize,m_lfit,w_' + panoSize + '/crop,w_512,h_512,';
+                } else {
+                    d = 'tiles/4k/' + id + '_skybox' + h + '.jpg?x-oss-process=image/crop,w_512,h_512,';
+                }
+                //起始位置
+                if (t.tileX == 0) {
+                    d += 'x_0,';
+                } else {
+                    d += 'x_' + (512 * t.tileX - 1) + ',';
+                }
+
+                if (t.tileY == 0) {
+                    d += 'y_0';
+                } else {
+                    d += 'y_' + (512 * t.tileY - 1);
+                } 
+            }
+            
+            d = this.getTiles(d, sceneCode, useV4url);
+            g = "&" 
+        } 
+        
+        d += g + 'time='+o.pano.pointcloud.timeStamp  //加后缀
+         
+        return d;
+    }
+}();
+
+TileDownloader.tilegen = true;
+TileDownloader.IDLE_REFRESH_DELAY = 500;
+TileDownloader.ACTIVE_REFRESH_DELAY = 16;
+TileDownloader.DOWNLOAD_RETRIES = 4;
+
+
+
+
+/* 
+源:https://4dkk.4dage.com/images/imagesSS-t-ZZR7oKnlIl
+目标:https://4dkk.4dage.com/scene_view_data/SS-t-ZZR7oKnlIl/images
+
+源:https://4dkk.4dage.com/data/dataSS-t-ZZR7oKnlIl
+目标:https://4dkk.4dage.com/scene_view_data/SS-t-ZZR7oKnlIl/data
+
+源:https://4dkk.4dage.com/video/videoSS-t-ZZR7oKnlIl
+目标:https://4dkk.4dage.com/scene_view_data/SS-t-ZZR7oKnlIl/video
+ */
+
+
+// var tileconc = TileDownloader.tilegen ? 6 : 2;
+// publicObjectSet.tileDownloader = new TileDownloader({
+//     concurrentDownloads: tileconc
+// });
+
+// export default new  TileDownloader({
+//     concurrentDownloads: TileDownloader.tilegen ? 6 : 2
+// })
+
+
+/* export default new TileDownloader({
+    concurrentDownloads: TileDownloader.tilegen ? 6 : 2
+}) */
+
+export default TileDownloader

+ 395 - 0
src/custom/modules/panos/tile/TilePrioritizer.js

@@ -0,0 +1,395 @@
+ 
+
+import {Images360} from '../Images360.js'
+import TileUtils from './TileUtils.js'
+import cameraLight from '../../../utils/cameraLight.js'
+import math from '../../../utils/math.js'
+import Common from '../../../utils/Common.js' 
+import * as THREE from "../../../../../libs/three.js/build/three.module.js";
+
+
+let {DownloadStatus} = Potree.defines
+
+var h = Object.freeze({
+    None: 0,
+    DirectionalFOV: 1
+});
+
+var u = function () {
+    var e = function e(t, i) {
+        var n = e._panoSpaceDir,
+            r = e._fovThreshold,
+            o = e._fovThresholdNarrow,
+            a = Math.max(Math.min(n.dot(t.direction), 1), -1),
+            s = Math.max(Math.min(n.dot(i.direction), 1), -1);
+        return t._dot = a,
+            i._dot = s,
+            a >= r && s < r ? -1 : a < r && s >= r ? 1 : a >= o && s < o ? -1 : a < o && s >= o ? 1 : t.panoSize > i.panoSize ? 1 : i.panoSize > t.panoSize ? -1 : -(a - s)
+    };
+    return e._panoSpaceDir = new THREE.Vector3,
+        e._fovThreshold = -1,
+        e._fovThresholdNarrow = -1,
+        e
+}();
+
+export default class TilePrioritizer {//优先级处理序列
+    constructor(e,t, i, o, a) {
+        this.qualityManager = e;
+        this.maxNavQuality = this.qualityManager.getMaxNavPanoSize();
+        this.maxZoomQuality = this.qualityManager.getMaxZoomPanoSize();
+        this.baseSize = t;
+        this.standardSize = i;
+        this.highSize = o;
+        this.ultraHighSize = a;
+        this.priorityCriteria = new TilePrioritizer.PriorityCriteria(null, new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1), new THREE.Vector3(0, 0, -1));
+    }
+
+    updateCriteria(e, t, i, n) {//由player更新
+        this.priorityCriteria.pano = e,
+        this.priorityCriteria.cameraPosition.copy(t),
+        //this.priorityCriteria.cameraDir.copy(i),
+        this.priorityCriteria.cameraDirs = i
+        
+        this.priorityCriteria.upcomingPanos = n,
+        this.maxNavQuality = this.qualityManager.getMaxNavPanoSize(),
+        this.maxZoomQuality = this.qualityManager.getMaxZoomPanoSize()
+          
+            
+    }
+
+    canDownloadSize(e) {
+        return this.maxNavQuality >= e || this.maxZoomQuality >= e && this.zoomingActive
+    }
+
+   
+
+
+
+    /* populateNeighborPanos(e, t, i) {
+        i = i || [],
+            i.length = 0;
+        var n = t.getNeighbours(e);
+        for (var r in n)
+            if (n.hasOwnProperty(r)) {
+                var o = t.get(r);
+                if(!o){
+                    console.log(1)
+                }
+                i.push(o)
+            }
+        return i
+    } */
+
+    populateScoredPanos(e, t, i, dirs, a) {
+        i = i || [],
+            i.length = 0;
+        var s = [Images360.filters.inPanoDirection(e.position, dirs, TilePrioritizer.DIRECTION_SCORE_STRICTNESS), Images360.filters.not(e)],
+            l = [Images360.scoreFunctions.distanceSquared(e), Images360.scoreFunctions.direction(e.position, dirs)],
+            c = Common.sortByScore(t, s, l);  
+        if (c)
+            for (var h = 0; h < c.length && h < a; h++) {
+                var u = c[h].item;
+                i.push(u)
+            }
+        return i
+    }
+
+    queueTilesForPanos(e, t, i, n, r) {
+        for (var o = 0, a = 0; a < t.length; a++) {
+            var s = t[a],
+                l = this.queueTilesForPano(e, i, s, n);
+            if (o += l > 0 ? 1 : 0,
+                r && o >= r)
+                break
+        }
+        return o
+    }
+
+
+    /* queueTilesInDirectionForPanos(e, t, i, n, r, o, a, s) {//没用到
+        for (var l = 0, c = 0; c < i.length; c++) {
+            var h = i[c],
+                u = this.queueTilesInDirectionForPano(e, t, h, n, o, a);
+            if (l += u > 0 ? 1 : 0,
+                s && l >= s)
+                break
+        }
+        return l
+    }
+    */
+
+    canIncludeDescriptor(e) {
+        return e.status !== DownloadStatus.Downloading && e.status !== DownloadStatus.Downloaded
+    }
+
+    canIncludePano(e, t) {
+        return !e.isLoaded(t)
+    }
+
+    getFOVDotThreshold(e) {
+        return Math.cos(THREE.Math.degToRad(e / 2))
+    }
+
+    setZoomingActive(e) {
+        e !== this.zoomingActive && (this.zoomingActive = e)
+    }
+}
+
+TilePrioritizer.PriorityCriteria = function (e, t, i, n, o) {
+    this.pano = e,
+        this.cameraPosition = (new THREE.Vector3).copy(t),
+        
+        //this.cameraDir = (new THREE.Vector3).copy(i), 
+        this.cameraDirs = [], //
+        
+        this.panoSpaceDir = (new THREE.Vector3).copy(n),
+        this.upcomingPanos = o,
+        this.copy = function (e) {
+            this.pano = e.pano,
+            this.cameraPosition.copy(e.cameraPosition),
+            //this.cameraDir.copy(e.cameraDir),
+            this.cameraDirs = e.cameraDirs
+            
+            this.panoSpaceDir.copy(e.panoSpaceDir),
+            this.upcomingPanos = o
+        },
+        this.zoomingActive = !1
+}
+
+TilePrioritizer.DIRECTIONAL_FOV = 180;
+TilePrioritizer.DIRECTIONAL_FOV_NARROW = 120;
+TilePrioritizer.MAX_SCORED_PANOS_TOCONSIDER = 6;
+TilePrioritizer.MAX_SCORED_PANOS_TOADD = 2;
+TilePrioritizer.MAX_UPCOMING_PANOS_TOADD = 3;
+TilePrioritizer.DIRECTION_SCORE_STRICTNESS = .75;
+TilePrioritizer.appendQueue = function (e, t) {
+    if (e && t)
+        for (var i = 0; i < t.length; i++){
+            e.push(t[i])
+            //console.log(t[i])
+        }
+};
+
+TilePrioritizer.sortPanoTiles = function (descriptors, pano, dir) {
+    if(dir.datasetsLocal)  dir = dir.datasetsLocal.find(e=>e.datasetId == pano.pointcloud.dataset_id).direction;//add
+    u._panoSpaceDir.copy(dir) 
+    TileUtils.getRelativeDirection(pano.quaternion4dkk, u._panoSpaceDir) //应该是将dir根据quaternion转化下
+    u._fovThresholdNarrow = math.getFOVDotThreshold(TilePrioritizer.DIRECTIONAL_FOV_NARROW)
+    u._fovThreshold = math.getFOVDotThreshold(TilePrioritizer.DIRECTIONAL_FOV) 
+    descriptors.sort(u)
+};
+
+TilePrioritizer.insertSortedPanoTile = function (e, t, pano, dir) {
+    if(dir.datasetsLocal)  dir = dir.datasetsLocal.find(e=>e.datasetId == pano.pointcloud.dataset_id).direction;//add
+    u._panoSpaceDir.copy(dir),
+        TileUtils.getRelativeDirection(pano.quaternion4dkk, u._panoSpaceDir),
+        u._fovThresholdNarrow = math.getFOVDotThreshold(TilePrioritizer.DIRECTIONAL_FOV_NARROW),
+        u._fovThreshold = math.getFOVDotThreshold(TilePrioritizer.DIRECTIONAL_FOV);
+    for (var o = -1, a = 0; a < e.length; a++) {
+        var s = u(t, e[a]);
+        if (s <= 0) {
+            o = a;
+            break
+        }
+    }
+    if (o === -1)
+        e[e.length] = t;
+    else {
+        for (var h = e.length; h > o; h--)
+            e[h] = e[h - 1];
+        e[o] = t
+    }
+};
+
+
+
+TilePrioritizer.prototype.filterDepthTex = function (panos ) {//仅下载depthTex
+    if(!Potree.settings.useDepthTex || !this.priorityCriteria.pano)return
+    
+    let cameraDirLocals = this.priorityCriteria.cameraDirs.vectorForward
+    let t = [] 
+    //获得视野范围内的邻近点位序列t
+    this.populateScoredPanos(this.priorityCriteria.pano, panos, t, cameraDirLocals , TilePrioritizer.MAX_SCORED_PANOS_TOCONSIDER);
+    
+    t.forEach(p=>p.loadDepthImg()) 
+}
+
+
+
+TilePrioritizer.prototype.filterAndPrioritize = function () {//挑选出优先加载的 pano和tile (有点复杂,没看很懂)
+    var e = [],
+        t = [],
+        i = [];
+    return function (queue, panos, tileDownloader) {
+        //this.populateNeighborPanos(this.priorityCriteria.pano, panos, e);
+         
+        /* let cameraDirLocals = this.priorityCriteria.cameraDirs.map(e=>{ //add
+            var dataset = viewer.scene.pointclouds.find(u=>u.dataset_id == e.datasetId)
+            var matrix = new THREE.Matrix4().copy(dataset.rotateMatrix)
+            var direction = math.convertVector.YupToZup(e.direction)  
+        
+        
+            return {
+                datasetId:e.datasetId,
+                direction: direction.clone().applyMatrix4(matrix)
+            }
+        }) */
+        let cameraDirLocals = this.priorityCriteria.cameraDirs.vectorForward
+         
+        //获得视野范围内的邻近点位序列t
+        this.populateScoredPanos(this.priorityCriteria.pano, panos, t, cameraDirLocals , TilePrioritizer.MAX_SCORED_PANOS_TOCONSIDER);
+        
+        t.forEach(p=>p.loadDepthImg()) //add
+        
+        var s = this.baseSize //512
+            ,
+            l = this.standardSize //1024
+            ,
+            c = this.highSize //2048
+            ,
+            h = this.ultraHighSize; //4096
+            
+            
+        this.queueTilesForPano(queue, tileDownloader, this.priorityCriteria.pano, s);  //把当前pano的512下载了
+        
+        
+        if (this.priorityCriteria.upcomingPanos) {// 添加即将走到的点(之前用于导览路线)512 tiles
+            this.queueTilesForPanos(queue, this.priorityCriteria.upcomingPanos, tileDownloader, s, TilePrioritizer.MAX_UPCOMING_PANOS_TOADD);
+        }
+        i.length = 0;
+        
+        //把当前pano角度范围内的tile按照分辨率从低到高加入队列
+        
+        if (this.canDownloadSize(l)) {//1024如果在限制范围内的话
+            this.queueTilesInDirectionForPano(i, tileDownloader, this.priorityCriteria.pano, l, this.priorityCriteria.cameraPosition, this.priorityCriteria.cameraDirs, TilePrioritizer.DIRECTIONAL_FOV_NARROW);
+        } 
+        TilePrioritizer.sortPanoTiles(i, this.priorityCriteria.pano, this.priorityCriteria.cameraDirs); //排序
+        TilePrioritizer.appendQueue(queue, i);
+        
+        //添加邻近点t 512的tiles
+        this.queueTilesForPanos(queue, t, tileDownloader, s, TilePrioritizer.MAX_SCORED_PANOS_TOADD);
+        i.length = 0;
+        
+        
+        //NARROW    :
+        if (this.canDownloadSize(c)) {//2048
+            this.queueTilesInDirectionForPano(i, tileDownloader, this.priorityCriteria.pano, c, this.priorityCriteria.cameraPosition, this.priorityCriteria.cameraDirs, TilePrioritizer.DIRECTIONAL_FOV_NARROW);
+        }
+
+        if (this.canDownloadSize(h)) {//4096
+            this.queueTilesInDirectionForPano(i, tileDownloader, this.priorityCriteria.pano, h, this.priorityCriteria.cameraPosition, this.priorityCriteria.cameraDirs, TilePrioritizer.DIRECTIONAL_FOV_NARROW);
+        } 
+        TilePrioritizer.sortPanoTiles(i, this.priorityCriteria.pano, this.priorityCriteria.cameraDirs);//排序
+        TilePrioritizer.appendQueue(queue, i);
+        i.length = 0;
+
+        if (this.canDownloadSize(l)) {//1024
+            this.queueTilesInDirectionForPano(i, tileDownloader, this.priorityCriteria.pano, l, this.priorityCriteria.cameraPosition, this.priorityCriteria.cameraDirs, TilePrioritizer.DIRECTIONAL_FOV);
+        }
+
+        if (this.canDownloadSize(c)) {//2048
+            this.queueTilesInDirectionForPano(i, tileDownloader, this.priorityCriteria.pano, c, this.priorityCriteria.cameraPosition, this.priorityCriteria.cameraDirs, TilePrioritizer.DIRECTIONAL_FOV);
+        }
+
+        if (this.canDownloadSize(h)) {//4096
+            this.queueTilesInDirectionForPano(i, tileDownloader, this.priorityCriteria.pano, h, this.priorityCriteria.cameraPosition, this.priorityCriteria.cameraDirs, TilePrioritizer.DIRECTIONAL_FOV);
+        }
+
+        TilePrioritizer.sortPanoTiles(i, this.priorityCriteria.pano, this.priorityCriteria.cameraDirs);//排序
+        TilePrioritizer.appendQueue(queue, i);
+        
+        
+        
+        this.queueTilesForPanos(queue, e, tileDownloader, s); // 如果前面有populateNeighborPanos的话,这步就是加neibour
+    }
+}()
+TilePrioritizer.prototype.queueTilesInDirectionForPano = function () {
+    var e = {
+            filter: h.DirectionalFOV,
+            direction: new THREE.Vector3,
+            fov: 60
+        },
+        t = new THREE.Vector3;
+    return function (i, n, pano, o, a, dirs, c) {
+        
+        var dir = dirs.datasetsLocal.find(e=>e.datasetId == pano.pointcloud.dataset_id).direction;//add
+        //var dir = dirs
+        
+        t.copy(dir);
+        
+        TileUtils.getRelativeDirection(pano.quaternion4dkk, t);
+        e.direction.copy(t);
+        e.fov = c;
+        return this.filterAndQueueTileDownloadDescriptors(i, n, pano, o, e)
+    }
+}()
+
+TilePrioritizer.prototype.filterAndQueueTileDownloadDescriptors = function () {
+    var e = [];
+    return function (t, i, n, r, o) {
+        var a = i.getTileDownloadDescriptors(n, r);
+        e.length = 0,
+            this.filterTileDownloadDescriptors(n, a, e, o);
+        for (var s = 0, l = 0; l < e.length; l++) {
+            var c = e[l];
+            if (c) {
+                t.push(c);
+                s++;
+            }
+        }
+        return s
+    }
+}()
+
+TilePrioritizer.prototype.filterTileDownloadDescriptors = function () {
+    new THREE.Vector3;
+    return function (e, t, i, n) {
+        var r, o;
+        switch (n.filter) {
+            case h.DirectionalFOV:
+                for (r = 0; r < t.length; r++)
+                    o = t[r],
+                    TileUtils.isTileWithinFOV(o.panoSize, o.tileSize, o.face, o.tileX, o.tileY, n.direction, n.fov) && i.push(o);
+                break;
+            default:
+                for (r = 0; r < t.length; r++)
+                    o = t[r],
+                    i.push(o)
+        }
+        for (r = 0; r < i.length; r++)
+            o = i[r],
+            this.canIncludeDescriptor(o) || (i[r] = null)
+    }
+}()
+TilePrioritizer.prototype.queueTilesForPano = function () {
+    var e = {
+        filter: h.None
+    };
+    return function (t, i, n, r) {
+        return this.filterAndQueueTileDownloadDescriptors(t, i, n, r, e)
+    }
+}()
+
+
+
+/* TilePrioritizer.prototype.queueTilesForPanosInDirection = function () { //没用到
+    var e = new THREE.Vector3;
+    return function (t, i, n, r, o, a, s, l) {
+        for (var h = 0, u = 0; u < n.length; u++) {
+            var d = n[u];
+            e.copy(d.position),
+                e.sub(o),
+                e.normalize();
+            var p = Math.max(Math.min(a.dot(e), 1), -1),
+                f = c.getFOVDotThreshold(s);
+            if (p >= f) {
+                var g = this.queueTilesInDirectionForPano(t, i, d, r, o, a, s);
+                if (h += g > 0 ? 1 : 0,
+                    l && h >= l)
+                    break
+            }
+        }
+        return h
+    }
+}() */
+

+ 168 - 0
src/custom/modules/panos/tile/TileTree.js

@@ -0,0 +1,168 @@
+import * as THREE from "../../../../../libs/three.js/build/three.module.js";
+
+
+function Node(e, t) {
+    this.tree = e,  //所属树(TileTree)
+    this.parent = t,
+    this.children = [],
+    this.id = ++u;
+}
+
+function o(e, t, i, r, a, s, l, h) {
+    if (e) {
+        l = l || TileTree.TraversalType.PreOrder;
+        var u = r * c + i;
+        if (l === TileTree.TraversalType.PreOrder && (a && a(e, t, u, i, r),
+                s && s.push(e)),
+            e.children && 0 !== e.children.length) {
+            for (var d = r * c, p = i * c, f = 0; f < c; f++)
+                for (var g = 0; g < c; g++)
+                    o(e.children[g * c + f], t + 1, p + f, d + g, a, s, l, h);
+            l === TileTree.TraversalType.PostOrder && (a && a(e, t, u, i, r),
+                s && s.push(e))
+        }
+    }
+}
+
+function Plant(seed) {
+    seed.root = Branch(seed, null, 0)
+}
+
+function Branch(seed, parent, level) {
+    if (level > seed.levels)
+        return null;
+    var node = new Node(seed, parent);
+    seed.allNodes.push(node);
+    for (var o = 0; o < h; o++)
+        node.children[o] = Branch(seed, node, level + 1);
+    return node
+}
+
+function l(parent, t, level, n, r) {
+    if (!parent)
+        return null;
+    if (0 === level)
+        return parent;
+    if (!parent.children || 0 === parent.children.length)
+        return null;
+    var o = Math.pow(c, level),
+        a = o / c,
+        s = n % a,
+        h = r % a,
+        u = Math.floor(r / a),
+        d = Math.floor(n / a),
+        p = u * c + d,
+        f = parent.children[p];
+    return l(f, t + 1, level - 1, s, h)
+}
+
+
+/*  cube每个面都有一个分层树  用于代表瓦片图的细分?
+
+    树4096的分为三层,每层有4个子节点。(最后一层的四个子节点都是null)
+
+ */
+ 
+ 
+var c = 2,
+    h = c * c; //4个子节点
+var u = 0;
+
+
+
+
+export default class TileTree {
+    constructor(e, t) {
+        this.levels = t,
+        this.tileSize = e,
+        this.root = null,
+        this.allNodes = [],
+        Plant(this);
+    }
+
+    getSubNode(e, t, i) {
+        (!t || e < this.tileSize) && (t = 0),
+        (!i || e < this.tileSize) && (i = 0),
+        e < this.tileSize && (e = this.tileSize);
+        var level = TileTree.getLevelCountForSize(this.tileSize, e),
+            o = l(this.root, 0, level, t, i);
+        return o
+    }
+
+    breadthFirst(e) {//广度优先搜索
+        e = e || {};
+        var t = !!e.nullLevelEnd,
+            i = e.maxLevel,
+            n = e.minLevel,
+            r = e.callback,
+            o = e.saveVisited,
+            a = [],
+            s = {},
+            l = 0,
+            c = 0;
+        for (a.push(this.root),
+            a.push(s); a.length > 0 && !(i && l > i);) {
+            var h = a.shift();
+            if (h === s)
+                (!n || l >= n) && (r && t && r(null),
+                    o && t && o.push(null)),
+                a.length > 0 && a.push(s),
+                l++,
+                c = 0;
+            else {
+                if (h.children)
+                    for (var u = 0; u < h.children.length; u++) {
+                        var d = h.children[u];
+                        d && a.push(h.children[u])
+                    }
+                var p = this.getFaceIndexFromNode(h);
+                (!n || l >= n) && (r && r(h, l, p),
+                    o && o.push(h)),
+                c++
+            }
+        }
+    }
+
+    getFaceIndexFromNode(e) {
+        if (!e)
+            return -1;
+        for (var t = 1, i = e, n = 0, r = 0;;) {
+            var o = i.parent;
+            if (!o)
+                break;
+            for (var a = -1, s = 0; s < o.children.length; s++)
+                o.children[s] === i && (a = s);
+            var l = a % c,
+                h = Math.floor(a / c);
+            n = l * t + n,
+                r = h * t + r,
+                t *= c,
+                i = o
+        }
+        return r * t + n
+    }
+
+    depthFirst(e, t, i) {
+        o(this.root, 0, 0, 0, e, t, i, this.tileSize)
+    }
+}
+
+TileTree.TraversalType = Object.freeze({
+    PreOrder: 0,
+    PostOrder: 1
+});
+
+TileTree.getLevelCountForSize = function(tileSize, size) {//512->0 2024->1
+    var i = 0;
+    for (size < tileSize && (size = tileSize);;) {
+        if (size /= c,
+            size < tileSize)
+            break;
+        i++
+    }
+    return i
+};
+
+TileTree.getSizeForLevel = function(e, t) {
+    return Math.pow(c, t) * e
+};

+ 283 - 0
src/custom/modules/panos/tile/TileUtils.js

@@ -0,0 +1,283 @@
+ 
+import MathLight from '../../../utils/MathLight.js'
+import * as THREE from "../../../../../libs/three.js/build/three.module.js";
+
+let GLCubeFaces = Potree.defines.GLCubeFaces
+
+var TileUtils = {};
+TileUtils.TILE_SIZE = 512,
+TileUtils.FACES_PER_PANO = 6,
+TileUtils.LocationOnTile = {
+    Center: 0,
+    UpperLeft: 1,
+    UpperRight: 2,
+    LowerRight: 3,
+    LowerLeft: 4
+},
+
+
+/*
+ * 获取某tile在cube中的方向 direction (向量起点在cube中心,终点在tile图的指定位置)。spherical通过先求uv,再直接得到dir
+ * @param {*} size 面分辨率
+ * @param {*} cubeFace 所在面
+ * @param {*} Center 在tile上的目标位置,默认为中心,其他位置就是四个顶点
+ * @param {*} c 似乎是在tile的缩进百分比,根据所在面的不同,分别向不同方向缩进,但都是向tile的中心
+ * @param {*} dir 所求方向
+ */
+
+TileUtils.getTileVector = function() {//获取某tile在cube中的方向 direction (向量起点在cube中心,终点在tile图的中心)
+    return function(size, tileSize, cubeFace, tileX, tileY, Center, c, dir) {//c似乎是缩进百分比
+       
+        Center = Center || TileUtils.LocationOnTile.Center;
+        
+        //假设该cube边长为2:
+        var u = size / tileSize
+            , d = tileX / u;
+        tileY = -tileY + (u - 1);
+        var p = tileY / u
+            , f = tileSize / size
+            , g = 2 * f //一个tile的宽度   (乘以2是因为cube边长是2)
+            , m = g / 2
+            , v = 2 * d - 1 + m
+            , A = 2 * p - 1 + m;
+            
+        switch (Center) {//计算在tile中指定位置带来的偏移 
+            case TileUtils.LocationOnTile.UpperLeft:        //1
+                v -= m,
+                A += m,
+                v += c * g; //似乎是向内缩进
+                break;
+            case TileUtils.LocationOnTile.UpperRight:
+                v += m,
+                A += m,
+                A -= c * g;
+                break;
+            case TileUtils.LocationOnTile.LowerRight:
+                v += m,
+                A -= m,
+                v -= c * g;
+                break;
+            case TileUtils.LocationOnTile.LowerLeft:
+                v -= m,
+                A -= m,
+                A += c * g;
+                break;
+            case TileUtils.LocationOnTile.Center:   //0
+        }
+        switch (cubeFace) {
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_X:
+                MathLight.setVector(dir, -1, A, -v);
+                break;
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
+                MathLight.setVector(dir, 1, A, v);
+                break;
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_Y: //顶面
+                MathLight.setVector(dir, -v, 1, -A);
+                break;
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
+                MathLight.setVector(dir, -v, -1, A);
+                break;
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
+                MathLight.setVector(dir, -v, A, 1);
+                break;
+            case GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
+                MathLight.setVector(dir, v, A, -1)
+        }
+        MathLight.normalize(dir)
+    }
+}(),
+
+/*
+ * 获取该tile在第几个面(简易装载法) 
+ */
+TileUtils.getFaceForTile = function(size, index) {//获取该tile在第几个面
+    var tileSize = TileUtils.TILE_SIZE;
+    size < TileUtils.TILE_SIZE && (tileSize = size);
+    var n = Math.floor(size / tileSize)
+        , sum = n * n; //得每个面tile总数
+    return Math.floor(index / sum)
+}
+,
+
+
+TileUtils.getTileLocation = function(size, t, result) {
+    var tileSize = TileUtils.TILE_SIZE;
+    size < TileUtils.TILE_SIZE && (tileSize = size);
+    var r = TileUtils.getFaceForTile(size, t)
+        , a = Math.floor(size / tileSize)
+        , s = a * a
+        , l = t - r * s;
+    result.tileX = l % a;
+    result.tileY = Math.floor(l / a);
+    result.face = r;
+    result.faceTileIndex = l;
+    return result
+}
+,
+
+/*
+ * 求size分辨率需要多少张tile
+ */
+TileUtils.getTileCountForSize = function(e) {
+    if (e <= TileUtils.TILE_SIZE)
+        return TileUtils.FACES_PER_PANO;
+    var t = Math.floor(e / TileUtils.TILE_SIZE)
+        , i = t * t
+        , n = i * TileUtils.FACES_PER_PANO;
+    return n
+}
+,
+TileUtils.getRelativeDirection = function() {
+    var e = new MathLight.Matrix4
+        , t = new MathLight.Quaternion;
+    return function(i, n) {//i是pano.quaternion,  n是camera的direction  
+        t.copy(i),
+        t.inverse(),
+        e.makeRotationFromQuaternion(t),
+        e.applyToVector3(n),
+        MathLight.normalize(n)
+    }
+}(),
+
+/*
+ * 根据方向寻找合适的tile加载
+ */
+TileUtils.matchingTilesInDirection = function() {
+    var e = new MathLight.Vector3
+        , t = new MathLight.Vector3(0,0,-1)
+        , i = new MathLight.Quaternion
+        , n = function(e, t) {
+            e.push({
+                face: t.face,
+                faceTileIndex: t.faceTileIndex,
+                tileX: t.tileX,
+                tileY: t.tileY
+            })
+        }
+        , a = function() {
+            var e = {
+                face: -1,
+                faceTileIndex: -1,
+                tileX: -1,
+                tileY: -1
+            };
+            return function(size, i, r) {
+                for (var a = TileUtils.getTileCountForSize(size), s = 0, l = 0; l < a; l++)
+                    TileUtils.getTileLocation(size, l, e),
+                    i && !i(e) || (s++,
+                    r && n(r, e));
+                return s
+            }
+    }();
+    return function(pano, size, dir, hFov, vFov, result) {
+        var d = size < TileUtils.TILE_SIZE ? size : TileUtils.TILE_SIZE;
+        //TileUtils.getTileCountForSize(size);
+        if (!hFov && !vFov)
+            return a(size, null, result);
+        var p = !!vFov;
+        vFov = vFov || hFov,
+        vFov = Math.max(0, Math.min(vFov, 360)),
+        hFov = Math.max(0, Math.min(hFov, 360)),
+        MathLight.copyVector(dir, e),
+        TileUtils.getRelativeDirection(pano.quaternion4dkk, e)
+        if(p){//如果有vFov hFov
+            i.setFromUnitVectors(e, t);
+            var f = function(e) {
+                return TileUtils.isTileWithinFrustum(size, d, e.face, e.tileX, e.tileY, i, hFov, vFov)//在视野中的
+            };
+            return a(size, f, result)
+        }
+        var g = function(t) {//如果仅有hFov
+            return TileUtils.isTileWithinFOV(size, d, t.face, t.tileX, t.tileY, e, hFov)
+        };
+        return a(size, g, result)
+    }
+}(),
+
+/*
+ * 是否在屏幕范围内
+ */ 
+TileUtils.isTileWithinFrustum = function() {
+    var e = new MathLight.Vector3
+        , t = 1e-5;
+    return function(i, n, a, s, l, c, h, u) {
+        for (var d = Math.tan(.5 * u * MathLight.RADIANS_PER_DEGREE), p = -d, f = Math.tan(.5 * h * MathLight.RADIANS_PER_DEGREE), g = -f, m = TileUtils.mapFaceToCubemapFace(a), v = 0, A = 0, y = 0, C = 0, I = 0, E = 0, b = TileUtils.LocationOnTile.Center; b <= TileUtils.LocationOnTile.LowerLeft; b++){
+            TileUtils.getTileVector(i, n, m, s, l, b, 0, e),//get e   //  size, tileSize, cubeFace, tileX, tileY, Center, c, dir
+            MathLight.applyQuaternionToVector(c, e)      
+            if (e.z >= -t)//似乎是在相机背面
+                I++;
+            else { 
+                var w = -1 / e.z
+                    , _ = e.x * w
+                    , T = e.y * w;
+                T > d ? v++ : T < p && A++,  //这四种似乎代表在这个画框之外,如在左、在上、在下、在右
+                _ > f ? y++ : _ < g && C++,
+                E++
+            }
+        }
+        return A !== E && v !== E && y !== E && C !== E  //如果有一项和E相等代表要么是在相机背面要么是tile的四个顶点都画在画布的同一边,所以肯定不在画布上
+    }
+}(),
+
+
+/*
+ * 是否在FOV范围内
+ */
+TileUtils.isTileWithinFOV = function() {
+    var e = new MathLight.Vector3
+        , t = new MathLight.Vector3(0,1,0)
+        , i = new MathLight.Vector3(1,0,0);
+    return function(panoSize, tileSize, face, tileX, tileY, direction, fov) {//direction是作用了pano.quaternion的camera.direction
+        var d = TileUtils.mapFaceToCubemapFace(face);
+        MathLight.cross(direction, t, i) //get i  好像没用到
+        TileUtils.getTileVector(panoSize, tileSize, d, tileX, tileY, TileUtils.LocationOnTile.Center, 0, e) 
+        if (TileUtils.isWithinFOV(e, direction, fov, null))//先判断tile中心在不在FOV内
+            return !0;
+        for (var p = fov / 360, f = Math.floor(1 / p), g = 0, m = 0; m < f; m++) {
+            for (var v = TileUtils.LocationOnTile.UpperLeft; v <= TileUtils.LocationOnTile.LowerLeft; v++)
+                if (TileUtils.getTileVector(panoSize, tileSize, d, tileX, tileY, v, g, e),
+                TileUtils.isWithinFOV(e, direction, fov, null))
+                    return !0;
+            g += p   //可能是考虑到有可能tile比fov覆盖了fov(虽然一般不可能,除非fov特别小),所以将tile分成若干段,取tile中的点再检测下
+        }
+        return !1
+    }
+}(),
+
+
+TileUtils.isWithinFOV = function() {
+    var e = new MathLight.Vector3
+        , t = new MathLight.Vector3;
+    return function(dir, cameraDir, fov, a) {
+        if (MathLight.copyVector(dir, t),
+        a) {
+            MathLight.copyVector(a, e),
+            MathLight.normalize(e);
+            var s = MathLight.dot(e, dir);
+            e.x *= s,
+            e.y *= s,
+            e.z *= s,
+            MathLight.subVector(t, e)
+        }
+        var l = fov / 2 * MathLight.RADIANS_PER_DEGREE
+            , c = Math.cos(l)
+            , h = MathLight.dot(t, cameraDir);
+        return h >= c
+    }
+}(),
+
+TileUtils.mapFaceToCubemapFace = function() {
+    var e = {
+        0: GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
+        1: GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_Z,
+        2: GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_X,
+        3: GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
+        4: GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
+        5: GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y
+    };
+    return function(t) {
+        return e[t]
+    }
+}();
+
+export default TileUtils

+ 652 - 0
src/custom/modules/route/RouteGuider.js

@@ -0,0 +1,652 @@
+
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import {Utils} from "../../../utils.js"; 
+import Sprite from '../../objects/Sprite.js'
+import Common from "../../utils/Common.js";
+import browser from '../../utils/browser.js' 
+
+const texLoader = new THREE.TextureLoader()
+const arrowSpacing = 1 //间隔
+const arrowSize = arrowSpacing * 0.5
+const planeGeo = new THREE.PlaneBufferGeometry(1,1);
+
+const sphereSizeInfo = {
+      nearBound : 2, scale:arrowSize, restricMeshScale : true,
+}
+//const arrowsShowingCount = 25; //场景里最多展示多少个箭头
+const arrowShowMinDis = 10
+export class RouteGuider extends THREE.EventDispatcher{
+    constructor () {
+		super();
+        
+        this.route = [];
+        this.curve = []
+        this.scenePoints = []
+        this.sceneMeshGroup = new THREE.Object3D;
+        this.mapMeshGroup = new THREE.Object3D;
+        this.generateDeferred;
+        viewer.addEventListener('loadPointCloudDone',this.init.bind(this))
+        
+        this.lastResult;//保存上一个的结果,以便于反向
+        this.datasetIds = [];//起始和终点的datasetId
+    }
+    init(){
+        if(this.inited) return;
+        
+        let zoom;
+        viewer.mapViewer.addEventListener('camera_changed', e => {
+            if(!this.routeStart || !this.routeEnd) return   
+            var camera = e.viewport.camera
+           
+            Common.intervalTool.isWaiting('routeCameraInterval', ()=>{ //延时update,防止卡顿
+                if(camera.zoom != zoom){ 
+                    //console.log('updateMapArrows')
+                    this.updateMapArrows(true)
+                    zoom = camera.zoom         
+                    return true 
+                } 
+            }, browser.isMobile()?500:200)
+        })
+        
+   
+        
+       
+        //let lastPos = new THREE.Vector3
+        viewer.addEventListener('camera_changed', e => {
+            if(!this.routeStart || !this.routeEnd || !e.changeInfo.positionChanged) return
+            Common.intervalTool.isWaiting('routeCameraInterval', ()=>{ //延时update,防止卡顿
+                //let currPos = viewer.scene.getActiveCamera().position
+             
+                //if(!currPos.equals(lastPos)){
+                   // lastPos.copy(currPos)
+                    this.updateArrowDisplay() 
+                     
+                    return true 
+                //}
+            }, 1000)
+            
+            
+                        
+        })
+        
+        
+        
+        var polesMats = {
+            shadowMat: new THREE.MeshBasicMaterial({ 
+                transparent:true, depthTest:false,
+                map: texLoader.load(Potree.resourcePath+'/textures/pano_instruction_bottomMarker.png' )  
+            }),
+            sphereMat : new THREE.MeshBasicMaterial({
+                transparent:true, depthTest:false,
+                map: texLoader.load(Potree.resourcePath+'/textures/whiteCircle.png' )  
+            }), 
+            hatMats:{
+                start:  new THREE.MeshBasicMaterial({
+                    transparent:true, depthTest:false,
+                    map: texLoader.load(Potree.resourcePath+'/textures/pano_instruction_start_route.png' )  
+                }),
+                end:  new THREE.MeshBasicMaterial({
+                    transparent:true, depthTest:false,
+                    map: texLoader.load(Potree.resourcePath+'/textures/pano_instruction_target_reached.png' )  
+                }) 
+            }
+        }
+        polesMats.shadowMat.map.anisotropy = 4 
+        
+        this.poleStart = this.createPole(polesMats, 'start') 
+        this.poleEnd = this.createPole(polesMats, 'end') 
+        
+        this.sceneMeshGroup.add(this.poleStart)
+        this.sceneMeshGroup.add(this.poleEnd)
+        
+        
+        let map = texLoader.load(Potree.resourcePath+'/textures/routePoint_panorama.png' )  
+        map.anisotropy = 4 // 各向异性过滤 .防止倾斜模糊 
+        this.arrow = new THREE.Mesh(planeGeo, new THREE.MeshBasicMaterial({
+            transparent:true,
+            depthTest:false, 
+            map
+        }))
+        this.arrow.scale.set(arrowSize,arrowSize,arrowSize)
+        viewer.setObjectLayers(this.arrow, 'sceneObjects' )
+         
+        
+        /* this.testArrow = this.arrow.clone();
+        this.testArrow.material = this.arrow.material.clone()
+        this.testArrow.material.color = 'red' */
+        
+        this.arrows = new THREE.Object3D;
+        this.sceneMeshGroup.add(this.arrows)
+        
+        viewer.setObjectLayers(this.sceneMeshGroup, 'sceneObjects' )
+        //this.sceneMeshGroup.traverse(e=>e.renderOrder = 90)
+        
+        
+        viewer.scene.scene.add(this.sceneMeshGroup);
+        this.sceneMeshGroup.visible = /* this.poleStart.visibile = this.poleEnd.visibile = */ false
+       
+        //-------------map---------------------
+        
+        /* this.mapMarkStart = new THREE.Mesh( planeGeo, new THREE.MeshBasicMaterial({
+            transparent:true, depthTest:false,
+            map: texLoader.load(Potree.resourcePath+'/textures/map_instruction_start_route.png' )  
+        }))
+        this.mapMarkEnd = new THREE.Mesh( planeGeo, new THREE.MeshBasicMaterial({
+            transparent:true, depthTest:false,
+            map: texLoader.load(Potree.resourcePath+'/textures/map_instruction_target_reached.png' )  
+        }))
+        this.mapMarkStart.renderOrder = this.mapMarkEnd.renderOrder = 2//在箭头之上 */
+         
+        let map2 = texLoader.load(Potree.resourcePath+'/textures/routePoint_map_fsna.png' ) 
+        this.mapArrowMats = {
+            default: new THREE.MeshBasicMaterial({
+                transparent:true, depthTest:false,
+                map:map2, 
+            }),
+            
+            fade: new THREE.MeshBasicMaterial({
+                transparent:true, depthTest:false,
+                map:map2, 
+                opacity:0.4
+            }), 
+        }
+        
+        
+        
+        this.mapArrow = new THREE.Mesh( planeGeo, this.mapArrowMats.default) 
+        this.mapArrow.scale.set(arrowSize,arrowSize,arrowSize)
+        this.mapArrows = new THREE.Object3D;
+        this.mapArrows.name = 'mapArrows'
+         
+        
+        
+        this.mapMeshGroup.add(this.mapArrows)
+        this.mapMeshGroup.name = 'mapRouteLayer'
+        this.mapMeshGroup.visible = false
+        
+        viewer.mapViewer.dispatchEvent({type:'add', object:this.mapMeshGroup, name:'route'})
+        this.mapArrow.layers.mask = this.mapArrows.layers.mask // 修改成和map中的layer一样的
+        
+        
+        
+        viewer.modules.SiteModel.bus.addEventListener('FloorChange',()=>{
+            if(this.routeStart && this.routeEnd){
+                this.updateOpacityAtMap()
+            }  
+        }) 
+        this.inited = true
+    }
+    
+    updateOpacityAtMap(){//只有当前楼层的透明度为1
+        var currentFloor = viewer.modules.SiteModel.currentFloor
+        //console.log('updateOpacityAtMap', currentFloor && currentFloor.name)
+        const lift = 0.3 // 因为发送请求时用的是floorPosition的高度,而它可能会到画好的floor之下,所以有误差
+        this.mapArrows.children.forEach((arrow,index)=>{
+            let pos = this.mapPoints[index].clone()
+                pos.z += lift  
+            let inSide = currentFloor && currentFloor.ifContainsPoint(pos)
+            arrow.material = inSide ? this.mapArrowMats.default : this.mapArrowMats.fade
+            //console.log('arrow',index, arrow.material.opacity)
+        }) 
+        
+        viewer.mapViewer.dispatchEvent('content_changed')
+    }//但是如果楼层刚好只框柱相机位置而没框住地面位置就不好了……
+    
+    
+    
+    
+    
+    createPole(polesMats, name){
+        const height = 1.5, sphereCount = 6, shadowSize = sphereSizeInfo.scale,  sphereSize = 0.04
+        
+        var group = new THREE.Object3D;
+            group.name = 'pole_'+name
+        var shadow = new THREE.Mesh(planeGeo,polesMats.shadowMat)
+        shadow.scale.set(shadowSize,shadowSize,shadowSize)
+        var sliceDis = height / (sphereCount+1);
+        group.add(shadow) 
+         
+        for(let i=0;i<sphereCount;i++){
+            var sphere = new Sprite({mat: polesMats.sphereMat}) 
+            sphere.position.set(0,0,sliceDis*(i+1))
+            sphere.scale.set(sphereSize,sphereSize,sphereSize);
+            sphere.visible = false
+            group.add(sphere)
+        }
+        
+        var hatSphere = new Sprite({mat: polesMats.hatMats[name], sizeInfo:sphereSizeInfo}) 
+        sphere.visible = false
+        hatSphere.position.set(0,0,height)
+        hatSphere.scale.copy(shadow.scale)
+        group.add(hatSphere)
+        return group
+    }
+    
+    
+    addTestArrow(){
+        
+    }
+    
+    addArrow(position){ 
+        var arrow = this.arrow.clone()
+        arrow.position.copy(position); 
+        this.arrows.add(arrow); 
+    }
+    addMapArrow(position){ 
+        var mapArrow = this.mapArrow.clone()
+        mapArrow.position.copy(position).setZ(0) 
+        this.mapArrows.add(mapArrow);
+    }
+    
+    
+    setArrowDir(arrows,index){
+        let arrow = arrows[index]
+        var nextOne = arrows[index+1];
+        var nextPos = nextOne ? nextOne.position : this.endPolePos //routeEnd
+        var direction = new THREE.Vector3().subVectors(arrow.position, nextPos).setZ(0);
+        //direction.normalize();
+        //console.log(direction.toArray())
+        var angle = Math.atan2(direction.y, direction.x ) + Math.PI/2 //Math.PI/2是因为贴图本身箭头方向不朝x
+        arrow.rotation.z = angle
+        //console.log(angle)
+    }
+    
+    
+     
+     
+    
+    setRouteStart(pos, dealZ , datasetId  ){
+        if(this.routeStart && pos && this.routeStart.equals(pos)) return //可能重复设置
+        this.routeStart = pos && new THREE.Vector3().copy(pos)  
+        if(dealZ && this.routeStart){
+            this.routeStart.setZ(this.getZAtMap()) 
+            this.bus && this.bus.emit('reposStartMarker', this.routeStart)
+        }
+        console.log('setRouteStart',this.routeStart&&this.routeStart.toArray()) 
+        
+        this.datasetIds[0] = datasetId
+        
+        //this.setStartPole(pos)
+        
+        this.generateRoute()
+        
+        
+    }
+    
+    setStartPole(pos){
+        this.startPolePos = pos
+        this.bus && this.bus.emit('reposStartMarker', pos)
+    }
+     
+    
+    setRouteEnd(pos, dealZ , datasetId  ){ 
+        if(this.routeEnd && pos && this.routeEnd.equals(pos)) return 
+        this.routeEnd = pos && new THREE.Vector3().copy(pos)
+        if(dealZ && this.routeEnd){
+            this.routeEnd.setZ(this.getZAtMap())
+            this.bus && this.bus.emit('reposEndMarker', this.routeEnd)
+        }
+        console.log('setRouteEnd',this.routeEnd&&this.routeEnd.toArray())        
+        this.datasetIds[1] = datasetId
+        //this.setEndPole(pos)
+        this.generateRoute()
+        
+    }
+    
+    
+    getZAtMap(){  
+        
+        //找到position.z与当前高度最接近的漫游点 
+        let result = Common.sortByScore(viewer.images360.panos,[],[e=> -(Math.abs(e.position.z - viewer.images360.position.z)) ])
+        let pano = result && result[0] && result[0].item
+        
+        return pano ? pano.floorPosition.z : viewer.bound.boundingBox.min.z + 1 
+        //若在平面图上画实在得不到当前楼层的,大概率是楼层画得不好,那就只能去获取当前楼层的了
+        
+        //navvis的高度取的是主视图所在楼层的中心高度(可能再高些)
+        
+    }
+    
+    setEndPole(pos){
+        this.endPolePos = pos
+        this.bus && this.bus.emit('reposEndMarker', pos)
+    }
+    
+    getSourceProjectionIndex(route) {//真正的起始
+        var e = route.findIndex(function(t) {
+            return t.instruction && t.instruction.type === 'source_projection_to_navgraph'
+        });
+        return e < 0 ? 0 : e
+    }
+    getDestinationProjectionIndex(route) {//真正的终点
+        var e = route.findIndex(function(t) {
+            return t.instruction && t.instruction.type === "destination_projection_to_navgraph"
+        });
+        return e < 0 ? route.length - 1 : e
+    }
+    
+    generateRoute(){
+        if(!this.routeStart || !this.routeEnd){ 
+            
+            return
+        }
+        
+        
+        //array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
+        
+        
+        let create = ()=>{ 
+            this.routeLength = this.route.reduce((total, currentValue, currentIndex, arr)=>{
+                if(currentIndex == 0)return 0
+                return total + currentValue.distanceTo(arr[currentIndex-1]);
+            },0)
+            let count = Math.max(2,Math.round(this.routeLength / arrowSpacing))//点数
+            
+            const curve = new THREE.CatmullRomCurve3( this.route ); 
+            curve.curveType = 'chordal'//'centripetal'  'catmullrom'这个可能会超出路径外
+            this.curve = curve
+            
+            const scenePoints = curve.getSpacedPoints( count );//更平均
+            //const scenePoints = curve.getPoints( count );
+            scenePoints.splice(0,1);//去掉首尾
+            scenePoints.pop()
+            this.scenePoints = scenePoints
+            
+            this.updateMapArrows() 
+            this.displayRoute()
+            
+            {//map focus on this area
+                
+                const minBound = new THREE.Vector2(1,1)//针对垂直线,在地图上只有一个点
+                let bound = new THREE.Box2;
+                this.route.forEach(e=>{
+                    bound.expandByPoint(e)
+                })
+                let size = bound.getSize(new THREE.Vector2)
+                let markerSize = new THREE.Vector2(115,40) //起始和终点的标识呈长方形
+                let areaSize = viewer.mapViewer.viewports[0].resolution2
+                let areaArea = areaSize.x * areaSize.y
+                if(areaArea> 800 * 400){//是放大的 
+                    markerSize.multiplyScalar(areaArea / (800 * 400) /* / (size.x * size.y) */) 
+                }
+                let margin = size.clone().divide(viewer.mapViewer.viewports[0].resolution2).multiply(markerSize) ///边距 重点是起始和终点的标识占据较大
+                size.add(margin)
+                let center = bound.getCenter(new THREE.Vector2)
+                
+                size.x = Math.max(size.x, minBound.x )
+                size.y = Math.max(size.y, minBound.y )
+                let duration = 1000
+                viewer.mapViewer.moveTo(center, size, duration)
+            }
+            
+            this.bus.emit('gotResult', {dis:this.routeLength})
+            /* this.generateDeferred && this.generateDeferred.resolve({dis:this.routeLength})
+            this.generateDeferred = null */
+        }
+        
+        
+        if(Potree.fileServer){
+            let dealData = (data)=>{
+                
+                if(!data.data){
+                    console.log('没有数据')
+                    let result
+                    if(data && data.code == 4002){
+                        result = data;//正被修改数据集
+                    }else if(this.routeStart.distanceTo(this.routeEnd) < 1){
+                        result = { code: 500, msg: '距离太短,无法规划路线' }
+                    }else{
+                        result = { code: 500, msg: '超出数据集范围,无法规划路线' }
+                    }
+                    this.clearRoute() 
+
+
+                    this.setStartPole(this.routeStart) 
+                    this.setEndPole(this.routeEnd) 
+                    
+                    this.displayRoute() //还是要显示一下起始
+                    this.bus && this.bus.emit('gotResult', result )
+                    
+                    return //this.generateDeferred && this.generateDeferred.resolve()
+                }
+                
+                
+                data = data.data 
+                  
+                this.clearRoute()
+                let length = data.length
+                
+                if(length < 2){//可能距离太短 
+                    console.log('路径点数为'+length+',直接取起点和终点连线') 
+                    this.route = [this.routeStart, this.routeEnd];
+                }else{ 
+                    let startIndex = this.getSourceProjectionIndex(data)
+                    let endIndex = this.getDestinationProjectionIndex(data)
+                    
+                    
+                    let effectiveItems = data.slice(startIndex, endIndex + 1 );//只要点云范围内的点
+                    effectiveItems.forEach((item,i)=>{ 
+                        let pos = viewer.transform.lonlatToLocal.forward(item.location.slice(0))
+                        pos = new THREE.Vector3().fromArray(pos)//.setZ(item.z)
+                        this.route.push(pos)
+                    })
+                    
+                    console.log(this.route)
+                    
+                    
+                }
+                this.setStartPole(this.route[0]) 
+                this.setEndPole(this.route[this.route.length-1]) 
+                
+                create()
+                /*
+                    distance: 0.17581000000000116
+                    distance_to_previous: 0.17581000000000116
+                    id: 567
+                    instruction: {type: 'source_projection_to_navgraph'}
+                    latitude: 22.366605927999238
+                    location: (3) [113.5957510575092, 22.366605927999238, -1.12419]
+                    longitude: 113.5957510575092
+                    z: -1.12419
+                */
+            }
+            
+            
+            
+            
+            if(this.lastResult && (this.lastResult.data || this.lastResult.data.code != 4002)){//正被修改数据集的话要重新计算
+                let data = Common.CloneObject(this.lastResult.data) ,  use;  //直接用上次的结果
+                if(this.lastResult.routeStart.equals(this.routeStart) &&  this.lastResult.routeEnd.equals(this.routeEnd)){//和上次请求相同
+                    use = true 
+                }else if(this.lastResult.routeStart.equals(this.routeEnd) &&  this.lastResult.routeEnd.equals(this.routeStart)){//..反向
+                    use = true
+                    if(data.data){
+                        data.data = this.lastResult.data.data.slice(0).reverse()
+                    }    
+                }
+                if(use){
+                    console.log('直接用上次的结果')
+                    return setTimeout(()=>{dealData(data)}, 1)//延迟是为了等待获得 RouteGuider.generateDeferred
+                       
+                }
+                
+            }
+            
+            
+            
+            
+            let start = this.routeStart.clone();
+            let end = this.routeEnd.clone();
+            let startLonlat = viewer.transform.lonlatToLocal.inverse(start)
+            let endLonlat = viewer.transform.lonlatToLocal.inverse(end)
+            
+            var query = {
+                source_longitude: startLonlat.x,
+                source_latitude: startLonlat.y,
+                source_z: start.z,
+                destination_longitude: endLonlat.x,
+                destination_latitude: endLonlat.y,
+                destination_z: end.z
+            };
+            
+            
+            //let url = `/laser/route/${Potree.settings.number}/getRoute/${this.datasetIds[0]}/${this.datasetIds[1]}?`
+            let url = `/laser/route/${Potree.settings.number}/getRoute/${Potree.settings.originDatasetId}?`
+            for(let i in query){
+                url+= (i + '='+ query[i] +'&')
+            }
+            
+            Potree.fileServer.get(url).then((data)=>{
+                console.log(data.data)
+                if(!this.routeStart || !this.routeEnd)return 
+                
+                this.lastResult = {//保存数据
+                    routeStart : this.routeStart.clone(),
+                    routeEnd: this.routeEnd.clone(),
+                    data,
+                     
+                }
+                
+                dealData(data)
+                
+            })
+            
+            
+        }else{
+            //创个直线
+            /* const sliceDis = 1
+            let dis = this.routeStart.distanceTo(this.routeEnd);
+            let count = Math.max(2,Math.round(dis / sliceDis))//点数
+            let realSlideDis = dis / (count-1);
+            let dir = new THREE.Vector3().subVectors(this.routeEnd, this.routeStart).normalize().multiplyScalar(realSlideDis);
+            this.route = [this.routeStart];
+            for(let i=0;i<count-1;i++){
+                let lastOne = this.route[i];
+                this.route.push(new THREE.Vector3().addVectors(lastOne,dir))
+            }
+            this.route.splice(0,1) //route不用包含收尾 */
+            this.clearRoute()
+            this.route = [this.routeStart, this.routeEnd]
+            create()
+            
+        }
+          
+    }
+    
+    updateMapArrows(ifReset){
+        if(this.route.length == 0)return  
+        var zoom = viewer.mapViewer.camera.zoom
+        let count = Math.max(2,Math.round(this.routeLength * zoom  / arrowSpacing / 25))//点数
+        
+        if(count == this.mapPoints.length+1)return//没变
+
+        const mapPoints = this.curve.getSpacedPoints( count ); 
+        mapPoints.splice(0,1);//去掉首尾
+        mapPoints.pop() 
+        this.mapPoints = mapPoints
+        
+        
+        var scale = 25/zoom
+        this.mapArrow.scale.set(scale*0.6,scale*0.6,scale*0.6) 
+        /* this.mapMarkStart.scale.set(scale,scale,scale) 
+        this.mapMarkEnd.scale.set(scale,scale,scale)  */
+        
+        
+        if(ifReset){//因为缩放而重新排布箭头
+            this.clearRoute({resetMap:true})
+            this.displayRoute({resetMap:true}) 
+        }
+        this.updateOpacityAtMap()
+    }
+    
+    
+    updateArrowDisplay(){//根据当前位置更新显示一定范围内的箭头'
+    
+        if(this.scenePoints.length == 0)return
+        
+        /* var a = Common.sortByScore(this.scenePoints , null, [(point)=>{   //是否还要再requires里限制最远距离?
+            var playerPos = viewer.scene.getActiveCamera().position.clone().setZ(0)
+            
+            var pos = point.clone().setZ(0) 
+            
+            return -pos.distanceTo(playerPos);
+            
+        }]);
+        //获得展示的起始点 
+        let start = a[0].item
+        let startIndex = this.scenePoints.indexOf(start)
+        this.arrows.children.forEach((e,i)=>{
+            if(i<startIndex || i>startIndex+arrowsShowingCount)e.visible = false
+            else e.visible = true
+        }) */
+        
+        let cameraPos = viewer.scene.getActiveCamera().position
+        this.arrows.children.forEach((e,i)=>{
+            if(e.position.distanceTo(cameraPos) < arrowShowMinDis) e.visible = true
+            else e.visible = false
+        })
+        
+        
+    }
+    
+    
+    displayRoute(o={}){
+        if(!o.resetMap){ 
+            
+            this.poleStart.position.copy(this.startPolePos || this.routeStart)
+            this.poleEnd.position.copy(this.endPolePos || this.routeEnd)
+            /* this.mapMarkStart.position.copy(this.routeStart).setZ(0)
+            this.mapMarkEnd.position.copy(this.routeEnd).setZ(0) */
+            this.scenePoints.forEach(e=>this.addArrow(e))
+            this.arrows.children.forEach((e,i)=>this.setArrowDir(this.arrows.children,i));
+        }
+        this.sceneMeshGroup.traverse(e=>e.visible = true)  
+        this.mapMeshGroup.visible = true
+        this.mapPoints.forEach(e=>this.addMapArrow(e))
+        this.mapArrows.children.forEach((e,i)=>this.setArrowDir(this.mapArrows.children,i));
+        viewer.mapViewer.dispatchEvent({'type':'content_changed'})
+        this.updateArrowDisplay()
+    }
+    
+    clearRoute(o={}){
+        if(!o.resetMap){
+            this.routeLength = 0
+            this.route = []
+            this.scenePoints = []
+            this.mapPoints = []
+            let arrows = this.arrows.children.slice(0)
+            arrows.forEach(e=>{
+                this.arrows.remove(e)
+            })
+        } 
+        
+        let mapArrows = this.mapArrows.children.slice(0) 
+        mapArrows.forEach(e=>{
+            this.mapArrows.remove(e)
+        })
+        
+        this.sceneMeshGroup.traverse(e=>e.visible = false)  //包括sprite也要设置,防止update
+        this.mapMeshGroup.visible = false
+        viewer.mapViewer.dispatchEvent({'type':'content_changed'})
+    }
+    
+    clear(){//退出
+        console.log('导航clear') 
+        this.routeStart = null
+        this.routeEnd = null
+        this.clearRoute()
+        
+    }
+}
+
+//大概每十米要花一秒
+
+
+
+/* 
+
+    存在的问题:
+    路径不准确。起始点和终点偏移。
+
+
+ */

+ 879 - 0
src/custom/modules/siteModel/BuildingBox.js

@@ -0,0 +1,879 @@
+
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import {ctrlPolygon} from '../../objects/tool/ctrlPolygon.js'
+import {LineDraw, MeshDraw } from "../../utils/DrawUtil.js";  
+import math  from "../../utils/math.js";
+import Sprite from '../../objects/Sprite.js'
+/* import {config} from '../settings' */
+import searchRings from "../../utils/searchRings.js";
+import DepthBasicMaterial from "../../materials/DepthBasicMaterial.js";
+
+let texLoader = new THREE.TextureLoader() 
+
+ 
+let markerMats
+let markerSizeInfo = {width2d:35}
+let color = new THREE.Color('#FFF')
+let faceMats
+let getFaceMat = (name)=>{
+    if(!faceMats){ //navvis材质可以搜gridTexture
+        let gridTex = texLoader.load( Potree.resourcePath+'/textures/gridmap.png' ) 
+            gridTex.wrapS = gridTex.wrapT = THREE.RepeatWrapping   
+            //gridTex.repeat.set(0.5,0.5)//放大一些
+        faceMats = { 
+            dataset: new THREE.MeshStandardMaterial({
+                color:812922,   
+                side:THREE.DoubleSide, 
+                opacity:0.2,
+                transparent:true,  
+                depthTest:false,
+                wireframe:true
+            }),
+            building: new THREE.MeshStandardMaterial({
+                color:812922,  metalness: 0.2, roughness:0.8,
+                side:THREE.DoubleSide, 
+                opacity:0.1,
+                transparent:true,  
+                depthTest:true
+            }),
+            buildingSelect: new THREE.MeshStandardMaterial({
+                color:36582,  metalness: 0, roughness:1,
+                side:THREE.DoubleSide,
+                opacity:0.1,
+                transparent:true,
+                depthTest:true
+            }),
+            floor:  new THREE.MeshStandardMaterial({
+                color:11708469,  metalness: 0.1, roughness:1,
+                side:THREE.DoubleSide,//BackSide,
+                opacity:0.05,
+                transparent:true,
+                depthTest:true, 
+            }),
+             
+            /* floorSelect: new THREE.MeshStandardMaterial({
+                color:16707151,  metalness: 0, roughness:1,
+                side:THREE.DoubleSide,
+                opacity:1,
+                transparent:true,
+                depthTest:true,
+                polygonOffset : true,//是否开启多边形偏移 
+				polygonOffsetFactor : -0.75,//多边形偏移因子
+				polygonOffsetUnits : -4.0,//多边形偏移单位 
+                map: gridTex,
+            }),  */ 
+            floorSelect: new DepthBasicMaterial({
+                map: gridTex,
+                color:16707151,  
+                side:THREE.DoubleSide,//BackSide, 
+                opacity:1,
+                transparent:true, 
+                useDepth : true,
+                /* polygonOffset : true,//是否开启多边形偏移 
+				polygonOffsetFactor : -0.75,//多边形偏移因子
+				polygonOffsetUnits : -4.0,//多边形偏移单位  */
+                
+                clipDistance : 1, occlusionDistance:1, /* occlusionDistance:变为backColor距离, clipDistance:opacity到达0或者1-maxClipFactor时的距离 */  
+                maxClipFactor:0.4, backColor:'#efe' //backColor:"#669988"  ,
+                  
+            }),
+            
+            room: new THREE.MeshStandardMaterial({
+                color:"#ff44ee",  metalness: 0, roughness:1,
+                side:THREE.DoubleSide,//BackSide,
+                opacity:0.08,
+                transparent:true, 
+                depthTest:false, 
+            }), 
+            /* roomSelect: new THREE.MeshStandardMaterial({
+                color:"#ff44ee",  metalness: 0.3, roughness:1,
+                side:THREE.DoubleSide,//BackSide,
+                opacity:1,
+                transparent:true,
+                depthTest:true,
+                polygonOffset : true,//是否开启多边形偏移.(开启是因为和floor重叠了会闪烁)
+				polygonOffsetFactor : -0.75,//多边形偏移因子
+				polygonOffsetUnits : -4.0,//多边形偏移单位  
+                map: gridTex,
+            }), */
+            roomSelect: new DepthBasicMaterial({
+                map: gridTex,
+                color:"#ff44ee", 
+                side:THREE.DoubleSide,//BackSide, 
+                opacity:1,
+                transparent:true, 
+                useDepth : true,
+                /* polygonOffset : true,//是否开启多边形偏移 
+				polygonOffsetFactor : -0.75,//多边形偏移因子
+				polygonOffsetUnits : -4.0,//多边形偏移单位  */
+                
+                clipDistance : 1, occlusionDistance:0.5, /* occlusionDistance:变为backColor距离, clipDistance:opacity到达0或者1-maxClipFactor时的距离 */  
+                maxClipFactor:0.6, backColor:'#ff88dd'//"#cc99c2"  ,
+                 
+            })
+        }
+    }
+    return faceMats[name]
+}
+
+
+
+ 
+
+export class BuildingBox extends ctrlPolygon{//建筑实体,包括building, floor, room
+    constructor(prop) {
+        prop.dimension = '3d'
+        //prop.name = Potree.config.siteModel.names[prop.buildType] + 
+         
+        super('siteModel_'+prop.buildType,  prop);
+        
+         
+        
+        this.midMarkers = []
+        this.buildChildren = []//子实体
+        this.holes = [] //在这创建的hole
+        this.parentHoles = [];//floor从building那得到的当层holes
+        this.mats = {} //材质
+        
+        this.panos = this.panos || [];
+        this.center //中心点
+        
+        if(this.buildType=='floor'){
+            
+            this.points = prop.points = this.buildParent.points;//完全等于建筑的点
+            this.buildParent.holes.forEach(hole=>{//从building获取holes
+                let floorHole = new BuildingBox({
+                    buildType : 'hole', 
+                    buildParent:this,
+                    originHole : hole, //整栋大楼在当层的hole
+                    ifDraw: this.ifDraw  || Potree.settings.drawEntityData
+                });
+                this.parentHoles.push(floorHole)
+                this.add(floorHole) 
+                
+                floorHole.points = hole.points//完全等于建筑的点
+            })
+        } 
+        if(this.buildType == 'room' || this.buildType == 'hole'){
+            this.restrictArea = this.buildParent  //不能超出的区域
+        }
+        
+        if(this.ifDraw){ //只存储空间模型信息,不绘制
+            if(this.buildType != 'hole'){
+                this.box = this.createBox()
+                this.add(this.box)
+            }
+            {
+                this.lineMesh = LineDraw.createLine([],{color})
+                this.lineMesh.name = 'buildingLines'
+                this.lineMesh.visible = false            
+                this.add(this.lineMesh) 
+                viewer.setObjectLayers(this.lineMesh, 'bothMapAndScene' ) 
+            }
+            
+            
+            this.addEventListener('dragChange',(e)=>{ //修改中点
+                this.updateTwoMidMarker(e.index)
+            }) 
+            
+        }
+        
+        this.initData(prop)
+        
+        
+    } 
+    
+    
+    initData(prop){
+        if(prop.ifDraw){
+            super.initData(prop) 
+        }else{
+            if(prop.points){
+                this.points = prop.points  
+            }
+            
+            
+        }
+        
+    } 
+    
+    intersectPointcloudVolume(pointcloud){//和pointcloud的重叠体积
+        var bound = this.getBound()
+        let bound2 = pointcloud.bound;
+        if(!bound.intersectsBox(bound2)) return 0;
+        
+        
+        let {zMin , zMax} =  this.getRealZ()
+        let min = Math.min(zMin, bound2.min.z);
+        let max = Math.max(zMax, bound2.max.z); 
+        let height1 = zMax - zMin
+        let height2 = bound2.max.z-bound2.min.z
+        let coverHeight = height1 + height2 - (max-min)//重叠高度 <=0是没重叠
+         
+        let boxPoints = pointcloud.getUnrotBoundPoint() //获取tightBound的四个点。 如果是有旋转角度的点云,这个和pointcloud.bound的四个点是不一致的,覆盖面积小于pointcloud.bound
+       
+        let areaWhole = 0  
+        let area1 = this.getArea()
+        let area2 = Math.abs(math.getArea(boxPoints))
+         
+        {//计算points与点云总面积 (但是把hole也加入了面积)(并集,重叠部分只算一次)  
+            let rings = math.getPolygonsMixedRings([this.points, boxPoints] )
+            
+            rings.forEach(e=>{ 
+                areaWhole+=e.area
+            })
+        }
+        
+        let coverHoleArea = 0 //holes与数据集重叠的部分
+        let holes = this.holes.concat(this.parentHoles)
+        let holesArea = 0    //所有holes面积相加
+        let areaHoleWithPointcloud = 0 //hole和点云的面积并集
+        
+        if(holes.length>0){//还要再扣除holes与数据集重叠的部分。其中holes为mix轮廓
+            let outHoles = []//没有重合的holes的外轮廓
+            /* if(holes.length>=2){//合并holes。如果能在绘制时直接合并holes就好啦,这步就转移到那去,但是要删除hole好麻烦
+                  
+                let holes_ = holes.map(e=>e.points)
+                
+                outHoles = math.getPolygonsMixedRings(holes_,  true )
+                outHoles.forEach(e=>{ 
+                    holesArea+=e.area
+                }) 
+                outHoles = outHoles.map(e=>e.points)
+                
+                
+            }else{
+                outHoles = holes.map(e=>e.points)
+                outHoles.forEach(e=> holesArea += Math.abs(math.getArea(e)))                
+            } */
+            holesArea = this.getHolesArea()
+            
+            //holes与数据集重叠的部分
+            
+            {    
+                let polygons = outHoles.concat([boxPoints]) 
+                let rings = math.getPolygonsMixedRings(polygons)
+                rings.forEach(e=>{ 
+                    areaHoleWithPointcloud+=e.area
+                })
+                coverHoleArea = holesArea + area2 - areaHoleWithPointcloud//hole和点云的交集
+            }
+             
+        }
+        
+        
+        let coverArea = area1 + area2 - areaWhole - coverHoleArea; //重叠面积
+         
+        return coverArea * coverHeight
+    }
+    
+    
+    addHole(points=[]){  
+        let prop = {
+            buildType : 'hole',
+            zMin : this.zMin,
+            zMax : this.zMax,
+            points, 
+            buildParent:this,
+            ifDraw: this.ifDraw || Potree.settings.drawEntityData
+        }
+        //hole的zMin zMax跟随buildParent
+        var hole = new BuildingBox(prop);
+        this.holes.push(hole)
+        
+        if(this.buildType == 'building'){//为每一层添加对应的hole
+            this.buildChildren.forEach(floor=>{
+                let floorHole = new BuildingBox({
+                    buildType : 'hole', 
+                    zMin : this.zMin,
+                    zMax : this.zMax,
+                    buildParent:floor,
+                    originHole : hole, //整栋大楼在当层的hole
+                    ifDraw: this.ifDraw  || Potree.settings.drawEntityData
+                });
+                floor.parentHoles.push(floorHole)
+                floor.add(floorHole)
+                floorHole.points = hole.points//完全等于建筑的点
+            })
+            
+        }
+        
+        this.add(hole);//直接加在这,不加meshGroup了
+        this.update() //update box mesh
+        return hole
+        
+        //hole不创建box,只有它的buildParent需要更新box。 但有线条和marker.  hole不在buildChildren里,但有buildParent
+        
+    }
+    
+    
+    removeHole(hole){// 这个hole不会是parentHoles里的。
+        hole.dispose()
+        
+        if(this.buildType == 'building'){ //若是整栋大楼的hole,在每层去除它的对应hole
+            this.buildChildren.forEach(floor=>{ 
+                let holeAtFloor = floor.parentHoles.find(e=>e.originHole == this )
+                let index = floor.parentHoles.indexOf(holeAtFloor)  
+                index > -1 && floor.parentHoles.splice(index, 1)                
+                holeAtFloor.dispose() 
+            })
+        } 
+        
+        let index = this.holes.indexOf(hole)
+        if(index>-1){
+            this.holes.splice(index, 1)
+        }
+        this.remove(hole)
+        this.update()
+    }
+    
+    
+    
+    
+    createBox(){ 
+        var geometry = new THREE.Geometry();
+        
+        
+        
+        this.mats.boxDefault = getFaceMat(this.buildType)  
+        this.mats.boxSelected = getFaceMat(this.buildType+'Select')     
+        
+        
+        var mesh = new THREE.Mesh(geometry, this.mats.boxDefault)
+        mesh.name = 'buildingBox';
+        if(this.buildType == 'floor'){
+            viewer.setObjectLayers(mesh, 'siteModelMapUnvisi' ) //楼层默认在地图不显示,为了不会叠加透明度
+        }else{
+            viewer.setObjectLayers(mesh, 'bothMapAndScene' )
+        }
+         
+        
+        
+        //mesh.frustumCulled = false;
+        return mesh
+    }
+    
+    
+    
+    addMarker(o={} ){
+        if(this.buildType=='floor')return; //楼层不需要marker
+        
+        let marker = new Sprite({mat:this.getMarkerMaterial('default'), renderOrder : 3, sizeInfo: markerSizeInfo, dontFixOrient: true, name:"building_marker"} )
+       
+         
+        viewer.setObjectLayers(marker, 'siteModeOnlyMapVisi' ) 
+        
+        o.marker = marker
+        super.addMarker(o)
+        
+        if(!this.selected)viewer.updateVisible(marker,'select',false) 
+         
+        let addClickEvent = (e)=>{ 
+            let click = (e) => {   
+                this.dispatchEvent({type:'clickMarker', marker } )  //由entity发送给sitemodel统一处理
+            }; 
+            marker.addEventListener('click', click); 
+            marker.addEventListener('clickSelect', (e)=>{
+                 this.setMarkerSelected(marker, e.state ? 'select' : 'unselect' );  
+            }); 
+            marker.removeEventListener('addHoverEvent',addClickEvent) 
+        }
+        marker.addEventListener('addHoverEvent',addClickEvent)//当非isNew时才添加事件
+        if(!this.isNew){
+            marker.dispatchEvent('addHoverEvent')
+        }
+     
+        return marker
+    }
+    
+    removeMarker(index){
+        super.removeMarker(index);
+        if(!this.isNew){
+            //重新添加midMarkers 
+            this.midMarkers.forEach(e=>this.remove(e));
+            this.midMarkers = [] 
+            this.addMidMarkers() 
+        }
+        
+        this.update();  
+        if(this.points.length == 2 && this.box){//清除原先length>=3时候的
+            this.box.geometry = new THREE.Geometry();
+        } 
+        
+     
+    }
+    
+    addMidMarker(index, point){
+        if(this.buildType=='floor')return; //楼层不需要marker
+        let marker = new Sprite({mat:this.getMarkerMaterial('midPrepare'), sizeInfo: markerSizeInfo, dontFixOrient: true, name:"building_midMarker"} )
+        this.midMarkers = [...this.midMarkers.slice(0,index), marker, ...this.midMarkers.slice(index,this.midMarkers.length)]
+        
+        marker.renderOrder = 3 
+         
+        viewer.setObjectLayers(marker, 'siteModeOnlyMapVisi' ) 
+        { // Event Listeners  
+            let mouseover = (e) => {
+                this.setMarkerSelected(e.object, 'hover', 'single'); 
+                viewer.dispatchEvent({
+                    type : "CursorChange", action : "add",  name:"markerMove"
+                }) 
+            };
+            let mouseleave = (e) => {
+                this.setMarkerSelected(e.object, 'unhover', 'single');
+                viewer.dispatchEvent({
+                    type : "CursorChange", action : "remove",  name:"markerMove"
+                })
+            }
+            let drag = (e) => {
+                let index = this.midMarkers.indexOf(marker)
+                let newMarker = this.addMarker({index:(index+1), point:marker.position.clone()  }) 
+                this.addMidMarker(index+1, new THREE.Vector3 )
+                this.updateTwoMidMarker(index+1) 
+                this.setMarkerSelected(marker, 'unhover') 
+                viewer.inputHandler.startDragging(newMarker , {/* dragViewport:viewer.mapViewer.viewports[0],  */   } ); //notPressMouse代表不是通过按下鼠标来拖拽.  dragViewport指定了只能在地图上拖拽
+            }
+            marker.addEventListener('drag', drag );
+            //marker.addEventListener('drop', drop);
+            marker.addEventListener('mouseover', mouseover);
+            marker.addEventListener('mouseleave', mouseleave);
+        }
+        this.add(marker)
+        this.updateMarker(marker, point)
+        if(!this.selected)viewer.updateVisible(marker,'select',false) 
+        return marker
+    }
+    
+    
+    
+    
+    
+    addMidMarkers(){//第一次画好所有marker后,一次性为线段增加中点marker
+        let length = this.points.length
+        this.points.forEach((point,index)=>{
+            let nextPoint = this.points[(index+1)%length]
+            let midPoint = new THREE.Vector3().addVectors(point, nextPoint).multiplyScalar(0.5)
+            this.addMidMarker(index, midPoint )
+        })
+    }
+    
+    updateTwoMidMarker(index){//更新第index个marker两边的midMarker
+        if(!this.midMarkers.length)return
+        let length = this.points.length
+        let last = this.points[(index-1+length)%length] //它之前的marker位置
+        let next = this.points[(index+1)%length];//它之后的marker位置
+        let current = this.points[index]//当前位置 
+        let lastMid = new THREE.Vector3().addVectors(last, current).multiplyScalar(0.5)//上一个中点
+        let nextMid = new THREE.Vector3().addVectors(next, current).multiplyScalar(0.5)//下一个中点
+        let lastMidMarker = this.midMarkers[(index-1+length)%length];
+        let nextMidMarker = this.midMarkers[index]
+        this.updateMarker(lastMidMarker, lastMid) 
+        this.updateMarker(nextMidMarker, nextMid) 
+    }
+    
+     
+    
+    
+    
+    dispose(){//销毁geo、remove from parent
+        super.dispose()
+        this.box && this.box.geometry.dispose();
+        this.lineMesh && this.lineMesh.geometry.dispose();
+        this.holes.forEach(e=>e.dispose()) 
+        this.parentHoles.forEach(e=>e.dispose()) 
+        //this.buildChildren.forEach(e=>e.dispose())
+        this.dispatchEvent('dispose')
+    }
+    
+    
+    
+    updateBox(){
+        if(!this.box)return
+        this.box.geometry.dispose()
+        var shrink = this.buildType == 'room' ? 0.11 : this.buildType == 'floor' ?  0.082 :  0.2   ;//防止mesh重叠冲突(给一个不寻常的数字)  但离远了还是会有点闪烁
+        if(this.points.length >= 3){ 
+            let holes = this.holes.concat(this.parentHoles)
+            let holesPoints = holes.filter(e=>e.points.length>2).map(e=>e.points)
+            this.box.geometry = MeshDraw.getExtrudeGeo(this.points, holesPoints, {
+                depth:this.zMax-this.zMin-shrink,
+                UVGenerator: new MetricUVGenerator()
+            }) 
+            if(this.buildType == 'building' ){
+                this.box.position.z = this.zMin - shrink / 2  
+            }else{
+                this.box.position.z = this.zMin + shrink / 2 
+            }
+             
+        }
+    }
+    
+     
+    
+    update(options={}){ 
+        super.update(this.buildType != 'floor' && options.ifUpdateMarkers)
+        let length = this.points.length
+        
+        
+       
+        {//确保一下一样
+            if(this.originHole){ 
+                this.points = this.originHole.points //完全等于building的hole
+            }
+            if(this.buildType == 'hole'){
+                this.zMin = this.buildParent.zMin;
+                this.zMax = this.buildParent.zMax;
+            } 
+        }
+        
+        
+        
+        if(!options.dontUpdateBox){
+            let boxOwner
+            if(this.buildType == 'hole'){
+                if(this.buildParent.buildType == 'building'){ //若是整栋大楼的hole,在每层都要更新下它的对应hole
+                    this.buildParent.buildChildren.forEach(floor=>{
+                        let holeAtFloor = floor.parentHoles.find(e=>e.originHole == this ) 
+                        holeAtFloor && holeAtFloor.update()  //刚开始创建时还没创建对应的 holeAtFloor会为null
+                    })
+                } 
+                boxOwner = this.buildParent 
+            }else{
+                boxOwner = this
+            }
+            boxOwner.updateBox()
+        }
+        
+        
+        
+        {//update lines
+            
+            let positions = [];
+           
+            this.points.forEach((point, index)=>{
+                
+                //竖线:
+                positions.push(point.clone().setZ(this.zMin), point.clone().setZ(this.zMax))
+                
+                //横线
+                let nextPoint = this.points[(index+1)%length]; 
+                if(!nextPoint)return;//when length==1
+                
+                positions.push(point.clone().setZ(this.zMax), nextPoint.clone().setZ(this.zMax))//上横线
+                positions.push(point.clone().setZ(this.zMin), nextPoint.clone().setZ(this.zMin))//下横线
+            }) 
+            
+            LineDraw.moveLine(this.lineMesh,positions)
+             
+        }
+        
+        if(!options.dontUpdateChildren){
+            if(this.buildType == 'building'  ){
+                this.buildChildren.forEach(floor=>{
+                    floor.points = this.points 
+                    floor.update()
+                })
+                
+            } 
+            
+            { 
+                let holes = this.holes.concat(this.parentHoles) 
+                holes.forEach(hole=> {  
+                    
+                    hole.update({dontUpdateBox:true})//父级更新了box,hole就不需要更新box了
+                }) 
+                    
+            } 
+        
+        } 
+    }
+    
+    
+     
+    
+    
+    
+    getHolesArea(){
+        let holes = this.holes.concat(this.parentHoles)
+        let outHoles, holesArea = 0
+        if(holes.length>=2){//合并holes。如果能在绘制时直接合并holes就好啦,这步就转移到那去,但是要删除hole好麻烦
+              
+            let holes_ = holes.map(e=>e.points)
+            
+            outHoles = math.getPolygonsMixedRings(holes_,  true )
+            outHoles.forEach(e=>{ 
+                holesArea+=e.area
+            }) 
+            outHoles = outHoles.map(e=>e.points)
+            
+            
+        }else{
+            outHoles = holes.map(e=>e.points)
+            outHoles.forEach(e=> holesArea += Math.abs(math.getArea(e)))                
+        }
+        return holesArea
+        
+    }
+    
+    getArea(ifRidOfHoles){//面积
+        //不排除hole
+        return Math.abs(math.getArea(this.points)) - (ifRidOfHoles ? this.getHolesArea() : 0)
+    }
+    
+    getVolume(ifRidOfHoles){//体积
+        let {zMin , zMax} = this.getRealZ()
+        let height = zMax - zMin;
+        if(isNaN(height))height = 0
+        return this.getArea(ifRidOfHoles) * height
+    
+    }
+    
+    getRealZ(){//求真实高度时用到的
+        let zMin , zMax
+        if (this.buildType == 'building') {
+            //building的zMax和zMin一样的所以要算
+            let top = this.buildChildren[this.buildChildren.length - 1]
+            let btm = this.buildChildren[0] 
+            zMin = btm ? btm.zMin : 0  //建好的建筑不加楼的话是0
+            zMax = top ? top.zMax : 0
+        }else if(this.buildType == 'hole'){
+            return this.buildParent.getRealZ()
+        }else{
+            zMin = this.zMin,  zMax = this.zMax
+        }
+        return {zMin,zMax}
+    }
+    
+    
+    /* getDrawZ(){ //画线和box时用到的z
+        let zMin , zMax
+        if(this.buildType == 'hole'){
+             if(this.buildParent.buildType == 'building' && atFloor){ 
+                 zMin = atFloor.zMin,  zMax = atFloor.zMax 
+             }else{
+                 zMin = this.buildParent.zMin,  zMax = this.buildParent.zMax
+             }
+        }else{
+            zMin = this.zMin,  zMax = this.zMax
+        }
+        
+        return {zMin, zMax}
+        
+    } */
+    
+    
+    
+    getBound(){
+        let bound = new THREE.Box3
+         
+        let {zMin , zMax} = this.getRealZ()
+        
+        let points = this.buildType == 'floor' ? this.buildParent.points : this.points
+        points.forEach(p=>{
+            bound.expandByPoint(p.clone().setZ(zMin))
+            bound.expandByPoint(p.clone().setZ(zMax))
+        }) 
+        return bound
+    }
+    
+     
+    getMarkerMaterial(type) {
+        if(!markerMats){
+            markerMats = {  
+                default:    new THREE.MeshBasicMaterial({  
+                    transparent: !0,
+                    color,
+                    opacity: 0.8,
+                    map: texLoader.load(Potree.resourcePath+'/textures/whiteCircle.png' ),  
+                    depthTest:false,
+                    
+                }),
+                midPrepare:    new THREE.MeshBasicMaterial({ //线中心的半透明点  
+                    transparent: !0,
+                    color,
+                    opacity: 0.4,
+                    map: texLoader.load(Potree.resourcePath+'/textures/whiteCircle.png' ), 
+                    depthTest:false,
+                }),  
+                hover:    new THREE.MeshBasicMaterial({   
+                    transparent: !0,
+                    color,
+                    opacity: 1,
+                    map: texLoader.load(Potree.resourcePath+'/textures/whiteCircle.png' ), 
+                    depthTest:false,
+                     
+                }),
+                select:    new THREE.MeshBasicMaterial({   
+                    transparent: !0,
+                    color:new THREE.Color('#00C8AF'),
+                    opacity: 1,
+                    map: texLoader.load(Potree.resourcePath+'/textures/whiteCircle.png' ), 
+                    depthTest:false,
+                     
+                }),   
+            }
+            
+        }
+        return markerMats[type]
+        
+    }
+    
+    
+    setMarkerSelected(marker, state, hoverObject){ 
+        //console.warn(marker.id , state, hoverObject)
+         
+        
+        if(state == 'select'){
+            marker.selected = true
+            marker.material = this.getMarkerMaterial('select')
+        }else if(state == 'unselect'){
+            marker.selected = false
+            marker.material = this.getMarkerMaterial('default')
+        }else{
+            if(marker.selected)return //选中时不允许修改为除了'unselect'以外的状态
+            
+            if(state == 'hover'){
+                marker.material = this.getMarkerMaterial('hover')
+            }else if(state == 'unhover'){
+                if(marker.name.includes('mid')){
+                    marker.material = this.getMarkerMaterial('midPrepare')
+                }else{
+                    marker.material = this.getMarkerMaterial('default')
+                } 
+            }
+        }
+    }
+    
+    
+    select(){
+        //最多再显示一层子级的线,如building不会显示room中的hole的线
+        //box是一直显示的,但会切换材质 
+        
+        /* 
+            选中                  box                                     线
+            
+            building            自己(底盘)选中                 自己, floor不带hole
+        
+            floor               自己选中                         自己, room不带hole
+            
+            room                自己选中                         自己   
+        
+         */  //注:自己的就代表定包括hole,如果有parentHoles的也(building上的hole的对应)
+        
+        
+        //console.log('select '+this.name,   this.selected)
+        
+        if(this.selected)return
+        
+        if(this.box){
+            this.box.material = this.mats.boxSelected;
+        }
+        
+        if(this.buildType == 'building'|| this.buildType == 'floor'){
+            this.buildChildren.forEach(e=>{
+                e.lineMesh.visible = true
+            })
+              
+            if(this.buildType == 'floor'){
+                viewer.setObjectLayers(this.box, 'bothMapAndScene' ) 
+                viewer.setObjectLayers(this.buildParent.box, 'siteModelMapUnvisi' ) //当选中floor或room时,building在地图不可见
+            }
+        }else if(this.buildType == 'room'){
+            viewer.setObjectLayers(this.buildParent.box, 'bothMapAndScene' )
+            viewer.setObjectLayers(this.buildParent.buildParent.box, 'siteModelMapUnvisi' )
+        }
+        
+        
+        
+        
+        
+        this.lineMesh.visible = true
+        this.markers && this.markers.forEach(e=>viewer.updateVisible(e,'select',true) )
+        this.midMarkers && this.midMarkers.forEach(e=>e.visible = true)
+        
+        let holes = this.holes.concat(this.parentHoles)
+        holes.forEach(e=>e.select()) 
+        
+        this.selected = true
+        this.dispatchEvent({type:'select'})
+    }
+    
+    
+    unselect(){
+        if(!this.selected)return
+        //console.log('unselect '+this.name  )
+        if(this.box){
+            this.box.material = this.mats.boxDefault;
+        }
+        
+        if(this.buildType == 'building' || this.buildType == 'floor'){ 
+            this.buildChildren.forEach(e=>{  //(这里要保证选中前要先取消选中,否则如选中房间后取消了楼层,房间线就隐藏了)
+                e.lineMesh.visible = false
+            })  
+            
+            if(this.buildType == 'floor'){
+                viewer.setObjectLayers(this.box, 'siteModelMapUnvisi' ) 
+                viewer.setObjectLayers(this.buildParent.box, 'bothMapAndScene' ) 
+            }
+            
+        }else if(this.buildType == 'room'){
+            viewer.setObjectLayers(this.buildParent.box, 'siteModelMapUnvisi' )
+            viewer.setObjectLayers(this.buildParent.buildParent.box, 'bothMapAndScene' )
+        }
+        
+        this.lineMesh.visible = false
+        this.markers && this.markers.forEach(e=>viewer.updateVisible(e,'select',false) )
+        this.midMarkers && this.midMarkers.forEach(e=>e.visible = false)
+        
+        let holes = this.holes.concat(this.parentHoles)
+        holes.forEach(e=>e.unselect()) 
+        
+        this.selected = false
+        this.dispatchEvent({type:'unselect'})
+    }
+    
+     
+    
+    
+    ifContainsPoint(position){//看它所定义的空间是否包含某个坐标(要排除hole)
+    
+        let {zMin , zMax} = this.getRealZ()
+        if(position.z < zMin ||  position.z > zMax   ) return
+    
+        let holes = this.holes.concat(this.parentHoles)
+        let holesPoints = holes.filter(e=>e!=this && e.points.length>2).map(e=>e.points) 
+        let inShape = math.isPointInArea(this.points, holesPoints, position) 
+        
+         
+        return !!inShape 
+    }
+     
+}
+
+
+
+
+
+class MetricUVGenerator{
+    constructor(){
+        this.a = new THREE.Vector3,
+        this.b = new THREE.Vector3,
+        this.c = new THREE.Vector3,
+        this.d = new THREE.Vector3
+    }
+    generateTopUV(t, e, n, r, o) {
+        return [new THREE.Vector2(e[3 * n],e[3 * n + 1]), new THREE.Vector2(e[3 * r],e[3 * r + 1]), new THREE.Vector2(e[3 * o],e[3 * o + 1])]
+    }
+    
+    generateSideWallUV(t, e, n, r, o, a) {
+        var s = e;
+        this.a.set(s[3 * n], s[3 * n + 1], s[3 * n + 2]),
+        this.b.set(s[3 * r], s[3 * r + 1], s[3 * r + 2]),
+        this.c.set(s[3 * o], s[3 * o + 1], s[3 * o + 2]),
+        this.d.set(s[3 * a], s[3 * a + 1], s[3 * a + 2]);
+        var c = this.a.x !== this.b.x
+          , l = c ? this.b : this.d
+          , u = this.a.distanceTo(l)
+          , d = l.distanceTo(this.c);
+        return [new THREE.Vector2(this.a.x,0), c ? new THREE.Vector2(this.a.x + u,0) : new THREE.Vector2(this.a.x,d), new THREE.Vector2(this.a.x + u,d), c ? new THREE.Vector2(this.a.x,d) : new THREE.Vector2(this.a.x + u,0)]
+    }
+}
+ 

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1281 - 0
src/custom/modules/siteModel/SiteModel.js


+ 7 - 10
src/custom/note.txt

@@ -12,10 +12,12 @@ start.js除了通过引用shim文件来修改Potree, 也可在此文件修改,
 
 
 ----
-因为viewer太复杂了就去掉了所有和sidebar有关的代码。也就是说不支持非official的情况。需要用这个测试的需要换分支
-
+因为viewer太复杂了就去掉了所有和sidebar有关的代码(没完全继承原先的viewer的函数)。也就是说不支持非official的情况(no sidebar)。需要用这个测试的需要换分支
+鉴于经常要测试,需要保持两个分支代码同步更新。
 
 
+Potree.js文件随便改
+ 
 
 
 
@@ -24,17 +26,12 @@ start.js除了通过引用shim文件来修改Potree, 也可在此文件修改,
 
 bug:
 
-pick时是showPanos模式会报错,为什么node.geometry没有?!
-
-
-
-
-
-
+ 
 
-modules文件夹改好了
+还没测融合页面
 
 
+ 
 
 
 

+ 121 - 0
src/custom/objects/Axis.js

@@ -0,0 +1,121 @@
+
+
+import * as THREE from "../../../libs/three.js/build/three.module.js"; 
+import {MeshDraw,LineDraw} from '../utils/DrawUtil.js'
+ 
+/*  
+
+                 z   
+                 |
+                 |
+                 |
+                 | 
+       x <-------|   中心为点云position加boudingbox中心
+                /
+               /
+             y
+
+ */
+ 
+var lineLen = 2, stemLen = 4, arrowLen = 2, lineDisToStem = 5;
+var opacity = 0.5
+export default class Axis extends THREE.Object3D {// 坐标轴
+	constructor (position) {
+		super()
+        this.getArrow() 
+        this.createArrows()
+        //this.position.copy(position) 点云的中心点就是在(0,0,0)
+        //this.scale.set(2,2,2)
+    }
+    getArrow(){
+        var arrowGroup = new THREE.Object3D()
+        
+        
+        
+        var line = LineDraw.createLine([new THREE.Vector3, new THREE.Vector3(0,0,lineLen)])
+        var stem = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, stemLen)) 
+            stem.position.set(0,0,lineLen+lineDisToStem+stemLen/2);
+        var arrow = new THREE.Mesh(new THREE.CylinderBufferGeometry( 0, 0.6, arrowLen, 12, 1, false ));//radiusTop = 1, radiusBottom = 1, height = 1, radialSegments = 8, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2
+            arrow.position.set(0,0,lineLen+lineDisToStem+stemLen + arrowLen/2);
+            arrow.rotation.set(Math.PI/2,0,0)
+            
+        arrowGroup.add(stem)
+        arrowGroup.add(line)
+        arrowGroup.add(arrow)
+        
+        this.arrowGroup = arrowGroup
+        
+    } 
+    
+    
+    
+    createArrows(){ 
+
+
+        var material = new THREE.MeshBasicMaterial({color:"#00d7df",side:2,transparent:true,opacity:0.8, depthWrite:false});
+        let axis = Object.keys(Potree.config.axis)
+        axis.forEach((axisText)=>{
+            let color =  Potree.config.axis[axisText].color
+            var group = this.arrowGroup.clone()
+            
+            group.children.forEach(e=>{
+                e.material = e.material.clone()
+                /* e.material.opacity = opacity
+                e.material.transparent = true */
+                e.material.color.set(color)
+            })  
+            
+            var label = this.createLabel(axisText, color)  
+                label.position.set(0, 0, lineLen + stemLen + arrowLen + lineDisToStem + 3);
+                group.add(label) 
+        
+
+            if(axisText == 'y'){
+                group.rotation.x = -Math.PI / 2
+            }else if(axisText == 'x'){
+                group.rotation.y = Math.PI / 2
+            } 
+            
+            this.add(group)
+        }) 
+        
+    }
+
+    createLabel(text,color){
+        var canvas = document.createElement("canvas")
+        var context = canvas.getContext("2d");
+        canvas.width = 256,
+        canvas.height = 256; 
+        var fontSize = 120  
+        context.fillStyle =  color //"#00ffee";
+        context.font = "normal " + fontSize + "px 微软雅黑" 
+        var textWidth = context.measureText(text).width; 
+        context.clearRect(0,0,canvas.width,canvas.height);
+        context.fillText(text, (canvas.width - textWidth) / 2 , (canvas.height + fontSize) / 2);
+         
+        var tex = new THREE.Texture(canvas); 
+        tex.needsUpdate = true
+
+
+        tex.minFilter = THREE.NearestFilter//防止边缘发黑
+        tex.magFilter = THREE.NearestFilter//防止边缘发黑
+        var sprite = new THREE.Sprite(new THREE.SpriteMaterial({ 
+            map: tex  , // depthWrite:false,
+             
+        })) 
+        
+        sprite.renderOrder = 1//防止在透明后还是出现白矩形挡住其他mesh
+        sprite.scale.set(3,3,3)
+        return sprite
+       
+    }
+   
+}
+
+
+
+
+
+
+
+

+ 141 - 0
src/custom/objects/InfiniteGridHelper.js

@@ -0,0 +1,141 @@
+// Author: Fyrestar https://mevedia.com (https://github.com/Fyrestar/THREE.InfiniteGridHelper)
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+
+
+
+class InfiniteGridHelper extends THREE.Mesh{
+     
+    constructor(size1, size2, color, distance, opacity1=0.2, opacity2=1){
+        
+        color = color || new THREE.Color('white');
+        size1 = size1 || 10;
+        size2 = size2 || 100;
+
+        distance = distance || 8000; //可视距离?越远越模糊
+
+        const geometry = new THREE.PlaneBufferGeometry(2, 2, 1, 1);
+
+        const material = new THREE.ShaderMaterial({
+
+            side: THREE.DoubleSide,
+
+            uniforms: {
+                uSize1: {
+                    value: size1
+                },
+                uSize2: {
+                    value: size2
+                },
+                
+                opacity1:{//线条1的
+                    value: opacity1
+                },
+                opacity2:{//线条2的
+                    value: opacity2
+                },
+                
+                uColor: {
+                    value: color
+                },
+                uDistance: {
+                    value: distance
+                }
+            },
+            transparent: true,
+            vertexShader: `
+               
+               varying vec3 worldPosition;
+               
+               uniform float uDistance;
+               
+               void main() {
+               
+                    vec3 pos = position.xyz * uDistance;
+                    pos.xy += cameraPosition.xy;
+                    
+                    worldPosition = pos;
+                    
+                    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
+               
+               }
+               `,
+
+
+            fragmentShader: `
+               
+               varying vec3 worldPosition;
+               
+               uniform float uSize1;
+               uniform float uSize2;
+               uniform float opacity1;
+               uniform float opacity2;
+               uniform vec3 uColor;
+               uniform float uDistance;
+                
+                
+                
+                float getGrid(float size) {
+                
+                    vec2 r = worldPosition.xy / size;
+                    
+                    
+                    vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r);
+                    float line = min(grid.x, grid.y);
+                    
+                
+                    return 1.0 - min(line, 1.0);
+                }
+                //为何侧面看不到线,因为mesh的正侧面都看不到?
+
+               void main() {
+               
+                    
+                      float d = 1.0 - min(distance(cameraPosition.xy, worldPosition.xy) / uDistance, 1.0);
+                    
+                      float g1 = getGrid(uSize1);
+                      float g2 = getGrid(uSize2);
+                      
+                      
+                      gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 3.0));
+                      //gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2);
+                        gl_FragColor.a = mix(opacity1 * gl_FragColor.a, opacity2 * gl_FragColor.a, g2);
+                        
+                        
+                      if ( gl_FragColor.a <= 0.0 ) discard;
+                    
+               
+               }
+               
+               `,
+
+            extensions: {
+                derivatives: true
+            }
+            
+            
+            
+        })
+        
+        
+        super(geometry, material)
+        this.frustumCulled = false;
+
+    }
+
+
+    
+
+    
+
+};
+
+
+
+export default InfiniteGridHelper
+/* 
+THREE.InfiniteGridHelper.prototype = {
+    ...THREE.Mesh.prototype,
+    ...THREE.Object3D.prototype,
+    ...THREE.EventDispatcher.prototype
+};
+ */

+ 83 - 0
src/custom/objects/Label.js

@@ -0,0 +1,83 @@
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+import {Utils} from "../../utils.js";  
+ 
+
+class Label  extends THREE.EventDispatcher{
+    constructor(o={}){
+        super()
+        
+        this.position = o.pos;
+        this.text = o.text || '';
+        this.elem = $('<div class="hide"><a></a></div>');
+        o.className && this.elem.addClass(o.className)
+        this.elem.find('a').html(this.text); 
+        $("#potree_labels").append(this.elem)
+        this.pos2d = new THREE.Vector3
+        this.dom = o.dom || viewer.renderArea
+        this.camera = o.camera || viewer.scene.getActiveCamera() 
+        
+        
+        let update = (e)=>{
+            this.update(e) 
+        }
+        viewer.addEventListener('camera_changed',  update)
+           
+         
+        this.addEventListener('dispose', ()=>{
+            viewer.removeEventListener('camera_changed',  update)
+            this.dispose()
+            
+        })
+        
+    }
+     
+    update(){
+        if(!this.position || this.elem.hasClass('unvisible'))return
+        var p = Utils.getPos2d(this.position,this.camera,this.dom, viewer.mainViewport);
+        if(!p.trueSide){
+            this.elem.addClass("hide");  return;
+        }
+        this.elem.css({
+            left: p.pos.x +'px',
+            top: p.pos.y +'px'
+        })
+        
+        
+        
+        this.elem.removeClass("hide");
+        this.pos2d = p.vector;
+        
+        
+        
+        
+        
+    }
+    
+    setVisible(visi){
+        if(!visi){
+            this.elem.addClass("unvisible");  
+        }else{
+            this.elem.removeClass("unvisible");
+            this.update()
+        }
+    }
+    
+    setText(text){
+        this.text = text || '';
+        this.elem.find('a').html(this.text); 
+    }
+    setPos(pos){
+        this.position = pos;
+    }
+    
+    dispose(){
+        this.elem.remove();
+        this.removeAllListeners()
+    }
+     
+     
+}
+
+export default Label;
+
+

+ 350 - 0
src/custom/objects/Magnifier.js

@@ -0,0 +1,350 @@
+
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+import math from '../utils/math.js'
+import browser from '../utils/browser.js'
+import Viewport from '../viewer/Viewport.js'
+ 
+const texLoader = new THREE.TextureLoader() 
+const circleGeo = new THREE.CircleGeometry(1.45,100);
+const sphereGeo = new THREE.SphereBufferGeometry(0.018,10,10);
+ 
+ 
+const magDistance_ = 1;//相机离目标位置的距离的分界线,当离得远时要缩小fov以使看到的视野固定(望远镜效果)
+/* const radius_ = 0.2; //当相机离目标位置的距离>magDistance_时,希望看到的视野的半径
+const maxFov = THREE.Math.radToDeg(Math.atan(radius_ / magDistance_ )) * 2//提前计算出当相机离目标位置的距离<magDistance_时的fov,均使用=magDistance_时的fov。只要保证该fov大于主相机的fov就会有放大效果 
+ */
+let w = 200/1.43;
+let maxPX = 1366*1024 //ipad pro.  大于这个分辨率的就直接用devicePixelRatio, 如macbook也是
+const width2dPX = Math.round(window.devicePixelRatio >= 2 ? ( window.screen.width * window.screen.height >= maxPX ? window.devicePixelRatio/1.2 : window.devicePixelRatio/1.5)*w : w)  //触屏或高分辨率的可能要放大些。但在手机上不能太大
+//console.log('width2dPX', width2dPX)
+
+
+
+export default class Magnifier extends THREE.Object3D {//放大镜or望远镜
+	constructor (viewer) {
+		super()
+        this.width = this.height = width2dPX/*  * window.devicePixelRatio */;
+        this.camera = new THREE.PerspectiveCamera(50, 1, 0.01, 10000);  //fov aspect near far
+        this.camera.up = new THREE.Vector3(0,0,1) 
+        
+        
+        this.viewport = new Viewport( null, this.camera, {
+            left:0, bottom:0, width:1, height: 1, name:'magnifier' , cameraLayers:['magnifierContent'],
+            pixelRatio:1
+        })
+        this.viewport.setResolution(this.width, this.height,0,0)
+        
+        { 
+            let density
+            let sizeType
+            let colorType
+            let opacityBefore = new Map()
+            this.viewport.beforeRender = ()=>{
+                viewer.scene.pointclouds.forEach(e=>{//因为更改pointDensity时会自动变opacity,所以这项最先获取
+                    opacityBefore.set(e,e.temp.pointOpacity)  
+                }) 
+                
+                
+                //使放大镜里的pointDensity是'magnifier'  最高质量。
+                density = Potree.settings.pointDensity 
+                Potree.settings.pointDensity = 'magnifier' 
+                 
+                viewer.scene.pointclouds.forEach(e=>{//因为全景模式的pointSizeType是fixed所以要还原下
+                    sizeType = e.material.pointSizeType  
+                    e.material.pointSizeType = Potree.config.material.pointSizeType  
+                     
+                    //材质
+                    colorType = e.material.activeAttributeName
+                    e.material.activeAttributeName = 'rgba'
+                    e.changePointOpacity(1) 
+                   
+                }) 
+            };
+            
+            
+            this.viewport.afterRender = ()=>{
+                Potree.settings.pointDensity = density
+                
+                viewer.scene.pointclouds.forEach(e=>{
+                    e.material.pointSizeType = sizeType
+                    e.material.activeAttributeName = colorType  
+                    e.changePointOpacity(opacityBefore.get(e))  
+                }) 
+            } 
+        }
+        
+        
+        
+        
+        this.renderTarget = new THREE.WebGLRenderTarget(this.width,this.height, { 
+            minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter,
+            format: THREE.RGBAFormat ,
+            /* type: THREE.FloatType,
+            minFilter: THREE.NearestFilter,
+			magFilter: THREE.NearestFilter, 
+			  */ 
+        } )
+        
+		this.rtEDL = new THREE.WebGLRenderTarget(this.width, this.height, {  //好像没用到? 因为这里不绘制测量线
+			minFilter: THREE.NearestFilter,
+			magFilter: THREE.NearestFilter,
+			format: THREE.RGBAFormat,
+			type: THREE.FloatType,
+			depthTexture: new THREE.DepthTexture(undefined, undefined, THREE.UnsignedIntType)
+		});
+        
+        
+        
+        this.mesh = new THREE.Mesh(circleGeo, new THREE.MeshBasicMaterial({
+            side: THREE.DoubleSide , 
+            map: this.renderTarget.texture ,
+            transparent:true,
+            depthTest: !1,
+            //depthWrite: !1,
+        }))
+        this.overlayMesh = new THREE.Mesh(circleGeo, new THREE.MeshBasicMaterial({
+            side: THREE.DoubleSide , 
+            map:texLoader.load(Potree.resourcePath+'/textures/crosshair.png') ,
+            transparent:true,
+            depthTest: !1,
+            //depthWrite: !1,
+        }))
+        this.targetPoint = new THREE.Object3D;
+        
+        this.targetPoint.add(new THREE.Mesh(sphereGeo, new THREE.MeshBasicMaterial({ 
+            color:"#ff0000",
+            transparent:true,
+            opacity:0.5,  
+        })))
+        this.targetPoint.add(new THREE.Mesh(sphereGeo, new THREE.MeshBasicMaterial({ 
+            color:"#ff0000",
+            transparent:true,
+            opacity:0.2, 
+            depthTest:false  //被遮挡层
+        })))
+        
+        this.targetPoint.name = 'magnifierPointTarget'
+        viewer.scene.scene.add(this.targetPoint)
+        viewer.setObjectLayers(this.targetPoint, 'magnifierContent' )
+        
+        this.add(this.mesh)
+        this.add(this.overlayMesh)
+        
+        this.position.set(-1000,-1000,-100000)//令它看不见
+        this.mesh.renderOrder = 10;
+        this.overlayMesh.renderOrder = 11;
+        this.aimPos
+        viewer.setObjectLayers(this, 'magnifier' )
+        //viewer.inputHandler.addInputListener(this)
+        
+        
+        
+        
+        viewer.addEventListener('camera_changed',(e)=>{ // 平移、滚轮时更新
+            if(e.viewport == viewer.mainViewport) this.update() //不过intersectPoint没更新 
+        }) 
+            
+         
+        
+        
+        this.mesh.layers.set(Potree.config.renderLayers.magnifier);
+        this.overlayMesh.layers.set(Potree.config.renderLayers.magnifier);
+        //this.layers.set(Potree.config.renderLayers.magnifier);//这句在外层写没用
+        
+        this.dontRender = false 
+        viewer.addEventListener('global_drag', (e)=>{//拖拽时不渲染。主要是右键平移时渲染延迟了,会闪烁。 
+            this.dontRender = true 
+        })
+        viewer.addEventListener('global_drop', (e)=>{
+            this.dontRender = false
+        })
+        viewer.addEventListener('global_mouseup', (e)=>{//测量时拖拽场景再mouseup
+            this.dontRender = false
+        })
+        
+        var updateVisi = (e)=>{
+            if(e.hoverViewport == viewer.mainViewport){
+                viewer.updateVisible(this,"atViewport", true)
+                this.update(e.intersect && e.intersect.location)
+            }else{
+                viewer.updateVisible(this,"atViewport", false) //小地图不显示
+            } 
+            
+        }
+        
+        viewer.addEventListener('global_mousemove', updateVisi)
+        viewer.addEventListener('global_touchstart', updateVisi)
+        
+        
+        /* viewer.addEventListener("beginSplitView",()=>{
+            this.updateVisible("splitView", false) 
+        })
+        viewer.addEventListener("finishSplitView",()=>{
+            this.updateVisible("splitView", true) 
+        })  */
+         
+         
+        this.addEventListener("setEnable",(e)=>{
+            viewer.updateVisible(this, "enable", e.value) //界面开关
+            /* if(Potree.settings.displayMode == 'showPanos') && e.value){
+                Potree.settings.pointDensity = 'magnifier'
+            }else if() */
+            
+        })
+         
+         
+        if(Potree.settings.isOfficial){
+            viewer.updateVisible(this, "enable", false) 
+        }else{
+            viewer.updateVisible(this, "measure", false) 
+            viewer.addEventListener("measureMovePoint",()=>{//测量开始
+                viewer.updateVisible(this, "measure", true) 
+            })
+            viewer.addEventListener("endMeasureMove",()=>{
+                viewer.updateVisible(this, "measure", false) 
+            })
+        }
+        
+        
+        viewer.scene.view.addEventListener('flyingDone',()=>{
+            if(!this.visible)return
+            let pickWindowSize = 100
+            let intersect = viewer.inputHandler.getIntersect(viewer.mainViewport, viewer.mainViewport.camera, true, pickWindowSize )
+            this.update(intersect && intersect.location)
+        })
+    }
+    
+    
+   
+    
+    //注意:在鼠标没有移动的时候,无法获取到最新的intersect, 放大镜内的内容可能是错误的。全景模式下更奇怪,原因未知
+    update(aimPos){//相机靠近 navvis的做法 
+        var dontRender = this.dontRender || !(aimPos instanceof THREE.Vector3) || Potree.settings.displayMode == 'showPanos' && viewer.images360.flying
+        aimPos = aimPos instanceof THREE.Vector3 ?  aimPos : this.aimPos
+        if(!aimPos  || !this.visible)return
+        
+        
+        
+        var playerCamera = viewer.scene.getActiveCamera()
+        var playerPos = playerCamera.position;//viewer.scene.view.getPivot()
+        var dis = playerPos.distanceTo(aimPos);
+        var dirToCamera = new THREE.Vector3().subVectors(playerPos, aimPos ).normalize()
+        
+        
+        //相机位置
+        var finalDisToAim = dis>magDistance_ ? magDistance_ : dis / 2;
+        this.camera.position.copy(aimPos).add(dirToCamera.multiplyScalar(finalDisToAim))
+        this.camera.lookAt(aimPos)
+        this.camera.fov = playerCamera.fov / 2
+        this.camera.updateProjectionMatrix()
+        
+        
+
+         
+        //自身位置 
+        //let pos2d = viewer.inputHandler.pointer.clone();   //跟随鼠标 
+        let pos2d = Potree.Utils.getPos2d(aimPos, playerCamera, viewer.renderArea, viewer.mainViewport).vector   //更新目标点的实时二维位置
+        let margin = 0.4, maxY = 0.4
+        let screenPos = pos2d.clone().setY(pos2d.y + (pos2d.y>maxY ? -margin : margin ))
+        
+        let newPos = new THREE.Vector3(screenPos.x,screenPos.y,0.8).unproject(playerCamera); //z:-1朝外       
+        let dir = newPos.clone().sub(playerPos).normalize().multiplyScalar(10);//这个数值要大于playerCamera.near
+        let s = dis>magDistance_ ? 1 : dis / magDistance_  ;
+        
+        this.position.copy(playerPos.clone().add(dir))
+        this.quaternion.copy(playerCamera.quaternion); 
+        this.targetPoint.position.copy(aimPos); 
+        this.targetPoint.scale.set(s,s,s)
+        this.aimPos = aimPos
+       
+        
+        var scale = math.getScaleForConstantSize({// 
+            width2d : width2dPX,
+            camera:viewer.scene.getActiveCamera(),  position: this.getWorldPosition(new THREE.Vector3()),
+            resolution: viewer.mainViewport.resolution2 
+        })
+        this.scale.set(scale, scale, scale); 
+ 
+        if(!dontRender){
+            this.waitRender = true
+        } 
+    
+    }
+    
+    
+    /* update(aimPos){  //仅改fov的版本
+        
+        aimPos = aimPos instanceof THREE.Vector3 ?  aimPos : this.aimPos
+        if(!aimPos  || !this.visible)return
+        
+    
+        //相机位置
+        var playerCamera = viewer.scene.getActiveCamera()
+        var playerPos = playerCamera.position;//viewer.scene.view.getPivot()
+        var dis = playerPos.distanceTo(aimPos);
+         
+        
+        if(dis<magDistance_){
+            this.camera.fov = maxFov
+        }else{
+            this.camera.fov = THREE.Math.radToDeg(Math.atan(radius_ / dis )) * 2 //radius_是能看到的范围半径。当dis大于magDistance_时就放大,否则维持fov为maxFov
+        }
+       
+        this.camera.updateProjectionMatrix()
+        this.camera.position.copy(playerPos)
+        this.camera.lookAt(aimPos)
+        
+        this.quaternion.copy(playerCamera.quaternion);
+  
+        let pointer = viewer.inputHandler.pointer.clone();
+        let margin = 0.4, maxY = 0.4
+        let screenPos = pointer.clone().setY(pointer.y + (pointer.y>maxY ? -margin : margin ))
+       
+                
+        let newPos = new THREE.Vector3(screenPos.x,screenPos.y,0.8).unproject(playerCamera); //z:-1朝外       
+        let dir = newPos.clone().sub(playerPos).normalize().multiplyScalar(10);//这个数值要大于playerCamera.near
+        
+        this.position.copy(playerPos.clone().add(dir))
+          
+        this.aimPos = aimPos
+        this.targetPoint.position.copy(aimPos);
+        
+        var scale = math.getScaleForConstantSize({// 
+            width2d : width2dPX,
+            camera:viewer.scene.getActiveCamera(),  position: this.getWorldPosition(new THREE.Vector3()),
+            resolution: viewer.mainViewport.resolution2 
+        })
+        this.scale.set(scale, scale, scale); 
+ 
+        if(!this.dontRender){
+            this.waitRender = true
+        }   
+    }//位置需要计算,不仅仅是点云,所以需要深度图
+     */
+    
+    
+    
+    
+    
+    render(){ 
+ 
+        if(!this.waitRender)return
+        //this.visible = false;//防止放大镜里有自己
+        viewer.render({
+            target : this.renderTarget,
+            viewports : [this.viewport],
+            camera : this.camera,
+            magnifier : true,
+            rtEDL: this.rtEDL 
+            /* width :this.renderTarget.width,
+            height: this.renderTarget.height, */
+        })
+        //this.visible = true;
+        this.waitRender = false
+        
+        
+        
+        
+    }
+   
+}

+ 274 - 0
src/custom/objects/Reticule.js

@@ -0,0 +1,274 @@
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+ 
+import {transitions, easing, lerp} from '../utils/transitions.js'
+import math from '../utils/math.js'
+
+
+let texLoader = new THREE.TextureLoader()
+let defaultOpacity =  0.7
+let Buttons = Potree.defines.Buttons
+
+
+//鼠标指示小圆片 
+export default class Reticule extends THREE.Mesh{
+    constructor(viewer){
+        var defaultTex = texLoader.load(Potree.resourcePath+'/textures/whiteCircle.png'/* reticule-256x256.png'  */)  
+        super(new THREE.PlaneBufferGeometry(0.11,0.11,1,1),new THREE.MeshBasicMaterial({
+            side: THREE.DoubleSide , 
+            map: defaultTex,
+            transparent:true,
+            depthTest: !1,
+            opacity: defaultOpacity,
+            //depthWrite: !1,
+        })) 
+        this.name = 'reticule'
+        this.defaultTex = defaultTex
+        this.crosshairTex = texLoader.load(Potree.resourcePath+'/textures/reticule_cross_hair.png') 
+        this.forbitTex = texLoader.load(Potree.resourcePath+'/textures/pic-forbid.png') 
+        
+        this.defaultTex.anisotropy = 4 
+        this.crosshairTex.anisotropy = 4 
+        this.forbitTex.anisotropy = 4 
+        //this.layers.set(0/* RenderLayers.RETICULE */);
+        this.renderOrder = 100
+        this.layers.set(Potree.config.renderLayers.marker);
+        
+        
+        this.direction = new THREE.Vector3;
+       
+        this.mouseLastMoveTime = Date.now();
+        this.hoverViewport;
+        this.matrixMap = new Map 
+        this.matrixAutoUpdate = false 
+        
+
+        this.hide(0)
+
+        //viewer.inputHandler.addInputListener(this);
+        Potree.settings.intersectWhenHover && viewer.addEventListener('global_mousemove',this.move.bind(this))
+        //viewer.addEventListener('global_click',this.move.bind(this))
+        viewer.addEventListener('global_mousedown',this.move.bind(this))//主要针对触屏
+        
+        
+        
+        
+        this.state = {}
+        
+        let startCrossStyle = ()=>{
+            this.state.cross = true
+            this.judgeTex()
+        }
+        let endCrossStyle = ()=>{
+            this.state.cross = false
+            this.judgeTex()
+        }
+        
+        viewer.addEventListener('measureMovePoint',startCrossStyle) 
+        viewer.addEventListener('endMeasureMove',endCrossStyle)
+        viewer.addEventListener('start_inserting_tag',startCrossStyle) 
+        viewer.addEventListener('endTagMove',endCrossStyle)
+        
+        viewer.addEventListener('reticule_forbit',(e)=>{
+            if(this.state.forbit != e.v){
+                console.log('change forbit ',e.v)
+            }
+            this.state.forbit = e.v
+            this.judgeTex() 
+        })
+        
+         
+        
+        viewer.setObjectLayers(this, 'sceneObjects' )
+    }
+
+    judgeTex(){ 
+        if(this.state.forbit){
+            this.material.map = this.forbitTex 
+        }else if(this.state.cross){
+            this.material.map = this.crosshairTex 
+        }else{
+            this.material.map = this.defaultTex 
+        }
+        
+         
+        viewer.mapViewer && viewer.mapViewer.dispatchEvent({type:'content_changed'})
+    }
+
+
+
+
+    move(e){ 
+        if(e.type == "global_mousemove" && (e.isTouch || e.buttons != Buttons.NONE) && this.state != 'crosshair'){
+            return//按下时不更新,除非拖拽测量
+        }
+           
+        this.mouseLastMoveTime = Date.now()
+        
+        this.updatePosition(e.intersect, e.hoverViewport)
+         
+    }
+
+    hide(duration = 500){ 
+        if(this.hidden)return
+        
+ 
+        
+        this.hidden = !0 
+        transitions.start(lerp.property(this.material , "opacity", 0), duration)
+             
+        this.dispatchEvent({type:'update', visible:false})
+        
+        setTimeout(()=>{
+            this.dispatchEvent({type:'update', visible:false})
+        },duration)
+        
+    }
+
+    show(duration = 300){
+         
+        if(!viewer.getObjVisiByReason(this, 'force'))return
+        //console.log("show Reticule")
+        this.hidden = !1
+        
+        if(this.material.opacity <= 0){
+            transitions.start(lerp.property(this.material, "opacity", defaultOpacity), duration)
+        
+            this.dispatchEvent({type:'update', visible:true})
+            
+            setTimeout(()=>{
+                this.dispatchEvent({type:'update', visible:false})
+            },duration)
+                
+        }
+        
+        
+        
+    }
+
+    //鼠标静止一段时间它就会消失
+    updateVisible(){
+        Date.now() - this.mouseLastMoveTime > 1500 && !this.hidden && this.hide()
+    }
+    
+    
+    updateScale(viewport){
+        let s, camera = viewport.camera
+        if(camera.type == "OrthographicCamera"){
+            
+            var sizeInfo = this.state.cross ? {width2d:500} : {minSize : 100,  maxSize : 400,   nearBound : 100 , farBound :  700}
+            s = math.getScaleForConstantSize($.extend(   sizeInfo ,  
+            {position:this.position, camera, resolution:viewport.resolution/* 2 */} ))
+            
+        }else{
+            
+            let n = camera.position.distanceTo(this.position)
+            s = 1 + .1 * n;
+            n < 1 && (s -= 1 - n)
+        }
+        this.scale.set(s, s, s);
+        
+    }
+
+    updateAtViewports(viewport){//当多个viewports时更新。更新大小等
+    
+        if(viewport.name == 'magnifier' )return
+    
+        if(this.hoverViewport && this.hoverViewport.name == 'mapViewport' && viewport != this.hoverViewport){
+            //若是在地图上更新,在其他viewport要隐藏。因为在地图上无法得知高度。     
+            viewer.updateVisible(this, 'hoverMap', false)
+            return; 
+        }
+        viewer.updateVisible(this, 'hoverMap', true)   
+    
+        var matrix = this.matrixMap.get(viewport)
+        if(!matrix){ 
+            this.updateScale(viewport)
+            this.updateMatrix(); 
+            //this.updateMatrixWorld()
+            this.matrixMap.set(viewport, this.matrix.clone())
+        }else{
+            this.matrix.copy(matrix) 
+            //this.updateMatrixWorld()
+        }
+        
+    }
+   
+    
+    
+
+    
+
+    updatePosition(intersect, viewport ){ //在地图(当地图融合到viewer时)和场景里都显示且完全相同(大小可能不同)
+         
+        if (viewer.getObjVisiByReason(this, 'force')) {//没有被强制隐藏,如进入某个页面后强制不显示
+            if (!intersect /* || !intersect.point.normal */){ 
+                 return //this.hide();   
+            }
+                
+            var atMap = !intersect.location
+            let location = intersect.location || intersect.orthoIntersect.clone()
+            let normal  
+            
+            
+            //地图上要瞬间变化 , 因为要使needRender为true很麻烦
+            this.show(atMap ? 0 : 300);
+            
+            
+            if(atMap){ 
+                normal =  new THREE.Vector3(0,0,1)//地图无normal
+                location.setZ(0);//低于相机高度即可
+                this.direction = normal.clone()
+            }else{
+                /* if(intersect.point){ 
+                    if(intersect.pointcloud){
+                        normal = new THREE.Vector3().fromArray(intersect.point.normal ).applyMatrix4( intersect.pointcloud.rotateMatrix  );
+                    }else{//mesh 
+                        normal = new THREE.Vector3().copy(intersect.point.normal).applyQuaternion(intersect.object.quaternion) 
+                    } 
+                }else{
+                    normal = intersect.normal  //when showPanos
+                } */
+                normal = intersect.normal 
+                if(normal){
+                    let ratio = /* Potree.settings.useDepthTex ? 1 : */ 0.2;   
+                    this.direction = this.direction.multiplyScalar(1-ratio); 
+                    this.direction.add(normal.clone().multiplyScalar(ratio)); 
+                }
+                //this.direction = normal.clone() //改为瞬间变化,否则刚hover上某个点时看起来不太对
+            }
+             
+             
+            
+            
+            this.position.copy(location)/* .add(normal.clone().multiplyScalar(.01));  */
+            this.updateMatrix();  //lookAt之前要保证得到matrix
+            this.lookAt(this.position.clone().add(this.direction));
+            
+            
+            this.hoverViewport = viewport //记录下最近一次hover过的viewport  
+            this.updateScale(viewport)
+            
+            
+            {//存储matrix,节省计算
+                this.updateMatrix(); 
+                //this.updateMatrixWorld()
+                this.matrixMap.clear();//重新计算
+                this.matrixMap.set(viewport, this.matrix.clone())
+                //别处会updateMatrixWorld
+            }
+            
+            this.dispatchEvent({type:'update'})
+            
+            //为什么navvis在校准数据集时每个viewport里reticule的朝向都刚好垂直于屏幕,似乎限定在了一定范围内,还是在pick时就只pick范围内的点?
+            
+            
+        }
+    }
+    
+    //navvis在地图等地方看reticule是有厚度的
+    
+    /* updateMatrixWorld(force){
+        console.log('updateMatrixWorld', force)
+        super.updateMatrixWorld(force) 
+    } */
+}

+ 166 - 0
src/custom/objects/Sprite.js

@@ -0,0 +1,166 @@
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+import DepthBasicMaterial from "../materials/DepthBasicMaterial.js";
+import math from "../utils/math.js";
+
+const geo = new THREE.PlaneBufferGeometry(1,1)
+export default class Sprite extends THREE.Mesh{
+    
+    constructor(options){  
+        super(geo, options.mat || new DepthBasicMaterial(options))/* ({map:options.map, useDepth:options.useDepth})) */
+         
+        this.root = options.root || this;
+        this.renderOrder = options.renderOrder != void 0 ? options.renderOrder : 4;
+        this.pickOrder = options.pickOrder || 0
+        this.sizeInfo = options.sizeInfo
+        this.dontFixOrient = options.dontFixOrient
+        
+        this.root.matrixAutoUpdate = false;
+        this.matrixMap = new Map()
+        this.name = options.name || 'sprite'
+        this.useViewport = null
+        this.viewports = options.viewports//指定更新的viewports
+        this.visible_ = true
+        
+        
+        let update = (e)=>{
+            this.update(e) 
+        }
+        viewer.mapViewer && viewer.mapViewer.addEventListener("camera_changed",  update) 
+        viewer.addEventListener("camera_changed",  update) 
+        /* if(viewer.viewports.length == 1){//直接更新。如果有多个不在这更新,在"render.begin"
+            this.update(e)
+        } */
+         
+        
+        let applyMatrix = (e)=>{
+            this.applyMatrix(e)
+        }
+        viewer.addEventListener("raycaster", applyMatrix)        //before render
+        viewer.addEventListener("render.begin", applyMatrix) //before render  //magnifier时要禁止吗
+            
+        this.addEventListener('dispose', ()=>{
+            viewer.mapViewer && viewer.mapViewer.removeEventListener("camera_changed",  update) 
+            viewer.removeEventListener("camera_changed",  update) 
+            viewer.removeEventListener("raycaster", applyMatrix)        //before render
+            viewer.removeEventListener("render.begin", applyMatrix)
+             
+            this.dispose()
+        })
+         
+    }
+    
+    set visible(v){
+        this.visible_ = v  
+        if(v){
+            this.update()
+        }
+    }
+    get visible(){
+        return this.visible_ 
+    }
+    
+    realVisible(){
+        let parent = this
+        
+        let v
+        while(parent){
+            if(parent.visible === false){
+                v = false
+                break; 
+            }
+            parent = parent.parent
+        }
+        v = true;
+        if(!this.latestRealVisi && v){//变为可见后先update 
+            this.latestRealVisi = true
+            setTimeout(()=>{
+                this.update()
+            },1)//延迟 防止无限调用
+            return false
+        }
+        
+        this.latestRealVisi = v
+        return v;
+    }
+    
+    update(e){
+        if(!e){
+            (this.viewports || viewer.viewports).forEach(view=>{
+                this.update({viewport:view}) 
+            })
+            return;
+        }
+        if(!this.root || ! this.realVisible() /* this.visible */ )return
+        if(this.viewports && !this.viewports.includes(e.viewport) )return
+        if(e.viewport.name == 'magnifier')return
+        
+        let camera = e.viewport.camera
+        //rotation
+        this.dontFixOrient || this.root.quaternion.copy(camera.quaternion)
+        
+        //scale
+        var info = this.sizeInfo
+        if(info){
+            this.root.updateMatrix();//先更新,getWorldPosition才能得到正确的
+            this.root.updateMatrixWorld(true)
+            
+            
+            var scale
+            if(info.restricMeshScale){//仅限制最大或最小的话,不判断像素大小,直接限制mesh的scale
+                var dis = camera.position.distanceTo(this.root.getWorldPosition(new THREE.Vector3()))
+                if(dis < info.nearBound){
+                    scale = info.scale * dis / info.nearBound
+                }else{
+                    scale = info.scale
+                }
+            }else{ 
+                scale = math.getScaleForConstantSize($.extend(info,{//规定下最小最大像素 
+                    camera , position:this.root.getWorldPosition(new THREE.Vector3()) ,
+                    resolution: e.viewport.resolution//2
+                }))
+                
+            } 
+            
+            if(!isNaN(scale)){
+                this.root.scale.set(scale, scale, scale); 
+            } 
+        }
+        this.root.updateMatrix();
+        this.root.updateMatrixWorld(true)
+        this.matrixMap.set(e.viewport, this.root.matrix.clone())
+        
+        this.useViewport = e.viewport
+    }
+    
+    applyMatrix(e){
+        if(!e)e = {viewport:viewer.mainViewport}//随便写一个viewport
+        if(e.viewport.name == 'magnifier')return
+        if(this.viewports && !this.viewports.includes(e.viewport) )return
+        if( !this.root || !this.realVisible()  )return
+        
+        var matrix = this.matrixMap.get(e.viewport);
+          
+        if(!matrix){
+            this.update(e)
+            matrix = this.matrixMap.get(e.viewport);
+        }
+        
+        if(e.viewport == this.useViewport){
+            return
+        }       
+        this.useViewport = e.viewport   
+        this.root.matrix.copy(matrix) 
+        this.root.updateMatrixWorld(true)
+        //console.log(this.root.name + e.viewport.name + " : "+this.root.matrixWorld.elements)
+    }
+    
+    setUniforms(name,value){
+        this.material.setUniforms(name,value) 
+    }
+    
+     
+    dispose(){
+        this.removeAllListeners()
+        this.parent && this.parent.remove(this)
+    }
+}

+ 161 - 0
src/custom/objects/Tag.js

@@ -0,0 +1,161 @@
+
+
+
+import * as THREE from "../../../libs/three.js/build/three.module.js"; 
+
+import {LineDraw, MeshDraw} from "../utils/DrawUtil.js";  
+import {TextSprite} from './TextSprite.js' 
+import Sprite from './Sprite.js' 
+
+const renderOrders = {
+    line: 0 ,
+    spot: 15, //高过模型
+}
+const planeGeo = new THREE.PlaneGeometry(1,1)
+let texLoader = new THREE.TextureLoader() 
+
+let lineMat = new THREE.LineBasicMaterial({
+    color: '#ffffff', 
+})
+let spotMat 
+const defaultLineLength = 0.6
+const defaultSpotScale = 0.4
+
+class Tag extends THREE.Object3D{
+    constructor(o){
+        
+        super()
+        
+         
+        this.lineLength = o.lineLength != void 0 ? o.lineLength : defaultLineLength
+        this.position.copy(o.position)
+        this.normal = o.normal != void 0 ? o.normal : new THREE.Vector3(0,0,-1)
+        this.root = o.root
+        
+        
+        //this.matrixAutoUpdate = false
+        
+        this.build()
+        
+        /* this.spot.addEventListener('mouseover',()=>{
+            
+             
+        }) */
+        
+    }
+    
+    
+    
+    
+    build(){
+        
+        if(!spotMat){
+            spotMat = new THREE.MeshBasicMaterial({
+                transparent:true,
+                map: texLoader.load(Potree.resourcePath+'/textures/spot_default.png' ),  
+            })
+        }
+        let endPos = this.normal.clone().multiplyScalar(this.lineLength) 
+        
+  
+        this.line = LineDraw.createLine([
+            new THREE.Vector3(0,0,0), 
+            endPos
+        ],  {mat:lineMat})
+        
+        
+        let group = new THREE.Object3D()
+        this.spot = new THREE.Mesh(planeGeo, spotMat)  
+        this.spot.scale.set(defaultSpotScale,defaultSpotScale,defaultSpotScale) 
+        this.titleLabel = new TextSprite({root: group, text:'1', sizeInfo:{width2d:200}, 
+            textColor:{r:255,g:255,b:255,a:1.0},
+            backgroundColor:{r:0,g:0,b:0,a:0.8},
+            borderRadius: 6,  
+            fontsize:13,  fontWeight:'',//thick
+            renderOrder : renderOrders.spot, pickOrder:renderOrders.spot,
+        }) //更新sprite时,实际更新的是root: spot的矩阵
+        this.spot.renderOrder = renderOrders.spot;
+        /* const mainLabelProp = { 
+            backgroundColor: {r: defaultColor.r*255, g: defaultColor.g*255, b: defaultColor.b*255, a:config.measure.default.opacity},
+            textColor: {r: textColor.r*255, g: textColor.g*255, b: textColor.b*255, a: 1.0},
+            fontsize:16, 
+            useDepth : true ,
+            renderOrder : 5, pickOrder:5, 
+        } */
+            
+        this.titleLabel.position.set(0,0.4,0)
+        this.titleLabel.sprite.material.depthTest = this.titleLabel.sprite.material.depthWrite = true
+        
+        
+        group.position.copy(endPos)
+        group.add(this.spot)
+        group.add(this.titleLabel)
+        this.add(group);
+        this.add(this.line)
+        
+        
+        
+        
+        viewer.scene.tags.add(this)
+        
+        
+    }
+    
+    
+    changeTitle(title){
+        this.titleLabel.changeText(title)
+    }
+    
+    
+    updateMatrixWorld(force){ //重写,只为了将root当做parent
+         
+        this.updateMatrix() 
+        this.matrixWorld.multiplyMatrices( this.root.matrixWorld, this.matrix );
+         
+        const children = this.children;
+        for ( let i = 0, l = children.length; i < l; i ++ ) {
+            children[ i ].updateMatrixWorld( force );
+        }  
+    }
+    
+
+    updateWorldMatrix( updateParents, updateChildren ) {//重写,只为了将root当做parent
+ 
+        if ( updateParents === true && this.root !== null ) {
+            this.root.updateWorldMatrix( true, false );
+        }
+
+        if ( this.matrixAutoUpdate ) this.updateMatrix();
+        this.matrixWorld.multiplyMatrices( this.root.matrixWorld, this.matrix );
+
+        if ( updateChildren === true ) {
+            const children = this.children;
+            for ( let i = 0, l = children.length; i < l; i ++ ) {
+                children[ i ].updateWorldMatrix( false, true );
+            }
+        }
+
+    } 
+
+    
+    dispose(){
+        this.parent.remove(this);
+        this.titleLabel.dispatchEvent({type:'dispose'})
+        
+    } 
+    
+    
+    
+}
+
+
+
+
+export default Tag
+
+
+
+
+
+
+

+ 177 - 0
src/custom/objects/TextSprite.js

@@ -0,0 +1,177 @@
+
+
+// /**
+//  * adapted from http://stemkoski.github.io/Three.js/Sprite-Text-Labels.html
+//  */
+
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+import Sprite from './Sprite.js' 
+
+
+//可能还是要用html写,因为要加按钮和图片
+
+export class TextSprite extends THREE.Object3D{ 
+    //注:为了分两层控制scale,不直接extend Sprite
+	constructor( options={}){ 
+        super()
+		let map = new THREE.Texture();
+		map.minFilter = THREE.LinearFilter;
+		map.magFilter = THREE.LinearFilter;
+        
+        this.sprite = new Sprite( Object.assign({
+                root:this
+            }
+            ,options,
+            { 
+                map,
+            })
+        )
+        this.add(this.sprite)
+        
+        this.fontWeight = options.fontWeight == void 0 ? 'Bold' : options.fontWeight
+		this.rectBorderThick = options.rectBorderThick || 0
+		this.textBorderThick = options.textBorderThick || 0
+		this.fontface = 'Arial';
+		this.fontsize = options.fontsize ||  16; 
+        this.textBorderColor = options.textBorderColor || { r: 0, g: 0, b: 0, a: 0.0 };
+		this.backgroundColor = options.backgroundColor || { r: 255, g: 255, b: 255, a: 1.0 };
+		this.textColor = options.textColor || {r: 0, g: 0, b: 0, a: 1.0};
+        this.borderColor = options.borderColor || { r: 0, g: 0, b: 0, a: 0.0 };
+		this.borderRadius = options.borderRadius || 6;
+        if(options.text != void 0)this.setText(options.text)
+        this.name = options.name 
+         
+		//this.setText(text);
+        
+        
+        this.addEventListener('dispose', this.dispose.bind(this)) 
+	}
+
+	setText(text){
+		if (this.text !== text){
+			this.text = text + '';
+
+			this.updateTexture();
+		}
+	}
+
+	setTextColor(color){
+		this.textColor = color;
+
+		this.updateTexture();
+	}
+
+	setBorderColor(color){
+		this.borderColor = color;
+
+		this.updateTexture();
+	}
+
+	setBackgroundColor(color){
+		this.backgroundColor = color;
+
+		this.updateTexture();
+	}
+    setPos(pos){
+        this.position.copy(pos)
+        this.sprite.update()
+    }
+    update(){
+        this.sprite.update()
+    }
+    setVisible(v){
+        this.visible = v
+    }
+    setUniforms(name,value){
+        this.sprite.setUniforms(name,value)
+    }
+	updateTexture(){
+		let canvas = document.createElement('canvas');
+		let context = canvas.getContext('2d');
+		context.font = this.fontWeight + ' ' + this.fontsize + 'px ' + this.fontface; 
+       
+        //context["font-weight"] = 100; //语法与 CSS font 属性相同。
+		// get size data (height depends only on font size)
+        
+        //this.text = '啊啊啊啊啊啊fag'
+        
+		let metrics = context.measureText(this.text );
+		let textWidth = metrics.width;
+		let margin = new THREE.Vector2(this.fontsize, Math.max(  this.fontsize*0.4, 10)  );
+		let spriteWidth = 2 * margin.x + textWidth + 2 * this.rectBorderThick;
+		let spriteHeight = 2 * margin.y + this.fontsize + 2 * this.rectBorderThick; 
+		context.canvas.width = spriteWidth;
+		context.canvas.height = spriteHeight;
+		context.font = this.fontWeight + ' ' + this.fontsize + 'px ' + this.fontface; 
+
+         
+        let diff = 2//针对英文大部分在baseLine之上所以降低一点(metrics.fontBoundingBoxAscent - metrics.fontBoundingBoxDescent) / 2
+
+        context.textBaseline = "middle"
+        
+        // border color
+        context.strokeStyle = 'rgba(' + this.borderColor.r + ',' + this.borderColor.g + ',' +
+            this.borderColor.b + ',' + this.borderColor.a + ')';
+  
+        context.lineWidth = this.rectBorderThick;
+		// background color
+		context.fillStyle = 'rgba(' + this.backgroundColor.r + ',' + this.backgroundColor.g + ',' +
+			this.backgroundColor.b + ',' + this.backgroundColor.a + ')';
+        this.roundRect(context, this.rectBorderThick / 2, this.rectBorderThick / 2,
+            spriteWidth - this.rectBorderThick, spriteHeight - this.rectBorderThick, this.borderRadius);
+        
+		// text color
+        if(this.textBorderThick){
+            context.strokeStyle = 'rgba(' + this.textBorderColor.r + ',' + this.textBorderColor.g + ',' +
+                this.textBorderColor.b + ',' + this.textBorderColor.a + ')';
+            context.lineWidth = this.textBorderThick;
+            context.strokeText(this.text , this.rectBorderThick + margin.x,spriteHeight/2  + diff );
+        }
+        
+		context.fillStyle = 'rgba(' + this.textColor.r + ',' + this.textColor.g + ',' +
+			this.textColor.b + ',' + this.textColor.a + ')';
+		context.fillText(this.text , this.rectBorderThick + margin.x, spriteHeight/2  + diff );//x,y
+
+		let texture = new THREE.Texture(canvas);
+		texture.minFilter = THREE.LinearFilter;
+		texture.magFilter = THREE.LinearFilter;
+		texture.needsUpdate = true;
+		//this.material.needsUpdate = true; 
+        
+        if(this.sprite.material.map){
+            this.sprite.material.map.dispose()
+        }
+		this.sprite.material.map = texture;
+		 
+        
+        
+		this.sprite.scale.set(spriteWidth * 0.01, spriteHeight * 0.01, 1.0);
+	}
+
+	roundRect(ctx, x, y, w, h, r){
+		ctx.beginPath();
+		ctx.moveTo(x + r, y);
+		ctx.lineTo(x + w - r, y);
+        ctx.arcTo(x + w, y, x + w, y + r, r );//圆弧。前四个参数同quadraticCurveTo
+		//ctx.quadraticCurveTo(x + w, y, x + w, y + r); //二次贝塞尔曲线需要两个点。第一个点是用于二次贝塞尔计算中的控制点,第二个点是曲线的结束点。
+		ctx.lineTo(x + w, y + h - r);
+		ctx.arcTo(x + w, y + h, x + w - r, y + h, r );
+		ctx.lineTo(x + r, y + h);
+		ctx.arcTo(x, y + h, x, y + h - r, r );
+		ctx.lineTo(x, y + r);
+		ctx.arcTo(x, y, x + r, y, r );
+		ctx.closePath();
+		ctx.fill();
+		ctx.stroke();
+	}
+    
+    dispose(){
+        this.sprite.material.uniforms.map.value.dispose()
+        this.parent && this.parent.remove(this)
+        this.sprite.dispatchEvent({type:'dispose'})
+        this.removeAllListeners()
+    }
+
+}
+
+

+ 36 - 0
src/custom/objects/fireParticle/Tween.js

@@ -0,0 +1,36 @@
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import Common  from "../../utils/Common.js";
+
+
+
+class Tween {
+
+    constructor(times, values) {
+      this.times = times || []
+      this.values = values || []
+    }
+  
+    lerp(t) {
+      if(this.times.length == 0) return
+      let i = 0, n = this.times.length
+      while(i < n && t > this.times[i]) i++
+      if(i == 0) return this.values[0]
+      if(i == n) return this.values[n-1]
+      const ratio = (t - this.times[i-1]) / (this.times[i] - this.times[i-1])
+      if(this.values[0] instanceof THREE.Vector3) {
+        return this.values[i-1].clone().lerp(this.values[i], ratio)
+      } else {
+        return this.values[i-1] + ratio * (this.values[i] - this.values[i-1])
+      }
+      
+    }
+    
+    
+    clone () {
+        return Common.CloneClassObject(this)
+	}
+    
+  
+  }
+  
+  export default Tween

+ 475 - 0
src/custom/objects/fireParticle/explode/ExplodeParticle.js

@@ -0,0 +1,475 @@
+import*as THREE from "../../../../../libs/three.js/build/three.module.js";
+import {vertexShader, fragmentShader} from './shader.js'
+import Tween from '../Tween.js'
+import Particle from './Particle.js'
+import {util} from './Util.js'
+import {Shape} from './const.js'
+
+let particleTexture
+
+const getTexture = ()=>{
+    if (!particleTexture) {
+        particleTexture = new THREE.TextureLoader().load(Potree.resourcePath + '/textures/explode.png')
+    }
+    return particleTexture
+}
+const sphereGeo = new THREE.SphereBufferGeometry(1, 10,4);
+const sphereMat = new THREE.MeshBasicMaterial({wireframe:true, color:"#ffffff"})
+
+const defaults = {
+    position: new THREE.Vector3(0,0,1),
+
+    positionShape: Shape.SPHERE,
+
+    positionRange: new THREE.Vector3(1,1,1),
+    //cube
+
+    radius: 1.3,
+    //sphere
+
+    velocityShape: Shape.SPHERE,
+
+    velocity: new THREE.Vector3(0,0,2),
+    //cube
+    velocityRange: new THREE.Vector3(0,0,3),
+    
+    //sphere
+    speed: 0.4, 
+    speedRange: 1,
+
+    size: 0.4,
+    sizeRange: 2,
+    //sizeTween: new Tween( [0, 0.05, 0.3, 0.45], [0, 1, 3, 0.1] ),
+    sizeTween: [[0, 0.04, 0.2, 1],[0.1, 1, 6, 8]] ,
+
+    color: new THREE.Vector3(1.0,1.0,1.0),
+    colorRange: new THREE.Vector3(0,0,0),
+    colorTween: new Tween(),
+
+    opacity: 1.0,
+    opacityRange: 0.0,
+    opacityTween: new Tween([0, 0.06, 0.3, 0.8, 1],[0, 1, 0.3, 0.05, 0]),
+    blendMode: THREE.AdditiveBlending,
+
+    acceleration: 0.5,
+    accelerationRange: 0,
+
+    angle: 0,
+    angleRange: 0,
+    angleVelocity: 0,
+    angleVelocityRange: 0,
+    angleAcceleration: 0,
+    angleAccelerationRange: 0,
+    
+    strength:1,
+    
+    //particlesPerSecond: 8,
+    particleDeathAge: 0.7 , 
+    recycleTimes : 3 , //每个粒子在一次爆炸后循环次数,循环完毕进入particleSpaceTime,等待下一次爆炸.
+    //爆炸时长: particleDeathAge * (recycleTimes+1)
+    particleSpaceTime:   3, //间隔
+    
+    
+}
+
+class ExplodeParticle extends THREE.Points {
+
+    constructor(params) {
+        super()
+         
+
+        this.age = 0
+        this.alive = true
+        //this.deathAge = 60
+        this.loop = true
+
+        this.blendMode = THREE.NormalBlending
+
+        this.setParameters(params)
+        this.createParticles()
+        this.frustumCulled = false//似乎是禁止相机裁剪,否则会在某些角度消失。但是会不会更耗性能呢?
+        //------------------------------------
+
+        this.setSize({viewport:viewer.mainViewport})
+        this.setFov(viewer.fov)
+        
+        let setSize = (e)=>{
+            if(e.viewport.name != "MainView")return
+            this.setSize(e)
+        }
+        let setFov = (e)=>{
+            this.setFov(e.fov) 
+        }
+            
+        /* let reStart = (e)=>{
+            if(e.v){//重新一个个放出粒子,否则会一股脑儿全部出来,因为同时大于粒子周期了一起重新生成出现。
+                setTimeout(()=>{//会先update一次delta为pageUnvisile的时间才触发 
+                    //console.log('归零') 
+                    //this.reStart()
+                },1) 
+            } 
+        } */
+        viewer.addEventListener('resize',setSize) 
+        viewer.addEventListener('fov_changed',setFov)
+        //viewer.addEventListener('pageVisible', reStart)
+        
+        this.addEventListener('dispose',()=>{
+            viewer.removeEventListener('resize',setSize) 
+            viewer.removeEventListener('fov_changed',setFov)
+            //viewer.removeEventListener('pageVisible', reStart)
+        })  
+ 
+        
+    }
+    
+    
+    computeParams(){
+        if(this.curve){
+            this.position.copy(this.curve.points[0])
+        }
+         
+         
+         
+        const minSize = 0.8, maxSize = 10, minRadiusBound = 0.2, maxRadiusBound = 20;
+        let size = minSize + (maxSize - minSize) * THREE.Math.smoothstep(this.radius*this.strength , minRadiusBound, maxRadiusBound);
+        
+        this.sizeTween.values = this.defaultSizeTween.values.map(e=> e*size)
+        
+        this.particleCount = Math.ceil( this.strength  * this.radius * 5 /*  * this.radius  * this.radius */  )
+     
+        this.speed = defaults.speed * this.radius;
+        this.speedRange = defaults.speedRange * this.radius;
+     
+        console.log(this.particleCount)
+        
+        {
+            this.boundPoints = []
+            this.boundPoints.push(this.position.clone())    
+            let maxSize = this.sizeTween.values.slice().sort((a,b)=>b-a)[0]  
+            let margin = maxSize * 0.35 + 0.5;  
+            let scale = this.radius+margin
+            let sphere = new THREE.Sphere(this.position, scale)//加上防止剪裁
+            this.boundingSphere = sphere //虽然还是会有一些后续移动的会超出 
+            this.boundingBox = new THREE.Box3().setFromCenterAndSize(this.position, new THREE.Vector3(scale*2,scale*2,scale*2)) 
+            /* if(!this.debugSphere){
+                this.debugSphere = new THREE.Mesh(sphereGeo, sphereMat)
+                this.add(this.debugSphere)
+            } 
+            this.debugSphere.scale.set(scale,scale,scale)  */
+        }
+    }
+    getPointsForBound(){
+        return this.boundPoints; //可以用于expand实时bound的点, 不含particle的size等边距
+    }   
+
+    reStart(){
+        this.age = 0;
+        
+        this.createParticles()  
+    }
+
+
+    setParameters(params) {
+        
+        params = $.extend({}, defaults, params)
+         
+         
+        for (var key in params) { 
+            let value = params[key] 
+            if (key == 'position')
+                this.position.copy(value)
+            else if (value instanceof Array && value[0]instanceof Array){
+                this[key] = new Tween(...value)
+            }else if(value instanceof THREE.Vector3 || value instanceof THREE.Color){
+                this[ key ] = value.clone()
+            }else{
+                this[key] = value;
+            }
+        }
+        
+        
+        this.defaultSizeTween = this.sizeTween.clone()
+        //Object.assign(this, params) 
+
+        this.particles = []
+        this.age = 0.0
+        this.alive = true
+        
+        this.geometry = new THREE.BufferGeometry() 
+        this.computeParams() 
+        this.material = new THREE.ShaderMaterial({
+            uniforms: {
+                u_sampler: {
+                    value: this.texture || getTexture()
+                },
+                heightOfNearPlane: {
+                    type: "f",
+                    value: 0
+                }//相对far ,以确保画面缩放时点的大小也会缩放
+            },
+            vertexShader,
+            fragmentShader,
+            transparent: true,
+            alphaTest: 0.5,
+            depthTest: this.blendMode == THREE.NormalBlending,
+            blending: this.blendMode
+        })
+         
+         
+    }
+
+
+    createParticles() {
+        this.particles = []
+        const count = this.particleCount
+        const positionArray = new Float32Array(count * 3)
+        const colorArray = new Float32Array(count * 3)
+
+        const sizeArray = new Float32Array(count)
+        const angleArray = new Float32Array(count)
+        const opacityArray = new Float32Array(count)
+        const visibleArray = new Float32Array(count)
+
+        for (let i = 0; i < count; i++) {
+            const particle = this.createParticle()
+            /* positionArray[i * 3] = particle.position.x
+            positionArray[i * 3 + 1] = particle.position.y
+            positionArray[i * 3 + 2] = particle.position.z
+            colorArray[i * 3] = particle.color.r
+            colorArray[i * 3 + 1] = particle.color.g
+            colorArray[i * 3 + 2] = particle.color.b
+            sizeArray[i] = particle.size
+            angleArray[i] = particle.angel
+            opacityArray[i] = particle.opacity
+            visibleArray[i] = particle.alive */
+            this.particles[i] = particle
+        }
+        this.geometry.setAttribute('position', new THREE.BufferAttribute(positionArray,3))
+        this.geometry.setAttribute('color', new THREE.BufferAttribute(colorArray,3))
+        this.geometry.setAttribute('angle', new THREE.BufferAttribute(angleArray,1))
+        this.geometry.setAttribute('size', new THREE.BufferAttribute(sizeArray,1))
+        this.geometry.setAttribute('visible', new THREE.BufferAttribute(visibleArray,1))
+        this.geometry.setAttribute('opacity', new THREE.BufferAttribute(opacityArray,1))
+
+        
+
+    }
+    
+    
+    
+     
+
+    createParticle() {
+
+        const particle = new Particle()
+        particle.sizeTween = this.sizeTween
+        particle.colorTween = this.colorTween
+        particle.opacityTween = this.opacityTween
+        particle.deathAge = this.particleDeathAge
+
+        if (this.positionShape == Shape.CUBE) {
+            particle.position = util.randomVector3(new THREE.Vector3, this.positionRange)
+        }
+
+        if (this.positionShape == Shape.SPHERE) {
+            /* const z = 2 * Math.random() - 1
+              const t = Math.PI * 2 * Math.random()
+              const r = Math.sqrt(1 - z*z)
+              const vec3 = new THREE.Vector3(r * Math.cos(t), r * Math.sin(t), z)
+              particle.position = vec3.multiplyScalar(this.radius)  */
+
+            const y = 2 * Math.random() - 1
+            const t = Math.PI * 2 * Math.random();
+            const r = Math.sqrt(1 - y * y);
+            const vec3 = new THREE.Vector3(r * Math.cos(t),y,r * Math.sin(t));
+            particle.position = vec3.multiplyScalar(this.radius)
+
+        }
+
+        if (this.velocityShape == Shape.CUBE) {
+            particle.velocity = util.randomVector3(this.velocity, this.velocityRange)
+
+        }
+
+        if (this.velocityShape == Shape.SPHERE) {
+            const direction = new THREE.Vector3().addVectors(particle.position, new THREE.Vector3(0,0,this.radius*2))//向上升?
+            const speed = util.randomValue(this.speed, this.speedRange)
+            particle.velocity = direction.normalize().multiplyScalar(speed)
+        }
+
+        particle.acceleration = util.randomValue(this.acceleration, this.accelerationRange)
+
+        particle.angle = util.randomValue(this.angle, this.angleRange)
+        particle.angleVelocity = util.randomValue(this.angleVelocity, this.angleVelocityRange)
+        particle.angleAcceleration = util.randomValue(this.angleAcceleration, this.angleAccelerationRange)
+
+        particle.size = util.randomValue(this.size, this.sizeRange)
+
+        const color = util.randomVector3(this.color, this.colorRange)
+        particle.color = new THREE.Color().setHSL(color.x, color.y, color.z)
+
+        particle.opacity = util.randomValue(this.opacity, this.opacityRange)
+     
+
+        return particle
+    }
+
+    update(dt) {
+        if(!viewer.getObjVisiByReason(this,'force')){//被手动隐藏了
+            return
+        }
+        if(this.delayStartTime>0){ // 爆炸延迟
+            return this.delayStartTime -= dt 
+        }
+        
+        
+        
+        if(!Potree.Utils.isInsideFrustum(this.boundingSphere, viewer.scene.getActiveCamera())){
+            viewer.updateVisible(this,'isInsideFrustum', false ) //不在视野范围
+            return
+        }else{
+            viewer.updateVisible(this,'isInsideFrustum', true )
+        }
+        //const timeRatio = 0.5
+        if(dt > 1){
+            console.log('update dt>1', dt)
+        }
+        //dt *= timeRatio
+        let particleDeathAge = this.particleDeathAge/*  * timeRatio */
+        let particleSpaceTime = this.particleSpaceTime /* * timeRatio */
+
+        const recycleIndices = []
+        const recycleAges = []
+        const recycleRebornCount = []
+        
+        const positionArray = this.geometry.attributes.position.array
+        const opacityArray = this.geometry.attributes.opacity.array
+        const visibleArray = this.geometry.attributes.visible.array
+        const colorArray = this.geometry.attributes.color.array
+        const angleArray = this.geometry.attributes.angle.array
+        const sizeArray = this.geometry.attributes.size.array
+
+        for (let i = 0; i < this.particleCount; i++) {
+            const particle = this.particles[i]
+            if (particle.alive) {
+                particle.update(dt)
+                if (particle.age > particleDeathAge) { 
+                    particle.alive = 0.0
+                    if(particle.rebornCount >= this.recycleTimes){
+                        particle.deadAge = particle.age - particleDeathAge //已死亡时间
+                    }else{//直接循环
+                        recycleIndices.push(i)
+                        recycleAges.push(/* ( */particle.age - particleDeathAge/* )%(this.particleDeathAge ) */)
+                        recycleRebornCount.push(particle.rebornCount+1)
+                    }
+                    
+                }
+                positionArray[i * 3] = particle.position.x
+                positionArray[i * 3 + 1] = particle.position.y
+                positionArray[i * 3 + 2] = particle.position.z
+                colorArray[i * 3] = particle.color.r
+                colorArray[i * 3 + 1] = particle.color.g
+                colorArray[i * 3 + 2] = particle.color.b
+                visibleArray[i] = particle.alive
+                opacityArray[i] = particle.opacity
+                angleArray[i] = particle.angle
+                sizeArray[i] = particle.size
+            }else{
+                if(particle.rebornCount >= this.recycleTimes){
+                    if(particle.age > particleDeathAge) {//其他已经死亡的粒子的时间继续增加
+                        particle.deadAge += dt
+                    }
+                }
+            }                
+                
+             
+            
+            if (particle.rebornCount >= this.recycleTimes && particle.age > particleDeathAge) {//已经死亡 
+                if(particle.deadAge >=  particleSpaceTime){//死亡时间超过设定的间隔时间后重启 
+                    recycleIndices.push(i)
+                    let wholeTime = particleDeathAge * (this.recycleTimes+1) + particleSpaceTime 
+                    recycleAges.push((particle.deadAge - particleSpaceTime)% wholeTime ) //剩余时间就是重生后的age
+                    recycleRebornCount.push(0)
+                } 
+            }
+            
+        }
+        
+        
+        
+
+        this.geometry.attributes.size.needsUpdate = true
+        this.geometry.attributes.color.needsUpdate = true
+        this.geometry.attributes.angle.needsUpdate = true
+        this.geometry.attributes.visible.needsUpdate = true
+        this.geometry.attributes.opacity.needsUpdate = true
+        this.geometry.attributes.position.needsUpdate = true
+
+        if (!this.alive)
+            return
+
+        if (this.age < particleDeathAge) {
+            let startIndex = Math.round(this.particleCount * (this.age + 0)/ particleDeathAge)
+            let endIndex = Math.round(this.particleCount * (this.age + dt)/ particleDeathAge)
+            if (endIndex > this.particleCount) {
+                endIndex = this.particleCount
+            }
+            for (let i = startIndex; i < endIndex; i++) {
+                this.particles[i].alive = 1.0
+            }
+        }
+
+
+
+        for (let j = 0; j < recycleIndices.length; j++) {
+            let i = recycleIndices[j]
+            
+            this.particles[i] = this.createParticle()
+            this.particles[i].alive = 1.0  //出生
+            this.particles[i].age = recycleAges[j]  
+            this.particles[i].rebornCount= recycleRebornCount[j]
+            /* if(this.particles[i].age < particleDeathAge){
+                positionArray[i * 3] = this.particles[i].position.x
+                positionArray[i * 3 + 1] = this.particles[i].position.y
+                positionArray[i * 3 + 2] = this.particles[i].position.z
+                visibleArray[i] = particle.alive?
+            } */
+        }
+        this.geometry.attributes.position.needsUpdate = true
+
+        this.age += dt
+
+        if (this.age > this.deathAge && !this.loop) {
+            this.alive = false
+        }
+
+    }
+
+    setSize(e) {
+        let viewport = e.viewport
+        this.screenHeight = viewport.resolution.y
+        this.setPerspective(this.fov, this.screenHeight)
+    }
+
+    setFov(fov) {
+        this.fov = fov
+        this.setPerspective(this.fov, this.screenHeight)
+    }
+
+    setPerspective(fov, height) {
+        //this.uniforms.heightOfNearPlane.value = Math.abs(height / (2 * Math.tan(THREE.Math.degToRad(fov * 0.5))));
+        let far = Math.abs(height / (2 * Math.tan(THREE.Math.degToRad(fov * 0.5))));
+        this.material.uniforms.heightOfNearPlane.value = far
+    }
+    updateGeometry(){ 
+        this.computeParams()
+        this.reStart()  
+    }
+    dispose(){
+        this.geometry.dispose();
+        this.material.dispose();
+        this.dispatchEvent('dispose') 
+    }
+}
+
+export default ExplodeParticle

+ 57 - 0
src/custom/objects/fireParticle/explode/Particle.js

@@ -0,0 +1,57 @@
+// import { vertexShader, fragmentShader } from './shader'
+// import Tween from './tween'
+import * as THREE from "../../../../../libs/three.js/build/three.module.js";
+
+const DEG2RAD = Math.PI / 180
+
+class Particle {
+  
+  constructor() {
+    
+    this.position = new THREE.Vector3()
+    this.velocity = new THREE.Vector3()
+     
+    
+    this.angle = 0
+    this.angleVelocity = 0
+    this.angleAcceleration = 0
+    this.size = 16
+    this.color = new THREE.Color()
+    this.opacity = 1
+
+    this.rebornCount = 0//重生次数
+    this.age = 0
+    this.alive = 0 //注意,一开始时是未出生的
+    this.deadAge = 0//已死亡时间
+    this.sizeTween = null
+    this.colorTween = null
+    this.opacityTween = null
+  }
+
+  update(dt) { 
+    //s = s0 + (v0 + at) * t 或 lastS + delta(vt)
+    
+    this.position.add(this.velocity.clone().multiplyScalar(dt))
+    this.velocity.multiplyScalar( 1+this.acceleration*dt )
+    
+    this.angle += this.angleVelocity * DEG2RAD * dt
+    this.angleVelocity += this.angleAcceleration * DEG2RAD * dt
+    this.age += dt
+
+    if(this.sizeTween.times.length > 0) {
+      this.size = this.sizeTween.lerp(this.age/this.deathAge)
+    }
+
+    if(this.colorTween.times.length > 0) {
+      const colorHSL = this.colorTween.lerp(this.age/this.deathAge)
+      this.color = new THREE.Color().setHSL(colorHSL.x, colorHSL.y, colorHSL.z)
+    }
+
+    if(this.opacityTween.times.length > 0) {
+      this.opacity = this.opacityTween.lerp(this.age/this.deathAge)
+    }
+  }
+
+}
+
+export default Particle

+ 18 - 0
src/custom/objects/fireParticle/explode/Util.js

@@ -0,0 +1,18 @@
+import * as THREE from "../../../../../libs/three.js/build/three.module.js";
+export class Util {
+    constructor() {}
+
+    randomValue(min, max) {
+        //return min + max * (Math.random() - 0.5)
+        let p = Math.random()
+        return min * p + max * (1-p)
+    }
+
+    randomVector3(min, max) {
+        const rand3 = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5)
+        return new THREE.Vector3().addVectors(min, new THREE.Vector3().multiplyVectors(max, rand3))
+    }
+}
+
+const util = new Util();
+export { util };

+ 4 - 0
src/custom/objects/fireParticle/explode/const.js

@@ -0,0 +1,4 @@
+export const Shape = {
+    CUBE: 1,
+    SPHERE: 2
+  }

+ 40 - 0
src/custom/objects/fireParticle/explode/shader.js

@@ -0,0 +1,40 @@
+export const vertexShader = `
+  attribute vec3 color;
+  attribute float size;
+  attribute float angle;
+  attribute float opacity;
+  attribute float visible;
+  varying vec4 vColor;
+  varying float vAngle;
+  uniform float heightOfNearPlane;
+  
+  void main() {
+    if(visible > 0.5) {
+      vColor = vec4(color, opacity);
+    } else {
+      vColor = vec4(0.0, 0.0, 0.0, 0.0);
+    }
+    vAngle = angle;
+    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
+    gl_Position = projectionMatrix * mvPosition;
+    
+    gl_PointSize = ( heightOfNearPlane * size ) / gl_Position.w;
+  }
+` 
+
+export const fragmentShader = `
+  uniform sampler2D u_sampler;
+  varying vec4 vColor;
+  varying float vAngle;
+  void main() {
+    gl_FragColor = vColor;
+    float u = cos(vAngle);
+    float v = sin(vAngle);
+    vec2 uv = vec2(
+      u * (gl_PointCoord.x - 0.5) + v * (gl_PointCoord.y - 0.5) + 0.5, 
+      u * (gl_PointCoord.y - 0.5) - v * (gl_PointCoord.x - 0.5) + 0.5
+    );
+    vec4 texture = texture2D(u_sampler, uv);
+    gl_FragColor = gl_FragColor * texture;
+  }
+`

+ 274 - 0
src/custom/objects/fireParticle/fire/FireParticle.js

@@ -0,0 +1,274 @@
+// import { vertexShader, fragmentShader } from './shaders'
+// import Tween from './tween'
+import * as THREE from "../../../../../libs/three.js/build/three.module.js";
+import { vertexShader, fragmentShader } from './shader.js'
+
+let ONE_SPRITE_ROW_LENGTH = 0.25    //对应着色器的0.25
+let texture 
+ 
+
+const getTexture = ()=>{
+    if(!texture){
+        texture = new THREE.TextureLoader().load( Potree.resourcePath+'/textures/fire.png')
+    }
+    return texture
+}
+
+const boxGeo = new THREE.BoxBufferGeometry(1,1,1,1);
+const boxMat = new THREE.MeshBasicMaterial({wireframe:true, color:"#ffffff"})
+
+
+
+class FireParticle extends THREE.Points{
+  
+    constructor (prop) {
+        super()
+        
+        for ( var key in prop ){
+            this[key] = prop[key]
+        }
+        
+        
+         
+        this.strength = this.strength || 1
+        
+        
+         
+        this.radius = prop.radius || 1;
+        this.height = prop.height || 5;  
+        
+        this.computeParams()
+        this.geometry = this.createGeometry( this.radius, this.height, this.particleCount );
+         
+        
+        if(this.color == void 0)   this.color = 0xff3200
+        this.createMaterial( );  //小蓝火:0x00338f
+        
+      
+         
+        //---?:
+        this.velocity = new THREE.Vector3()
+        this.acceleration = new THREE.Vector3()
+        
+        this.angle = 0
+        this.angleVelocity = 0
+        this.angleAcceleration = 0
+        this.size = 16
+        
+        this.opacity = 1
+
+        this.age = 0
+        this.alive = 0
+
+        this.sizeTween = null
+        this.colorTween = null
+        this.opacityTween = null
+
+        
+         
+        this.setSize({viewport:viewer.mainViewport})
+        this.setFov(viewer.fov)
+        
+        let setSize = (e)=>{
+            if(e.viewport.name != "MainView")return
+            this.setSize(e)
+        }
+        let setFov = (e)=>{
+            this.setFov(e.fov) 
+        }
+            
+        viewer.addEventListener('resize',setSize) 
+        viewer.addEventListener('fov_changed',setFov)
+          
+        this.addEventListener('dispose',()=>{
+            viewer.removeEventListener('resize',setSize) 
+            viewer.removeEventListener('fov_changed',setFov)
+        })  
+          
+    }
+
+ 
+    computeParams(){  
+        let length = (this.curve ? this.curve.wholeLength : 0) + this.radius * 2 //加上首尾的半径
+        
+        
+        const minSize = 0.3, maxSize = 3, minRadiusBound = 0.3, maxRadiusBound = 10;
+        this.size = minSize + (maxSize - minSize) * THREE.Math.smoothstep(this.radius, minRadiusBound, maxRadiusBound);
+        //console.log('fire material  particle size:', size )
+        
+        this.particleCount =  Math.ceil(   length * Math.sqrt(this.strength  * this.height )   * this.radius / (this.size * this.size) *  25  )        
+        //console.log('fire particleCount',this.particleCount)
+    }
+    getPointsForBound(){
+        return this.boundPoints; //可以用于expand实时bound的点, 不含particle的size等边距
+    }
+
+    getBound(points){ // points为生成点(圆心)
+        this.boundPoints = []  
+        let boundingBox = new THREE.Box3()
+        
+         
+        let margin = this.size * 0.13 + 0.3; 
+        
+        points.forEach(bottom=>{ 
+            let top = bottom.clone()
+            top.z +=  this.height 
+            boundingBox.expandByPoint(bottom);
+            boundingBox.expandByPoint(top);
+            this.boundPoints.push(bottom,top)            
+        })
+        let xyExpand = this.radius+margin 
+        boundingBox.expandByVector(new THREE.Vector3(xyExpand,xyExpand,margin)) 
+        this.boundingBox = boundingBox
+        
+        /* if(!this.debugBox){
+            this.debugBox = new THREE.Mesh(boxGeo, boxMat)
+            this.add(this.debugBox)
+        }
+        
+        this.debugBox.scale.copy(boundingBox.getSize(new THREE.Vector3))
+        this.debugBox.position.copy(boundingBox.getCenter(new THREE.Vector3))  */
+         
+    }
+
+
+    createGeometry( radius, height, particleCount){
+        let geometry = new THREE.BufferGeometry()
+         
+        
+        let count , points
+        if(this.positions.length>1){
+             
+            const spaceDis = 0.2;//间隔距离
+            
+            count = Math.ceil(this.curve.wholeLength / spaceDis) + 1 
+            //console.log('count', count)
+            points = this.curve.getSpacedPoints( count );  //得到的数量会比count多一个
+            count = points.length  
+            //得到的点不太均匀,两端容易点少。
+            this.getBound(points) 
+            
+        }else{
+            this.getBound(this.positions) 
+        }
+         
+        
+        var position = new Float32Array(particleCount * 3);
+        var randam = new Float32Array(particleCount);
+        var sprite = new Float32Array(particleCount);
+        var centerHeight = new Float32Array(particleCount);
+        
+        for (var i = 0; i < particleCount; i++) {
+            
+            var center = new THREE.Vector3().copy(this.positions.length>1 ? points[Math.floor(i/particleCount * count)] : this.positions[0])
+            centerHeight[i] = center.z
+            
+            if (i === 0) { 
+                // to avoid going out of Frustum
+                position[i * 3 + 0] = center.x;
+                position[i * 3 + 1] = center.y;
+                position[i * 3 + 2] = center.z;
+            }else{  
+                var r = Math.sqrt(Math.random()) * radius;
+                var angle = Math.random() * 2 * Math.PI;
+                position[i * 3 + 0] = center.x + Math.cos(angle) * r; 
+                position[i * 3 + 1] = center.y + Math.sin(angle) * r;
+                position[i * 3 + 2] = center.z + (radius - r) / radius * height/2 + height/2; //不太明白这句为什么能达到height高度
+               
+                sprite[i] = 0.25 * (Math.random() * 4 | 0);
+                randam[i] = Math.random();
+                //center在底部
+            }
+            
+            
+        }
+        
+        geometry.setAttribute('centerHeight', new THREE.BufferAttribute(centerHeight, 1));
+        geometry.setAttribute('position', new THREE.BufferAttribute(position, 3));
+        geometry.setAttribute('randam', new THREE.BufferAttribute(randam, 1));
+        geometry.setAttribute('sprite', new THREE.BufferAttribute(sprite, 1));
+        return geometry;
+    }
+    
+    
+    
+    
+    updateGeometry(){ 
+        this.computeParams()
+        this.geometry.dispose() 
+        this.geometry = this.createGeometry( this.radius, this.height, this.particleCount )
+        this.material.uniforms.size.value = this.size
+    }
+
+
+
+
+    createMaterial(){
+         
+        
+        const material = new THREE.ShaderMaterial( {
+            uniforms:{
+                color: { type: "c", value: new THREE.Color(this.color) },
+                size: { type: "f", value: this.size},
+                u_sampler: { type: "t", value: getTexture() },
+                time: { type: "f", value: 0.0 },
+                heightOfNearPlane: { type: "f", value:0},  //相对far ,以确保画面缩放时点的大小也会缩放
+                height :{ type: "f", value:this.height}  ,
+            },
+            vertexShader,
+            fragmentShader,
+            blending: THREE.AdditiveBlending, //加法融合模式 glBlendFunc(GL_ONE, GL_ONE)
+            depthTest: true,
+            depthWrite: false,
+            transparent: true
+
+        } ); 
+        this.material = material
+        this.setPerspective(this.fov, this.screenHeight)
+    }
+
+
+    setSize(e){
+        let viewport = e.viewport
+        this.screenHeight = viewport.resolution.y
+        this.setPerspective(this.fov, this.screenHeight)  
+    }
+    
+    setFov(fov){
+        this.fov = fov
+        this.setPerspective(this.fov, this.screenHeight) 
+    }
+    
+    
+    setPerspective(fov, height){
+        //this.uniforms.heightOfNearPlane.value = Math.abs(height / (2 * Math.tan(THREE.Math.degToRad(fov * 0.5))));
+        let far = Math.abs(height / (2 * Math.tan(THREE.Math.degToRad(fov * 0.5))));
+        this.material.uniforms.heightOfNearPlane.value = far 
+    }
+
+    
+    
+    update(delta){
+        if(!viewer.getObjVisiByReason(this,'force')){//被手动隐藏了
+            return
+        }
+        if(!Potree.Utils.isInsideFrustum(this.boundingBox, viewer.scene.getActiveCamera())){
+            viewer.updateVisible(this,'isInsideFrustum', false ) //不在视野范围
+            //console.log('unvi')
+            return
+        }else{
+            viewer.updateVisible(this,'isInsideFrustum', true )
+        } 
+        delta *= 1//更改速度
+        
+        this.material.uniforms.time.value = (this.material.uniforms.time.value + delta) % 1; 
+    }
+    
+    dispose(){
+        this.geometry.dispose();
+        this.material.dispose();
+        this.dispatchEvent('dispose') 
+    }
+}
+
+export default FireParticle

+ 63 - 0
src/custom/objects/fireParticle/fire/shader.js

@@ -0,0 +1,63 @@
+export const vertexShader = `
+    attribute float randam;
+    attribute float sprite;
+    attribute float centerHeight;  //add
+    
+    //uniform float fireHeight;  //add 
+    uniform float time;
+    uniform float size;
+    uniform float heightOfNearPlane;
+    
+    
+    
+    
+    //varying float heightRatio;
+    varying float vSprite;
+    varying float vOpacity; 
+    float PI = 3.14;
+
+    float quadraticIn( float t ) 
+    { 
+        float tt = t * t;
+        return tt * tt; 
+        //变化曲线  越来越快
+    } 
+    
+    void main() {
+        float progress = fract( time + ( 2.0 * randam - 1.0 ) );
+        float progressNeg = 1.0 - progress;
+        float ease = quadraticIn( progress );
+        float influence = sin( PI * ease );
+        //vec3 newPosition = position * vec3( 1.0,  1.0 , ease);
+        vec3 newPosition = position;
+        newPosition.z = (newPosition.z - centerHeight) * ease + centerHeight;
+         
+        gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
+        gl_PointSize = ( heightOfNearPlane * size ) / gl_Position.w;
+        vOpacity = min( influence * 4.0, 1.0 ) * progressNeg;
+        vSprite = sprite;
+        
+        //heightRatio = (newPosition.z - centerHeight) / fireHeight ;
+        
+    }
+` 
+
+export const fragmentShader = `
+    uniform vec3 color;
+    uniform sampler2D u_sampler;
+
+    varying float vSprite;
+    varying float vOpacity;
+    //varying float heightRatio;
+
+    void main() 
+    {
+        
+       
+        vec2 texCoord = vec2(gl_PointCoord.x * 0.25 + vSprite, gl_PointCoord.y);
+         
+        gl_FragColor = vec4( texture2D( u_sampler, texCoord ).xyz * color * vOpacity, 1.0 );
+          
+         
+    }
+`

+ 57 - 0
src/custom/objects/fireParticle/smoke/Particle.js

@@ -0,0 +1,57 @@
+import * as THREE from "../../../../../libs/three.js/build/three.module.js";
+import Tween from '../Tween.js'
+export default class Particle{
+    constructor(prop={}){
+        this.position     = new THREE.Vector3();
+        this.velocity     = new THREE.Vector3(); // units per second
+         
+        this.angle             = 0;
+        this.angleVelocity     = 0; // degrees per second
+        this.angleAcceleration = 0; // degrees per second, per second
+        this.size = 16.0;
+    
+        this.color   = new THREE.Color();
+        this.opacity = 1.0;
+                
+        this.age   = 0;
+        this.alive = 0; // use float instead of boolean for shader purposes	
+        this.lastChangeVage = 0 //add
+
+
+        this.sizeTween    = prop.sizeTween || new Tween( [0, 1], [32, 128] );
+		this.opacityTween = prop.opacityTween || new Tween( [0.8, 2], [0.5, 0] );
+		this.colorTween   = prop.colorTween || new Tween( [0.4, 1], [ new THREE.Vector3(0,0,0.2), new THREE.Vector3(0, 0, 0.5) ] );
+   
+   
+   
+   }
+
+    update(dt)
+    {
+        this.position.add(this.velocity.clone().multiplyScalar(dt))
+        this.velocity.multiplyScalar( 1+this.acceleration*dt )
+        
+        // convert from degrees to radians: 0.01745329251 = Math.PI/180
+        this.angle         += this.angleVelocity     * 0.01745329251 * dt;
+        this.angleVelocity += this.angleAcceleration * 0.01745329251 * dt;
+
+        this.age += dt;
+        
+        // if the tween for a given attribute is nonempty,
+        //  then use it to update the attribute's value
+
+        if ( this.sizeTween.times.length > 0 )
+            this.size = this.sizeTween.lerp( this.age/this.deathAge );
+                    
+        if ( this.colorTween.times.length > 0 )
+        {
+            var colorHSL = this.colorTween.lerp( this.age/this.deathAge );
+            this.color = new THREE.Color().setHSL( colorHSL.x, colorHSL.y, colorHSL.z );
+        }
+        
+        if ( this.opacityTween.times.length > 0 )
+        {
+            this.opacity = this.opacityTween.lerp( this.age/this.deathAge);
+        }
+    }
+}

+ 594 - 0
src/custom/objects/fireParticle/smoke/SmokeParticle.js

@@ -0,0 +1,594 @@
+import * as THREE from "../../../../../libs/three.js/build/three.module.js";
+import Particle from './Particle.js'
+import Tween from '../Tween.js'
+import { vertexShader, fragmentShader } from './shader.js'
+
+const Type = Object.freeze({ "CUBE":1, "SPHERE":2 });
+let particleTexture  
+
+const getTexture = ()=>{
+    if(!particleTexture){
+        particleTexture = new THREE.TextureLoader().load( Potree.resourcePath+'/textures/smokeparticle.png')
+    }
+    return particleTexture
+}
+const boxGeo = new THREE.BoxBufferGeometry(1,1,1,1);
+const boxMat = new THREE.MeshBasicMaterial({wireframe:true, color:"#ffffff"})
+
+
+
+
+const defaults = 
+{
+    positions: [],
+    positionStyle    : "sphere",
+    positionBase     : new THREE.Vector3( 0, 0, 0 ),
+ 
+    positionSpread   : new THREE.Vector3( 1, 1, 0), //cube
+     
+    radius   :   1,       // sphere
+        
+    velocityStyle    : 'cube',
+     
+    velocityBase     : new THREE.Vector3( 0,  0,  0.5),     // cube  基础速度
+    velocitySpread   : new THREE.Vector3( 1, 1, 0.3), 
+    
+    accelerationBase : 0.3,             //基础加速度
+    accelerationSpread : 0.6,	
+    
+    //没使用
+    speedBase  : 0.1,       //sphere
+    speedSpread : 0.5,
+          
+    
+
+    angleBase               : 0,
+    angleSpread             : 360,
+    angleVelocityBase       : 1,
+    angleVelocitySpread     : 30,
+    angleAccelerationBase   : 1,
+    angleAccelerationSpread : 5,
+        
+    sizeBase    :   0,  
+    sizeSpread  :   0,
+    sizeTween    : [[0, 0.3,   1], [0.3, 1.4,  6 ]], 
+    
+    
+    colorBase   :   new THREE.Vector3(0.0, 1.0, 0.5), 
+    colorSpread :   new THREE.Vector3(0.0, 0.0, 0.0),
+    colorTween   : new Tween( [0.2, 1], [ new THREE.Vector3(0,0,0.4), new THREE.Vector3(0, 0, 0.1) ] ),
+
+    opacityBase     :   0.1,//1.0,
+    opacitySpread   :   0.2,
+    opacityTween :[ [0, 0.1, 0.9, 1], [0.1, 0.4 , 0.03, 0 ] ], 
+     
+    //particlesPerSecond : 20,
+    strength : 1,
+    particleDeathAge   : 3,  //从底下升起后能持续的时间		
+    //emitterDeathAge    : 60 // time (seconds) at which to stop creating particles.
+    height : 3,
+};
+
+
+const debugSphere = new THREE.Mesh(new THREE.SphereBufferGeometry(0.03, 5,5), new THREE.MeshBasicMaterial({color:'white',depthTest:false}))
+
+export default class SmokeParticle extends THREE.Points{
+    constructor(prop={}) {
+        super()
+        
+         
+        this.blendStyle = THREE.NormalBlending; // false; 
+        this.emitterAge = 0.0;
+        //this.emitterAlive = true;
+       
+        prop = $.extend({}, defaults, prop)
+        for ( var key in prop ){
+            let value = prop[key] 
+            if(value instanceof Array && value[0] instanceof Array ) this[ key ] = new Tween(...value)
+            else if(value instanceof THREE.Vector3 || value instanceof THREE.Color){
+                this[ key ] = value.clone()
+            }else{
+                this[ key ] = value
+            }
+        }
+        
+        this.defaultSizeTween = this.sizeTween.clone()
+        this.defaultOpacityTween = this.opacityTween.clone()
+        
+        
+        this.geometry = new THREE.BufferGeometry()
+        this.computeParams()
+        this.createMaterial()
+        this.createGeometry()
+        
+        this.dynamic = true;
+        this.sortParticles = true; 
+        this.frustumCulled = false//似乎是禁止相机裁剪,否则会在某些角度消失。但是会不会更耗性能呢?
+       
+        prop.position && this.position.copy(prop.position)
+        
+        
+        
+        
+        //---------------------------------------
+        this.setSize({viewport:viewer.mainViewport})
+        this.setFov(viewer.fov)
+        
+        let setSize = (e)=>{
+            if(e.viewport.name != "MainView")return
+            this.setSize(e)
+        }
+        let setFov = (e)=>{
+            this.setFov(e.fov) 
+        }
+        /* let reStart = (e)=>{
+            if(e.v){//重新一个个放出粒子,否则会一股脑儿全部出来,因为同时大于粒子周期了一起重新生成出现。
+                setTimeout(()=>{//会先update一次delta为pageUnvisile的时间才触发 
+                    //console.log('归零') 
+                    //this.reStart()
+                },1) 
+            } 
+        } */
+        viewer.addEventListener('resize',setSize) 
+        viewer.addEventListener('fov_changed',setFov)
+        //viewer.addEventListener('pageVisible', reStart)
+        
+        this.addEventListener('dispose',()=>{
+            viewer.removeEventListener('resize',setSize) 
+            viewer.removeEventListener('fov_changed',setFov)
+            //viewer.removeEventListener('pageVisible', reStart)
+        })   
+        
+    }
+    
+    
+    
+    computeParams(){   
+         
+        let length = (this.curve ? this.curve.wholeLength : 0) + this.radius * 2 //加上首尾的半径
+        //注意:烟最低高度一米, 0<strength<1
+        if(this.positionStyle == 'cube'){
+            this.positionSpread.set(this.radius,this.radius,0)
+        } 
+        this.velocityBase.set(0,0, (this.height - 0.5 * this.accelerationBase * this.particleDeathAge * this.particleDeathAge) / this.particleDeathAge )
+        //let height = this.velocityBase.z * this.particleDeathAge + 0.5 * this.accelerationBase * this.particleDeathAge * this.particleDeathAge;//s = V0 * t + 0.5 * a * t*t ;     
+        this.velocityBase.z = Math.max(0,this.velocityBase.z);
+        this.particleCount =  Math.ceil(  length * Math.sqrt(this.strength * this.height * this.radius )   )  
+        this.particleCount = Math.max(5,this.particleCount)
+        { 
+            const minSize = 1, maxSize = 2, minBound = 0.01, maxBound = 1;
+            let size = minSize + (maxSize - minSize) * THREE.Math.smoothstep( this.strength, minBound, maxBound);
+                
+            this.sizeTween.values = this.defaultSizeTween.values.map(e=> e*size)
+        }
+        { 
+            const minSize = 1 , maxSize = 1.5, minBound = 0.01, maxBound = 1;
+            let opac = minSize + (maxSize - minSize) * THREE.Math.smoothstep( this.strength, minBound, maxBound);
+                
+            this.opacityTween.values = this.defaultOpacityTween.values.map(e=> e*opac )
+        }
+         
+        //console.log('smoke  particleCount',this.particleCount)
+        
+        
+        
+        
+    }
+
+    reStart(){
+        this.emitterAge = 0; 
+        this.createGeometry()  
+    }
+
+
+    updateGeometry(){ 
+        this.computeParams()
+        this.reStart() 
+        
+    }
+    
+    
+    createParticle(center)
+    {
+        var particle = new Particle({
+            sizeTween : this.sizeTween,
+            opacityTween : this.opacityTween,
+            colorTween : this.colorTween,
+        });
+        particle.deathAge = this.particleDeathAge
+        particle.center = center
+         
+        
+        if (this.positionStyle == 'cube')
+            particle.position = this.randomVector3( this.positionBase, this.positionSpread ); 
+        if (this.positionStyle == 'sphere')
+        {
+            /* var z = 2 * Math.random() - 1    
+            var t = Math.PI * 2 * Math.random();
+            var r = Math.sqrt( 1 - z*z ) ;
+            var vec3 = new THREE.Vector3( r * Math.cos(t), r * Math.sin(t), z );
+            particle.position = new THREE.Vector3().addVectors( this.positionBase, vec3.multiplyScalar( this.radius ) );
+           */
+            //怎么改半径
+            let y = 2 * Math.random() - 1    
+            let t = Math.PI * 2 * Math.random();
+            let r = Math.sqrt( 1 - y*y ) ; //因为 r*r = 1-y*y = x*x + z*z = r*r(cos^2 + sin^2 );
+            let lowDownRatio = 0.2 //压低近平面
+            let vec3 = new THREE.Vector3( r * Math.cos(t), y, Math.abs(r * Math.sin(t) ) * lowDownRatio);
+            particle.position = new THREE.Vector3().addVectors( this.positionBase, vec3.multiplyScalar( this.radius ) );
+           
+          
+        } 
+         
+        particle.position.add(center)//add
+         
+         
+         
+         
+         
+        if ( this.velocityStyle == 'cube' )
+        {
+            particle.velocity  = this.randomVector3( this.velocityBase,  this.velocitySpread ); 
+        }
+        if ( this.velocityStyle == 'sphere' )  
+        {
+            //var direction = particle.position.clone()
+            var direction = new THREE.Vector3(0,0,1) //烟应该都是向上的
+            var speed     = this.randomValue( this.speedBase, this.speedSpread );
+            particle.velocity  = direction.normalize().multiplyScalar( speed );
+        }
+        
+        particle.acceleration = this.randomValue( this.accelerationBase, this.accelerationSpread ); 
+
+        particle.angle             = this.randomValue( this.angleBase,             this.angleSpread );
+        particle.angleVelocity     = this.randomValue( this.angleVelocityBase,     this.angleVelocitySpread );
+        particle.angleAcceleration = this.randomValue( this.angleAccelerationBase, this.angleAccelerationSpread );
+
+        particle.size = this.randomValue( this.sizeBase, this.sizeSpread );
+
+        var color = this.randomVector3( this.colorBase, this.colorSpread );
+        particle.color = new THREE.Color().setHSL( color.x, color.y, color.z );
+        
+        particle.opacity = this.randomValue( this.opacityBase, this.opacitySpread );
+
+        particle.age   = 0;
+        particle.alive = 0; // particles initialize as inactive
+        return particle;
+    }			
+
+
+    getPointsForBound(){
+        return this.boundPoints; //可以用于expand实时bound的点, 不含particle的size等边距
+    }
+
+    getBound(points){ // points为生成点(圆心)
+        this.boundPoints = [] 
+        let boundingBox = new THREE.Box3()
+        
+        
+        let maxSize = this.sizeTween.values.slice().sort((a,b)=>b-a)[0]            
+        let margin0 = maxSize * 0.11
+        let margin1 = margin0 + 0.5   ;//保守估计还会飘出这么多距离吧: size + 飘动  
+        
+         
+        points.forEach(bottom=>{ 
+            let top = bottom.clone()
+            top.z +=  this.height 
+            boundingBox.expandByPoint(bottom);
+            boundingBox.expandByPoint(top); 
+            this.boundPoints.push(bottom,top)
+        })
+        let xyExpand = this.radius+margin1 
+        boundingBox.expandByVector(new THREE.Vector3(xyExpand,xyExpand,0))
+        boundingBox.min.z -= margin0 
+        boundingBox.max.z += margin1 
+       
+       
+       
+        this.boundingBox = boundingBox
+        
+        /* if(!this.debugBox){
+            this.debugBox = new THREE.Mesh(boxGeo, boxMat)
+            this.add(this.debugBox)
+        }
+        
+        this.debugBox.scale.copy(boundingBox.getSize(new THREE.Vector3))
+        this.debugBox.position.copy(boundingBox.getCenter(new THREE.Vector3))  */  
+         
+    }
+
+    createGeometry(){
+        this.particleArray = []
+        const positions = [];
+        const colors = [];
+        const alives = [];
+        const opacitys = [];
+        const sizes = [];
+        const angles = [];
+               
+        let count, points;
+        if(this.positions.length>1){
+             
+            const spaceDis = 0.6;//间隔距离
+            
+            count = Math.ceil(this.curve.wholeLength / spaceDis) + 1 
+             
+            points = this.curve.getSpacedPoints( count );  
+            
+            count = points.length
+            
+            /* points.forEach(e=>  { 
+                var sphere = debugSphere.clone();
+                sphere.position.copy(e)
+                viewer.scene.scene.add(sphere)
+            }) */
+            let haventGetPoints = points.slice() 
+            var getRanPoints = function(i){
+                var a = Math.random()
+                let choseIndex = Math.floor(haventGetPoints.length * a)
+                var point = haventGetPoints[choseIndex]
+                if(haventGetPoints.length == 1){
+                    haventGetPoints = points.slice()  
+                }else{
+                    haventGetPoints.splice(choseIndex, 1)
+                }
+                return point
+            }
+            
+            
+            this.getBound(points)
+        }else{
+            this.getBound(this.positions)
+        }
+        
+        
+        
+        
+        
+        for (var i = 0; i < this.particleCount; i++)
+        {
+            var center = new THREE.Vector3().copy(this.positions.length>1 ? getRanPoints(i)  : this.positions[0])
+             
+            //var center = new THREE.Vector3().copy(this.positions.length>1 ? points[Math.floor(i/this.particleCount * count)] : this.positions[0])
+              
+             
+            // remove duplicate code somehow, here and in update function below.
+            this.particleArray[i] = this.createParticle(center);
+            positions[3*i] = this.particleArray[i].position.x
+            positions[3*i+1] = this.particleArray[i].position.y
+            positions[3*i+2] = this.particleArray[i].position.z
+
+            colors[3*i] = this.particleArray[i].color.r 
+            colors[3*i+1] = this.particleArray[i].color.g
+            colors[3*i+2] = this.particleArray[i].color.b
+
+            alives[i] = this.particleArray[i].alive
+            opacitys[i] = this.particleArray[i].opacity
+            sizes[i] = this.particleArray[i].size
+            angles[i] = this.particleArray[i].angle
+        }
+
+        this.geometry.setAttribute( 'position', new THREE.BufferAttribute( new Float32Array(positions), 3  ));
+        this.geometry.setAttribute( 'customColor', new THREE.BufferAttribute( new Float32Array(colors), 3 ) );
+        this.geometry.setAttribute( 'customVisible', new THREE.BufferAttribute( new Float32Array(alives), 1 ) );
+        this.geometry.setAttribute( 'customOpacity', new THREE.BufferAttribute( new Float32Array(opacitys), 1 ) );
+        this.geometry.setAttribute( 'customSize', new THREE.BufferAttribute( new Float32Array(sizes), 1 ) );
+        this.geometry.setAttribute( 'customAngle', new THREE.BufferAttribute( new Float32Array(angles), 1 ) );
+    }
+    
+    createMaterial(){
+        this.material = new THREE.ShaderMaterial( 
+        {
+            uniforms: 
+            {
+                u_sampler:   { type: "t", value: getTexture() },
+                heightOfNearPlane: { type: "f", value:0}  //相对far ,以确保画面缩放时点的大小也会缩放
+            },
+            vertexShader:   vertexShader,vertexShader,
+            fragmentShader: fragmentShader,
+            transparent: true,
+            alphaTest: 0.5, // if having transparency issues, try including: alphaTest: 0.5, 
+            blending: this.blendStyle,
+            depthTest: this.blendStyle != THREE.NormalBlending
+        });
+        
+        
+        this.setPerspective(this.fov, this.screenHeight)
+        
+        
+    }
+    
+
+    update(dt){
+        if(!viewer.getObjVisiByReason(this,'force')){//被手动隐藏了
+            return
+        }
+        if(!Potree.Utils.isInsideFrustum(this.boundingBox, viewer.scene.getActiveCamera())){
+            viewer.updateVisible(this,'isInsideFrustum', false ) //不在视野范围
+            //console.log('unvi')
+            return
+        }else{
+            viewer.updateVisible(this,'isInsideFrustum', true )
+        } 
+        
+        
+        
+        if(dt > 1){
+            console.log('update dt>1', dt)
+        }
+         
+        //dt *= 0.5;
+        
+        const recycleIndices = [];
+        const recycleAges = []
+        
+        
+        const positions = [];
+        const colors = [];
+        const alives = [];
+        const opacitys = [];
+        const sizes = [];
+        const angles = [];
+    
+       
+        
+        
+        
+        
+        for (var i = 0; i < this.particleCount; i++)
+        {
+            if ( this.particleArray[i].alive )
+            {
+                  
+                if ( this.velocityStyle == 'cube' )
+                {        //一定几率改变下方向
+                    let ratio = Math.random()
+                    if(this.particleArray[i].age - this.particleArray[i].lastChangeVage > this.particleDeathAge*ratio  ){
+                        
+                        this.particleArray[i].velocity = this.randomVector3( this.velocityBase, this.velocitySpread ); 
+                        
+                        this.particleArray[i].lastChangeVage = this.particleArray[i].age
+                    }
+                }else{
+                    /* if(this.particleArray[i].age - this.particleArray[i].lastChangeVage > this.particleDeathAge*0.3  ){
+                        if( Math.random()>0.1){//一定几率改变下方向
+                            var speed  = this.randomValue( this.speedBase, this.speedSpread ); 
+                            this.particleArray[i].velocity = this.randomVector3( new THREE.Vector3,   new THREE.Vector3(1,1,1) ); 
+                            this.particleArray[i].velocity.normalize().multiplyScalar( speed );
+                        }
+                        this.particleArray[i].lastChangeVage = this.particleArray[i].age
+                    } */
+                    
+                    
+                }
+                 
+                
+                this.particleArray[i].update(dt);
+
+                // check if particle should expire
+                // could also use: death by size<0 or alpha<0.
+                if ( this.particleArray[i].age > this.particleDeathAge ) 
+                {
+                    this.particleArray[i].alive = 0.0;
+                    recycleIndices.push(i);
+                    recycleAges.push((this.particleArray[i].age - this.particleDeathAge)%(this.particleDeathAge ))
+                } 
+                
+                
+                // update particle properties in shader
+                positions[3*i] = this.particleArray[i].position.x
+                positions[3*i+1] = this.particleArray[i].position.y
+                positions[3*i+2] = this.particleArray[i].position.z
+
+                colors[3*i] = this.particleArray[i].color.r 
+                colors[3*i+1] = this.particleArray[i].color.g
+                colors[3*i+2] = this.particleArray[i].color.b
+
+                alives[i] = this.particleArray[i].alive
+                opacitys[i] = this.particleArray[i].opacity
+                sizes[i] = this.particleArray[i].size
+                angles[i] = this.particleArray[i].angle
+            }		
+        }
+
+        // check if particle emitter is still running
+        //if ( !this.emitterAlive ) return;
+
+        this.geometry.setAttribute( 'position', new THREE.BufferAttribute( new Float32Array(positions), 3 ) );
+        this.geometry.setAttribute( 'customColor', new THREE.BufferAttribute( new Float32Array(colors), 3 ) );
+        this.geometry.setAttribute( 'customVisible', new THREE.BufferAttribute( new Float32Array(alives), 1 ) );
+        this.geometry.setAttribute( 'customOpacity', new THREE.BufferAttribute( new Float32Array(opacitys), 1 ) );
+        this.geometry.setAttribute( 'customSize', new THREE.BufferAttribute( new Float32Array(sizes), 1 ) );
+        this.geometry.setAttribute( 'customAngle', new THREE.BufferAttribute( new Float32Array(angles), 1 ) );
+
+        this.geometry.attributes.customColor.needsUpdate = true;
+        this.geometry.attributes.customVisible.needsUpdate = true;
+        this.geometry.attributes.customOpacity.needsUpdate = true;
+        this.geometry.attributes.customSize.needsUpdate = true;
+        this.geometry.attributes.customAngle.needsUpdate = true;
+
+        // if no particles have died yet, then there are still particles to activate
+        if ( this.emitterAge < this.particleDeathAge ) //开始时一个个放出来
+        {
+            
+            let particlesPerSecond = this.particleCount / this.particleDeathAge
+            // determine indices of particles to activate
+            var startIndex = Math.round( particlesPerSecond * (this.emitterAge +  0) );
+            var endIndex = Math.round( particlesPerSecond * (this.emitterAge + dt) );
+            if  ( endIndex > this.particleCount ) 
+                endIndex = this.particleCount; 
+                
+            for (var i = startIndex; i < endIndex; i++)
+                this.particleArray[i].alive = 1.0;		
+        }
+
+        // if any particles have died while the emitter is still running, we imediately recycle them
+        for (var j = 0; j < recycleIndices.length; j++)
+        {
+            var i = recycleIndices[j]; 
+            this.particleArray[i] = this.createParticle(this.particleArray[i].center);
+            this.particleArray[i].alive = 1.0; // activate right away
+            this.particleArray[i].age = recycleAges[j]
+            positions[3*i] = this.particleArray[i].position.x
+            positions[3*i+1] = this.particleArray[i].position.y
+            positions[3*i+2] = this.particleArray[i].position.z
+        }
+        this.geometry.setAttribute( 'position', new THREE.BufferAttribute( new Float32Array(positions), 3 ) );
+        this.geometry.attributes.position.needsUpdate = true;
+
+        // stop emitter?
+        this.emitterAge += dt;
+        //if ( this.emitterAge > this.emitterDeathAge )  this.emitterAlive = false;
+    }
+
+    randomValue(base, spread)
+    {
+        //return base + spread * (Math.random() - 0.5);
+        let p = Math.random()
+        return base * p + spread * (1-p)
+        
+    }
+
+    randomVector3(base, spread)
+    {
+        var rand3 = new THREE.Vector3( Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5 );
+        return new THREE.Vector3().addVectors( base, new THREE.Vector3().multiplyVectors( spread, rand3 ) );
+    }
+    
+    
+    
+    
+    setSize(e){
+        let viewport = e.viewport
+        this.screenHeight = viewport.resolution.y
+        this.setPerspective(this.fov, this.screenHeight)  
+    }
+    
+    setFov(fov){
+        this.fov = fov
+        this.setPerspective(this.fov, this.screenHeight) 
+    }
+    
+    
+    setPerspective(fov, height){
+        //this.uniforms.heightOfNearPlane.value = Math.abs(height / (2 * Math.tan(THREE.Math.degToRad(fov * 0.5))));
+        let far = Math.abs(height / (2 * Math.tan(THREE.Math.degToRad(fov * 0.5))));
+        this.material.uniforms.heightOfNearPlane.value = far 
+    }
+    
+    dispose(){
+        this.geometry.dispose();
+        this.material.dispose();
+        this.dispatchEvent('dispose') 
+    }
+}
+
+
+/* 
+    改进:如果有必要
+    
+    根据curve中分成的点,分成多个簇,每个簇掌管该部分的可见性和particle的数量。
+    在camera_changed时根据远近修改每个簇的particle的数量,当然不会大于初始创建的个数。多出的随机隐藏。
+
+
+ */

+ 44 - 0
src/custom/objects/fireParticle/smoke/shader.js

@@ -0,0 +1,44 @@
+export const vertexShader = `
+    attribute vec3  customColor;
+    attribute float customOpacity;
+    attribute float customSize;
+    attribute float customAngle;
+    attribute float customVisible;  
+    uniform float heightOfNearPlane;
+    
+    
+    varying vec4  vColor;
+    varying float vAngle;
+    void main()
+    {
+        if ( customVisible > 0.5 ) 				
+            vColor = vec4( customColor, customOpacity ); 
+        else							
+            vColor = vec4(0.0, 0.0, 0.0, 0.0);		
+            
+        vAngle = customAngle;
+
+        vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
+        //gl_PointSize = customSize * ( 300.0 / length( mvPosition.xyz ) );     
+        gl_Position = projectionMatrix * mvPosition;
+        gl_PointSize = ( heightOfNearPlane * customSize ) / gl_Position.w;
+        
+        
+    }
+` 
+
+export const fragmentShader = `
+    uniform sampler2D u_sampler;
+    varying vec4 vColor;	
+    varying float vAngle;  
+    void main()
+    {
+        gl_FragColor = vColor;
+        
+        float c = cos(vAngle);
+        float s = sin(vAngle);
+        vec2 rotatedUV = vec2(c * (gl_PointCoord.x - 0.5) + s * (gl_PointCoord.y - 0.5) + 0.5, c * (gl_PointCoord.y - 0.5) - s * (gl_PointCoord.x - 0.5) + 0.5);  
+        vec4 rotatedTexture = texture2D( u_sampler,  rotatedUV );
+        gl_FragColor = gl_FragColor * rotatedTexture;   
+    }
+`

+ 296 - 0
src/custom/objects/tool/Compass.js

@@ -0,0 +1,296 @@
+ 
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+ 
+
+const initDir = new THREE.Vector3(0,1,0)//指南针模型的北方向 向屏幕里
+
+class Compass extends THREE.EventDispatcher{
+    
+    constructor(dom, viewport){
+        super()
+        this.angle = 0;
+        this.show = false; 
+        if(dom){
+            this.dom = $(dom);
+        }
+        
+        this.viewport = viewport
+        this.init()
+        
+
+        
+
+    }
+    init(){ 
+        var width = 100, height = 100
+        if(!this.dom){ 
+            this.dom = $('<div name="compass"></div>')
+            $(viewer.renderArea).append(this.dom)
+        } 
+        this.dom.css({ display:"none",  position:"absolute",right:"1%",top: "60px",width:width+"px",height:height+"px", "z-index":100,"pointer-events":"none" })
+
+        let child = $("<div class='dirText north'><span>"+/* (config.lang=='zh'? */'北'/* :'N') */+"</span></div><div class='center'></div>")
+        this.dom.append(child)  
+
+         
+        this.dom.find(".dirText").css({textAlign:"center","font-size":"10px","position":"absolute",
+                width: "100%",
+                height: "25px",
+        "line-height": "25px"})
+         
+        this.dom.find(".north").css({"color":"#02a0e9","top":"0"})
+        this.dom.find(".south").css({"color":"#ff1414","bottom":"0"})
+        this.dom.find(".center").css({
+            //"background":`url(${config.getStaticResource('img')}/dire.png)`,
+            width: width/2+"px",
+            height: height/2+"px",
+            "background-size": "contain",
+            "background-position": "center",
+            left: "50%",
+            top: "50%",
+            transform: "translate(-50%,-50%)",
+            position: "absolute" 
+        })
+        this.dom.find(".dirText").css({
+            "text-align": "center",
+            "font-size": "10px", 
+            "color": "rgb(255, 255, 255)",
+            "position": "absolute",
+            "top": "50%",
+            "left": "50%",
+            "width": "45%",
+            "height": "0px",
+            "transform-origin": "left center",
+            
+        })
+        this.dom.find(".dirText span").css({
+            display: "block",
+            position: "absolute",
+            right: "5px",
+            top: "0",
+            width: "20px",
+            height: "20px",
+            "line-height": "20px", 
+           // "font-size": ".75rem ",
+            "margin-top": "-10px",
+            
+        })   
+    
+        try { 
+            this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha:true })//许钟文 添加个抗锯齿,否则添加的线条锯齿严重,
+            this.renderer.autoClear = !0
+            this.renderer.setPixelRatio(window.devicePixelRatio ? window.devicePixelRatio : 1)
+            this.renderer.domElement.setAttribute('name','compass')
+            this.renderer.setClearAlpha(0.0)
+            //xst修改
+			//this.renderer.setSize(width/2, height/2, false, window.devicePixelRatio ? window.devicePixelRatio : 1);
+            //xst修改
+			//this.renderer.setPixelRatio(window.devicePixelRatio ? window.devicePixelRatio : 1);
+			//this.renderer.setSize(width/2, height/2);
+			//xst修改
+			this.renderer.setDrawingBufferSize( width/2, height/2, window.devicePixelRatio ? window.devicePixelRatio : 1 )
+			//this.emit(SceneRendererEvents.ContextCreated)
+        } catch (e) {
+            viewer.dispatchEvent('webglError', {msg:e})
+        }
+        
+        this.dom.find(".center")[0].appendChild(this.renderer.domElement);
+        this.renderer.domElement.style.width = this.renderer.domElement.style.height = '100%'
+        
+        
+        this.camera = new THREE.PerspectiveCamera;
+        this.camera.fov = 50;
+        this.camera.updateProjectionMatrix()
+        this.scene = new THREE.Scene,
+        this.scene.add(this.camera)
+        
+        
+        
+        this.createCompass()
+          
+        viewer.addEventListener('camera_changed', e => {
+            if (e.viewport == this.viewport && (e.changeInfo.positionChanged || e.changeInfo.quaternionChanged)) {
+                 this.update()
+            } 
+        })
+        
+        this.setDomPos()
+        if(this.viewport)this.setDisplay(true)
+    }   
+        
+        
+    createCompass(){
+        //ConeBufferGeometry(radius : Float, height : Float, radialSegments : Integer, heightSegments : Integer, openEnded : Boolean, thetaStart : Float, thetaLength : Float)
+        const height = 2;
+        const geometry1 = new THREE.ConeBufferGeometry( 0.7, height, 4, true );
+        const geometry2 = new THREE.ConeBufferGeometry( 0.7, height, 4, true );
+        const material = new THREE.MeshBasicMaterial({   
+             vertexColors :true
+        })
+        
+        //指南针由两个四棱锥拼成,为了渐变颜色,采用指定vertexColor的方式。
+        var setColor = function(geometry, color1,color2){ 
+            const colors = [];
+            for ( let i = 0, n = geometry.attributes.position.count; i < n; ++ i ) { 
+                colors.push( 1, 1, 1 ); 
+            } 
+            var set = function(index, color){//设置第index个点的颜色
+                colors[index*3+0] = color[0]
+                colors[index*3+1] = color[1]
+                colors[index*3+2] = color[2]
+            }
+            var mid = [(color1[0]+color2[0])/2, (color1[1]+color2[1])/2, (color1[2]+color2[2])/2 ]
+            set(1,color1); set(5,color1);set(6,color1);
+            set(2,mid); set(3,mid);set(7,mid);
+            set(4,color2); set(8,color2);set(9,color2);
+            geometry.setAttribute("color", new THREE.BufferAttribute(new Float32Array(colors), 3))  
+        }
+        var blue1 = [1/255,238/255,245/255] //逐渐变深
+        var blue2 = [20/255,146/255,170/255]
+        var blue3 = [40/255,60/255,103/255]
+        setColor(geometry1, blue1,blue2)
+        setColor(geometry2, blue2,blue3)
+        
+        /*  朝箭头方向看点构成如下  虽然geometry.attributes.position.count = 19  只有1-9设置的颜色是有效的  另外为什么7决定了上下两边的颜色呢…… 5、9可将其分成上下两个颜色
+             6 
+            /|\   
+           / | \
+        7 /_2|1_\ 5
+          \ 3|4 / 9
+           \ | /
+            \|/
+             8
+         */
+        const cone = new THREE.Mesh( geometry1, material );
+        cone.position.setY(height/2)
+        geometry1.computeVertexNormals()//computeFaceNormals
+        geometry2.computeVertexNormals()
+        
+        const cones = new THREE.Object3D();
+        cones.add(cone)
+         
+        let cone2 = new THREE.Mesh( geometry2, material );
+        cone2.rotation.x = Math.PI;
+        cone2.position.setY(-height/2)
+        cones.add(cone2)  
+        //cones.rotation.x = Math.PI / 2;//转向initDir的方向
+        //cones.rotation.z = Math.PI / 2;
+        cones.rotation.z = Math.PI ;//转向initDir的方向
+        cones.scale.set(0.7,0.7,0.7)
+        this.scene.add(cones)
+        this.cones = cones
+    }
+    
+    
+    
+    setNorth(){ //设置北方向,这决定了指南针自身的朝向。 
+        const floors = store.getters['scene/houstFloor'].floors
+        if(!floors || !floors.length){
+            return 
+        }
+        const floor = floors[0] 
+        const metadata = app.store.getters['scene/metadata'] || {}
+         
+        this.angle = (floor && floor.dire || 0) + THREE.Math.radToDeg(parseFloat(metadata.floorPlanAngle || 0))  //基础朝向  
+        this.cones.rotation.y = Math.PI / 2 - THREE.Math.degToRad(this.angle) 
+        //console.log("dir:"+floor.dire+", floorPlanAngle:"+metadata.floorPlanAngle)
+        this.update() 
+   
+    }
+    
+    update(quaternion){
+        if(!this.show)return;
+        if(!quaternion) quaternion = this.viewport.camera.quaternion.clone();
+        this.updateCamera(quaternion)
+        this.updateLabel(quaternion)
+        this.render()
+         
+    }
+    
+    
+    /*updateLabel(quaternion){//更新北标签
+          
+        var dir = viewer.mainViewport.view.direction;
+        var oriDir = initDir.clone()  //指南针最初始时的北方向
+        var extraQua 
+        if(objects.player.mode == "transitioning"){//当transitioning时,相机的quaternion不是用control的lookAt算出来,而是直接由一个quaternion过渡到另一个,这样相机将会是歪的,投影面也就不会是原先的水平面。
+            var tempCamera = new THREE.Camera();   //借用camera的lookAt算出如果正视同样的target, quaternion会是什么值。 将它乘以当前相机quaternion,得到的就是相机歪的旋转值。
+            tempCamera.position.copy(this.camera.position); 
+            tempCamera.lookAt(tempCamera.position.clone().add(dir)) 
+            var q = tempCamera.quaternion.inverse()
+            extraQua = q.premultiply(quaternion) //歪掉的额外旋转值
+            
+        }  
+         
+        //北标签的方向为指南针轮盘方向,也就是要将camera的方向投影到水平面上。 但是如果相机歪了,看到的世界都会歪一定角度,投影面也要歪一定角度。
+        var up = new THREE.Vector3(0,0,1) //投影水平面的法线,也是相机的摆正的up方向
+        extraQua && up.applyQuaternion(extraQua)
+        dir.projectOnPlane(up)   //将方向投影到水平面上; 如果相机不是正视(extraQua不为0001),就要将水平面也转动 
+        oriDir.projectOnPlane(up)//为什么initDir投影了和没有投影angle结果一样 
+        var angle = dir.angleTo(oriDir)
+        if(dir.cross(oriDir).y > 0)angle = -angle
+         
+        var deg = this.angle - 90 + THREE.Math.radToDeg(angle) //因为css写的样式初始是指向右方,和initDir差了90°,所以减去。
+        
+        
+        this.dom.find(".dirText").css( "transform","rotate("+deg+"deg)" )
+        this.dom.find(".dirText span").css("transform","rotate("+(-deg)+"deg)")
+    } */
+    
+
+
+    updateLabel(quaternion){//更新北标签
+        let deg = THREE.Math.radToDeg(this.viewport.view.yaw) - 90
+        this.dom.find(".dirText").css( "transform","rotate("+deg+"deg)" )
+        this.dom.find(".dirText span").css("transform","rotate("+(-deg)+"deg)")
+    }
+
+    
+    updateCamera(quaternion){ //更新canvas中的指南针表现,也就是更新相机,和场景中的相机朝向一致。 
+         const radius = 5;  //相机距离
+          
+         this.camera.quaternion.copy(quaternion);
+         var dir = this.viewport.view.direction;  //相机朝向
+         this.camera.position.copy(dir.multiplyScalar(radius).negate())  //相机绕着指南针中心(000)转动
+    } 
+
+    changeViewport(viewport){
+        this.viewport = viewport;
+        this.update(); //因相机更新了
+    }
+
+
+
+    render(){
+        this.renderer.render(this.scene, this.camera)
+    }
+    
+    setDisplay(state){
+        this.show = !!state;
+        if(this.show){
+            this.update() 
+            this.dom.fadeIn(100) 
+        }else{
+            this.dom.fadeOut(100)
+        }  
+         
+    }
+    
+    
+    autoJudgeDisplay(){
+         
+    }
+    
+    
+    
+    setDomPos(){
+        if(!this.viewport)return
+        let right = this.viewport.left + this.viewport.width
+        this.dom.css({'right':((1-right)*100 + 1) + '%'})
+         
+    }
+    
+}
+
+
+export default Compass;

+ 229 - 0
src/custom/objects/tool/CurveCtrl.js

@@ -0,0 +1,229 @@
+
+
+ 
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import {LineDraw} from "../../utils/DrawUtil.js";
+import math  from "../../utils/math.js";
+import HandleSvg from  "./HandleSvg.js";
+import HandleSprite from  "./HandleSprite.js";
+
+const sphere = new THREE.Mesh(new THREE.SphereBufferGeometry(0.08,0.08,3,2), new THREE.MeshBasicMaterial({color:'#f88'}))
+
+
+
+export default class CurveCtrl extends THREE.Object3D {
+    
+    constructor(points, lineMat, color, name, options={}){
+        super()
+        this.curve = new THREE.CatmullRomCurve3(points, false, "centripetal"    /* , tension */)
+        this.name = name || 'curveNode'
+        this.handleMat = options.handleMat
+        this.lineMat = lineMat
+        this.createPath(); 
+        this.color = color;
+        this.handles = []; 
+        this.wholeLength = 0 
+        this.viewports = options.viewports || [viewer.mainViewport] //for HandleSprite
+        for(let i=0,j=this.points.length; i<j;i++){
+            this.handles.push(this.createHandle(this.points[i]))
+        }   
+        this.visible_ = true
+        
+         
+        
+        if(Potree.settings.isTest){   
+            this.spheres = new THREE.Object3D;
+            /* let i = Count+1;
+            while(i>0){
+                this.spheres.add(sphere.clone());
+                i--;
+            } */
+            this.add(this.spheres)
+        }
+        
+        this.updatePath()
+        
+        
+    }
+    
+    
+    addPoint(position, index, ifUpdate){
+        let length = this.points.length 
+        
+        if(index == void 0 ){
+			index = length;
+		}
+        
+        let handle = this.createHandle(position);
+         
+        this.handles = [...this.handles.slice(0,index), handle, ...this.handles.slice(index,length)]
+      
+        this.points = [...this.points.slice(0,index), position, ...this.points.slice(index,length)]
+        
+        ifUpdate && (this.updatePath(), this.updateHandle(index))
+    
+    }
+    removePoint(index){
+        let handle = this.handles[index]
+        handle.dispose();
+        
+        this.handles.splice(index,1)
+        this.points.splice(index,1)
+        this.updatePath()
+    }
+    
+    createPath(){ 
+   
+        const line = LineDraw.createFatLine( [ ],this.lineMat)
+        this.line = line;
+        this.add(line);
+    }
+    
+    
+    updatePath(){ 
+        this.curve.needsUpdate = true; //如果不更新,得到的点不均匀,开头点少。 
+        
+        let points, length = this.points.length
+        
+        this.wholeLength = this.points.reduce((total, currentValue, currentIndex, arr)=>{ //所有端点的距离总和
+            if(currentIndex == 0)return 0
+            return total + currentValue.distanceTo(arr[currentIndex-1]);
+        },0)
+        
+        if(length > 1){ 
+            const count = THREE.Math.clamp(Math.ceil(this.wholeLength * 5), 30, 500);
+         
+            points = this.curve.getSpacedPoints( count ); 
+              
+              
+            if(this.needsPercent){ //获取每个节点在整条中的百分比,便于定位(但不精确)
+                this.pointsPercent = [0];
+                let sums = [0]
+                let sum = 0, last = points[0]
+                for(let i=1;i<length;i++){
+                    let point = this.points[i];
+                    sum += point.distanceTo(last);  //参考getLengths函数,根据长度得到百分比
+                    last = point;
+                    sums.push(sum) 
+                }
+                for(let i=1;i<length;i++){
+                    this.pointsPercent.push(sum == 0 ? i/length : sums[i] / sum);
+                }
+                
+                
+            }
+              
+            
+              
+              
+              
+            if(Potree.settings.isTest){
+                this.spheres.children.forEach(e=>e.visible = false);
+                points.forEach((e,i)=>{
+                    let sphere1 = this.spheres.children[i]
+                    if(!sphere1){
+                        sphere1 = sphere.clone();
+                        this.spheres.add(sphere1);
+                    }                        
+                    sphere1.position.copy(e)
+                    sphere1.visible = true
+                })
+            }
+        }else{
+            points = []
+        } 
+        
+        
+        LineDraw.updateLine(this.line, points)      
+        
+        
+        this.dispatchEvent('updatePath')
+        
+    }
+    
+    
+    createHandle(position){
+        if(this.handleMat){
+            var handle = new HandleSprite(position,  {mat:this.handleMat, viewports:this.viewports})
+            this.add(handle)
+        }else{
+            var handle = new HandleSvg(position,  this.color)
+        }
+		
+        handle.visible = this.visible 
+        handle.addEventListener('dragged',(e)=>{
+            let index = this.handles.indexOf(handle) 
+            this.points[index].copy(e.position) 
+            
+            this.updatePath()
+            
+            this.dispatchEvent({type:'dragCurvePoint', index})
+        })
+        
+        return handle
+	}
+    
+    
+    
+    updateHandle(index){
+        if(!this.visible)return
+        
+        this.handles[index].update() 
+            
+    }
+    
+    updateHandles(){
+        
+        this.handles.forEach((handle,index)=>{
+            this.updateHandle(index) 
+        }) 
+    }
+    
+    update(){
+        this.updateHandles()
+        this.updatePath()
+    }
+    
+    set visible(v){ 
+        if(v != this.visible_ ){
+            this.visible_ = v 
+            this.visible = v 
+            if(this.handles){ 
+                this.handles.forEach(e=>e.visible = v  )
+                if(v) this.updateHandles() //因为不可见时没更新位置
+            }
+        }
+    }
+    get visible(){
+        return this.visible_
+    }  
+    
+    
+    /* set visible(v){ 
+        this.handles.forEach(e=>e.svg.style.display = v ? "" : "none" )
+        if(v) this.updateHandles() //因为不可见时没更新位置
+    } */
+    get points(){
+        return this.curve.points;
+    }
+    set points(points){
+        this.curve.points = points
+    }
+    getPointAt(t){
+        return this.curve.getPointAt(t)
+    }
+    getSpacedPoints(t){
+        return this.curve.getSpacedPoints(t)
+    }
+    dispose(){
+        this.parent && this.parent.remove(this);
+        
+        this.handles.forEach(e=>e.dispose() )
+        
+        this.line.geometry && this.line.geometry.dispose()
+        
+    }
+    
+    
+    
+}

+ 78 - 0
src/custom/objects/tool/HandleSprite.js

@@ -0,0 +1,78 @@
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import Sprite from  "../Sprite.js";
+/* 
+两种拖拽方式:
+1 只依附在点云上
+2 平行于镜头view移动 */
+
+
+
+
+
+
+const geo = new THREE.PlaneBufferGeometry(1,1)
+export default class HandleSprite extends Sprite{
+    constructor(position,options={}){
+        
+        options.sizeInfo = {width2d:60} 
+        super(options)
+        this.position.copy(position);
+         
+        this.dragStyle =  options.dragStyle || 'default'    //'default'||'onPointCloud'  
+        
+        
+        this.bindEvent()
+        
+        
+    }
+    
+    
+    bindEvent(){
+        let projectedStart, pointerStart
+        
+        const drag = (e)=>{ 
+            /* if(e.hoverViewport != e.drag.dragViewport){//不能使用e.dragViewport,要使用drag中的,因为drag中存储的要一直继承下来,不因mouseup了而改变。
+                viewer.dispatchEvent({
+                    type : "CursorChange", action : "add",  name:"polygon_AtWrongPlace"
+                })
+                return
+            } */ 
+            const camera = viewer.scene.getActiveCamera();
+            if(projectedStart){
+                let move2d = new THREE.Vector2().subVectors(e.pointer, pointerStart)
+                let projectNow = projectedStart.clone()
+                projectNow.x += move2d.x;
+                projectNow.y += move2d.y; 
+                let unprojected = projectNow.clone().unproject(camera);
+                this.position.set(unprojected.x, unprojected.y, unprojected.z);
+            }else{
+                projectedStart = this.position.clone().project(camera);
+                pointerStart = e.pointer.clone()
+            }
+             
+            this.update()
+            this.dispatchEvent({type:'dragged', position: this.position }) 
+        }
+        
+        const drop = (e)=>{
+            projectedStart = null, pointerStart = null
+        }
+        
+        const mouseover = (e) => {  
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "add",  name:"markerMove"
+            }) 
+        };
+        const mouseleave = (e) => {  
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "remove",  name:"markerMove"
+            })
+        } 
+        
+        this.addEventListener('drag', drag) 
+        this.addEventListener('drop', drop)  
+        this.addEventListener('mouseover', mouseover);
+        this.addEventListener('mouseleave', mouseleave);
+    }
+    
+}

+ 154 - 0
src/custom/objects/tool/HandleSvg.js

@@ -0,0 +1,154 @@
+
+import math  from "../../utils/math.js";
+ 
+import * as THREE from "../../../../libs/three.js/build/three.module.js"; 
+ 
+export default class HandleSvg extends THREE.EventDispatcher{
+    constructor(position, color){
+        super()
+        this.position = position
+		this.color = '#'+new THREE.Color(color).getHexString()
+        this.svg = this.create() 
+        this.visible_ = true  
+        
+        let update = ()=>{
+            this.update()
+        }
+        viewer.addEventListener("camera_changed", update)
+        
+        this.addEventListener('dispose', ()=>{ 
+            viewer.removeEventListener("camera_changed",  update)  
+        })
+    }
+    
+    
+    create(){
+        
+        const svgns = "http://www.w3.org/2000/svg";
+		const svg = document.createElementNS(svgns, "svg");
+
+		svg.setAttribute("width", "2em");
+		svg.setAttribute("height", "2em");
+		svg.setAttribute("position", "absolute");
+
+		svg.style.left = "50px";
+		svg.style.top = "50px";
+		svg.style.position = "absolute";
+		svg.style.zIndex = "10000";
+        svg.style.cursor = 'grab'
+        svg.style.transform = 'translate(-50%,-50%)'
+        
+		const circle = document.createElementNS(svgns, 'circle');
+		circle.setAttributeNS(null, 'cx', "1em");
+		circle.setAttributeNS(null, 'cy', "1em");
+		circle.setAttributeNS(null, 'r', "0.5em");
+		circle.setAttributeNS(null, 'style', 'fill: '+this.color+'; stroke: black; stroke-width: 0.2em;' );
+		svg.appendChild(circle);
+         
+		const element = viewer.renderer.domElement.parentElement;
+		element.appendChild(svg);
+
+
+		const startDrag = (evt) => {
+            /* if(evt.button === THREE.MOUSE.RIGHT){
+                return
+            } */
+			this.selectedElement = svg;
+
+			document.addEventListener("mousemove", drag);
+		};
+
+		const endDrag = (evt) => {
+			this.selectedElement = null;
+
+			document.removeEventListener("mousemove", drag);
+		};
+
+		const drag = (evt) => {
+			if (this.selectedElement) {
+                 
+				evt.preventDefault();
+
+				const rect = viewer.renderer.domElement.getBoundingClientRect();
+
+				const x = evt.clientX - rect.x;
+				const y = evt.clientY - rect.y;
+
+				const {width, height} = viewer.renderer.getSize(new THREE.Vector2());
+				const camera = viewer.scene.getActiveCamera();
+				 
+                const projected = this.position.clone().project(camera);
+          
+
+				projected.x = ((x / width) - 0.5) / 0.5;
+				projected.y = (-(y - height) / height - 0.5) / 0.5;
+
+				const unprojected = projected.clone().unproject(camera);
+				this.position.set(unprojected.x, unprojected.y, unprojected.z);
+                
+                this.update()
+                this.dispatchEvent({type:'dragged', position: this.position})
+                
+			}
+		};
+
+		svg.addEventListener('mousedown', startDrag);
+		svg.addEventListener('mouseup', endDrag);
+        svg.style.display = this.visible ? "" : "none" 
+         
+        
+        this.addEventListener('dispose',()=>{
+            svg.removeEventListener('mousedown', startDrag);
+            svg.removeEventListener('mouseup', endDrag); 
+        })
+        
+        
+        
+        return svg
+    }
+    
+    
+    
+   
+   
+    set visible(v){
+        this.visible_ = v  
+        if(v){
+            this.update()
+        }else{
+            this.svg.style.display = "none"
+        } 
+    }
+    get visible(){
+        return this.visible_ 
+    }
+   
+   
+    update(){
+        if(!this.visible)return
+        
+        let camera = viewer.scene.getActiveCamera() 
+          
+        
+        var p = Potree.Utils.getPos2d( this.position, camera , viewer.renderArea, viewer.mainViewport);
+        if(!p.trueSide){
+            return this.svg.style.display = 'none'; 
+        }
+        this.svg.style.left =  p.posInViewport.x  
+        this.svg.style.top = p.posInViewport.y 
+          
+        
+         
+        this.svg.style.display = ''
+            
+    }
+    
+    
+    
+    dispose(){
+        this.svg.remove()
+        this.dispatchEvent('dispose')
+    }
+}
+
+ 	

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1245 - 0
src/custom/objects/tool/Measure.js


+ 647 - 0
src/custom/objects/tool/MeasuringTool.js

@@ -0,0 +1,647 @@
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import {Measure} from "./Measure.js";
+import {Utils} from "../../../utils.js"; 
+import math from "../../utils/math.js";
+import {CameraMode} from "../../../defines.js"; 
+import {TextSprite} from '../TextSprite.js'
+ 
+ 
+ 
+ 
+ 
+function updateAzimuth(viewer, measure){
+    if(!measure.showAzimuth)return
+	const azimuth = measure.azimuth;
+
+	const isOkay = measure.points.length === 2;
+
+	azimuth.node.visible = isOkay  
+
+	if(!azimuth.node.visible){
+		return;
+	}
+
+	const camera = viewer.scene.getActiveCamera();
+	const renderAreaSize = viewer.renderer.getSize(new THREE.Vector2());
+	const width = renderAreaSize.width;
+	const height = renderAreaSize.height;
+	
+	const [p0, p1] = measure.points;
+	const r = p0.position.distanceTo(p1.position);
+	const northVec = Utils.getNorthVec(p0.position, r, viewer.getProjection());
+	const northPos = p0.position.clone().add(northVec);
+
+	azimuth.center.position.copy(p0.position);
+	azimuth.center.scale.set(2, 2, 2);
+	
+	azimuth.center.visible = false;
+	// azimuth.target.visible = false;
+
+
+	{ // north
+		azimuth.north.position.copy(northPos);
+		azimuth.north.scale.set(2, 2, 2);
+
+		let distance = azimuth.north.position.distanceTo(camera.position);
+		let pr = Utils.projectedRadius(1, camera, distance, width, height);
+
+		let scale = (5 / pr);
+		azimuth.north.scale.set(scale, scale, scale);
+	}
+
+	{ // target
+		azimuth.target.position.copy(p1.position);
+		azimuth.target.position.z = azimuth.north.position.z;
+
+		let distance = azimuth.target.position.distanceTo(camera.position);
+		let pr = Utils.projectedRadius(1, camera, distance, width, height);
+
+		let scale = (5 / pr);
+		azimuth.target.scale.set(scale, scale, scale);
+	}
+
+ 
+	azimuth.circle.position.copy(p0.position);
+	azimuth.circle.scale.set(r, r, r);
+	azimuth.circle.material.resolution.set(width, height);
+
+	// to target
+	azimuth.centerToTarget.geometry.setPositions([
+		0, 0, 0,
+		...p1.position.clone().sub(p0.position).toArray(),
+	]);
+	azimuth.centerToTarget.position.copy(p0.position);
+	azimuth.centerToTarget.geometry.verticesNeedUpdate = true;
+	azimuth.centerToTarget.geometry.computeBoundingSphere();
+	azimuth.centerToTarget.computeLineDistances();
+	azimuth.centerToTarget.material.resolution.set(width, height);
+
+	// to target ground
+	azimuth.centerToTargetground.geometry.setPositions([
+		0, 0, 0,
+		p1.position.x - p0.position.x,
+		p1.position.y - p0.position.y,
+		0,
+	]);
+	azimuth.centerToTargetground.position.copy(p0.position);
+	azimuth.centerToTargetground.geometry.verticesNeedUpdate = true;
+	azimuth.centerToTargetground.geometry.computeBoundingSphere();
+	azimuth.centerToTargetground.computeLineDistances();
+	azimuth.centerToTargetground.material.resolution.set(width, height);
+
+	// to north
+	azimuth.centerToNorth.geometry.setPositions([
+		0, 0, 0,
+		northPos.x - p0.position.x,
+		northPos.y - p0.position.y,
+		0,
+	]);
+	azimuth.centerToNorth.position.copy(p0.position);
+	azimuth.centerToNorth.geometry.verticesNeedUpdate = true;
+	azimuth.centerToNorth.geometry.computeBoundingSphere();
+	azimuth.centerToNorth.computeLineDistances();
+	azimuth.centerToNorth.material.resolution.set(width, height);
+
+	// label
+	const radians = Utils.computeAzimuth(p0.position, p1.position, viewer.getProjection());
+	let degrees = THREE.Math.radToDeg(radians);
+	if(degrees < 0){
+		degrees = 360 + degrees;
+	}
+	const txtDegrees = `${degrees.toFixed(2)}°`;
+	const labelDir = northPos.clone().add(p1.position).multiplyScalar(0.5).sub(p0.position);
+	if(labelDir.length() > 0){
+		labelDir.z = 0;
+		labelDir.normalize();
+		const labelVec = labelDir.clone().multiplyScalar(r);
+		const labelPos = p0.position.clone().add(labelVec);
+		azimuth.label.position.copy(labelPos);
+	}
+	azimuth.label.setText(txtDegrees);
+	let distance = azimuth.label.position.distanceTo(camera.position);
+	let pr = Utils.projectedRadius(1, camera, distance, width, height);
+	let scale = (70 / pr);
+	azimuth.label.scale.set(scale, scale, scale);
+}
+
+export class MeasuringTool extends THREE.EventDispatcher{
+	constructor (viewer) {
+		super();
+
+		this.viewer = viewer;
+		this.renderer = viewer.renderer;
+
+		this.viewer.addEventListener('start_inserting_measurement', e => {
+			this.viewer.dispatchEvent({
+				type: 'cancel_insertions'
+			});
+		});
+
+		this.showLabels = true;
+        this.scene = new THREE.Scene();
+		this.scene.name = 'scene_measurement';
+		//this.light = new THREE.PointLight(0xffffff, 1.0);
+		//this.scene.add(this.light);  
+		this.viewer.inputHandler.registerInteractiveScene(this.scene);
+		  
+        
+        //this.scene = viewer.overlay//
+        
+        
+		this.onRemove = (e) => { e.measurement.dispose()/* this.scene.remove(e.measurement); */};
+		this.onAdd = e => {this.scene.add(e.measurement);};
+
+		for(let measurement of viewer.scene.measurements){
+			this.onAdd({measurement: measurement});
+		}
+		 
+        viewer.addEventListener('camera_changed',(e)=>{ 
+            if(e.viewport == viewer.mainViewport ) this.update()
+        })
+        
+        
+		//viewer.addEventListener("update", this.update.bind(this));
+		viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this));
+		viewer.addEventListener("scene_changed", this.onSceneChange.bind(this));
+
+		viewer.scene.addEventListener('measurement_added', this.onAdd);
+		viewer.scene.addEventListener('measurement_removed', this.onRemove);
+        
+        viewer.addEventListener('resize',this.setSize.bind(this))
+        
+	}
+
+	onSceneChange(e){
+		if(e.oldScene){
+			e.oldScene.removeEventListener('measurement_added', this.onAdd);
+			e.oldScene.removeEventListener('measurement_removed', this.onRemove);
+		}
+
+		e.scene.addEventListener('measurement_added', this.onAdd);
+		e.scene.addEventListener('measurement_removed', this.onRemove);
+	}
+
+
+    
+
+
+    
+    createMeasureFromData(data){//add 
+    
+        const measure = new Measure(data);
+        if(measure.failBuilded){
+            return 
+        }
+        viewer.scene.addMeasurement(measure);
+        
+        if(measure.guideLine)measure.guideLine.visible = false
+        return  measure       
+    }
+    
+    
+	update(){
+        return;
+        
+        
+        
+        
+		let camera = this.viewer.scene.getActiveCamera();
+		let domElement = this.renderer.domElement;
+		let measurements = this.viewer.scene.measurements;
+
+	 
+		// make size independant of distance
+        let mainLabels = [], subLabels = [];
+        
+      
+        
+		for (let measure of measurements) {
+			measure.lengthUnit = this.viewer.lengthUnit;
+			measure.lengthUnitDisplay = this.viewer.lengthUnitDisplay;
+			//measure.update();
+
+			updateAzimuth(this.viewer, measure);
+
+			 
+            /*  [...measure.markers, ...measure.edgeLabels, measure.areaLabel].forEach(e=>{
+                e && e.update() 
+            }); */
+             
+
+			// labels
+			/* let labels = measure.edgeLabels.concat(measure.angleLabels);
+			for(let label of labels){ 
+                label.update()
+                if(label.elem.hasClass('sub')){
+                    subLabels.push(label)
+                }else{ 
+                    mainLabels.push(label)
+                }
+			}
+
+			// coordinate labels
+			for (let j = 0; j < measure.coordinateLabels.length; j++) { 
+				let label = measure.coordinateLabels[j]; 
+                label.update() 
+                mainLabels.push(label)
+			} 
+ 
+
+			if(measure.showArea){ // area label
+				let label = measure.areaLabel; 
+                label.update()
+                mainLabels.push(label)
+			} */
+             
+            
+
+			/* if(measure.showCircle){ // radius label
+				let label = measure.circleRadiusLabel;
+				let distance = label.position.distanceTo(camera.position);
+				let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
+
+				let scale = (70 / pr);
+				label.scale.set(scale, scale, scale);
+			} */ 
+			if(!this.showLabels){ 
+				const labels = [
+					...measure.sphereLabels,  
+					...measure.angleLabels,  
+					measure.circleRadiusLabel,
+				]; 
+				for(const label of labels){
+					label.visible = false;
+				}
+			}
+		}
+        //this.updateLabelZIndex([{labels:subLabels},{labels:mainLabels}])
+        
+	}
+    setSize(e){ //e.resolution
+        /* if(Measure.lineMats){
+            for(var m in Measure.lineMats){
+                Measure.lineMats[m].resolution.set(e.canvasWidth, e.canvasHeight);
+            }  
+        }  
+        if(Measure.sphereMats){
+            for(var s in Measure.sphereMats){
+                Measure.sphereMats[s].uniforms.resolution.value.set(e.canvasWidth, e.canvasHeight);
+            }
+        }
+        for (let measure of this.viewer.scene.measurements) { 
+            measure.edgeLabels.concat(measure.areaLabel).forEach(label=>{ 
+                label.sprite.material.uniforms.resolution.value.set(e.canvasWidth, e.canvasHeight);
+            })
+        } */
+    }
+    
+    updateLabelZIndex(group){//[{labels:[]},{}] 顺序按照z-index低到高
+        
+        group.forEach((e,i)=>{
+            e.base = group[i-1] ? group[i-1].base + group[i-1].labels.length : 0
+         
+            var labels = e.labels.sort((a,b)=>{
+                return b.pos2d.z - a.pos2d.z
+            }) 
+            labels.forEach((label,index)=>{
+                $(label.elem).css('z-index', e.base+index) 
+            }) 
+        }) 
+        
+    }
+    
+    
+    
+    
+    editStateChange(e){
+        //console.log("editStateChange" , e.state)
+        let state = e.state
+        if(!state){
+            state = viewer.scene.measurements.some(e=>e.isEditing)
+        }
+        
+        if(state){
+            viewer.dispatchEvent({type:"measureMovePoint"})
+        }else{
+            viewer.dispatchEvent({type:"endMeasureMove"})
+        }
+        
+        
+        //this.editing = 
+    }
+    
+    
+    
+	startInsertion (args = {}, callback, cancelFun) {
+        
+        
+		let domElement = this.viewer.renderer.domElement;
+
+		
+
+		const pick = (defaul, alternative) => {
+			if(defaul != null){
+				return defaul;
+			}else{
+				return alternative;
+			}
+		};
+
+		args.showDistances = (args.showDistances === null) ? true : args.showDistances;
+
+		args.showArea = pick(args.showArea, false);
+		args.showAngles = pick(args.showAngles, false);
+		args.showCoordinates = pick(args.showCoordinates, false);
+		args.showHeight = pick(args.showHeight, false);
+		args.showCircle = pick(args.showCircle, false);
+		args.showAzimuth = pick(args.showAzimuth, false);
+		args.showEdges = pick(args.showEdges, true);
+		args.closed = pick(args.closed, false);
+		args.maxMarkers = pick(args.maxMarkers, Infinity);
+        args.direction = args.direction//add
+		args.type = args.type    /*  || 'Measurement'; */
+        args.showGuideLine = pick(args.showGuideLine, false);  
+        args.isRect = pick(args.isRect, false);
+        
+ 
+        let measure = new Measure(args);
+        this.scene.add(measure);
+        measure.isNew = true
+        
+        
+		this.viewer.dispatchEvent({
+			type: 'start_inserting_measurement',
+			measure: measure
+		});
+         
+        measure.addEventListener('editStateChange', this.editStateChange.bind(this))
+        measure.editStateChange(true)
+        
+        let timer;
+
+ 
+
+		let endDragFun = (e) => { 
+            let length = measure.points.length
+			if (e.button == THREE.MOUSE.LEFT || e.isTouch) { 
+				if (length >= measure.maxMarkers) {
+                    end({finish:true});
+				}else{  
+                    var marker = measure.addMarker({point:measure.points[length - 1].clone()})
+                     
+                    if(args.isRect && measure.markers.length == 3){//marker全可见
+                        measure.addMarker({point:measure.points[0].clone()})
+                        
+                    }else{ 
+                        measure.markers[length].visible = false
+                        measure.edges[length].visible = false 
+                    }
+                    measure.edges[length-1].visible = true 
+                    
+                    measure.markers[length-1].visible = true;
+                    
+                    measure.editStateChange(true) //重新激活reticule状态
+                    
+                    marker.isDragging = true 
+                    measure.continueDrag(marker, e)    
+                } 
+				 
+			} else if (e.button === THREE.MOUSE.RIGHT ) { //触屏怎么取消?
+				if(e.pressDistance < Potree.config.clickMaxDragDis )end(e);//非拖拽的话
+                else measure.continueDrag(null, e)     
+                 
+			}
+		};
+
+		let end = (e={}) => {//确定、结束
+            if(!measure.isNew)return
+            if(args.minMarkers != void 0){
+                
+                if(!e.finish && measure.markers.length<=args.minMarkers){//右键  当个数不够时取消
+                    //this.viewer.scene.removeMeasurement(measure)
+                    //cancelFun && cancelFun()
+                    //重新开始画
+                    measure.markers[0].removeEventListener('mousedown',end)
+                    measure.reDraw()
+                     
+                    
+                    this.viewer.addEventListener('global_click', click, 10)
+                   
+                    measure.editStateChange(true)
+                    return
+                    
+                    /* if(!Potree.settings.isOfficial) this.viewer.scene.removeMeasurement(measure)
+                    else if(e.drag){ //正式版本不允许右键退出, 继续
+                        continueDrag(e.drag.object)
+                        measure.editStateChange(true)
+                        return
+                    } */
+                } 
+            }
+            if (/* !e.finish&& */ measure.markers.length > args.minMarkers) {
+				measure.removeMarker(measure.points.length - 1); 
+                measure.markers[0].removeEventListener('mouseover', mouseover);
+                measure.markers[0].removeEventListener('mouseleave', mouseleave);
+                measure.markers[0].removeEventListener('click'/* 'mousedown' */,Exit) 
+                
+                if(e.byClickMarker && measure.markers.length > args.minMarkers){//通过点击第一个marker而结束的话,会多一个marker
+                    measure.removeMarker(measure.points.length - 1); 
+                }
+			}
+            measure.isNew = false
+            let length = measure.points.length 
+            if(length){
+                measure.markers[length-1].visible = true; 
+                measure.edges[length-1].visible = true  
+                
+                measure.markers.forEach(marker=>{marker.dispatchEvent('addHoverEvent') })
+                measure.edges.forEach(edge=>{edge.dispatchEvent('addHoverEvent') })
+              
+            }
+            clearTimeout(timer) 
+			this.viewer.removeEventListener('cancel_insertions', Exit);
+            //pressExit && this.viewer.inputHandler.removeEventListener('keydown', pressExit);
+            this.viewer.removeEventListener('global_click', click)
+            this.viewer.removeEventListener('global_mousemove', ifAtWrongPlace) 
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "remove",  name:"polygon_AtWrongPlace"
+            });
+            
+            viewer.inputHandler.dispatchEvent({type:'isMeasuring',  v:false, cause:'stopInsertion'}  ) 
+            
+            e.remove || callback && callback()  
+            /* this.viewer.dispatchEvent({
+                type: 'finish_inserting_measurement',
+                measure: measure
+            }); */
+		};
+
+        
+        let Exit = (e)=>{//强制退出
+        
+            if(e.measure && e.measure != measure){
+                return;//若指定了退出的measure但和该measure不一致,就返回
+            }
+            console.log('Exit: ' +  measure.id)
+            if(e.remove){
+                viewer.scene.removeMeasurement(measure)  
+            }
+            
+            measure.editStateChange(false)
+            measure.cannotConfirmNormal = false  //一些dropMarker中的句子
+            measure.guideLine &&(measure.guideLine.visible = false)
+            /*
+            if(this.viewer.inputHandler.drag && !e.remove ){//还未触发drop的话   
+                  this.viewer.inputHandler.drag.object.dispatchEvent({  //这句会导致又增一个marker
+                    type: 'drop',
+                    drag: this.viewer.inputHandler.drag, 
+                    viewer: this.viewer,
+                    pressDistance:0,
+                    button : THREE.MOUSE.RIGHT  
+                }); 
+                 
+            }   else  {*///未结束时添加新的measure时会触发
+                end({finish:true, remove:e.remove, byClickMarker: e.type == 'click'})  
+            //}
+            this.viewer.inputHandler.drag && (this.viewer.inputHandler.drag.object = null)
+            
+        }
+        
+        
+        
+        this.viewer.addEventListener('cancel_insertions', Exit);
+        
+        /*let pressExit
+         if(!Potree.settings.isOfficial){
+            pressExit = (e)=>{ 
+                if(e.keyCode == 27){//Esc
+                    //Exit()
+                    //怎么模拟右键???//现由前端发出
+                }
+            } 
+            this.viewer.inputHandler.addEventListener('keydown', pressExit) 
+        } */ 
+		let mouseover = (e) => { 
+            measure.setMarkerSelected(e.object, 'hover', 'single'); 
+              
+        };
+        let mouseleave = (e) => { 
+            measure.setMarkerSelected(e.object, 'unhover', 'single'); 
+        }  
+          
+                 
+        let click = (e)=>{//一旦点击就立刻增加两marker  
+        
+            if(ifAtWrongPlace(e))return  
+            if(e.clickElement)return  //如点击label时focusOnObject
+             
+            
+            if(e.button === THREE.MOUSE.RIGHT)return 
+            
+            //console.log('measure clicked33', !!e.intersectPoint)
+             
+            //var I = e.intersectPoint && (e.intersectPoint.orthoIntersect || e.intersectPoint.location)
+            var I = e.intersect && (e.intersect.orthoIntersect || e.intersect.location)
+            if(!I){
+                return measure.dispatchEvent('intersectNoPointcloud') 
+            }
+            var atMap = e.drag.dragViewport.name == 'mapViewport'
+            //在地图上测量的首个点按楼层高度(暂时先只按mainViewport相机高度吧,但navvis是按楼层,画在楼层的地面上,可能因为平面图显示的是楼层近地面),
+            
+            if(atMap){ 
+                I = I.clone().setZ(viewer.mainViewport.camera.position.z ) 
+            }
+             
+            var marker = measure.addMarker({point:I})
+            marker.isDragging = true 
+            this.viewer.inputHandler.startDragging(marker , {endDragFun, notPressMouse:true} ); //notPressMouse代表不是通过按下鼠标来拖拽
+            e.drag = this.viewer.inputHandler.drag
+            e.drag.endDragFun = endDragFun
+            e.drag.notPressMouse = true
+            
+            //if(!measure.dragMarker(e) || !measure.dropMarker(e))return
+             
+            measure.dragMarker(e) 
+            measure.dropMarker(e)
+            
+            if(measure.maxMarkers > 1 ){
+                measure.markers[1].visible = false
+                measure.edges[1].visible = false
+            }
+            if(measure.maxMarkers>2 && !measure.isRect){ 
+                measure.markers[0].addEventListener('mouseover', mouseover);
+                measure.markers[0].addEventListener('mouseleave', mouseleave);
+                measure.markers[0].addEventListener('click'/* 'mousedown' */,Exit) //点击到第一个marker就结束 
+            }
+            
+            
+            this.viewer.removeEventListener('global_click', click)///* global_drop */
+            
+            
+            
+            //console.log('measure clicked')
+            e.consume && e.consume()
+            
+            return {stopContinue:true}//防止继续执行别的侦听,如flytopano
+        }
+        
+        //点击第n下拥有n+1个marker, n>0
+        
+        viewer.inputHandler.dispatchEvent({type: 'isMeasuring', v: true, cause:'startInsertion'})
+        
+        this.viewer.addEventListener('global_click', click, 10)//add importance:10
+            
+        let ifAtWrongPlace = (e)=>{
+            if(measure.unableDragAtMap && e.hoverViewport.name == 'mapViewport' ){ 
+                if(e.isTouch){
+                    viewer.dispatchEvent({type:'reticule_forbit', v:true})
+                }else{
+                    viewer.dispatchEvent({
+                        type : "CursorChange", action : "add",  name:"polygon_AtWrongPlace"
+                    });
+                } 
+                return true
+            }else{ 
+                if(e.isTouch){
+                    viewer.dispatchEvent({type:'reticule_forbit',v:false})
+                }else{
+                    viewer.dispatchEvent({
+                        type : "CursorChange", action : "remove",  name:"polygon_AtWrongPlace"
+                    });
+                }
+            }
+        }
+        
+        
+        
+        if(measure.unableDragAtMap){ 
+            this.viewer.addEventListener('global_mousemove', ifAtWrongPlace) 
+        }
+        
+        
+		this.viewer.scene.addMeasurement(measure);
+        
+		return measure;
+	}
+	
+    
+	render(o={}){
+        if(this.scene.children.length == 0)return
+        
+        viewer.setCameraLayers(o.camera, ['measure'])
+		
+        if(o.screenshot && this.viewer.ssaaRenderPass.enabled){ //抗锯齿
+            this.viewer.ssaaRenderPass.sampleLevel = 4
+            this.viewer.composer.render(this.scene, o.camera );  
+            /* viewer.scene.measurements.forEach(e=>{ //隐藏除了label以外的
+                e.children.forEach((c)=>{
+                    if(!(c instanceof TextSprite)){
+                        c.visible = false
+                    } 
+                }) 
+            })  */
+        }else{
+            this.viewer.renderer.render(this.scene, o.camera );
+        }
+	}
+};

+ 94 - 0
src/custom/objects/tool/TagTool.js

@@ -0,0 +1,94 @@
+
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+
+import math from "../../utils/math.js";
+import {Utils} from "../../../utils.js"; 
+
+import Tag from '../Tag.js'
+ 
+export class TagTool extends THREE.EventDispatcher{
+	constructor (viewer) {
+		super();
+        
+        
+        
+        
+        this.viewer = viewer
+        
+        
+        
+        this.viewer.addEventListener('start_inserting_tag', e => {
+			this.viewer.dispatchEvent({
+				type: 'cancel_insertions'
+			});
+		});
+    }
+    
+    
+    
+    
+    createTagFromData(data){
+        let tag = new Tag({
+            title: data.title, position: data.position,  normal: data.normal,
+            root: data.root   //e.intersect.pointcloud || e.intersect.object
+        })
+        
+        return tag
+        
+    }
+     
+    
+    startInsertion (args = {}, callback, cancelFun) {
+        let deferred = $.Deferred();
+         
+        this.viewer.dispatchEvent({
+			type: 'start_inserting_tag' 
+			 
+		});
+        this.adding = true
+        
+        let cancel = ()=>{
+            end()
+        }
+        let end = ()=>{
+            this.adding = false
+            viewer.dispatchEvent({type:"endTagMove"})
+            this.viewer.removeEventListener('global_click', click)
+        }
+        let click = (e)=>{
+            
+            
+            var worldPos = e.intersect && (/* e.intersect.orthoIntersect ||  */e.intersect.location)
+            if(!worldPos){
+                return  
+            }
+            
+            let localPos = Potree.Utils.datasetPosTransform({ toDataset: true, pointcloud:e.intersect.pointcloud, object:e.intersect.object,  position:worldPos })
+
+            
+            let tag = new Tag({
+                title: '1', position: localPos,  normal:e.intersect.normal,
+                root: e.intersect.pointcloud || e.intersect.object
+            }) 
+            
+            //pointcloud里加一个normal 的非float32
+            
+            
+            
+            end()
+            e.consume && e.consume()
+            deferred.resolve(tag)
+            
+            
+            return {stopContinue:true}
+            
+        }
+        this.viewer.addEventListener('global_click', click, 10)
+        return deferred.promise()
+    }  
+    
+    
+    
+    
+}
+

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1981 - 0
src/custom/objects/tool/TransformControls.js


+ 723 - 0
src/custom/objects/tool/ctrlPolygon.js

@@ -0,0 +1,723 @@
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import {LineDraw, MeshDraw} from "../../utils/DrawUtil.js";
+import math from "../../utils/math.js";
+
+
+
+
+
+
+const verticalLine = new THREE.Line3()
+
+//控制点和边的合集。具有可以拖拽修改的功能,拖拽时能防止线相交。
+export class ctrlPolygon extends THREE.Object3D { 
+    constructor (type, prop) {
+        super()
+        this.type = type
+        
+        this.maxMarkers = Number.MAX_SAFE_INTEGER;
+       
+         
+        this.transformData(prop);
+        for(let i in prop){
+            this[i] = prop[i]
+        }
+        
+        if(this.closed && this.dimension == '2d'){
+            this.areaPlane = this.createAreaPlane(); 
+            this.add(this.areaPlane)
+        }
+        
+        //数据--刚开始一定是空的
+        this.points = []; 
+        //mesh 不一定有
+        this.markers = []; 
+		this.edges = [];
+        
+        this.center
+        
+        
+        
+        
+    }
+    
+    initData(prop){
+        //开始加数据  
+        if(Potree.settings.editType == 'merge'){ //融合页面没有地图,measure的不需要指定datasetId,每个点都有各自的datasetId,跟着各自的模型走
+            if(this.dataset_points){
+                this.dataset_points = this.dataset_points.map(e=>{
+                    return e && new THREE.Vector3().copy(e) 
+                })
+                prop.points = this.dataset_points.map((p,i)=>{
+                    return Potree.Utils.datasetPosTransform({fromDataset:true, datasetId:this.points_datasets[i], position: p})
+                })
+                if(prop.points.some(e=>e == void 0)){
+                    return false
+                }               
+            }else{
+                this.dataset_points = []
+            }
+        } 
+        
+        
+        
+        if(prop.points){ 
+        
+            for(const p of prop.points){
+                const pos = new THREE.Vector3().copy(p) 
+                this.addMarker({point:pos}); 
+            }
+            
+            
+            if(Potree.settings.editType != 'merge'){ 
+                if(this.datasetId != void 0){//初始化位置
+                    if(this.dataset_points){
+                        this.dataset_points = this.dataset_points.map(e=>{
+                            return e && new THREE.Vector3().copy(e) 
+                        })
+                        this.transformByPointcloud() //根据dataset_points生成points  
+                    }  
+                }else{
+                    if(prop.dataset_points && prop.dataset_points.some(e=>e != void 0)){
+                        console.error('存在测量线的datasetId为空而dataset_points有值,请检查并删除:'+this.sid)//存在过的bug,原因未知,可能是后台处理dataset时替换的错误:http://192.168.0.21/index.php?m=bug&f=view&bugID=23601
+                        console.log(this)
+                    }
+                } 
+            }  
+            
+            this.getFacePlane()
+            this.getPoint2dInfo(this.points)
+            
+            this.update({ifUpdateMarkers:true})
+            //this.dragChange(new THREE.Vector3().copy(prop.points[prop.points.length-1]), prop.points.length-1); 
+            this.setSelected(false )
+            this.markers.forEach(marker=>{marker.dispatchEvent('addHoverEvent') })
+            return true
+        }
+    }
+    
+    
+    
+    
+    
+    addMarker(o={}){
+        var index = o.index == void 0 ? this.points.length : o.index  //要当第几个
+        
+        this.points = [...this.points.slice(0,index), o.point, ...this.points.slice(index,this.points.length)]
+		//this.points.push(o.point);
+         
+        if(o.marker){
+            this.add(o.marker)
+            this.markers = [...this.markers.slice(0,index), o.marker, ...this.markers.slice(index,this.markers.length)]
+            this.updateMarker(o.marker, o.point)
+            o.marker.addEventListener('drag', this.dragMarker.bind(this));
+            o.marker.addEventListener('drop', this.dropMarker.bind(this));
+            
+            
+            let addHoverEvent = (e)=>{
+                let mouseover = (e) => { 
+                    this.setMarkerSelected(e.object, 'hover', 'single'); 
+                    viewer.dispatchEvent({
+                        type : "CursorChange", action : "add",  name:"markerMove"
+                    }) 
+                };
+                let mouseleave = (e) => { 
+                    this.setMarkerSelected(e.object, 'unhover', 'single'); 
+                    viewer.dispatchEvent({
+                        type : "CursorChange", action : "remove",  name:"markerMove"
+                    })
+                } 
+                o.marker.addEventListener('mouseover', mouseover);
+                o.marker.addEventListener('mouseleave', mouseleave);
+                o.marker.removeEventListener('addHoverEvent',addHoverEvent) 
+            }
+            o.marker.addEventListener('addHoverEvent',addHoverEvent)//当非isNew时才添加事件
+            if(!this.isNew){
+                o.marker.dispatchEvent('addHoverEvent')
+            }
+            
+        } 
+        
+        if(o.edge){
+            this.add(o.edge)
+            this.edges = [...this.edges.slice(0,index), o.edge, ...this.edges.slice(index,this.edges.length)]
+        } 
+        
+        
+    }
+    
+    
+    
+    
+    dragMarker(e){
+        
+        var I, atMap 
+        
+        if(e.hoverViewport != e.drag.dragViewport){//不能使用e.dragViewport,要使用drag中的,因为drag中存储的要一直继承下来,不因mouseup了而改变。
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "add",  name:"polygon_AtWrongPlace"
+            })
+            return
+        }
+        
+        viewer.dispatchEvent({
+            type : "CursorChange", action : "remove",  name:"polygon_AtWrongPlace"
+        })
+        
+        atMap = e.drag.dragViewport.name == 'mapViewport' 
+        
+        if(atMap && this.unableDragAtMap){ 
+            e.drag.object = null //取消拖拽
+            return 
+        }
+        e.drag.object.isDragging = true 
+        
+        
+        I = e.intersect && (e.intersect.orthoIntersect || e.intersect.location)
+        
+        //记录数据集
+        
+        //在三维中脱离点云(在map中拉到周围都没有点云的地方)的顶点,无法拖拽怎么办
+         
+        if (I) {  
+            let i = this.markers.indexOf(e.drag.object);
+            if (i !== -1) {  
+                this.dragChange(I.clone(), i, atMap) 
+                
+                /* if(this.points_datasets){
+                    if(e.intersect.pointcloud) this.points_datasets[i] = e.intersect.pointcloud.dataset_id
+                    else this.points_datasets[i] = null
+                } */ 
+                if(this.points_datasets){
+                    if(e.intersect.pointcloud) this.points_datasets[i] = e.intersect.pointcloud.dataset_id
+                    else if(e.intersect.object) this.points_datasets[i] = e.intersect.object.dataset_id
+                    else this.points_datasets[i] = null
+                }
+            }
+            this.editStateChange(true)
+            return true
+        }
+        
+        
+        
+        
+    };
+    
+    
+    
+    
+    
+    
+    dragChange(intersectPos, i, atMap){
+        let len = this.markers.length 
+        let oldPoint = this.points[i]; 
+        if(atMap){
+            intersectPos.setZ(oldPoint.z) //在地图上拖拽,不改变其高度。
+        }
+        let location = intersectPos.clone()
+        
+        
+        
+        if(this.faceDirection && this.maxMarkers == 2 && len == 2){//add 固定方向的点不直接拖拽
+            var p1 = this.markers[0].position
+            if(this.faceDirection == 'horizontal'){
+                var projectPos = location.clone().setZ(p1.z)
+            }else{
+                var projectPos = p1.clone().setZ(location.z)
+            }
+            //var p2 = p1.clone().add(this.direction)
+            //var projectPos = math.getFootPoint(location, p1, p2)
+            
+            
+            LineDraw.updateLine(this.guideLine, [location, projectPos])
+            location = projectPos
+            this.guideLine.visible = true
+        }else if( len > 1){ 
+             
+            var points = this.points.map(e=>e.clone())
+            points[i].copy(location) //算normal需要提前确认point
+            
+            //若为定义了面朝向的矩形
+            if(this.faceDirection == 'horizontal'){
+                if(len == 2){
+                    location.setZ(points[0].z) 
+                }
+                if(!this.facePlane){//一个点就能确定面
+                    this.facePlane = new THREE.Plane().setFromNormalAndCoplanarPoint( new THREE.Vector3(0,0,1), this.points[0]  )
+                }
+            }else if(this.faceDirection == 'vertical'){//当有两个点时, 有两个方向的可能
+                if(len == 2){
+                    if(this.isRect){
+                        let vec = points[0].clone().sub(location) 
+                        if(Math.sqrt(vec.x*vec.x+vec.y*vec.y) > Math.abs(vec.z) ){//水平(高度差小于水平距离时)
+                            location.setZ(points[0].z) 
+                            //this.cannotConfirmNormal = false;//能确定面为水平方向
+                        }else{//垂直 (当两点一样时也属于这种)
+                            location.setX(points[0].x);
+                            location.setY(points[0].y);
+                            //this.cannotConfirmNormal = true; //不能确定面,因第三点可绕着纵轴线自由移动
+                        }
+                    }
+                }else{ 
+                    {//判断cannotConfirmNormal. 如果前几段都在竖直线上,就不能固定出面方向。
+                        this.cannotConfirmNormal = true
+                        let max = this.isRect ? 1 : len-2 
+                        for(let i=0;i<max;i++){
+                            let p1 = points[i].clone()
+                            let p2 = points[i+1].clone()
+                            let vec = p1.sub(p2);
+                            if(vec.x != 0 || vec.y != 0){
+                                this.cannotConfirmNormal = false
+                                break;
+                            } 
+                        } 
+                    }
+                    
+                    if(!this.facePlane || this.cannotConfirmNormal){//三个点且为水平方向时,计算面
+                          
+                        var points_ = points.map(e=>new THREE.Vector2(e.x,e.y))
+                        var points2 = getDifferentPoint(points_, 2); 
+                        if(points2){
+                            let normal = math.getNormal2d({p1:points2[0], p2:points2[1]})  
+                            normal = new THREE.Vector3(normal.x, normal.y, 0)
+                            this.facePlane = new THREE.Plane().setFromNormalAndCoplanarPoint( normal, this.points[0]  )
+                        } 
+                    }
+                }
+            } 
+
+            if(len > 2){//area
+            
+                if(!this.faceDirection){ 
+                    if(len == 3 || this.isRect) this.cannotConfirmNormal = true //当第三个点固定后(有四个点时)才能固定面
+                    if(!this.facePlane || this.cannotConfirmNormal){
+                        var points3 = getDifferentPoint(points, 3);//只有找到三个不同的点算拥有面和area
+                        if(points3){
+                            this.facePlane = new THREE.Plane().setFromCoplanarPoints(...points3 )
+                        }
+                    }
+                }
+                
+                if( this.facePlane && !this.cannotConfirmNormal  ){//之后加的点一定要在面上  
+                    if(atMap){ 
+                        //地图上用垂直线,得到和面的交点。
+                        verticalLine.set(location.clone().setZ(100000), location.clone().setZ(-100000))//确保长度范围覆盖所有测量面 
+                        location = this.facePlane.intersectLine(verticalLine, new THREE.Vector3() )
+                        if(!location) return; 
+                    }else{ 
+                        location = this.facePlane.projectPoint(intersectPos, new THREE.Vector3() )
+                    }
+                }  
+                
+                
+                points[i].copy(location)//再copy确认一次
+                
+                if(this.isRect){ //是矩形 (即使没有faceDirection也能执行)
+                    //根据前两个点计算当前和下一个点
+                    var p1 = points[(i-2+len)%len]
+                    var p2 = points[(i-1+len)%len]
+                    if(p1.equals(p2)){//意外情况:重复点两次 ( bug点,改了好多遍)
+                        if(this.faceDirection == 'vertical'){
+                            p2.add(new THREE.Vector3(0,0,0.0001))
+                        }else{
+                            p2.add(new THREE.Vector3(0,0.0001,0))
+                        } 
+                    } 
+                    //p3 : location 
+                    var foot = math.getFootPoint(location, p1, p2)//p2 修改p2到垂足的位置
+                    var vec = foot.clone().sub(location)
+                    var p4 = p1.clone().sub(vec) 
+                     
+                    
+                    points[(i-1+len)%len].copy(foot) 
+                    points[(i+1)%len].copy(p4) 
+                    this.setPosition((i-1+len)%len, foot);//p2
+                    this.setPosition((i+1)%len, p4);
+                    
+                }  
+                
+                /* let points2d;
+                if(this.facePlane){
+                    var originPoint0 = points[0].clone() 
+                    var qua = math.getQuaBetween2Vector(this.facePlane.normal, new THREE.Vector3(0,0,1), new THREE.Vector3(0,0,1));
+                    points2d = points.map(e=>e.clone().applyQuaternion(qua))  
+                } */
+                this.getPoint2dInfo(points)
+                
+                var isIntersectSelf = !this.isRect && len > 3 && this.intersectSelf(this.point2dInfo.points2d)//检测相交
+                if(isIntersectSelf){
+                    //not-allowed
+                    viewer.dispatchEvent({
+                        type : "CursorChange", action : "add",  name:"polygon_isIntersectSelf"
+                    })
+                    this.isIntersectSelf = true
+                    return
+                }else{
+                    this.isIntersectSelf = false
+                    viewer.dispatchEvent({
+                        type : "CursorChange", action : "remove",  name:"polygon_isIntersectSelf"
+                    })
+                    /* this.facePlane && (this.point2dInfo = {
+                        originPoint0 ,
+                        points2d,
+                        quaInverse : qua.clone().invert()
+                    }) */
+                }
+                
+                
+                 
+            }
+            
+            var showGuideLine = len>1 && (this.faceDirection || len > 3)
+            if(showGuideLine && this.guideLine){
+                LineDraw.updateLine(this.guideLine, [intersectPos, location])
+                this.guideLine.visible = true
+            } 
+             
+            //console.log(this.points.map(e=>e.toArray())) 
+
+        } 
+         
+        
+        if(this.restrictArea){  
+            let holes = this.restrictArea.holes.concat(this.restrictArea.parentHoles)           
+            let holesPoints = holes.filter(e=>e!=this && e.points.length>2).map(e=>e.points) 
+            if(!math.isPointInArea(this.restrictArea.points, holesPoints, location)){
+                viewer.dispatchEvent({
+                    type : "CursorChange", action : "add",  name:"polygon_AtWrongPlace"
+                })
+                this.isAtWrongPlace = true
+                return
+            }
+            //就不处理相交线了。 有个缺点:floor上的hole可以限制room,但hole不受room限制,会导致room的marker被框在hole里而动不了。只能去调整hole了
+        } 
+        
+        
+        
+        viewer.dispatchEvent({
+            type : "CursorChange", action : "remove",  name:"polygon_AtWrongPlace"
+        })
+        this.isAtWrongPlace = false
+        this.setPosition(i, location); 
+        this.update()
+        
+        this.dispatchEvent({type:'dragChange', index:i})
+        
+        
+        
+    }
+    
+    dropMarker(e){
+        //console.log('dropMarker')
+        if (this.isNew && e.pressDistance>Potree.config.clickMaxDragDis){//拖拽的话返回
+            return this.continueDrag(null,e)   
+        } 
+        
+        if(e.isTouch){ 
+            if(e.hoverViewport != viewer.mainViewport && this.unableDragAtMap){
+                viewer.dispatchEvent({type:'reticule_forbit', v:true})
+                //console.log('reticule_forbit',true)
+                return this.continueDrag(null,e)    
+            }else{
+                viewer.dispatchEvent({type:'reticule_forbit', v:false})
+                //console.log('reticule_forbit',false)
+            }
+            this.dragMarker(e) //触屏时必须先更新下点 
+            
+        }
+        
+        
+        if (e.button != THREE.MOUSE.RIGHT && (//右键click的话继续执行,因为会停止
+                this.isIntersectSelf && this.isNew //有线相交了
+                || this.isAtWrongPlace && this.isNew
+                || !e.isAtDomElement && this.isNew//如果是刚添加时在其他dom点击, 不要响应
+                ||  e.hoverViewport != viewer.mainViewport && this.unableDragAtMap //垂直的测量线不允许在地图上放点
+                || this.isNew && !getDifferentPoint(this.points, this.points.length )   //不允许和之前的点相同, 但这句在点云稀疏时会导致难结束 
+            ) 
+        ){
+            return this.continueDrag(null,e)    
+        } 
+         
+        //console.log('drop marker' )
+         
+        let i = this.markers.indexOf(e.drag.object); 
+        if (i !== -1) {
+            this.dispatchEvent({
+                'type': 'marker_dropped', 
+                'index': i
+            });
+            if(this.markers.length>2 && this.facePlane)this.cannotConfirmNormal = false
+            this.guideLine &&(this.guideLine.visible = false)
+        }
+        
+       
+        
+        this.setMarkerSelected(e.drag.object, 'unhover', 'single') 
+        
+        this.editStateChange(false)
+        
+        
+        
+        
+        e.drag.endDragFun && e.drag.endDragFun(e)//  addmarker
+         
+        //if(this.changeCallBack)this.changeCallBack()
+           
+        return true
+    };
+    
+    getFacePlane(){//最普通一种get方法,根据顶点。且假设所有点已经共面,且不重合
+        if(this.points.length<3) return
+        this.facePlane = new THREE.Plane().setFromCoplanarPoints(...this.points.slice(0,3) )
+         
+    }
+    
+    getPoint2dInfo(points){ //在更新areaplane之前必须更新过point2dInfo
+        if(this.facePlane){ 
+            var originPoint0 = points[0].clone() 
+            var qua = math.getQuaBetween2Vector(this.facePlane.normal, new THREE.Vector3(0,0,1), new THREE.Vector3(0,0,1));
+            let points2d = points.map(e=>e.clone().applyQuaternion(qua))  
+        
+            this.point2dInfo = {
+                originPoint0 ,
+                points2d,
+                quaInverse : qua.clone().invert()
+            } 
+        }    
+    }
+    
+    
+    
+    
+    setPosition (index, position) {//拖拽后设置位置
+		let point = this.points[index];
+		point.copy(position);
+        /* if(this.datasetId){
+            this.dataset_points[index] = Potree.Utils.datasetPosTransform({toDataset:true, datasetId:this.datasetId, position:point.clone()})
+        } */
+        
+        /* if(Potree.settings.editType == 'merge'){ 
+            this.dataset_points[index] = Potree.Utils.datasetPosTransform({toDataset:true,this.points_datasets[i], position:point.clone()})
+        } */
+        
+        let marker = this.markers[index];  
+        this.updateMarker(marker, point)
+ 
+	 
+	} 
+    
+    updateMarker(marker, pos){
+        marker.position.copy(pos);
+        marker.update();
+    }
+    
+    
+    intersectSelf(points2d){//add
+        var len =  points2d.length
+        for(var i=0;i<len;i++){
+            for(var j=i+2;j<len;j++){
+                if(Math.abs(j-len-i)<2)continue;//不和邻边比
+                
+                var p1 =  points2d[i]
+                var p2 =  points2d[i+1]
+                var p3 =  points2d[j]
+                var p4 =  points2d[(j+1)%len] 
+                if(p1.equals(p2) || p3.equals(p4) || p1.equals(p3) || p2.equals(p3) || p1.equals(p4) || p2.equals(p4))continue 
+             
+                
+                var line1 = [p1,p2]
+                var line2 = [p3,p4]
+                var intersect = math.isLineIntersect(line1, line2, false, 0.001)
+                if(intersect){
+                    return true
+                    break
+                } 
+            }
+        }
+          
+    }
+	
+    removeMarker (index) {
+        
+		this.points.splice(index, 1); 
+        
+        const marker = this.markers[index]
+		//this.remove(marker); 
+        this.markers.splice(index, 1);   
+        marker.dispatchEvent({type:'dispose'})
+        
+        
+		let edgeIndex = index           //(index === 0) ? 0 : (index - 1);
+        const edge = this.edges[edgeIndex]
+        if(edge){
+            this.remove(edge);
+            this.edges.splice(edgeIndex, 1);
+            edge.dispatchEvent({type:'dispose'})
+        }
+        this.point2dInfo && this.point2dInfo.points2d.splice(index, 1); //add
+
+        this.dispatchEvent({type:'removeMarker',index,marker})
+		
+        
+	} 
+    
+    createAreaPlane(mat){ 
+        var geometry = new THREE.Geometry();
+        var mesh = new THREE.Mesh(geometry, mat)
+        
+        return mesh
+    }
+    
+    updateAreaPlane(){
+        if(!this.point2dInfo)return
+        this.areaPlane.geometry.dispose()
+        if(this.points.length>2){
+            this.areaPlane.geometry = MeshDraw.getShapeGeo(this.point2dInfo.points2d)
+            var center = math.getCenterOfGravityPoint(this.point2dInfo.points2d) //重心 
+                  
+            var firstPos =  this.point2dInfo.points2d[0].clone()
+            firstPos.z = 0                  //因为shape只读取了xy,所以位移下, 再算出最终位置,得到差距
+            firstPos.applyQuaternion(this.point2dInfo.quaInverse)
+            var vec = this.point2dInfo.originPoint0.clone().sub(firstPos)
+            center = new THREE.Vector3(center.x, center.y, 0)
+            center.applyQuaternion(this.point2dInfo.quaInverse)
+            this.areaPlane.quaternion.copy(this.point2dInfo.quaInverse) 
+            this.areaPlane.position.copy(vec)       
+            center.add(vec)
+            this.center = center 
+        }else{
+            this.areaPlane.geometry = new THREE.Geometry()
+            
+        } 
+        
+        
+    }
+
+
+    
+    
+    
+     
+
+    update(options={}){
+        if(this.points.length === 0){
+			return;
+		} 
+        
+        
+        
+        
+        
+        let lastIndex = this.points.length - 1;
+            
+       
+        for (let index = 0; index <= lastIndex; index++) {
+            
+            let nextIndex = (index + 1 > lastIndex) ? 0 : index + 1;
+            let previousIndex = (index === 0) ? lastIndex : index - 1;
+
+            let point = this.points[index];
+            let nextPoint = this.points[nextIndex];
+            let previousPoint = this.points[previousIndex];
+
+            if(options.ifUpdateMarkers){
+                this.updateMarker(this.markers[index], point)
+            }   
+
+            { // edges
+                let edge = this.edges[index]; 
+                if(edge){
+                    LineDraw.updateLine(edge, [point, nextPoint]) 
+                    //edge.visible = index < lastIndex || this.isRect || (this.closed && !this.isNew);
+                }
+                
+                
+            }
+
+            
+             
+        }
+        
+        if(this.areaPlane){
+            this.updateAreaPlane() 
+        }
+        
+        //this.dispatchEvent({type:'update'})     
+        viewer.mapViewer && viewer.mapViewer.dispatchEvent('content_changed') //暂时先这么都通知
+        
+    } 
+    
+    dispose(){//add 
+        this.parent.remove(this)
+        this.markers.concat(this.edges).forEach(e=>e.dispatchEvent({type:'dispose'})) 
+    }
+    
+    
+    reDraw(restMarkerCount=0){//重新开始画
+        let pointCount = this.points.length - restMarkerCount // restMarkerCount为需要留下的marker数量
+        while(pointCount > 0){
+            this.removeMarker(--pointCount)
+        }
+        this.point2dInfo = null
+        this.facePlane = null   
+         
+        
+                    
+    }
+    
+    
+    setMarkerSelected(){}
+    editStateChange(state){
+        if(!state){
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "remove",  name:"polygon_isIntersectSelf"
+            })
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "remove",  name:"polygon_AtWrongPlace"
+            })
+            viewer.dispatchEvent({type:'reticule_forbit', v:false})
+            
+            this.markers.forEach(e=>e.isDragging = false )
+        }
+    }
+    
+    transformData(){}
+    setSelected(){}
+    
+    
+    
+    continueDrag(marker, e){
+        let object = marker || e.drag.object
+        object.isDragging = true 
+        var timer = setTimeout(()=>{//等 drag=null之后 //右键拖拽结束后需要重新得到drag 
+            if(this.parent && object.isDragging){
+                //console.log('continueDrag')        
+                viewer.inputHandler.startDragging( object ,
+                    {endDragFun: e.drag.endDragFun, notPressMouse:e.drag.notPressMouse, dragViewport:e.drag.dragViewport} 
+                )
+            } 
+        },1)
+        return timer
+    }
+ 
+    
+}
+
+
+
+function getDifferentPoint(points, count){//for facePlane
+    var result = [];
+    for(let i=0;i<points.length;i++){
+        var p = points[i];
+        if(result.find(e=>e.equals(p)))continue;
+        else result.push(p)
+        if(result.length == count)break
+    }
+    if(result.length == count)return result
+}
+
+
+ 
+
+
+ 

+ 347 - 0
src/custom/objects/tool/mapClipBox.js

@@ -0,0 +1,347 @@
+
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import {ctrlPolygon} from './ctrlPolygon.js'
+import {LineDraw } from "../../utils/DrawUtil.js";
+import Sprite from '../../objects/Sprite.js'
+import {config} from '../../settings.js'
+
+import math from "../../utils/math.js";
+let texLoader = new THREE.TextureLoader() 
+
+let color = new THREE.Color(config.clip.color)
+
+let markerMats
+let markerSizeInfo = {width2d:30}
+
+
+
+
+export class mapClipBox extends ctrlPolygon {
+    constructor (center, scale) { 
+        center = center.clone().setZ(0);//所有Z都为0
+        
+        
+        let prop = {
+            points : getPoints(center, scale, 0),
+            closed : true,
+            isRect : true,
+            dimension : '2d'
+        }
+         
+        
+        super('mapClipBox', prop)
+        
+        this.angle = 0
+        this.createRotateBar();
+        this.edgeMarkers = [];
+        
+        //addMarkers:
+        this.initData(prop)
+        
+        
+        
+        
+        {//areaPlane event 能拖动 
+            this.areaPlane.addEventListener('mouseover',()=>{ 
+                viewer.dispatchEvent({
+                    type : "CursorChange", action : "add",  name:"mapClipMove"
+                })  
+            })
+            this.areaPlane.addEventListener('mouseleave',()=>{ 
+                viewer.dispatchEvent({
+                    type : "CursorChange", action : "remove",  name:"mapClipMove"
+                })
+            })
+            
+            let lastPos;
+            let drag = (e)=>{
+                let intersect = e.intersect.orthoIntersect 
+                if(lastPos){ 
+                    let moveVec = new THREE.Vector3().subVectors(intersect, lastPos).setZ(0)
+                    this.center.add(moveVec)
+                    this.updatePoints()
+                    this.dispatchEvent({type:'repos'})
+                     
+                } 
+                lastPos = intersect.clone();                
+            }
+            let drop = (e)=>{
+                lastPos = null
+            }
+            this.areaPlane.addEventListener('drag', drag)
+            this.areaPlane.addEventListener('drop', drop)
+              
+        }
+        
+        
+        /* this.addEventListener('dragChange',()=>{
+            this.updateTwoMidMarker(index+1)
+        }) */
+        
+        
+        
+        viewer.setObjectLayers(this, 'mapObjects' )
+    }
+   
+   
+    
+    
+    
+    getScale(){
+        return new THREE.Vector3(this.points[0].distanceTo(this.points[1]), this.points[1].distanceTo(this.points[2]) ,1)
+    }
+ 
+   
+    
+    addMarker(o={} ){
+        let marker = new Sprite({mat:this.getMarkerMaterial('default'), sizeInfo: markerSizeInfo, dontFixOrient: true, viewports:viewer.mapViewer.viewports, name:"mapClipBox_marker",   } )
+        
+        marker.renderOrder = 3 
+        //marker.markerSelectStates = {}
+        
+        let edge = LineDraw.createLine([new THREE.Vector3,new THREE.Vector3],{color  })
+        let edgeMarker = new Sprite({mat:this.getMarkerMaterial('default'), sizeInfo: markerSizeInfo, dontFixOrient: true, viewports:viewer.mapViewer.viewports, name:"mapClipBox_edgePoint"} )
+        let mouseover = (e) => {
+            this.setMarkerSelected(e.object, true, 'single');
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "add",  name:"markerMove"
+            }) 
+        };
+        let mouseleave = (e) => {
+            this.setMarkerSelected(e.object, false, 'single'); 
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "remove",  name:"markerMove"
+            })
+        }
+        edgeMarker.addEventListener('mouseover', mouseover);
+        edgeMarker.addEventListener('mouseleave', mouseleave);
+        let dragInfo = {lastPos:null};
+        edgeMarker.addEventListener('drag', this.dragEdge.bind(this,dragInfo));
+        edgeMarker.addEventListener('drop', this.dropEdge.bind(this,dragInfo));   
+        this.edgeMarkers.push(edgeMarker)
+        this.add(edgeMarker)
+        marker.dispatchEvent('addHoverEvent')  
+      
+        super.addMarker({point:o.point, marker:marker,  edge})
+        
+    }
+    
+    
+    
+    dragEdge(dragInfo, e){//拖拽一个边(或一个边类型的marker),带动它的两个端点。 可以转化为拖拽marker往法线方向
+        var I, atMap 
+         
+        atMap = e.dragViewport.name == 'mapViewport'
+        I = e.intersect.orthoIntersect 
+        
+        if (I && dragInfo.lastPos) {
+            let i = this.edgeMarkers.indexOf(e.drag.object);
+            if (i !== -1) {  
+                let lineNormal = math.getNormal2d({p1:this.points[i], p2:this.points[(i+1)%4]}); 
+                let moveVec = new THREE.Vector3().subVectors(I, dragInfo.lastPos).setZ(0)//移动的向量
+                let dragVec = moveVec.projectOnVector(lineNormal).setZ(0)//在法线上移动的向量
+                let newPos = new THREE.Vector3().addVectors(this.points[i], dragVec) //marker应到的位置
+                this.dragChange(newPos, i, atMap) //转化为这个marker的拖拽
+            }
+        }
+        dragInfo.lastPos = I.clone();  
+    }
+    dropEdge(dragInfo, e){
+        dragInfo.lastPos = null 
+        this.setMarkerSelected(e.drag.object, false, 'single') //拖拽时似乎没有触发mouseout
+    }
+    
+    
+    
+    createAreaPlane(){
+        var planeMat =  new THREE.MeshBasicMaterial({
+            color,//"#00eeff",
+            side:THREE.DoubleSide,
+            opacity:0.3,
+            transparent:true,
+            depthTest:false
+        })
+        
+        return super.createAreaPlane(planeMat)
+    }
+    
+    
+    
+    getMarkerMaterial(type) {
+        if(!markerMats){
+            markerMats = {  
+                default:    new THREE.MeshBasicMaterial({  
+                    transparent: !0,
+                    color,
+                    opacity: 0.8,
+                    map: texLoader.load(Potree.resourcePath+'/textures/whiteCircle.png' ),  
+                }),
+                select:    new THREE.MeshBasicMaterial({   
+                    transparent: !0,
+                    color,
+                    opacity: 1,
+                    map: texLoader.load(Potree.resourcePath+'/textures/whiteCircle.png' ), 
+                     
+                }),   
+            }
+            mapClipBox.markerMats = markerMats
+        }
+        return markerMats[type]
+        
+    }
+    
+    setMarkerSelected(marker, state, hoverObject){ 
+        //console.warn(marker.id , state, hoverObject)
+        if(state == 'hover'){
+            marker.material = this.getMarkerMaterial('select')
+        }else{
+            marker.material = this.getMarkerMaterial('default')
+        }
+        
+        /* marker.markerSelectStates[hoverObject] = state
+        let absoluteState = false
+        for(var i in marker.markerSelectStates){
+            if(marker.markerSelectStates[i]){
+                absoluteState = true; break;
+            }
+        }
+        if(absoluteState){
+            marker.material = this.getMarkerMaterial('select')
+        }else{
+            marker.material = this.getMarkerMaterial('default')
+        }
+        
+        marker.selected = absoluteState */
+        
+        viewer.mapViewer.dispatchEvent('content_changed') 
+    }
+    
+    createRotateBar(){
+        //中心点在线的一端,整体初始在box上方
+        const lineLen = 1.5, circleWidth = 2, barOpacity = 0.7;
+       
+        let object = new THREE.Object3D;
+        
+        let bar = new Sprite({mat:new THREE.MeshBasicMaterial({ 
+                side:THREE.DoubleSide,
+                opacity: barOpacity,
+                transparent:true,
+                depthTest:false, 
+                map: texLoader.load(Potree.resourcePath+'/textures/rotation_circle.png' ), 
+            }) ,
+            root:object,
+            sizeInfo: markerSizeInfo, 
+            dontFixOrient: true,
+            viewports:viewer.mapViewer.viewports,
+            name:"mapClipRotateBar"
+        })
+        bar.position.set(0,lineLen+circleWidth/2,0)
+        bar.scale.set(circleWidth,circleWidth,circleWidth)
+        
+        bar.addEventListener('mouseover',()=>{
+            bar.material.opacity = 1
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "add",  name:"mapClipRotate"
+            })
+            viewer.mapViewer.dispatchEvent('content_changed') 
+        })
+        
+        let leave = ()=>{ 
+            bar.material.opacity = barOpacity
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "remove",  name:"mapClipRotate"
+            })
+            viewer.mapViewer.dispatchEvent('content_changed') 
+            
+        } 
+        bar.addEventListener('mouseleave',leave)
+        this.addEventListener('dispose', leave)    
+        
+        let lastPos;
+        bar.addEventListener('drag',(e)=>{
+            var intersect = e.intersect.orthoIntersect   
+            if(lastPos){ 
+                let vec1 = new THREE.Vector3().subVectors(lastPos, this.center).setZ(0)
+                let vec2 = new THREE.Vector3().subVectors(intersect, this.center).setZ(0)
+                let angle = math.getAngle(vec1,vec2,'z')  
+                this.angle += angle 
+                this.rotateBar.rotation.z = this.angle
+                this.updatePoints() 
+                this.dispatchEvent({type:'rotate', angle: this.angle})
+            }
+            lastPos = intersect.clone();       
+        })
+        bar.addEventListener('drop',()=>{
+            lastPos = null 
+            viewer.dispatchEvent({
+                type : "CursorChange", action : "remove",  name:"mapClipRotate"
+            })
+        })
+        
+        let line = LineDraw.createLine([new THREE.Vector3, new THREE.Vector3(0,lineLen,0)],{color})
+        
+        
+        object.add(bar)
+        object.add(line) 
+        this.add(object)
+        this.rotateBar = object
+        this.rotateBar.bar = bar
+    }
+    
+    updatePoints(scale){
+        this.points = getPoints(this.center, scale || this.getScale(), this.angle)
+        this.getPoint2dInfo(this.points)
+        this.update({ifUpdateMarkers:true}) 
+    }
+    
+    update(options={}){ 
+        super.update(options)
+        
+        {//update rotateBar
+            let center = new THREE.Vector3().addVectors(this.points[0], this.points[1]).multiplyScalar(0.5); 
+            this.rotateBar.position.copy(center) 
+            this.rotateBar.bar.update()//更新sprite matrix
+        }
+        {
+            for(let i=0;i<4;i++){
+                let current = this.points[i];
+                let next = this.points[(i+1)%4];
+                let mid = new THREE.Vector3().addVectors(current, next).multiplyScalar(0.5)
+                
+                this.updateMarker(this.edgeMarkers[i], mid)
+            }
+            
+            
+        }
+        
+        
+    }
+    
+    dispose(){
+        super.dispose()
+        this.dispatchEvent('dispose')
+        
+    }
+     
+}
+
+
+
+
+
+
+function getPoints(center, scale, angle=0){  
+    let points = [
+        new THREE.Vector3(-scale.x/2, +scale.y/2, 0),  
+        new THREE.Vector3(+scale.x/2, +scale.y/2, 0), 
+        new THREE.Vector3(+scale.x/2, -scale.y/2, 0),  
+        new THREE.Vector3(-scale.x/2, -scale.y/2, 0),     
+    ] 
+    var rotMatrix = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0,0,1), angle)//再旋转 
+    points.forEach(e=>{
+        e.applyMatrix4(rotMatrix) 
+        e.add(center) 
+    })
+    return points
+}
+ 

+ 453 - 0
src/custom/settings.js

@@ -0,0 +1,453 @@
+//xzw add  
+import browser from './utils/browser.js'
+
+const config = {//配置参数   不可修改
+    displayMode:{ 
+		showPointCloud:{
+			atPano:{
+                showPoint : true,
+                showSkybox: false,
+                pointUsePanoTex : false 
+                
+			},
+            transition:{
+                showPoint: true,
+                showSkybox: false,
+                pointUsePanoTex: false  
+            },
+            canLeavePano: true   //是否能离开pano位置 
+		},
+
+		showPanos:{
+            atPano:{
+                showPoint : false,
+                showSkybox: true,
+                pointUsePanoTex : false  
+			},
+            transition:{
+                //showPoint: true,
+                showSkybox: true,
+                //pointUsePanoTex: true //是否使用全景贴图
+            },
+            canLeavePano: false    
+		}, 
+        
+		showBoth:{
+            atPano:{
+                showPoint : true,
+                showSkybox: true,
+                pointUsePanoTex : false //?  
+			},
+            transition:{
+                showPoint: true,
+                showSkybox: true,
+                pointUsePanoTex: true  
+            },
+            canLeavePano: true   //是否能离开pano位置   离开后自动变为showPointCloud
+		},
+        //test:
+        pointUsePanoTex:{ //---静止时调点云
+            atPano:{
+                showPoint : true,
+                showSkybox: false,
+                pointUsePanoTex : true  
+			},
+            transition:{
+                showPoint: true,
+                showSkybox: true,
+                pointUsePanoTex: true //是否使用全景贴图
+            },
+            canLeavePano: false    
+		},
+
+	},
+    
+    
+    
+    urls:{
+        //localTextures:'../resources/textures/', 
+        prefix1: 'https://laser-oss.4dkankan.com',//oss
+        prefix2: 'https://testlaser.4dkankan.com',
+        prefix3: 'https://4dkk.4dage.com',
+        prefix4: 'https://uat-laser.4dkankan.com',//test.4dkankan
+        prefix5: 'https://laser.4dkankan.com',
+        prefix6: 'https://mix3d.4dkankan.com/backend',
+        
+    },
+     
+    /* transitionsTime:{
+        flyTime : 1000  // 毫秒/米
+        panoToPano: 1000, 
+        flyIn:1000,
+        flyOut:1000,
+    } */
+    transitionsTime:{
+        flyMinTime : 500 /* * 3 */,  // 毫秒/米
+        flytimeDistanceMultiplier: 150/* * 3  */,
+        panoToPanoMax: 2000/*  * 3 */, 
+        flyIn:1000,
+        flyOut:1000
+    }
+    
+    ,
+    moveSpeedAdujust : 0.5  //越小越慢
+    ,
+    view:{
+        fov:70,  //navvis:50 
+        near:0.1,
+        far: 10000,
+    },
+    
+    map:{//mapViewer
+        mapHeight : -1000,//要比点云低。最低
+        cameraHeight : 1000,  //最高  ,注意(如sitemodel)其他的物体不能超过这个高度
+    },
+    
+    
+    pointDensity:{
+        magnifier:{  
+            maxLevelPercent: 1,
+            pointBudget : 8*1000*1000, 
+        },
+        panorama:{//显示全景时的漫游。因为点只能显示1个像素的大小,所以必须很密集,但又要限制点的数量
+            maxLevelPercent: 0.6,
+            pointBudget : /* 4*1000*1000// */browser.isMobile() ?  0.1*1000*1000 :  0.4*1000*1000,  //点云总最大数
+        }, 
+        
+        fourViewports:{//分四屏时防止卡顿
+            maxLevelPercent: 0.4,  
+            pointBudget :1*1000*1000, // 只要限制这个就足够 (为什么分屏focus区域不同会闪烁,navvis不会)(navvis:maxLevel:5,pointBudget:1*1000*1000)
+        }, 
+        fourViewportsMain:{//分四屏时防止卡顿
+            maxLevelPercent: 0.8,  
+            pointBudget :1*1000*1000, // 只要限制这个就足够 (为什么分屏focus区域不同会闪烁,navvis不会)(navvis:maxLevel:5,pointBudget:1*1000*1000)
+        }   
+        ,
+        panoEdit:{
+            maxLevelPercent: 1,  //在远处时由于pointBudget限制而展示稀疏,凑近时就变为最高质量了
+            pointBudget :4*1000*1000, //避免卡顿
+            percentByUser:true,
+        },
+        
+        low:{//highPerformance
+            maxLevelPercent: 0.4, //最小为0
+            percentByUser:true, //如果用户定义了percent,使用用户的
+            pointBudget : 1*1000*1000,
+        }, 
+        middle:{//balanced  //不同场景相同级别所产生的numVisibleNodes和numVisiblePoints不同,如果分层比较细,可能要到level8才能看清,那么level5看到的点就很大且很少,如隧道t-e2Kb2iU
+            maxLevelPercent: 0.7,
+            percentByUser:true,
+            pointBudget:browser.isMobile() ? 4*1000*1000 : 2*1000*1000, 
+        },
+        high:{//highQuality
+            maxLevelPercent: 1, 
+            percentByUser:true,
+            pointBudget:browser.isMobile() ? 8*1000*1000 : 4*1000*1000,
+        }
+        //browser.isMobile() 时要不要限制下pointBudget,还是让用户自己调低质量?
+        //minNodeSize?
+        //数值由testLevelSteps得来,其中nodeMaxLevel为2时,low和middle的都是1,如果真有这么低的点云就单独处理下。
+        //多个viewport尽量保证pointBudget一样,或者pointBudget不能太低于所需,否则会反复加载了又清除
+    },  
+     
+    
+     
+    clip:{
+        color: '#FFC266', //map
+        
+    },
+    panoFieldRadius : 10, //当前位置多远范围内可以切全景模式
+    
+    measure:{
+        color:'#00C8AF',
+        default:{
+            color:"#64C8BB",//"#00c7b2",
+            opacity:0.7
+        },
+        highlight:{
+            color:'#00C8AF',//"#00c7b2",
+            opacity:1
+        },
+        guide:{
+            color:'#FFFFFF', 
+            opacity:1
+        }
+        ,   
+        backColor:'#333333',
+         
+        lineWidth: 4,
+       
+        textColor: "#FFFFFF"
+        
+    },
+    material:{//初始化
+        pointSize: 0.1,  
+        realPointSize : 0.1,//实际上的ui滑动条默认大小(兼容旧的版本)
+        minSize: 0.1,
+        maxSize: 10000,
+        pointSizeType: 'ATTENUATED', //'ADAPTIVE'//'ADAPTIVE' \ FIXED //ADAPTIVE的在小房间里大小会不太匹配,但在远景似乎更好
+        /* 
+            ATTENUATED : 衰减   真实大小,靠近时感觉是点云一点点变多,缝隙变小
+            ADAPTIVE:  自适应   大小根据level变化,越高越小。靠近时感觉点云由大慢慢细分成小份。这个感觉更佳但是navvis为何不用这个
+         */
+        
+        absolutePanoramaSize: 1.3 ,//全景漫游时的size  是fixed的模式
+        
+        //sizeAtPanoRtEDL : 2000,  
+        pointColor:'#ffffff',
+        
+        
+        //sizeAddAtPanoRtEDL : 0.5, //全景模式静止时的size
+        //ADAPTIVE : 字会失真扭曲
+        //'ATTENUATED' 往旁边看有缝隙、点在浮动
+        //navvis的shader在哪里 为什么不会抖动
+    }
+    ,
+     
+    
+    renderLayers:{//渲染层,方便分批渲染管理,替代scene的创建。数字不代表顺序。(数字不能太大)
+        bg: 20, 
+        bg2: 21,
+        
+        skybox: 1,
+        pointcloud: 11,
+        sceneObjects:0,//default
+        model : 2,   
+        
+        measure:4,  
+        magnifier:5, 
+        magnifierContent:16,
+        volume:6,
+        transformationTool:7,
+       
+        map:8,
+        mapObjects:9,//default
+        
+         
+        bothMapAndScene: 3,
+        
+        siteModeOnlyMapVisi:12,//只能mapViewer可见
+        siteModelMapUnvisi:13,//只有mapViewer不可见
+        siteModeSideVisi:14,//只有侧面可见
+        
+        
+        layer1: 10,// 备用1
+        layer2: 15,// 备用2
+    },
+    
+    renderOrders:{ //会影响到绘制、pick时的顺序。
+        model:10
+    }, 
+    siteModel:{
+        names:{
+            'building': '建筑',
+            'floor':'楼层',
+            'room':'房间'
+        },
+        floorHeightDefault: 5,//一层楼的高度
+        
+        
+    },
+    
+    panosEdit:{
+         
+    },
+     
+    tiling: {
+        panoPreRenderRepeatDelay: 2500,
+        panoPreRenderDelay: 500,
+        preRenderTourPanos: browser.valueFromHash("tileprerender", 0),
+        tilingFlagNames: ["usetiles", "tiles"],
+        maxNavPanoQuality: browser.valueFromHash("maxtileq", null),
+        maxZoomPanoQuality: browser.valueFromHash("maxztileq", null),
+        overlayStyle: browser.valueFromHash("tileoverlay", 0),
+        uploadIntervalDelay: browser.valueFromHash("tileupdelay", 10),
+        initialIntervalDelay: browser.valueFromHash("itiledelay", 0),
+        maxNonBaseUploadsPerFrame: browser.valueFromHash("maxnbtpf", 2),
+        maxBaseUploadsPerFrame: browser.valueFromHash("maxbtpf", 6),
+        customCompression: browser.valueFromHash("tilecustcomp", 0),
+        mobileHighQualityOverride: !1,
+        allowUltraHighResolution: !0
+    },
+    navigation: {
+        panoScores: !1,
+        mouseDirection: !0,
+        filterStrictness: .75,
+        angleFactor: -30,
+        directionFactor: 10,
+        distanceFactor: -1,
+        optionalityFactor: 3
+    } 
+    ,
+    axis : {   'x':{color:'#d0021b'/* 'red' */}, 'y':{ color:'#86c542' /* 'green' */},  'z': {color:'#3399c8' /* 'blue' */}},  
+    
+    
+    highQualityMaxZoom: 2,
+    ultraHighQualityMaxZoom: 3,
+    
+    clickMaxDragDis:5,
+    clickMaxPressTime:500, //ms
+    doubleClickTime:200,//双击间隔时间
+     
+     
+    background: '#232323',
+    mapBG:'#F5F5F5', //地图的clearColor
+
+    colors: {  //from navvis
+        red:  [213,0,0],
+        pink:  [197,17,98],
+        purple: [170,0,255],
+        "deep purple": [98,0,234],
+        blue: [ 41,98,255],
+        "light blue": [ 0,145,234],
+        cyan: [ 0,184,212],
+        teal: [ 0,191,165],
+        green: [0,200,83],
+        "light green": [ 100,221,23],
+        lime: [ 174,234,0],
+        yellow: [ 255,214,0],
+        amber: [ 255,171,0],
+        orange: [ 255,109,0],
+        "deep orange": [ 255,61,0],
+         
+    },
+}
+
+
+config.OrthoCameraLimit = {
+    standard:{ 
+        zoom:{min:0.001, max:500}, //如果camera缩太小,地图会因为数字边界问题而扭曲
+        posBound:{ 
+            min: {x:-1e5, y:-1e5,z: config.map.cameraHeight},
+            max: {x:1e5, y:1e5, z:1 / 0  }
+        }  
+    },
+    expand:{ //范围再大些,用于编辑空间模型等(但是万一中心点靠近地图边缘的话,就很容易扭曲了)
+        zoom:{min:0.0004, max:100}, //如果camera缩太小,地图会因为数字边界问题而扭曲
+        posBound:{ 
+            min: {x:-5000000, y:-1000000,z:config.map.cameraHeight},
+            max: {x:5000000, y:1000000, z:1 / 0  }
+        }//40075017
+    }
+    
+}
+
+
+
+/* 显示模式:
+
+1只显示点云: 滚轮为前进后退,方向键可以行走。进入漫游点时自动变为混合(这样全景可以弥补缝隙),过渡时只显示点云。
+2只显示全景: 不能任意行走。 过渡时显示贴图材质非edl的点云(否则有折痕不贴合)。
+3混合:都显示。 不能任意行走。过渡时显示贴图材质非edl的点云(因为只显示点云的话不太美,点云很碎,不细腻)
+ */
+window.testLevelSteps = function(steps){//[0.4,0.7,1]
+    if(!steps){
+        let s = Potree.config.pointDensity;
+        steps = [s.low.maxLevelPercent, s.middle.maxLevelPercent, s.high.maxLevelPercent, ]
+    }
+    let max = 1
+    while(++max<=12){ 
+        let r1 = steps.map(e=>e * max);
+        let r2 = steps.map(e=>Math.round(e * max));
+        console.log(`当nodeMaxLevel为${max}时,每一级的level分别为${r2}, (小数:${r1})`) 
+    }
+    console.log('请检查每一层的三个level是否有重复')
+     
+} 
+function getPrefix(){  
+    let u = window.location.href.split('//')
+    let v = u[1].split('/');
+    return v[0]
+}
+
+ 
+let settings = {//设置   可修改
+    editType : '',
+    number: '', //场景序号
+    originDatasetId:'',//场景原本的数据集id,应该就是数据集第一个吧
+    isOfficial:false,
+    webSite:'testdata',//正式:'datav1', //不同环境对应的静态文件的地址不同
+     
+    isLocal:false, //是否本地 局域网版本
+    libsUrl:'../libs/',
+    displayMode:'',
+    isTest :browser.urlHasValue('test'),
+    prefix: getPrefix(),
+    pointDensity: '',    UserPointDensity:'',//pointDensity会随着进入不同的模块而自动改变,UserPointDensity记录了用户的设置
+    UserDensityPercent:null,//点云密度百分比 
+    ifShowMarker:true,//显示漫游点
+    floorplanType:{},//平面图类型 'default' | 'diy'  不同数据集不同{datasetId:...}
+    floorplanEnable:false,
+    floorplanEnables:{},
+    floorplanRequests:{},//开始加载了的
+    
+    mapEnable:true,//地图区域是否加载地图
+    cameraFar : config.view.far, //相机最远范围 1-300
+    //limitFar: true, //是否使用setting的cameraFar来限制(如在点云裁剪时为false)
+    showPanoMesh:false, //显示小球,
+    dblToFocusPoint:false,//调试时如果需要双击飞向某个点云的点,就打开。此时不在漫游点的话单击将无法漫游。//因和单击漫游冲突 
+    
+    unableNavigate : false,//进入如裁剪界面时 禁止漫游
+    sizeFitToLevel: false,//当type为衰减模式时自动根据level调节大小。每长一级,大小就除以2
+    zoom:{
+        enabled : true,
+        min:1,
+        max: config.highQualityMaxZoom 
+                                 
+    },
+    navConstantly:true,
+    navTileClass: browser.isMobile() ? '1k' : '2k',  //默认加载到
+    tileClass:'4k',     //最高可达
+    /* loadTilesWhenUnfocus:false, //页面unfocus时也仍在加载tiles
+    loadPointsWhenUnfocus:true, //页面unfocus时也仍在加载点云 */
+   
+    //initialShowPano:true
+    drawEntityData: false,
+    
+    zoomFromPointert:{//定点缩放(包括点云模式、全景模式、地图)
+        whenPanos:true,
+        whenPointCloud:true,
+        map:true,
+    },
+    rotAroundPoint:true,//点云模式是否能绕intersectPoint旋转
+    tourTestCameraMove:false, //测试镜头时,不移动真实的镜头, 只移动frustum
+    cameraAniSmoothRatio : 20, //镜头动画平滑系数,越高越平滑
+    urls  : $.extend({}, config.urls, {
+        prefix : config.urls.prefix4 //主要使用的 是测试环境,根据不同工程更改
+    }), 
+    
+    
+    useDepthTex: true,//使用深度贴图,但不代表一定有(得到的intersect更快速准确和稳定)   SS-t-7DUfWAUZ3V  
+    //matUseDepth:false, 
+    //panoEdit:
+    datasetsPanos:{},
+    
+    //mergeModel:
+    boundAddObjs:false,
+    intersectOnObjs:false,
+    intersectWhenHover:true,
+
+    notAdditiveBlending:false, //点云是否使用普通的blend, 否则会曝光过渡
+    precision:2,  // 两位小数
+    
+    testV4url:false, //v4的全景图等路径不一样 scene_view_data
+}
+  
+
+//https://4dkk.4dage.com/images/imagesSS-V4-aaeaQxWA/tiles/4k/0_skybox2.jpg?x-oss-process=image/resize,h_512&time=20221024105628
+
+//JSON.parse(localStorage.getItem('setting'))
+
+settings.isLocalhost = settings.prefix.includes('localhost')
+
+
+/* 
+    关于maxLevel:
+    viewer.scene.pointclouds[0].root.getLevel() 是 0
+    hierarchyStepSize是什么?见 if ((this.level % this.pcoGeometry.hierarchyStepSize) === 0 && this.hasChildren
+ 
+ 
+ */
+ 
+ 
+export {config, settings}

+ 911 - 0
src/custom/start.js

@@ -0,0 +1,911 @@
+import * as THREE from "../../libs/three.js/build/three.module.js";
+
+
+ 
+
+
+import math from './utils/math.js' 
+import browser from './utils/browser.js' 
+import './three.shim.js'  
+import "./potree.shim.js"
+
+ 
+ 
+export function start(dom, mapDom, number ){ //t-Zvd3w0m
+    /* {
+        let obj = JSON.parse(localStorage.getItem('setting'))
+        for(let i in obj){
+            console.log(i + ': ' +  obj[i])
+        }
+    }
+     */ 
+    Potree.settings.number = number || 't-o5YMR13'// 't-iksBApb'// 写在viewer前
+    
+    
+     
+    
+    let viewer = new Potree.ExtendViewer(dom , mapDom);
+    
+    let Alignment = viewer.modules.Alignment
+    
+    
+    //let pointDensity = config.pointDensity.middle
+	viewer.setEDLEnabled(false);
+    viewer.setFOV(Potree.config.view.fov);
+    //viewer.setPointBudget(pointDensity.pointBudget);
+    //viewer.loadSettingsFromURL(); 
+    
+    
+    if(!Potree.settings.isOfficial){ 
+        /* viewer.loadGUI(() => {
+            viewer.setLanguage('en');
+            //$("#menu_appearance").next().show();
+            $("#menu_tools").next().show();
+            $("#menu_scene").next().show();
+            $("#siteModel").show();
+            //$("#alignment").show();
+            viewer.toggleSidebar();
+        }); */
+        Potree.settings.sizeFitToLevel = true//当type为衰减模式时自动根据level调节大小。每长一级,大小就除以2
+    }
+
+    Potree.loadDatasetsCallback = function(data, ifReload){
+        if(!data || data.length == 0)return console.error('getDataSet加载的数据为空')
+           
+        Potree.datasetData = data
+        viewer.transform = null
+        var datasetLength = data.length 
+        var pointcloudLoaded = 0
+        var panosLoaded = 0
+        var pointcloudLoadDone = function(){//点云cloud.js加载完毕后 
+        }
+        
+         
+        var panosLoadDone = function(){   
+             
+            viewer.images360.loadDone() 
+            viewer.scene.add360Images(viewer.images360); 
+            viewer.mapViewer.addListener(viewer.images360)
+            
+            viewer.updateModelBound() //需等pano加载完
+          
+            let {boundSize, center} = viewer.bound
+           
+            Potree.Log(`中心点: ${math.toPrecision(center.toArray(),2)}, boundSize: ${math.toPrecision(boundSize.toArray(),2)} ` , null, 12)
+            
+            if(!Potree.settings.isOfficial){
+                Potree.loadMapEntity('all') //加载floorplan 
+            }
+            
+            
+            if(!ifReload){    
+                viewer.scene.view.setView({ 
+                    position: center.clone().add(new THREE.Vector3(10,5,10)), 
+                    target: center
+                })
+                 
+                viewer.dispatchEvent({type:'loadPointCloudDone'})
+            
+                if(!Potree.settings.UserPointDensity){
+                    Potree.settings.UserPointDensity = 'high'//'middle' 
+                }
+                 
+                Potree.Log('loadPointCloudDone  点云加载完毕', null, 10)  
+            }    
+            
+                                        
+                                                         
+                                                          
+            
+            
+            {//初始位置 
+                var urlFirstView = false
+                var panoId = browser.urlHasValue('pano',true);
+                if(panoId !== ''){
+                    var pos
+                    var pano = viewer.images360.panos.find(e=>e.id==panoId);
+                    if(pano){
+                        viewer.images360.focusPano({
+                            pano,
+                            duration:0, 
+                            callback:()=>{/* Potree.settings.displayMode = 'showPanos' */}
+                        })
+                          
+                    }
+                }else{//考虑到多数据集距离很远,或者像隧道那种场景,要使视野范围内一定能看到点云,最好初始点设置在漫游点上
+                    
+                    let {boundSize, center} = viewer.bound
+                    
+                    let pano = viewer.images360.findNearestPano(center)
+                     
+                    /* pano && viewer.scene.view.setView({ 
+                        position: pano.position.clone().add(new THREE.Vector3(10,10,10)), 
+                        target: pano.position
+                    }) */
+                     
+                    pano && viewer.images360.flyToPano({
+                        pano,  duration:0,
+                        target : viewer.images360.bound.center     
+                    })
+                    
+                    
+                } 
+            }
+            
+            
+            
+            viewer.addVideo()//addFire()
+            
+            console.log('allLoaded')
+            viewer.dispatchEvent('allLoaded')
+        }
+        
+        var transformPointcloud = (pointcloud, dataset)=>{
+            var locationLonLat = dataset.location.slice(0,2)
+            //当只有一个dataset时,无论如何transform 点云和漫游点都能对应上。
+            var location = viewer.transform.lonlatToLocal.forward(locationLonLat)  //transform.inverse()
+            //初始化位置 
+            
+            viewer.sidebar && viewer.sidebar.addAlignmentButton(pointcloud) 
+            
+            //dataset.orientation = 0
+            
+            Alignment.rotate(pointcloud, null, dataset.orientation)   
+            Alignment.translate(pointcloud, new THREE.Vector3(location[0], location[1], dataset.location[2])) 
+            
+            pointcloud.updateMatrixWorld()
+            
+            
+            Potree.Log(`点云${pointcloud.dataset_id}旋转值:${pointcloud.orientationUser}, 位置${math.toPrecision(pointcloud.translateUser.toArray(),3)}, 经纬度 ${locationLonLat}, spacing ${pointcloud.material.spacing}`, null, 17 )
+            
+            
+            //-------------------
+             
+            //viewer.mapView.showSources(false);  
+        }
+        
+        if(!Potree.settings.originDatasetId)Potree.settings.originDatasetId = data[0].id
+        var originDataset =  data.find(e=>e.id == Potree.settings.originDatasetId)  
+        
+        {//拿初始数据集作为基准。它的位置是000
+            var locationLonLat = originDataset.location.slice(0,2)
+            proj4.defs("NAVVIS:TMERC", "+proj=tmerc +ellps=WGS84 +lon_0=" + locationLonLat[0].toPrecision(15) + " +lat_0=" + locationLonLat[1].toPrecision(15));
+            proj4.defs("WGS84", "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs");
+             
+            let transform1 = proj4("WGS84", "NAVVIS:TMERC"); //这个ok  TMERC是展开的平面投影
+            let transform2 = proj4("+proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs;");
+            
+            
+            viewer.transform = {
+                lonlatToLocal : transform1,
+                lonlatTo4550 : transform2       // 转大地坐标EPSG:4550  
+            } 
+            
+            viewer.mapViewer && viewer.mapViewer.mapLayer.maps[0].updateProjection()
+            
+        }
+         
+        
+        data.forEach((dataset,index)=>{  
+            if(!ifReload){
+                var datasetCode = dataset.sceneCode || dataset.name //对应4dkk的场景码
+                var cloudPath = `${Potree.settings.urls.prefix1}/${Potree.settings.webSite}/${datasetCode}/data/${datasetCode}/webcloud/cloud.js` 
+                var timeStamp = dataset.createTime ? dataset.createTime.replace(/[^0-9]/ig,'') : '';  //每重算一次后缀随createTime更新一次 
+                //console.warn(dataset.name, 'timeStamp', timeStamp)
+                Potree.loadPointCloud(cloudPath, dataset.name ,datasetCode, timeStamp, e => {
+                    let scene = viewer.scene;
+                    let pointcloud = e.pointcloud; 
+                    let config = Potree.config.material
+                    let material = pointcloud.material; 
+                    
+                    pointcloud.hasDepthTex = Potree.settings.useDepthTex && (!!dataset.has_depth  ||  Potree.settings.isLocalhost && Potree.settings.number == 'SS-t-7DUfWAUZ3V') //test   
+                    material.minSize =  config.minSize
+                    material.maxSize =  config.maxSize   
+                    material.pointSizeType = config.pointSizeType //Potree.PointSizeType[config.pointSizeType]//Potree.PointSizeType.ADAPTIVE;//FIXED
+                    pointcloud.changePointSize(config.realPointSize)  //material.size =  config.pointSize;
+                    pointcloud.changePointOpacity(1)
+                    material.shape = Potree.PointShape.SQUARE; 
+                    pointcloud.color = pointcloud.material.color = dataset.color  
+                    pointcloud.dataset_id = dataset.id;//供漫游点找到属于的dataset点云
+                    pointcloud.timeStamp = timeStamp 
+                    transformPointcloud(pointcloud,dataset)
+                    scene.addPointCloud(pointcloud);
+                    
+                    if(!Potree.settings.isOfficial){ 
+                        Potree.settings.floorplanEnables[dataset.id] = true
+                        Potree.settings.floorplanType[dataset.id] = 'default'
+                    }
+                    
+                    
+                    pointcloudLoaded ++;
+                    if(pointcloudLoaded == datasetLength)pointcloudLoadDone()
+                    
+                    
+                
+                    Potree.loadPanos(dataset.id, (data) => { 
+                        //console.log('loadPanos',dataset.sceneCode, dataset.id, data)
+                        viewer.images360.addPanoData(data, dataset.id )
+                        panosLoaded ++; 
+                        if(panosLoaded == datasetLength){
+                            panosLoadDone() 
+                        } 
+                    })
+                })
+            }else{
+                let pointcloud = viewer.scene.pointclouds.find(p => p.dataset_id == dataset.id)
+                if(!pointcloud){
+                    Potree.Log('数据集id变了,自动使用第一个','#500')
+                    pointcloud = viewer.scene.pointclouds[0]
+                }
+                //先归零 
+                Alignment.translate(pointcloud,  pointcloud.translateUser.clone().negate())
+                Alignment.rotate(pointcloud, null,  - pointcloud.orientationUser)
+                
+                transformPointcloud(pointcloud, dataset)
+                 
+            } 
+                
+        })
+        
+        if(ifReload){ 
+            
+            //loadDone()
+        }
+        
+        
+    } 
+    
+    
+    
+    Potree.loadDatasets(Potree.loadDatasetsCallback) 
+     
+     
+
+    window.testTransform = function(locationLonLat, location1, location2){
+        proj4.defs("NAVVIS:test", "+proj=tmerc +ellps=WGS84 +lon_0=" + locationLonLat[0].toPrecision(15) + " +lat_0=" + locationLonLat[1].toPrecision(15));
+        
+        let transform = proj4("WGS84", "NAVVIS:test"); //这个ok  navvis里也是这两种转换 见proj4Factory
+        if(location1){//经纬度
+            return transform.forward(location1) 
+        }else{
+            return transform.inverse(location2) 
+        }            
+        
+    }       
+
+    window.THREE = THREE
+    window.buttonFunction = function(){
+        
+        
+         
+        viewer.scene.pointclouds.forEach(e=>e.predictNodeMaxLevel())
+               
+        
+        
+        /* 
+        viewer.startScreenshot({type:'measure', measurement:viewer.scene.measurements[0]}) 
+        
+        viewer.modules.RouteGuider.routeStart = new THREE.Vector3(0,0,-1.3)
+        viewer.modules.RouteGuider.routeEnd = new THREE.Vector3(-10,0,-1.3)
+       */
+        
+    }
+    
+    
+    if(Potree.settings.isLocalhost){
+        let before = {}
+        viewer.inputHandler.addEventListener('keydown',e=>{ //测试的代码
+            if(e.event.key == 't'){
+                viewer.images360.cube.visible = true
+                viewer.images360.cube.material.wireframe = true
+            }else if(e.event.key == 'y'){
+                viewer.images360.cube.material.wireframe = false
+                viewer.images360.cube.visible = Potree.settings.displayMode == 'showPanos'
+            }                
+        }) 
+        
+    }  
+    
+    
+}
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+//=======================================================================
+/* 
+            漫游点编辑
+ */
+//=======================================================================
+
+
+export function panoEditStart(dom, number, fileServer){
+    Potree.settings.editType = 'pano'
+    Potree.settings.number = number 
+    
+    Potree.settings.unableNavigate = true
+    Potree.settings.sizeFitToLevel = true//当type为衰减模式时自动根据level调节大小。每长一级,大小就除以2
+    
+    
+    let viewer = new Potree.ExtendViewer(dom); 
+    let Alignment = viewer.modules.Alignment
+	viewer.setEDLEnabled(false);
+    viewer.setFOV(Potree.config.view.fov); 
+    //viewer.loadSettingsFromURL(); 
+    let datasetLoaded = 0;
+  
+    if(!Potree.settings.isOfficial){ 
+        /* viewer.loadGUI(() => {
+            viewer.setLanguage('en'); 
+            $("#menu_tools").next().show();
+            $("#panos").show();
+            $("#alignment").show(); 
+            viewer.toggleSidebar();
+        }); */
+        Potree.settings.sizeFitToLevel = true 
+    }
+    
+    var pointcloudLoadDone = function( ){//所有点云cloud.js加载完毕后 
+         
+        
+        
+        viewer.scene.pointclouds.forEach(c=>{
+            transformPointcloud(c)
+        })
+        viewer.images360.loadDone() 
+        viewer.scene.add360Images(viewer.images360); 
+        
+        viewer.updateModelBound()
+        let {boundSize, center} = viewer.bound
+       
+        Potree.Log(`中心点: ${math.toPrecision(center.toArray(),2)}, boundSize: ${math.toPrecision(boundSize.toArray(),2)} ` , null, 12)
+          
+        viewer.scene.view.setView({ 
+            position: center.clone().add(new THREE.Vector3(10,5,10)), 
+            target: center
+        })
+         
+        viewer.dispatchEvent({type:'loadPointCloudDone'})
+    
+        if(!Potree.settings.UserPointDensity){
+            Potree.settings.UserPointDensity = 'panoEdit'//'middle' 
+        }
+         
+        Potree.Log('loadPointCloudDone  点云加载完毕', null, 10)  
+        
+        viewer.dispatchEvent('allLoaded');
+    }
+    
+    
+    var transformPointcloud = (pointcloud )=>{ //初始化位置  
+        viewer.sidebar && viewer.sidebar.addAlignmentButton(pointcloud) 
+         
+        let orientation =  pointcloud.panos[0].dataRotation.z + Math.PI
+        
+        let location = pointcloud.panos[0].dataPosition.clone()//.negate()
+        
+        Alignment.rotate(pointcloud, null,  orientation  )   
+        Alignment.translate(pointcloud, location )  
+        
+        pointcloud.updateMatrixWorld()
+          
+    }   
+     
+     
+     
+         
+    
+    let loadPanosDone = Potree.loadPanosDone = (datasetId, panoData )=>{ //一个数据集获取到它的panos后
+        
+        Potree.settings.datasetsPanos[datasetId] = {panoData, panos:[]}
+         
+        console.log('panoData', datasetId, panoData)
+        
+        let panoCount = panoData.length
+        let pointcloudLoaded = 0
+        
+        let datasetsCount = Object.keys(Potree.settings.datasetsPanos).length
+       
+       
+        panoData.forEach((pano, index)=>{
+            //let cloudPath = `${Potree.scriptPath}/data/panoEdit/uuidcloud/${pano.uuid}/cloud.js` 
+            let cloudPath = `https://laser-oss.4dkankan.com/${Potree.settings.webSite}/${Potree.settings.number}/data/bundle_${Potree.settings.number}/building/uuidcloud/${pano.uuid}/cloud.js`
+            
+            let name = datasetId + '-'+pano.uuid
+            let timeStamp = 0
+            pano.index = index //注意:index不等于uuid,因为有的uuid缺失。但是visibles中存的是下标! 
+            
+            Potree.loadPointCloud(cloudPath, name , name, timeStamp, e => { //开始加载点云
+                let scene = viewer.scene;
+                let pointcloud = e.pointcloud; 
+                let config = Potree.config.material
+                let material = pointcloud.material; 
+                material.minSize =  config.minSize
+                material.maxSize =  config.maxSize   
+                material.pointSizeType = /* 'ADAPTIVE'// */config.pointSizeType //Potree.PointSizeType[config.pointSizeType]//Potree.PointSizeType.ADAPTIVE;//FIXED
+                pointcloud.changePointSize( 0.1 /* config.realPointSize  */   )  //material.size =  config.pointSize;
+                pointcloud.changePointOpacity(1)
+                material.shape = Potree.PointShape.SQUARE; 
+                pointcloud.color = config.pointColor  
+                pointcloud.dataset_id = datasetId;   //多个点云指向一个datasetId
+                pointcloud.panoUuid = pano.uuid
+                pointcloud.timeStamp = timeStamp
+                  
+                //transformPointcloud(pointcloud, pano)
+                scene.addPointCloud(pointcloud);
+                pointcloudLoaded ++;
+                 
+                if(pointcloudLoaded == panoCount ){
+                    datasetLoaded ++
+                    viewer.images360.addPanoData(panoData ,  datasetId )
+                    if(datasetLoaded == datasetsCount){
+                        pointcloudLoadDone()
+                    }
+                    
+                }
+                
+            })
+            
+        })
+        
+    }
+    
+    if(!Potree.settings.isOfficial){ 
+        Potree.settings.datasetsPano = {'testDataset':null}
+        Potree.loadPanosInfo( data=>{loadPanosDone('testDataset',  data.sweepLocations)} )  
+    
+    }
+    
+}
+
+
+
+  
+
+export function mergeEditStart(dom){
+    Potree.settings.editType = 'merge' 
+    Potree.settings.intersectOnObjs = true
+    Potree.settings.boundAddObjs = true
+    Potree.settings.unableNavigate = true
+   
+    
+    
+    let viewer = new Potree.ExtendViewer(dom );
+   
+    let Alignment = viewer.modules.Alignment
+     
+	viewer.setEDLEnabled(false);
+    viewer.setFOV(Potree.config.view.fov); 
+    //viewer.loadSettingsFromURL(); 
+    { 
+        viewer.mainViewport.view.position.set(30,30,30) 
+        viewer.mainViewport.view.lookAt(0,0,0)
+        
+        viewer.updateModelBound()//init
+        //this.bound = new THREE.Box3(new THREE.Vector3(-1,-1,-1),new THREE.Vector3(1,1,1))
+        
+        viewer.transformationTool.setModeEnable('scale',false)
+        viewer.ssaaRenderPass.sampleLevel = 1 //  sampleLevel为1 的话,ground就不会变黑
+        
+        viewer.inputHandler.fixSelection = true //不通过点击屏幕而切换transfrom选中状态
+  
+    }
+    
+    Potree.settings.sizeFitToLevel = true//当type为衰减模式时自动根据level调节大小。每长一级,大小就除以2
+    Potree.loadPointCloudScene = function(sceneCode, type, id, done, onError){//对应4dkk的场景码
+          
+        let loadCloud = (cloudPath, sceneName, sceneCode, timeStamp, color)=>{
+            
+            Potree.loadPointCloud(cloudPath, sceneName , sceneCode, timeStamp, e => {
+                let scene = viewer.scene;
+                let pointcloud = e.pointcloud; 
+                let config = Potree.config.material
+                let material = pointcloud.material; 
+                
+                material.minSize =  config.minSize
+                material.maxSize =  config.maxSize   
+                material.pointSizeType = config.pointSizeType //Potree.PointSizeType[config.pointSizeType]//Potree.PointSizeType.ADAPTIVE;//FIXED
+                pointcloud.changePointSize(config.realPointSize)  //material.size =  config.pointSize;
+                pointcloud.changePointOpacity(1)
+                material.shape = Potree.PointShape.SQUARE; 
+                color && (pointcloud.color = pointcloud.material.color = color)     
+                pointcloud.timeStamp = timeStamp 
+                //transformPointcloud(pointcloud, originDataset)
+                scene.addPointCloud(pointcloud);
+                {
+                    
+                    viewer.updateModelBound()
+                    let {boundSize, center} = viewer.bound
+                    viewer.dispatchEvent({type:'loadPointCloudDone'})
+                    if(!Potree.settings.UserPointDensity){
+                        Potree.settings.UserPointDensity = 'high'//'middle' 
+                    }
+                     
+                    Potree.Log('loadPointCloudDone  点云加载完毕', null, 10)    
+                } 
+                    
+                /* Potree.loadPanos(dataset.id, (data) => { //暂时不加载panos了,因为没有id 
+                //console.log('loadPanos',dataset.sceneCode, dataset.id, data)
+                viewer.images360.addPanoData(data, dataset.id ) 
+                viewer.images360.loadDone() 
+                viewer.scene.add360Images(viewer.images360);    */   
+                viewer.dispatchEvent('allLoaded')
+                done(pointcloud)
+            },onError) 
+            
+            
+        }
+        
+        if(type == 'laser'){ 
+            Potree.loadDatasets((data)=>{
+                let originDataset = data.find(e=>e.sceneCode == sceneCode);//只加载初始数据集  
+                let timeStamp = originDataset.createTime ? originDataset.createTime.replace(/[^0-9]/ig,'') : '';  //每重算一次后缀随createTime更新一次 
+                let cloudPath = `${Potree.settings.urls.prefix1}/${Potree.settings.webSite}/${sceneCode}/data/${sceneCode}/webcloud/cloud.js` 
+                loadCloud(cloudPath, originDataset.sceneName, sceneCode, timeStamp, originDataset.color)
+            }, sceneCode, onError)
+        
+        }else{//las or ply
+            let name = type + '|' + id
+            let cloudPath = sceneCode + '/cloud.js' 
+            loadCloud(cloudPath, name, name, '' )
+        }   
+         
+    } 
+    
+    
+    
+    
+    
+    
+    
+    let setMatrix = (pointcloud)=>{//为了漫游点变换,要算一下 类似setMatrix
+                  
+        /* pointcloud.transformMatrix = new THREE.Matrix4().multiplyMatrices(pointcloud.matrix, pointcloud.pos1MatrixInvert)//还原一点位移
+        pointcloud.transformInvMatrix.copy(pointcloud.transformMatrix).invert()
+        
+        pointcloud.rotateMatrix = new THREE.Matrix4().makeRotationFromEuler(pointcloud.rotation);
+        pointcloud.rotateInvMatrix.copy(pointcloud.rotateMatrix).invert()
+        pointcloud.panos.forEach(e=>e.transformByPointcloud()) */
+        //pointcloud.updateBound()
+        //pointcloud.getPanosBound()  
+        viewer.updateModelBound()
+    } 
+
+    let moveModel = (e)=>{//根据鼠标移动的位置改变位置
+          
+        let camera = viewer.mainViewport.camera
+        var origin = new THREE.Vector3(e.pointer.x, e.pointer.y, -1).unproject(camera),
+        end = new THREE.Vector3(e.pointer.x, e.pointer.y, 1).unproject(camera)
+        var dir = end.sub(origin)
+        let planeZ = 0;
+        let r = (planeZ - origin.z)/dir.z
+        let x = r * dir.x + origin.x
+        let y = r * dir.y + origin.y
+        
+        //过后改为根据intersect的点来设置底部高度;这样的话,需要发送高度
+        
+        /*let pos = new THREE.Vector3(x,y,  planeZ  )
+         modelEditing.updateMatrixWorld()   
+        let boundCenter = modelEditing.boundingBox.getCenter(new THREE.Vector3).applyMatrix4(modelEditing.matrixWorld);
+         */
+        MergeEditor.moveBoundCenterTo(modelEditing,new THREE.Vector3(x,y, modelEditing.boundCenter.z))  //使模型中心的xy在鼠标所在位置
+         
+        modelEditing.dispatchEvent("position_changed") 
+         
+    }
+    let cancelMove = ()=>{ 
+        modelEditing = null
+        viewer.removeEventListener('global_mousemove', moveModel); 
+        viewer.removeEventListener('global_click', confirmPos); 
+    }
+    let confirmPos = ()=>{ 
+        MergeEditor.focusOn(modelEditing)
+        cancelMove()  
+        return {stopContinue:true}
+    }
+    
+     
+    
+    let modelType,  modelEditing, MergeEditor = viewer.modules.MergeEditor
+    Potree.addModel = function(prop, done, onProgress, onError){ //加载模型
+         
+    
+        let loadDone = (model)=>{ 
+            model.dataset_id = prop.id //唯一标识
+             
+            if(prop.position){
+                model.position.copy(prop.position)
+            }
+            if(prop.rotation){
+                model.rotation.setFromVector3(prop.rotation) 
+            }
+            if(prop.scale){
+                model.scale.set(prop.scale,prop.scale,prop.scale)
+            }
+              
+            if(model.isPointcloud){
+                model.renderOrder = Potree.config.renderOrders.model;  //same as glb
+            }
+            
+            if(Potree.settings.maintainBtmZ) 
+            {//transform --------维持离地高度和中心点的版本(local ver)
+                let updateBound = ()=>{ 
+                    model.updateMatrixWorld()
+                    viewer.updateModelBound() 
+                }  
+                let maintainBtmZAndCenter = ()=>{ 
+                    MergeEditor.maintainBoundXY(model)
+                    MergeEditor.setModelBtmHeight(model) 
+                    updateBound()
+                    model.dispatchEvent('transformChanged')  
+                }
+                model.addEventListener('position_changed', ()=>{
+                    updateBound()
+                    MergeEditor.getBoundCenter(model);//更新boundcenter
+                    MergeEditor.computeBtmHeight(model)
+                    if(prop.bottomRange && (model.btmHeight > prop.bottomRange.max || model.btmHeight < prop.bottomRange.min)){
+                        model.btmHeight = THREE.Math.clamp(model.btmHeight, prop.bottomRange.min, prop.bottomRange.max)
+                        MergeEditor.setModelBtmHeight(model)
+                        updateBound()
+                    }  
+                    model.dispatchEvent('transformChanged') 
+                })
+                model.addEventListener("rotation_changed", maintainBtmZAndCenter )
+                model.addEventListener("scale_changed", maintainBtmZAndCenter )
+                model.addEventListener('transformChanged', ()=>{
+                    MergeEditor.modelTransformCallback(model)
+                })
+                //离地高度只是boundingbox在transform后的最低点的高度,而非模型transform后的最低点的高度,所以旋转过后看起来不太准确
+            } else
+            
+            {//transform --------维持中心点的版本
+                let updateBound = ()=>{ 
+                    model.updateMatrixWorld()
+                    viewer.updateModelBound() 
+                }  
+                let maintainCenter = ()=>{ 
+                    //MergeEditor.maintainBoundXY(model) 
+                    MergeEditor.maintainBoundCenter(model) 
+                    updateBound()
+                    model.dispatchEvent('transformChanged')  
+                }
+                model.addEventListener('position_changed', ()=>{
+                    updateBound()
+                    MergeEditor.getBoundCenter(model);//更新boundcenter
+            
+                    model.dispatchEvent('transformChanged') 
+                })
+                model.addEventListener("rotation_changed", maintainCenter )
+                model.addEventListener("scale_changed", maintainCenter )
+                model.addEventListener('transformChanged', ()=>{
+                    MergeEditor.modelTransformCallback(model)
+                })
+            
+            } 
+            
+            model.updateMatrixWorld()
+            viewer.updateModelBound()
+            
+            MergeEditor.getBoundCenter(model) //初始化
+            model.lastMatrixWorld = model.matrixWorld.clone()
+            
+            done(model) // 先发送成功,因为2d界面会随机执行changePosition等初始化,然后这边再将模型移到中心地面上
+            
+            
+            if(prop.isFirstLoad){
+                
+                MergeEditor.moveBoundCenterTo(model, new THREE.Vector3(0,0,0))  
+                MergeEditor.setModelBtmHeight(model, 0) //初始加载设置离地高度为0
+                
+                if(prop.mode != 'single'){//如果不是模型展示页,模型会随着鼠标位置移动
+                    viewer.addEventListener('global_mousemove', moveModel); 
+                    viewer.addEventListener('global_click', confirmPos, 3);
+                    modelEditing = model;
+                }
+                model.dispatchEvent("position_changed") 
+            }else{
+                //MergeEditor.setModelBtmHeight(model, prop.bottom || 0) //默认离地高度为0
+                modelEditing = null
+            }
+            
+            
+            
+            
+        }
+        
+        
+        
+        
+        
+        
+        
+        if(prop.type == 'glb'){
+            
+            
+            
+            let callback = (object)=>{
+                //focusOnSelect(object, 1000)  
+                object.isModel = true
+                //object.dataset_id = Date.now() //暂时
+                
+                object.traverse(e=>e.material && (e.material.transparent = true))
+              
+                /* object.addEventListener('click',(e)=>{
+                    //只是为了能得到hoverElement识别才加这个侦听
+                }) */
+                 
+                loadDone(object)
+            }
+            
+             
+            let info = { 
+                name: prop.type, 
+                id: prop.id,
+                unlit:true,
+                /* transform : { 
+                    position : prop.position,
+                    rotation : new THREE.Euler().setFromVector3(prop.rotation), 
+                    scale: new THREE.Vector3(prop.scale,prop.scale,prop.scale),        
+                }  */               
+            }
+            
+            if(prop.type == 'glb'){
+                info.glburl = prop.url  
+            }
+                 
+              
+            viewer.loadModel(info , callback, onProgress, onError)
+            
+            
+            
+            
+            
+            
+            
+          }else{  
+            
+             //else if(prop.type == 'las' || prop.type == 'ply')
+ 
+            Potree.loadPointCloudScene(prop.url, prop.type, prop.modelId, (pointcloud)=>{  
+                pointcloud.matrixAutoUpdate = true
+                pointcloud.initialPosition = pointcloud.position.clone()
+                
+                pointcloud.pos1MatrixInvert = new THREE.Matrix4().setPosition(pointcloud.initialPosition).invert()
+                
+                /* let maintainBtmZ = ()=>{
+                    MergeEditor.setModelBtmHeight(pointcloud)
+                    updateMatrix()
+                }
+                let updateMatrix = ()=>{ 
+                    setMatrix(pointcloud) 
+                    pointcloud.dispatchEvent('transformChanged')
+                }
+                pointcloud.addEventListener('position_changed', updateMatrix )  
+                pointcloud.addEventListener("orientation_changed", maintainBtmZ )
+                pointcloud.addEventListener("scale_changed", maintainBtmZ ) */
+                
+                loadDone(pointcloud)
+                /* pointcloud.addEventListener('select',(e)=>{
+                    if(Potree.settings.displayMode == 'showPanos')return
+                    console.log('select',e) 
+                    //viewer.setControls(viewer.orbitControls) 
+                    MergeEditor.focusOnSelect(pointcloud) 
+                    
+                    viewer.outlinePass.selectedObjects = [pointcloud]
+                    return {stopContinue:true}
+                },1)
+                pointcloud.addEventListener('deselect',(e)=>{
+                    console.log('deselect',e) 
+                    //viewer.setControls(viewer.fpControls)  
+                    viewer.outlinePass.selectedObjects = []
+                }) */
+                
+            }, onError)
+        
+             
+            
+            
+        }
+    }
+    return {THREE}
+}
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+window.testCubeGeo = function(state){
+    if(state === false){
+        return viewer.images360.cube.material.wireframe = false
+    }
+    viewer.images360.cube.material.wireframe = true
+    viewer.images360.cube.visible = true
+    Potree.settings.rotAroundPoint = false
+}
+ 
+ 
+ 
+ 
+ 
+/* var changeLog = ()=>{ //如果移动端加了test反而出不来bug的话,用这个
+        
+    
+        var textarea = document.createElement('textarea');
+          textarea.id = "consoleLog";
+          
+          textarea.style.width = '160px';
+          textarea.style.height =  '200px'
+          textarea.style.position = 'fixed'
+          textarea.style.right = 0
+          textarea.style.bottom = '0'
+          textarea.style['z-index'] = 9999;
+          textarea.style.color = 'black';
+          textarea.style.opacity = 0.9;
+          textarea.style['font-size'] = '12px';
+          textarea.style['backgroundColor'] = '#ffffff'
+         
+          
+
+          document.getElementsByTagName("body")[0].appendChild(textarea);
+          var list = ["log", "error", "warn", "debug", "info", "time", "timeEnd"]
+          var exchange = function (o) {
+            console["old" + o] = console[o];
+            console[o] = function () {
+              var args =   Array.from(arguments)
+              console["old" + o].apply(this, arguments)
+              var t = document.getElementById("consoleLog").innerHTML;
+              
+              var str = ''
+              args.forEach(a=>{
+                  str += a + ' '
+              })
+              document.getElementById("consoleLog").innerHTML = str + "\n\n" + t;
+            }
+          }
+
+          for (var i = 0; i < list.length; i++) {
+            exchange(list[i])
+          } 
+   
+    
+    
+}
+ 
+changeLog() */
+ 
+ 
+ 
+ 
+
+   /* 
+
+
+坐标转换问题:
+
+由于控制点可以随便输入,所以本地和地理位置的转换也是可拉伸的。而navvis的转换是等比由中心展开,
+所以对比两种转化方式时误差较大。
+
+另外地理注册控制点是有参考数据集的,若参考数据集和我放置在0,0,0的数据集一致,就可直接使用,否则要转换。
+
+
+
+   */
+   
+   
+   
+   

+ 320 - 0
src/custom/utils/Common.js

@@ -0,0 +1,320 @@
+
+
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+
+
+var Common = {
+    /* sortByScore: function(list, request, rank){
+        var i = request ? Common.filterAll(list, request) : list
+        return 0 === i.length ? null : i = i.map(function(e) {
+            return {
+                item: e,
+                score: rank.reduce(function(t, i) {
+                    return t + i(e)
+                }, 0)
+            }
+        }).sort(function(e, t) {
+            return t.score - e.score;
+        })
+    }  */
+    
+    
+    sortByScore: function(list, request, rank){
+        var i = request ? Common.filterAll(list, request) : list
+        return 0 === i.length ? null : i = i.map(function(e) {
+            let scores = rank.map(function(f){return f(e)}) //add
+            
+            return {
+                item: e,
+                scores,
+                score: scores.reduce(function(t, i) {
+                    return t + i
+                }, 0)
+            }
+        }).sort(function(e, t) {
+            return t.score - e.score;
+        })
+    } 
+    
+    ,
+      
+    filterAll: function(e, t) {
+        return e.filter(function (e) {
+            return t.every(function (t) {
+                return t(e)
+            })
+        })
+    },
+    
+    
+    
+    //---------------
+    find : function(list, request, rank, sortByScore  ) { 
+        if(sortByScore){
+            var r = this.sortByScore(list, request, rank)
+            return  r && r[0] && r[0].item   
+        }else{
+            var i = request ? Common.filterAll(list, request) : list
+            return 0 === i.length ? null : (rank && rank.forEach(function(e) {
+                i = Common.stableSort(i, e)
+            }),
+            i[0])  
+        }
+        
+    }
+    
+    ,
+    stableSort: function(e, f) {//用到排序函数,涉及到两个item相减
+        return e.map(function(e, i) {
+            return {
+                value: e,
+                index: i
+            }
+        }).sort(function(e, u) { 
+            var n = f(e.value, u.value);
+            return 0 !== n ? n : e.index - u.index  //似乎就是加多了这一步:若差距为0,按照原顺序
+        }).map(function(e) {
+            return e.value
+        })
+    },
+    
+    average: function (e, t) {
+        if (0 === e.length)
+            return null;
+        for (var i = 0, n = 0, r = 0; r < e.length; r++) {
+            var o = t ? e[r][t] : e[r];
+            i += o,
+                n++
+        }
+        return i / n
+    },
+    
+    
+    //---------------------------
+    
+    
+    getMixedSet : function(arr1, arr2){//交集
+        return arr1.filter(item=>arr2.includes(item));
+    },
+    getUnionSet : function(arr1, arr2){//并集
+        return arr1.concat(arr2.filter(item=>!arr1.includes(item)))
+    },
+    getDifferenceSet : function(arr1, arr2){//差集  不能识别重复的,如getDifferenceSet([1,2,2],[1,1,2]) 为空
+        var arr11 = arr1.filter(item=>!arr2.includes(item));
+        var arr22 = arr2.filter(item=>!arr1.includes(item));
+        return arr11.concat(arr22)
+    },
+    getDifferenceSetMuti : function(arr){//收集绝对没有重复的元素,也就是判断出现次数=1的
+        var set = [];
+        arr.forEach(arr1=>{
+            arr1.forEach(item=>{
+                var index = set.indexOf(item)
+                if(index>-1){
+                    set.splice(index, 1)
+                }else{
+                    set.push(item)
+                }
+            })
+        })
+        return set;
+    }
+    ,
+    
+    
+    CloneJson : function(data){
+        var str = JSON.stringify(data)
+        return JSON.parse(str)
+    }
+    
+    ,
+     
+    CloneObject : function(copyObj, result, isSimpleCopy, simpleCopyList=[]) {
+        //isSimpleCopy 只复制最外层
+        //复制json		result的可能:普通数字或字符串、普通数组、复杂对象
+         
+        simpleCopyList.push(THREE.Object3D) //遇到simpleCopyList中的类直接使用不拷贝
+        
+        if(!copyObj || typeof copyObj == 'number' || typeof copyObj == 'string' || copyObj instanceof Function || simpleCopyList.some(className => copyObj instanceof className)){
+            return copyObj 
+        }
+        
+        result = result || {};
+        if (copyObj instanceof Array) {
+            return copyObj.map(e=>{ 
+                return this.CloneObject(e) 
+            }) 
+        }else{
+            if(copyObj.clone instanceof Function ){ //解决一部分
+                return copyObj.clone()
+            }
+        }
+        for (var key in copyObj) {
+            if (copyObj[key] instanceof Object && !isSimpleCopy)
+                result[key] = this.CloneObject(copyObj[key]);
+            else
+                result[key] = copyObj[key];
+            //如果是函数类同基本数据,即复制引用
+        }
+        return result;
+    }
+    ,
+    CloneClassObject :function(copyObj ){//复杂类对象
+        var newobj = new copyObj.constructor();
+        this.CopyClassObject(newobj, copyObj) 
+        
+        return newobj
+    }
+    
+    ,
+     
+    CopyClassObject :function(targetObj,  copyObj){//复杂类对象
+        for(let i in copyObj){
+            if(i in copyObj.__proto__)break; //到函数了跳出 
+              
+            targetObj[i] = this.CloneObject(copyObj[i], null )  
+            
+                
+            
+            /* else if(copyObj[i].clone instanceof Function ){
+                targetObj[i] = copyObj[i].clone()
+            }else{
+                targetObj[i] = copyObj[i];
+            } */ 
+        }
+    }
+    ,
+    
+    ifSame : function(object1, object2){
+        if(object1 == object2  )return true // 0 != undefined  , 0 == ''
+        else if(!object1 || !object2) return false
+        else if(object1.constructor != object2.constructor){
+            return false
+        }else if(object1 instanceof Array ) {
+            if(object1.length != object2.length)return false;
+            var _object2 = object2.slice(0);
+            
+            for(let i=0;i<object1.length;i++){ 
+                var u = _object2.find(e=>ifSame(object1[i], e));
+                if(u == void 0 && !_object2.includes(u) && !object1.includes(u))return false;
+                else{
+                    let index = _object2.indexOf(u);
+                    _object2.splice(index,1);
+                }
+            }
+            
+            return true
+        }else if(object1.equals instanceof Function ){//复杂数据仅支持这种,其他的可能卡住?
+            
+            return object1.equals(object2)
+              
+        }else if(typeof object1 == 'number' ||  typeof object1 == 'string'){
+            if(isNaN(object1) && isNaN(object2))return true
+            else return object1 == object2
+            
+        }else if(typeof object1 == "object"){
+            var keys1 = Object.keys(object1)
+            var keys2 = Object.keys(object2)
+            if(!ifSame(keys1,keys2))return false;
+            
+            for(let i in object1){
+                var same = ifSame(object1[i], object2[i]);
+                if(!same)return false
+            }
+            return true
+        }else{
+            console.log('isSame出现例外')
+        } 
+        
+    }
+    ,
+    replaceAll : function (str, f, e) {
+        //f全部替换成e
+        var reg = new RegExp(f, "g"); //创建正则RegExp对象  
+        return str.replace(reg, e);
+    } 
+    ,
+    downloadFile : function(data, filename, cb) {
+        var save_link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
+        save_link.href = data;
+        save_link.download = filename;
+        var event = document.createEvent('MouseEvents');
+        event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+        save_link.dispatchEvent(event);
+        cb && cb();
+    }, 
+    
+    intervalTool:{  //延时update,防止卡顿
+        list:[],
+        
+        isWaiting:function(name, func, delayTime){
+            if(!this.list.includes(name)){  //如果没有该项, 则开始判断
+                var needWait = func(); //触发了改变,则等待一段时间后再自动判断
+                if(needWait){
+                    this.list.push(name);
+                    setTimeout(()=>{
+                        var a = this.list.indexOf(name);
+                        this.list.splice(a,1);
+                        this.isWaiting(name, func, delayTime) //循环
+                    },delayTime)
+                } 
+            }
+        },
+        /* wait:function(name, delayTime){
+            this.list.push(name);
+            setTimeout(()=>{
+                
+            },delayTime)
+        }, */
+    }
+    ,
+    pushToGroupAuto : function(items, groups, recognizeFunction){//自动分组。 items是将分到一起的组合。items.length = 1 or 2. 
+    
+        recognizeFunction = recognizeFunction || function(){}
+    
+        var atGroups = groups.filter(group=>group.find(
+            item => items[0] == item || recognizeFunction(item, items[0]) || items[1] == item || items[1] && recognizeFunction(item, items[1])
+        
+        )) 
+        if(atGroups.length){//在不同组
+            //因为items是一组的,所以先都放入组1
+            items.forEach(item=> {if(!atGroups[0].includes(item)) atGroups[0].push(item);})
+             
+            if(atGroups.length>1){//如果在不同组,说明这两个组需要合并 
+                var combineGroup = []
+                atGroups.forEach(group=>{
+                    combineGroup = Common.getUnionSet(combineGroup, group)
+                    groups.splice(groups.indexOf(group),1)
+                    
+                }) 
+                groups.push(combineGroup) 
+                
+            }
+        }else{//直接加入为一组
+           groups.push(items) 
+        }
+    },
+     
+    addOrRemoveDefine(material, defineName, type, value=''){ 
+        let defines = material.defines
+        if(type == 'add'){
+            if(defines[defineName] != void 0 && defines[defineName] == value)return
+            defines[defineName] = value
+        }else{
+            if(defines[defineName] != void 0)return;
+            delete defines[defineName] 
+        }
+        material.needsUpdate = true;
+    },
+    
+    makeTexDontResize(map){//避免贴图因非2的次方而缩小。小心使用
+        if(!map || map.image && THREE.Math.isPowerOfTwo(map.image.width ) && THREE.Math.isPowerOfTwo(map.image.height ))return
+        map.wrapS = map.wrapT = THREE.ClampToEdgeWrapping; //原默认 RepeatWrapping 
+        map.minFilter = THREE.LinearFilter; // or THREE.NearestFilter  原默认 LinearMipmapLinearFilter
+    }
+    
+}  
+
+
+
+  
+export default Common 

+ 115 - 0
src/custom/utils/CursorDeal.js

@@ -0,0 +1,115 @@
+
+import Common from './Common.js' 
+
+
+//处理cursor优先级
+
+
+var CursorDeal = {
+    priorityEvent : [//在前面的优先级高
+        {'zoomInCloud':'zoom-in'},
+        {'hoverPano':'pointer'}, 
+          
+        {'connectPano':`url({Potree.resourcePath}/images/connect.png),auto`},
+        {'disconnectPano':`url({Potree.resourcePath}/images/connect-dis.png),auto`},
+         
+        {'hoverLine':'pointer'},
+        
+        
+        
+        {"movePointcloud":'move'}, 
+        {"polygon_isIntersectSelf":'not-allowed'},
+        {"polygon_AtWrongPlace":'not-allowed'},
+        {"markerMove":'grab'},
+        {'mapClipMove':'move'},
+        {'mapClipRotate':`url({Potree.resourcePath}/images/rotate-cursor.png),auto`},
+        {'rotatePointcloud':`url({Potree.resourcePath}/images/rotate-cursor.png),auto`},
+        {'siteModelFloorDrag':'row-resize'},
+        {'addSth':'cell'},//or  crosshair
+         
+    ], 
+    list:[], //当前存在的cursor状态
+    currentCursorIndex:null,
+    
+    init : function(viewer, viewers){
+        
+        this.priorityEvent.forEach(e=>{//刚开始Potree.resourcePath没值,现在换
+            for(let i in e){
+                e[i] = Common.replaceAll(e[i],'{Potree.resourcePath}',Potree.resourcePath)
+            }
+        })
+        
+         
+        
+        this.domElements = viewers.map(e=>e.renderArea)  
+        
+        viewer.addEventListener("CursorChange",(e)=>{
+            if(e.action == 'add'){
+                this.add(e.name)
+            }else{
+                this.remove(e.name)
+            } 
+        })
+        
+        
+    },
+    
+    
+    add : function(name){
+        var priorityItem = this.priorityEvent.find(e=>e[name])
+        if(!priorityItem){
+            console.error('CursorDeal  未定义优先级 name:'+ name);
+            return
+        }
+        
+        
+        if(!this.list.includes(name)){
+            
+            this.judge({addItem: priorityItem, name})
+            
+            this.list.push(name)
+        }
+         
+    },
+    
+    
+    remove : function(name){
+        var index = this.list.indexOf(name);
+        if(index > -1){
+            this.list.splice(index, 1)
+            this.judge()
+        }
+        
+        
+        
+    },
+    
+    judge:function(o={}){
+        //console.log(o,this.list)
+        if(o.addItem){
+            var addIndex = this.priorityEvent.indexOf(o.addItem) 
+            if(addIndex < this.currentCursorIndex || this.currentCursorIndex == void 0){ 
+                this.domElements.forEach(e=>e.style.cursor = o.addItem[o.name] )
+                this.currentCursorIndex = addIndex
+            }  
+        }else{
+            var levelMax = {index:Infinity, cursor:null }
+            this.list.forEach(name=>{
+                var priorityItem = this.priorityEvent.find(e=>e[name])
+                var index = this.priorityEvent.indexOf(priorityItem)
+                if(index < levelMax.index){
+                    levelMax.index = index;
+                    levelMax.cursor = priorityItem[name]
+                }
+            })
+            this.currentCursorIndex = levelMax.index
+            this.domElements.forEach(e=>e.style.cursor = levelMax.cursor || '')
+        }
+        
+    }
+    
+     
+}
+
+
+export default CursorDeal;

+ 401 - 0
src/custom/utils/DrawUtil.js

@@ -0,0 +1,401 @@
+
+ 
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+import math from './math.js';
+import {Line2} from "../../../libs/three.js/lines/Line2.js";
+import {LineGeometry} from "../../../libs/three.js/lines/LineGeometry.js";
+import {LineMaterial} from "../../../libs/three.js/lines/LineMaterial.js";
+//画线等函数--by 许钟文
+import {Features} from "../../Features.js";
+
+var defaultColor = new THREE.Color(1,1,1);//config.applicationName == "zhiHouse" ? Colors.zhiBlue : Colors.lightGreen;
+
+var LineDraw = {
+    
+	createLine: function (posArr, o={}) {
+        //多段普通线  (第二个点和第三个点之间是没有线段的, 所以不用在意线段顺序)
+        var mat
+        if(o.mat){
+            mat = o.mat
+        }else{
+            let prop = {
+                lineWidth: o.lineWidth || 1,
+                //windows无效。 似乎mac/ios上粗细有效 ? 
+                color: o.color || defaultColor,
+                transparent: o.dontAlwaysSeen ? false : true,
+                depthTest: o.dontAlwaysSeen ? true : false, 
+            }
+            if(o.deshed ){
+                prop.dashSize = o.dashSize || 0.1,
+                prop.gapSize = o.gapSize || 0.1
+            }
+            mat = new THREE[o.deshed ? "LineDashedMaterial" : "LineBasicMaterial"](prop) 
+        }
+         
+        
+        
+        var line = new THREE.LineSegments(new THREE.BufferGeometry, mat);
+		line.renderOrder = o.renderOrder || 4
+  
+        this.moveLine(line, posArr)
+        
+		return line;  
+
+	},
+    
+    
+	moveLine: function (line, posArr) {
+        if(posArr.length == 0)return
+        let position = []
+        posArr.forEach(e=>position.push(e.x,e.y,e.z))
+        line.geometry.setAttribute('position', new THREE.Float32BufferAttribute(/* new Float32Array( */position/* ) */, 3));
+      
+		line.geometry.attributes.position.needsUpdate = true;
+		line.geometry.computeBoundingSphere();
+        if(line.material instanceof THREE.LineDashedMaterial){
+            line.computeLineDistances()
+            //line.geometry.attributes.lineDistance.needsUpdate = true;
+             
+            line.geometry.verticesNeedUpdate = true; //没用
+             
+        }
+	}  
+	,
+     
+	 
+	createFatLineMat : function(o){ 
+    
+        var supportExtDepth = !!Features.EXT_DEPTH.isSupported()  
+        
+        let params = $.extend({}, {
+            //默认
+            lineWidth : 5,  
+            color:0xffffff,
+            transparent : true, depthWrite:false,  depthTest:false,
+            dashSize : 0.1, gapSize:0.1, 
+        }, o, {
+            //修正覆盖:
+            dashed: o.dashWithDepth ? supportExtDepth && !!o.dashed : !!o.dashed ,
+            dashWithDepth:!!o.dashWithDepth,//只在被遮住的部分显示虚线
+            useDepth: !!o.useDepth,  
+            supportExtDepth,
+            
+        })
+        
+        
+        
+		var mat = new LineMaterial(params)
+         
+        
+        //if(o.dashed)(mat.defines.USE_DASH = "") 
+         
+		return 	mat;			
+	},
+    
+    /* 
+        创建可以改变粗细的线。 
+     */
+	createFatLine : function(posArr, o){  
+		var geometry = new LineGeometry(); 
+		geometry.setColors( o.color || [1,1,1]);
+
+		var matLine = o.material || this.createFatLineMat(o);
+		var line = new Line2( geometry, matLine );
+		//line.computeLineDistances();
+         
+		line.scale.set( 1, 1, 1 );
+		line.renderOrder = 2;
+        
+        this.moveFatLine(line, posArr)
+        
+		return line;
+
+	},
+    
+    
+    
+	moveFatLine: function(line, posArr){
+		var geometry = line.geometry;
+        var positions = [];
+         
+        posArr.forEach(e=>{positions.push(...e.toArray())})
+        
+        
+		if(positions.length > 0){
+            if(!geometry){
+                geometry = line.geometry = new LineGeometry(); 
+            }
+            if(geometry.attributes.instanceEnd && geometry.attributes.instanceEnd.data.array.length != positions.length){//positions个数改变会有部分显示不出来,所以重建
+                geometry.dispose();
+                geometry = new LineGeometry();
+                line.geometry = geometry
+            }
+            geometry.setPositions( positions ) 
+            
+            if(line.material.defines.USE_DASH != void 0){
+                //line.geometry.verticesNeedUpdate = true; //没用
+                line.geometry.computeBoundingSphere(); //for raycaster
+                line.computeLineDistances(); 
+            } 
+        }else{
+            geometry.dispose()
+            line.geometry = new LineGeometry(); 
+             
+        }
+        
+        
+	},
+    
+    updateLine: function(line, posArr){
+        if(line instanceof Line2){
+            LineDraw.moveFatLine(line,posArr) 
+        }else{
+            LineDraw.moveLine(line,posArr)
+        }  
+    },
+     /* 
+     
+        为line创建用于检测鼠标的透明mesh,实际是个1-2段圆台。
+        由于近大远小的原因,假设没有透视畸变、创建的是等粗的圆柱的话, 所看到的线上每个位置的粗细应该和距离成反比。所以将圆柱改为根据距离线性渐变其截面半径的圆台,在最近点(相机到线的垂足)最细。如果最近点在线段上,则分成两段圆台,否则一段。
+      */
+	createBoldLine:function(points, o){ 
+		o = o || {}
+		var cylinder = o && o.cylinder;  
+		var CD = points[1].clone().sub(points[0]);
+		
+		var rotate = function(){//根据端点旋转好模型
+			cylinder.lastVector = CD;//记录本次的端点向量 
+			var AB = new THREE.Vector3(0,-1,0) 
+			var axisVec = AB.clone().cross(CD).normalize(); //得到垂直于它们的向量,也就是旋转轴
+			var rotationAngle = AB.angleTo(CD);
+			cylinder.quaternion.setFromAxisAngle( axisVec, rotationAngle ) 
+ 		} 
+		if(o && o.type == "init"){
+			cylinder = new THREE.Mesh()
+			cylinder.material = o.mat 
+			if(CD.length() == 0)return cylinder;
+			rotate()
+		}
+		
+		if(CD.length() == 0)return cylinder;
+		if(o.type != "update"){
+			var CDcenter = points[0].clone().add(points[1]).multiplyScalar(.5);
+			cylinder.position.copy(CDcenter);
+			
+			if(!cylinder.lastVector || o.type == "moveAndRotate")rotate()
+ 			else if(cylinder.lastVector &&  CD.angleTo(cylinder.lastVector)>0) rotate()//线方向改了or线反向了 重新旋转一下模型
+ 			if(config.isEdit && !objects.mainDesign.editing )return cylinder;//节省初始加载时间?
+		}
+ 
+		
+		//为了保证线段任何地方的可检测点击范围看起来一样大,更新圆台的结构(但是在镜头边缘会比中心看起来大)
+		var height = points[0].distanceTo(points[1]);
+		var standPos = o && o.standPos || objects.player.position;
+		var k = config.isMobile ? 20 : 40;
+		var dis1 = points[0].distanceTo(standPos);
+		var dis2 = points[1].distanceTo(standPos); 
+	 
+		var foot = math.getFootPoint(standPos, points[0], points[1]);//垂足
+		
+        if(o.constantBold || objects.player.mode != "panorama"){
+            var width = 0.1//0.08;
+            var pts = [new THREE.Vector2(width ,height/2),new THREE.Vector2(width ,-height/2)]  
+        }else if(foot.clone().sub(points[0]).dot( foot.clone().sub(points[1])   ) > 0){//foot不在线段上
+			var pts = [new THREE.Vector2(dis1 / k,height/2),new THREE.Vector2(dis2 / k,-height/2)]
+		}else{//在线段上的话,要在垂足这加一个节点,因它距离站位最近,而两端较远
+			var dis3 = foot.distanceTo(standPos); 
+			var len = foot.distanceTo(points[0])
+			var pts = [new THREE.Vector2(dis1 / k,height/2),  new THREE.Vector2(dis3 / k,height/2-len),  new THREE.Vector2(dis2 / k,-height/2)]
+		} 
+		cylinder.geometry && cylinder.geometry.dispose();//若不删除会占用内存
+ 		cylinder.geometry = new THREE.LatheBufferGeometry( pts, 4/* Math.min(dis1,dis2)<10?4:3 */ )  
+		cylinder.renderOrder = 2;
+		
+		return cylinder;
+	},  
+	updateBoldLine:function(cylinder, points, type, standPos, constantBold){  
+		this.createBoldLine(points,{type:type,  cylinder : cylinder, standPos:standPos, constantBold}) //type:move:平移 会改长短  , type:update根据距离和角度更新  不改长短
+	},	
+}
+
+var MeshDraw = { 
+    getShape:function(points, holes){
+        var shape = new THREE.Shape();
+        shape.moveTo( points[0].x, points[0].y );
+        for(var i=1,len=points.length; i<len; i++){
+            shape.lineTo(points[i].x, points[i].y ) 
+        } 
+        
+        /* var holePath = new THREE.Path()
+                .moveTo( 20, 10 )
+                .absarc( 10, 10, 10, 0, Math.PI * 2, true ) 
+            arcShape.holes.push( holePath );
+         */
+        if(holes){//挖空
+            holes.forEach((points)=>{
+                var holePath = new THREE.Path()
+                holePath.moveTo( points[0].x, points[0].y )
+                for(var i=1,len=points.length; i<len; i++){
+                    holePath.lineTo(points[i].x, points[i].y ) 
+                } 
+                shape.holes.push( holePath );
+            }) 
+        }
+        return shape        
+    },
+    getShapeGeo: function(points, holes){//获取任意形状(多边形或弧形)的形状面  //quadraticCurveTo() 这是弧形的含函数
+		
+		var geometry = new THREE.ShapeBufferGeometry( this.getShape(points, holes) );  //ShapeGeometry
+        
+       /*  var matrix = new THREE.Matrix4();//将竖直的面变为水平
+		matrix.set(//z = y 
+			1, 0, 0, 0,
+			0, 0, 0, 0,
+			0, 1, 0, 0,
+			0, 0, 0, 1 
+		)
+		geometry.applyMatrix(matrix) */
+		//geometry.computeVertexNormals();//对于光照需要的是点法线
+         
+		return geometry;
+		
+		
+	},
+    
+    
+    getExtrudeGeo:  function(points, holes, options={}){//获得挤出棱柱,可以选择传递height,或者extrudePath
+        var shape = this.getShape(points, holes) //points是横截面 [vector2,...]
+        
+        if(options.extrudePath ){// 路径 :[vector3,...]
+            
+            var length = extrudePath.reduce((total, currentValue, currentIndex, arr)=>{
+                if(currentIndex == 0)return 0
+                return total + currentValue.distanceTo(arr[currentIndex-1]);
+            },0)
+            options.extrudePath = new THREE.CatmullRomCurve3(extrudePath, options.closed ,  'catmullrom'  /* 'centripetal' */  , options.tension)
+            
+           
+        }
+         
+        var extrudeSettings = $.extend(options,{
+            steps: options.steps != void 0 ? options.steps : ( options.extrudePath ? Math.round(length/0.3) : 1),
+            bevelEnabled: false, //不加的话,height为0时会有圆弧高度
+            //depth
+        }) 
+        var geometry = new THREE.ExtrudeBufferGeometry( shape, extrudeSettings );
+        return geometry;
+    },
+    
+    
+	getUnPosPlaneGeo : function(){//获取还没有赋值位置的plane geometry
+		var e = new Uint16Array([0, 1, 2, 0, 2, 3])
+		//	, t = new Float32Array([-.5, -.5, 0, .5, -.5, 0, .5, .5, 0, -.5, .5, 0])
+			, i = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1])
+			, g = new THREE.BufferGeometry;
+		g.setIndex(new THREE.BufferAttribute(e, 1)),
+		//g.addAttribute("position", new n.BufferAttribute(t, 3)),
+		g.setAttribute("uv", new THREE.BufferAttribute(i, 2)) 
+		return function(){
+			return g
+		}	 
+	}(), 
+	getPlaneGeo : function(A,B,C,D){
+		var geo = this.getUnPosPlaneGeo().clone();
+		var pos = [
+			A.x, A.y, A.z, 
+			B.x, B.y, B.z, 
+			C.x, C.y, C.z, 
+			D.x, D.y, D.z  
+		] 
+		//geo.addAttribute("position", new THREE.BufferAttribute(pos, 3)) 
+        geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
+      
+        
+		geo.computeVertexNormals()
+		geo.computeBoundingSphere() //for raycaster
+		return geo;
+	}, 
+	drawPlane : function(A,B,C,D, material){   
+		var wall = new THREE.Mesh(this.getPlaneGeo(A,B,C,D), material); 
+		return wall;
+	  
+	}, 
+	movePlane: function(mesh, A,B,C,D){
+		var pos = new Float32Array([
+			A.x, A.y, A.z, 
+			B.x, B.y, B.z, 
+			C.x, C.y, C.z, 
+			D.x, D.y, D.z  
+		])
+		mesh.geometry.addAttribute("position", new THREE.BufferAttribute(pos, 3)) 
+		mesh.geometry.computeBoundingSphere()//for checkIntersect
+	}  
+    
+    ,
+    
+    createGeometry:function(posArr, faceArr, uvArr, normalArr ){//创建复杂mesh.  faceArr:[[0,1,2],[0,2,3]]
+        let geo = new THREE.BufferGeometry;
+        
+        let positions = []; 
+        posArr.forEach(p=>positions.push(p.x,p.y,p.z)); 
+        geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
+        
+        if(faceArr){
+            let indice = []
+            faceArr.forEach(f=>indice.push(...f));
+            geo.setIndex(indice) // auto set Uint16BufferAttribute or Uint32BufferAttribute
+        }
+        
+        if(uvArr){
+            let uvs = []
+            uvArr.forEach(uv=>uvs.push(uv.x,uv.y));
+            geo.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2)) 
+        } 
+        
+        if(normalArr){
+            let normals = []
+            normalArr.forEach(n=>normals.push(n.x,n.y,n.z));
+            geo.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3)) 
+        }
+        /*  
+        geo.computeVertexNormals()
+		geo.computeBoundingSphere() //for raycaster 
+          */
+        return geo
+    },
+    
+    
+    updateGeometry:function(geo, posArr, faceArr, uvArr, normalArr ){//创建复杂mesh.  faceArr:[[0,1,2],[0,2,3]]
+        
+        let positions = []; 
+        posArr.forEach(p=>positions.push(p.x,p.y,p.z)); 
+        geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
+        geo.attributes.position.needsUpdate = true;
+		 
+        if(faceArr){
+            let indice = []
+            faceArr.forEach(f=>indice.push(...f));
+            geo.setIndex(indice) // auto set Uint16BufferAttribute or Uint32BufferAttribute
+        }
+        
+        if(uvArr){
+            let uvs = []
+            uvArr.forEach(uv=>uvs.push(uv.x,uv.y));
+            geo.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2)) 
+        } 
+        
+        if(normalArr){
+            let normals = []
+            normalArr.forEach(n=>normals.push(n.x,n.y,n.z));
+            geo.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3)) 
+        }
+        /*  
+        geo.computeVertexNormals()
+		
+          */
+        geo.computeBoundingSphere() //for raycaster and visi
+        return geo
+    }
+} 
+
+export {LineDraw, MeshDraw} ;

+ 910 - 0
src/custom/utils/ExtendTransformationTool.js

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

+ 46 - 0
src/custom/utils/History.js

@@ -0,0 +1,46 @@
+ 
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+
+class History extends THREE.EventDispatcher{
+    
+    constructor(o){ 
+        super()
+        
+        this.list = []
+        
+        this.callback = o.callback
+    }
+    
+    undo(){
+        let last = this.list.pop();
+        last && this.callback && this.callback(last) 
+        this.dispatchEvent('undo')
+        
+    }
+    
+    
+    redo(){//暂时不写
+        
+        
+        
+    }
+    
+    writeIn(item){
+        
+        this.list.push(item)
+        
+    }
+    
+    
+    clear(){
+        
+        this.list.length = 0
+        
+    }
+    
+    
+    
+}
+
+
+export default History

+ 315 - 0
src/custom/utils/MathLight.js

@@ -0,0 +1,315 @@
+var MathLight = {};
+MathLight.RADIANS_PER_DEGREE = Math.PI / 180;
+MathLight.DEGREES_PER_RADIAN = 180 / Math.PI;
+MathLight.Vector3 = function(e, t, i) {
+    this.x = e || 0,
+    this.y = t || 0,
+    this.z = i || 0
+};
+
+MathLight.Matrix4 = function() {
+    this.elements = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
+    arguments.length > 0 && console.error("MathLight.Matrix4: the constructor no longer reads arguments. use .set() instead.")
+};
+
+MathLight.Matrix4.prototype = {
+    identity: function() {
+        return this.set(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1),
+        this
+    },
+    copy: function(e) {
+        return this.elements.set(e.elements),
+        this
+    },
+    applyToVector3: function(e) {
+        var t = e.x
+            , i = e.y
+            , n = e.z
+            , r = this.elements;
+        return e.x = r[0] * t + r[4] * i + r[8] * n + r[12],
+        e.y = r[1] * t + r[5] * i + r[9] * n + r[13],
+        e.z = r[2] * t + r[6] * i + r[10] * n + r[14],
+        this
+    },
+    getInverse: function(e, t) {
+        var i = this.elements
+            , n = e.elements
+            , r = n[0]
+            , o = n[1]
+            , a = n[2]
+            , s = n[3]
+            , l = n[4]
+            , c = n[5]
+            , h = n[6]
+            , u = n[7]
+            , d = n[8]
+            , p = n[9]
+            , f = n[10]
+            , g = n[11]
+            , m = n[12]
+            , v = n[13]
+            , A = n[14]
+            , y = n[15]
+            , C = p * A * u - v * f * u + v * h * g - c * A * g - p * h * y + c * f * y
+            , I = m * f * u - d * A * u - m * h * g + l * A * g + d * h * y - l * f * y
+            , E = d * v * u - m * p * u + m * c * g - l * v * g - d * c * y + l * p * y
+            , b = m * p * h - d * v * h - m * c * f + l * v * f + d * c * A - l * p * A
+            , w = r * C + o * I + a * E + s * b;
+        if (0 === w) {
+            var _ = "MathLight.Matrix4.getInverse(): can't invert matrix, determinant is 0";
+            if (t)
+                throw new Error(_);
+            return console.warn(_),
+            this.identity()
+        }
+        var T = 1 / w;
+        return i[0] = C * T,
+        i[1] = (v * f * s - p * A * s - v * a * g + o * A * g + p * a * y - o * f * y) * T,
+        i[2] = (c * A * s - v * h * s + v * a * u - o * A * u - c * a * y + o * h * y) * T,
+        i[3] = (p * h * s - c * f * s - p * a * u + o * f * u + c * a * g - o * h * g) * T,
+        i[4] = I * T,
+        i[5] = (d * A * s - m * f * s + m * a * g - r * A * g - d * a * y + r * f * y) * T,
+        i[6] = (m * h * s - l * A * s - m * a * u + r * A * u + l * a * y - r * h * y) * T,
+        i[7] = (l * f * s - d * h * s + d * a * u - r * f * u - l * a * g + r * h * g) * T,
+        i[8] = E * T,
+        i[9] = (m * p * s - d * v * s - m * o * g + r * v * g + d * o * y - r * p * y) * T,
+        i[10] = (l * v * s - m * c * s + m * o * u - r * v * u - l * o * y + r * c * y) * T,
+        i[11] = (d * c * s - l * p * s - d * o * u + r * p * u + l * o * g - r * c * g) * T,
+        i[12] = b * T,
+        i[13] = (d * v * a - m * p * a + m * o * f - r * v * f - d * o * A + r * p * A) * T,
+        i[14] = (m * c * a - l * v * a - m * o * h + r * v * h + l * o * A - r * c * A) * T,
+        i[15] = (l * p * a - d * c * a + d * o * h - r * p * h - l * o * f + r * c * f) * T,
+        this
+    },
+    makeRotationFromQuaternion: function(e) {
+        var t = this.elements
+            , i = e.x
+            , n = e.y
+            , r = e.z
+            , o = e.w
+            , a = i + i
+            , s = n + n
+            , l = r + r
+            , c = i * a
+            , h = i * s
+            , u = i * l
+            , d = n * s
+            , p = n * l
+            , f = r * l
+            , g = o * a
+            , m = o * s
+            , v = o * l;
+        return t[0] = 1 - (d + f),
+        t[4] = h - v,
+        t[8] = u + m,
+        t[1] = h + v,
+        t[5] = 1 - (c + f),
+        t[9] = p - g,
+        t[2] = u - m,
+        t[6] = p + g,
+        t[10] = 1 - (c + d),
+        t[3] = 0,
+        t[7] = 0,
+        t[11] = 0,
+        t[12] = 0,
+        t[13] = 0,
+        t[14] = 0,
+        t[15] = 1,
+        this
+    }
+};
+
+MathLight.Quaternion = function(e, t, i, n) {
+    this._x = e || 0,
+    this._y = t || 0,
+    this._z = i || 0,
+    this._w = void 0 !== n ? n : 1
+};
+
+MathLight.Quaternion.prototype = {
+    get x() {
+        return this._x
+    },
+    set x(e) {
+        this._x = e
+    },
+    get y() {
+        return this._y
+    },
+    set y(e) {
+        this._y = e
+    },
+    get z() {
+        return this._z
+    },
+    set z(e) {
+        this._z = e
+    },
+    get w() {
+        return this._w
+    },
+    set w(e) {
+        this._w = e
+    },
+    copy: function(e) {
+        this._x = e.x,
+        this._y = e.y,
+        this._z = e.z,
+        this._w = e.w
+    },
+    inverse: function() {
+        return this.conjugate().normalize()
+    },
+    conjugate: function() {
+        return this._x *= -1,
+        this._y *= -1,
+        this._z *= -1,
+        this
+    },
+    length: function() {
+        return Math.sqrt(this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w)
+    },
+    normalize: function() {
+        var e = this.length();
+        return 0 === e ? (this._x = 0,
+        this._y = 0,
+        this._z = 0,
+        this._w = 1) : (e = 1 / e,
+        this._x = this._x * e,
+        this._y = this._y * e,
+        this._z = this._z * e,
+        this._w = this._w * e),
+        this
+    },
+    setFromAxisAngle: function(e, t) {
+        var i = t / 2
+            , n = Math.sin(i);
+        return this._x = e.x * n,
+        this._y = e.y * n,
+        this._z = e.z * n,
+        this._w = Math.cos(i),
+        this
+    },
+    setFromUnitVectors: function() {
+        var e, t, i = 1e-6;
+        return function(n, o) {
+            return void 0 === e && (e = new MathLight.Vector3),
+            t = MathLight.dot(n, o) + 1,
+            t < i ? (t = 0,
+            Math.abs(n.x) > Math.abs(n.z) ? MathLight.setVector(e, -n.y, n.x, 0) : MathLight.setVector(e, 0, -n.z, n.y)) : MathLight.cross(n, o, e),
+            this._x = e.x,
+            this._y = e.y,
+            this._z = e.z,
+            this._w = t,
+            this.normalize()
+        }
+    }(),
+    multiply: function(e) {
+        return this.multiplyQuaternions(this, e)
+    },
+    premultiply: function(e) {
+        return this.multiplyQuaternions(e, this)
+    },
+    multiplyQuaternions: function(e, t) {
+        var i = e._x
+            , n = e._y
+            , r = e._z
+            , o = e._w
+            , a = t._x
+            , s = t._y
+            , l = t._z
+            , c = t._w;
+        return this._x = i * c + o * a + n * l - r * s,
+        this._y = n * c + o * s + r * a - i * l,
+        this._z = r * c + o * l + i * s - n * a,
+        this._w = o * c - i * a - n * s - r * l,
+        this
+    }
+};
+
+MathLight.convertWorkshopVector = function(e) {
+    return new MathLight.Vector3(-e.x,e.y,e.z)
+};
+
+MathLight.convertWorkshopQuaternion = function(e) {
+    return new MathLight.Quaternion(-e.x,e.y,e.z,-e.w).multiply(new MathLight.Quaternion(Math.sqrt(2) / 2,Math.sqrt(2) / 2,0,0))
+};
+
+MathLight.convertWorkshopOrthoZoom = function(e) {
+	//return e === -1 ? -1 : e / 16 * ($('#player').width() / $('#player').height()) / n.workshopApsect
+	return e === -1 ? -1 : e  * ($("#player").width() / $("#player").height())  ;
+};
+
+MathLight.convertWorkshopPanoramaQuaternion = function(e) {
+    return new MathLight.Quaternion(e.x,-e.y,-e.z,e.w).normalize().multiply((new MathLight.Quaternion).setFromAxisAngle(new MathLight.Vector3(0,1,0), 270 * MathLight.RADIANS_PER_DEGREE))
+};
+
+MathLight.normalize = function(e) {
+    var t = e.x * e.x + e.y * e.y + e.z * e.z
+        , i = Math.sqrt(t);
+    e.x /= i,
+    e.y /= i,
+    e.z /= i
+};
+
+MathLight.dot = function(e, t) {
+    return e.x * t.x + e.y * t.y + e.z * t.z
+};
+
+MathLight.cross = function(e, t, i) {
+    var n = e.x
+        , r = e.y
+        , o = e.z;
+    i.x = r * t.z - o * t.y,
+    i.y = o * t.x - n * t.z,
+    i.z = n * t.y - r * t.x
+};
+
+MathLight.setVector = function(e, t, i, n) {
+    e.x = t,
+    e.y = i,
+    e.z = n
+};
+
+MathLight.copyVector = function(e, t) {
+    t.x = e.x,
+    t.y = e.y,
+    t.z = e.z
+};
+
+MathLight.addVector = function(e, t) {
+    e.x += t.x,
+    e.y += t.y,
+    e.z += t.z
+};
+
+MathLight.subVector = function(e, t) {
+    e.x -= t.x,
+    e.y -= t.y,
+    e.z -= t.z
+};
+
+MathLight.applyQuaternionToVector = function(e, t) {
+    var i = t.x
+        , n = t.y
+        , r = t.z
+        , o = e.x
+        , a = e.y
+        , s = e.z
+        , l = e.w
+        , c = l * i + a * r - s * n
+        , h = l * n + s * i - o * r
+        , u = l * r + o * n - a * i
+        , d = -o * i - a * n - s * r;
+    t.x = c * l + d * -o + h * -s - u * -a,
+    t.y = h * l + d * -a + u * -o - c * -s,
+    t.z = u * l + d * -s + c * -a - h * -o
+};
+
+MathLight.angleBetweenVectors = function(e, t) {
+    return Math.acos(MathLight.dot(e, t))
+};
+
+
+
+export default MathLight

+ 193 - 0
src/custom/utils/SplitScreen.js

@@ -0,0 +1,193 @@
+import {ExtendView} from "../../viewer/ExtendView.js";
+import Viewport from "../viewer/Viewport.js";
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+ 
+ 
+
+class SplitScreen extends THREE.EventDispatcher{
+    constructor (args = {}) {
+		super();
+        
+    }
+    
+    
+    splitStart(cameraProps){ 
+        let viewports = []
+      
+        let subViewports = [viewer.mainViewport]
+        if(viewer.mapViewer){
+            subViewports.push(viewer.mapViewer.viewports[0])
+        }
+        
+        let length = cameraProps.length
+        for(let i=0;i<length;i++){
+            let prop = cameraProps[i];
+            let viewport;
+            let v = subViewports.find(e=>e.name == (prop.name2||prop.name)) 
+            if(v){
+                viewport = v
+                viewport.left = prop.left; viewport.bottom = prop.bottom; viewport.width = prop.width; viewport.height = prop.height;
+            }
+            
+            if(!viewport){
+                let view = new ExtendView()  
+                if(prop.limitBound)view.limitBound = prop.limitBound
+                prop.direction && (view.direction = prop.direction)
+                
+                viewport = new Viewport(view , this.getOrthoCamera(), prop )
+                if(prop.viewContainsPoints)viewport.viewContainsPoints = prop.viewContainsPoints
+                
+                //viewport.unableDepth = true //depthBasicMaterial等在此viewport中不开启depth 
+                
+            }    
+            if(viewport.camera.type == 'OrthographicCamera'  ){
+                viewport.targetPlane = new THREE.Plane()
+                viewport.shiftTarget = new THREE.Vector3 //project在targetPlane上的位置
+            }
+            viewport.fitMargin = prop.margin
+            viewports.push(viewport) 
+        } 
+        viewer.viewports = viewports;
+        viewer.updateScreenSize({forceUpdateSize:true})
+        viewports.forEach(viewport=>{
+            if(viewport.name == 'MainView')return
+            this.viewportFitBound(viewport, viewer.bound.boundingBox , viewer.bound.center , 0, viewport.fitMargin)
+        }) 
+        return viewports
+    } 
+    
+    
+    unSplit(){
+        this.unfocusViewport()
+        viewer.viewports = [viewer.mainViewport] 
+        viewer.mainViewport.width = 1;
+        viewer.mainViewport.height = 1;
+        viewer.mainViewport.left = 0
+        viewer.mainViewport.bottom = 0;   
+        viewer.updateScreenSize({forceUpdateSize:true})        
+    }
+    
+    viewportFitBound(viewport,  bound,  center, duration=0, margin){
+        let view = viewport.view
+        let info = {bound} 
+        let {boundSize, boundCenter} = this.getViewBound(viewport)
+        
+        
+        
+        viewport.targetPlane.setFromNormalAndCoplanarPoint( view.direction.clone(), boundCenter )  
+        viewport.targetPlane.projectPoint(center, viewport.shiftTarget)  //target转换到过模型中心的平面,以保证镜头一定在模型外 this.shiftTarget是得到的
+        
+        info.endPosition = this.getPosOutOfModel(viewport, boundSize) 
+        
+        //if(viewport.name == 'mapViewport')info.endPosition.z = Math.max(Potree.config.map.cameraHeight, info.endPosition.z) 
+         
+        info.margin = margin || {x:30, y:30}    
+        view.moveOrthoCamera(viewport, info ,  duration   )
+    } 
+    
+    
+    getViewBound(viewport){
+        let {boundSize, center} = viewer.bound
+        if(viewport.viewContainsPoints){//视野范围内必须要包含的点,直接算入模型区域。这时候得到的boundCenter和模型中心不重合
+            let boundingBox = viewer.bound.boundingBox.clone()
+            viewport.viewContainsPoints.forEach(point=>{
+                boundingBox.expandByPoint(point)
+            })
+            boundSize = boundingBox.getSize(new THREE.Vector3)
+            center = boundingBox.getCenter(new THREE.Vector3)
+        }
+         
+        return {boundSize, boundCenter:center }  
+    }
+    
+    getPosOutOfModel(viewport, boundSize){ 
+        //let {boundSize, center} = viewer.bound
+        boundSize = boundSize || this.getViewBound(viewport).boundSize
+        let expand = 10; 
+        let radius = boundSize.length() //    / 2  
+        let position = viewport.shiftTarget.clone().sub(viewport.view.direction.clone().multiplyScalar(radius + expand))  
+         
+        return position 
+    } 
+    
+    updateCameraOutOfModel(){//因为移动模型导致模型超出相机外,所以更新位置
+        viewer.viewports.forEach((viewport, i )=>{
+            if(viewport != viewer.mainViewport){
+                let {boundSize, boundCenter} = this.getViewBound(viewport)
+                viewport.targetPlane.setFromNormalAndCoplanarPoint( viewport.view.direction.clone(), boundCenter) 
+                viewport.targetPlane.projectPoint(viewport.view.position, viewport.shiftTarget)  //target转换到过模型中心的平面,以保证镜头一定在模型外 this.shiftTarget是得到的
+         
+                 let endPosition = this.getPosOutOfModel(viewport, boundSize) 
+                 //if(viewport.name == 'mapViewport')endPosition.z = Math.max(Potree.config.map.cameraHeight, endPosition.z) 
+                 viewport.view.position.copy(endPosition)
+            } 
+        })  
+    }  
+     
+    rotateSideCamera(viewport, angle){//侧视图绕模型中心水平旋转
+         
+        //let {boundSize, center} = viewer.bound
+        let {boundSize, boundCenter } = this.getViewBound(viewport)  
+        let center = this.focusCenter || boundCenter //旋转中心,一般是所有模型的中心,除非想指定中心点
+        
+        //找到平移向量
+        viewport.targetPlane.setFromNormalAndCoplanarPoint(viewport.view.direction  , center ) 
+        viewport.targetPlane.projectPoint(viewport.view.position,  viewport.shiftTarget )  //target转换到过模型中心的平面,以保证镜头一定在模型外
+        let vec = new THREE.Vector3().subVectors(center, viewport.shiftTarget)//相对于中心的偏移值,旋转后偏移值也旋转
+        
+        //旋转
+        var rotMatrix = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0,0,1), angle) 
+        
+        viewport.view.direction = viewport.view.direction.applyMatrix4(rotMatrix)
+         
+         
+        vec.applyMatrix4(rotMatrix)
+        viewport.shiftTarget.subVectors(center,vec) //新的
+        
+        
+        viewport.view.position = this.getPosOutOfModel(viewport, boundSize)
+        
+    }
+    
+    getOrthoCamera(){
+        let camera = new THREE.OrthographicCamera(-100, 100, 100, 100, 0.01, 10000)
+        camera.up.set(0,0,1)
+        return camera
+    } 
+    
+    focusOnViewport(name){//全屏
+        viewer.viewports.forEach((viewport, i )=>{
+            if(viewport.name == name){
+                this.focusInfo = {
+                    name,
+                    left : viewport.left, bottom : viewport.bottom, height : viewport.height, width : viewport.width
+                }
+                viewport.left = 0;  viewport.bottom = 0;   viewport.height = 1;  viewport.width = 1  
+                
+            }else{
+                viewport.active = false
+            }
+        })
+         
+        viewer.updateScreenSize({forceUpdateSize:true}) 
+    } 
+    
+    unfocusViewport(){
+        if(!this.focusInfo)return
+        viewer.viewports.forEach((viewport, i )=>{ 
+            if(this.focusInfo.name == viewport.name){//全屏的恢复 
+                viewport.left = this.focusInfo.left;
+                viewport.bottom = this.focusInfo.bottom;
+                viewport.height = this.focusInfo.height;
+                viewport.width = this.focusInfo.width;
+            }
+            viewport.active = true 
+        }) 
+        viewer.updateScreenSize({forceUpdateSize:true}) 
+        this.focusInfo = null
+    } 
+    
+}
+
+export default SplitScreen
+

+ 342 - 0
src/custom/utils/SplitScreen4Views.js

@@ -0,0 +1,342 @@
+ 
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+
+import SplitScreen from "./SplitScreen.js";
+
+
+const viewportProps = [
+    {
+        left:0.5,
+        bottom:0.5,
+        width: 0.5,height:0.5,
+        name : 'MainView',   
+        //view: viewer.scene.view,
+        active: true,
+    },
+    {
+        left:0,
+        bottom:0.5,
+        width: 0.5,height:0.5,
+        name : 'top',   
+        name2 : 'mapViewport', 
+        axis:["x","y"],
+        direction : new THREE.Vector3(0,0,-1), //镜头朝向
+        //axisSign:[1,1],
+        active: true,
+        //相机位置在z轴正向
+    },
+    {
+        left:0.5,
+        bottom:0,
+        width: 0.5,height:0.5,
+        name : 'right', 
+        axis:["y","z"],
+        direction : new THREE.Vector3(1,0,0),
+        //axisSign:[1,1],
+        active: true,
+        //相机位置在x轴负向  右下角屏
+    },
+    {
+        left:0,
+        bottom:0,
+        width: 0.5,height:0.5, 
+        name : 'back', 
+        axis:["x","z"],
+        direction : new THREE.Vector3(0,-1,0),
+        //axisSign:[-1,1],    // 从镜头方向看  x向左 所以取负 
+        active: true,
+        //相机位置在y轴正向  左下角屏
+    },
+]
+
+
+
+
+var SplitScreen4Views = new SplitScreen()
+
+  
+
+ 
+SplitScreen4Views.split = function(o={}){
+    var defaultCamera = viewer.scene.getActiveCamera()
+   
+    let {boundSize, center} = viewer.bound
+        
+    viewer.setLimitFar(false) 
+    viewer.mapViewer.attachToMainViewer(true,'split4Screens','dontSet') 
+    
+    let viewports = this.splitStart(viewportProps)
+    
+    //覆盖在map上、点云等其他物体之下的一层背景
+    let mapViewport = viewer.mapViewer.viewports[0]   
+    mapViewport.noPointcloud = false
+    //隐藏地图游标
+    //viewer.updateVisible(viewer.mapViewer.cursor, 'split4Screens', false)
+    /* viewer.images360.panos.forEach(pano=>{
+        viewer.updateVisible(pano.mapMarker, 'split4Screens', false) //希望这时候mapMarker已经建好了吧
+    }) */
+        
+        
+        
+        
+    //材质 
+    this.statesBefore = { 
+        pointDensity : Potree.settings.pointDensity,
+        displayMode : Potree.settings.displayMode,
+        
+        position: viewer.images360.position,
+        target: viewer.scene.view.getPivot(),
+         
+        
+        //---
+        //ifShowMarker : Potree.settings.ifShowMarker, 
+    }
+    
+    viewer.setPointStandardMat(true,null,true) //切换到标准模式(主要为了mainViewport)  点云使用标准大小 
+    
+    var matBefore = { 
+        opacity : new Map() 
+    } 
+    var newOpacityMap = new Map() 
+     
+    viewer.scene.pointclouds.forEach(e=>{
+        matBefore.opacity.set(e, e.temp.pointOpacity) 
+        matBefore.colorType = e.material.activeAttributeName
+        
+        /* { 
+            var map = new Map()
+            newOpacityMap.set(e, map )
+            var size = e.bound.getSize()
+            viewports.forEach(viewport=>{//根据bound设置opacity,越小的要靠越近,需要大的opacity。但似乎影响太大了
+                if(viewport.name == 'MainView')return;
+                var prop = viewportProps.find(v => viewport.name == v.name2||viewport.name == v.name)
+                let axis = prop.axis
+                var width = size[axis[0]]
+                var height = size[axis[1]]
+                var area = width * height
+                map.set(viewport, 5000/area);
+            })
+            
+        }  */ 
+    }) 
+    
+    let beforeRender = function(){
+        viewer.scene.pointclouds.forEach(e=>{ 
+            if(this.name == "MainView"){ 
+                e.material.activeAttributeName = matBefore.colorType // 'rgba'
+                
+                e.material.useFilterByNormal = false 
+                e.changePointOpacity(matBefore.opacity.get(e)) //1 //恢复下 e.temp.pointOpacity 其实就是1
+                
+                Potree.settings.pointDensity = 'fourViewportsMain'/* 'fourViewports' */ //本来想比另外三屏高一点质量,结果发现会闪烁,因为点云加载需要时间 (navvis仿版也是一样,以后看看能否优化)
+                
+            }else{ 
+                e.material.activeAttributeName = "color"
+                e.material.useFilterByNormal = true 
+                
+                Potree.settings.pointDensity = 'fourViewports' //强制降低点云质量
+                
+                e.changePointOpacity(0.6/* newOpacityMap.get(e).get(viewport), true */);  //多数据集有的数据集很小,放大后显示特别淡
+                //console.log(e.name, viewport.name, e.temp.pointOpacity, e.material.opacity)
+            }                 
+        })  
+    }    
+    viewports.forEach(viewport=>{viewport.beforeRender = beforeRender})
+     
+     
+     
+    this.enableMap(false)
+    this.enableFloorplan(false)
+    viewer.mapViewer.setViewLimit('expand') //多数据集距离远时可以任意远,所以不限制了。但是这样就会看到地图边界了怎么办?
+    //viewer.dispatchEvent({'type': 'beginSplitView' }) 
+    //viewer.updateScreenSize({forceUpdateSize:true})   
+    
+    
+      
+    //this.viewportFitBound(mapViewport, boundSize, center)
+    //Potree.settings.ifShowMarker = false
+    Potree.settings.displayMode = 'showPointCloud'
+} 
+
+
+ 
+  
+
+
+SplitScreen4Views.recover = function(){
+    this.unSplit()
+    
+    /* const {width, height} = viewer.renderer.getSize(new THREE.Vector2());
+    viewer.renderer.setViewport(0,0,width,height)
+    viewer.renderer.setScissorTest( false ); */
+    
+    viewer.setView({
+        position: this.statesBefore.position,
+        target: this.statesBefore.target,
+        duration:300,
+        callback:function(){ 
+        }
+    })
+    
+    
+    
+    viewer.mainViewport.beforeRender = null 
+    viewer.setLimitFar(true)
+    
+    let mapViewport = viewer.mapViewer.viewports[0]
+    viewer.mapViewer.attachToMainViewer(false) 
+    //viewer.updateVisible(viewer.mapViewer.cursor, 'split4Screens', true)
+    /* viewer.images360.panos.forEach(pano=>{
+        viewer.updateVisible(pano.mapMarker, 'split4Screens', true)
+    }) */
+    mapViewport.noPointcloud = true
+    { 
+        this.enableMap(Potree.settings.mapEnable)
+        this.enableFloorplan(Potree.settings.floorplanEnable)
+        if(this.floorplanListener){
+            viewer.mapViewer.mapLayer.removeEventListener( 'floorplanLoaded', this.floorplanListener )  
+            this.floorplanListener = null  
+        } 
+    }
+     
+    Potree.settings.pointDensity = this.statesBefore.pointDensity
+    if(!Potree.settings.isOfficial){
+        Potree.settings.displayMode = this.statesBefore.displayMode
+    }
+    
+    viewer.scene.pointclouds.forEach(e=>{ 
+        //e.material.color.set(this.statesBefore.mat.color)
+        //e.material.activeAttributeName = this.statesBefore.mat.colorType 
+        e.material.useFilterByNormal = false
+        //e.material.opacity = this.statesBefore.mat.opacity  
+    }) 
+    viewer.setPointStandardMat(false)
+    viewer.mapViewer.setViewLimit('standard')
+    
+    //Potree.settings.ifShowMarker = this.statesBefore.ifShowMarker
+    //viewer.dispatchEvent({'type': 'finishSplitView' }) 
+    //viewer.updateScreenSize({forceUpdateSize:true})  
+    
+} 
+
+ 
+
+
+SplitScreen4Views.updateMapViewerBG = function(){
+    let mapViewport = viewer.mapViewer.viewports[0]
+    if(this.floorplanEnabled || this.mapEnabled){
+        mapViewport.background = 'overlayColor'
+        mapViewport.backgroundColor = new THREE.Color(0,0,0)
+        mapViewport.backgroundOpacity = 0.5; 
+    }else{
+        mapViewport.background = null
+        mapViewport.backgroundColor = null
+        mapViewport.backgroundOpacity = null
+    }
+}
+
+SplitScreen4Views.setFloorplanDisplay = function(e, show=false){ 
+    //viewer.updateVisible(e.floorplan.objectGroup, 'splitScreen', !!show)  
+    //e.floorplan.objectGroup.visible = !!show  
+    //viewer.mapViewer.mapLayer.needUpdate = true
+    e.floorplan.setEnable(show)
+}
+
+ 
+SplitScreen4Views.enableMap = function(enable){ 
+    let map = viewer.mapViewer.mapLayer.maps.find(e=>e.name == 'map')
+    //viewer.updateVisible(map.objectGroup, 'splitScreen', !!enable) 
+    //map.objectGroup.visible = !!enable
+    //if(enable)viewer.mapViewer.mapLayer.needUpdate = true //加载地图
+    map.setEnable(!!enable)
+    
+    
+    //viewer.mapViewer.mapGradientBG = viewer.background == 'gradient' && !enable
+    this.mapEnabled = enable
+    this.updateMapViewerBG()
+   
+    
+    
+},
+//直接覆盖原设置
+
+SplitScreen4Views.enableFloorplan = function(enable){ //是否让自定义的平面图显示
+    let floorplans = viewer.mapViewer.mapLayer.maps.filter(e=>e.name.includes('floorplan'))
+    
+    if(this.floorplanListener){
+        viewer.mapViewer.mapLayer.removeEventListener( 'floorplanLoaded', this.floorplanListener )  
+    }
+    this.floorplanListener = (e)=>{
+        this.setFloorplanDisplay(e, enable) 
+    }
+    
+    viewer.mapViewer.mapLayer.addEventListener( 'floorplanLoaded', this.floorplanListener ) //万一之后才加载 
+    
+    
+    if(!enable){ 
+        //隐藏平面图 
+        floorplans.forEach(floorplan=>this.setFloorplanDisplay({floorplan},false))  
+         
+    }else{
+         
+        floorplans.forEach(floorplan=>this.setFloorplanDisplay({floorplan},true))  
+        
+    }
+    
+    
+    if (enable && floorplans.length == 0) Potree.loadMapEntity('all',true)
+    
+    this.floorplanEnabled = enable
+    this.updateMapViewerBG()
+},
+
+/* viewportFitBound:function(viewport, boundSize, center){  //使一个viewport聚焦在某个范围
+    var prop = viewportProps.find(v => viewport.name == v.name2||viewport.name == v.name)
+    let axis = prop.axis 
+    let expand = 10;
+    let position = center.clone()
+    var moveAtAxis = ['x','y','z'].find(e=>!(axis.includes(e))) 
+    
+    if(viewport.name == 'mapViewport'){ 
+        let ori = viewport.view.position[moveAtAxis] 
+        position[moveAtAxis] = ori //不改变这个值,尤其是mapViewer中的z
+    }else{
+        position[moveAtAxis] += boundSize[moveAtAxis]/2+expand//移动到bounding边缘外
+    }
+    
+    viewport.view.position.copy(position)
+    
+    var width = Math.max(boundSize[axis[0]],  boundSize[axis[1]] * viewport.camera.aspect)//视口宽度(米)
+    var margin = 50 //px
+    viewport.camera.zoom = (viewport.resolution.x - margin) / width  
+    viewport.camera.updateProjectionMatrix()
+},
+ */
+ 
+SplitScreen4Views.focusOnPointCloud = function(pointcloud){//三个屏都聚焦在这个点云 
+    var boundSize = pointcloud.bound.getSize(new THREE.Vector3);
+    var center = pointcloud.bound.getCenter(new THREE.Vector3); 
+    let target = pointcloud.panosBound && pointcloud.panosBound.center  //看向pano集中的地方,也就是真正有点云的地方。(因为需要展示所有点云,所以没办法用这个做为center)
+    this.focusOnObject(pointcloud.bound, center,target)
+    
+    viewer.flyToDataset({pointcloud, dontMoveMap:true, duration:0})
+}
+
+SplitScreen4Views.focusOnObject = function(bound, center, target, duration=0){
+    viewer.viewports.forEach(e=>{
+        if(e.name == 'MainView'){
+            /* let len = boundSize.length()
+            let distance = THREE.Math.clamp(e.view.position.distanceTo(center),  len * 0.01,  len*0.3 ) //距离限制
+            //viewer.focusOnObject({position:center}, 'point', duration, {distance, direction: e.view.direction,dontMoveMap:true} )//平移镜头
+            //为了方便定位,直接飞到中心位置:
+            e.view.setView({
+                position:center,  duration,  target   
+            }) */  
+        }else{
+            this.viewportFitBound(e, bound, center)
+        }
+    })
+} 
+    
+ 
+export default SplitScreen4Views

+ 393 - 0
src/custom/utils/UnitConvert.js

@@ -0,0 +1,393 @@
+ 
+const FEET_TO_INCHES_FACTOR = 12
+const EIGHTHS_SYMBOLS = ["", "⅛", "¼", "⅜", "½", "⅝", "¾", "⅞"]//eighths 八分之……
+
+
+class UnitOfMeasurement{//转化单位工具
+    constructor(t, e, n, i){
+        this.name = t,
+        this.symbol = e,
+        this.base = n,
+        this.factor = i 
+    }
+    toBase(t) {//换算到base
+        return t * this.factor
+    }
+   
+    fromBase(t) {//换算到当前
+        return t / this.factor
+    }
+}
+
+    
+    
+    
+
+/*  var o = function t(e) {
+    this.gettext = e,
+    t.METRIC = this.gettext("metric", void 0, "measurement system"),
+    t.IMPERIAL = this.gettext("imperial", void 0, "measurement system") 
+    
+    
+};
+e.UoMSystem = o;
+
+let UoMSystem = {
+    
+} */
+
+
+
+/* var MeasurementDomain = {
+
+    DISTANCE : "DISTANCE",
+    t.AREA = "AREA",
+    t.VOLUME = "VOLUME",
+    t.DATA = "DATA",
+    t
+}
+ 
+
+ */
+    
+    
+    
+    
+    
+     
+    
+var UnitsOfMeasurement = {
+ 
+    MILLIMETER : ["Millimeter", "mm"],
+    CENTIMETER : ["Centimeter", "cm"],
+    METER : ["Meter", "m"],
+    KILOMETER : ["Kilometer", "km"],
+    INCH : ["Inch", "in"],
+    FOOT : ["Foot", "ft"],
+    MILE : ["Mile", "mi"],
+    SQUAREMETER : ["SquareMeter", "m²"],
+    SQUAREFOOT : ["SquareFoot", "ft²"],
+    CUBICMETER : ["CubicMeter", "m³"],
+    CUBICFOOT : ["CubicFoot", "ft³"],
+    BYTE : ["Byte", "B"],
+    KILOBYTE : ["Kilobyte", "kB"],
+    MEGABYTE : ["Megabyte", "MB"],
+    GIGABYTE : ["Gigabyte", "GB"],
+    TERABYTE : ["Terabyte", "TB"],
+    PETABYTE : ["Petabyte", "PB"],
+    
+    
+    init : function() {
+        var e, n, i, a, s, c, l, u, d, p, h, 
+        f = new UnitOfMeasurement(UnitsOfMeasurement.METER[0],UnitsOfMeasurement.METER[1],void 0,1), 
+        g = new UnitOfMeasurement(UnitsOfMeasurement.SQUAREMETER[0],UnitsOfMeasurement.SQUAREMETER[1],void 0,1), 
+        m = new UnitOfMeasurement(UnitsOfMeasurement.CUBICMETER[0],UnitsOfMeasurement.CUBICMETER[1],void 0,1), 
+        v = new UnitOfMeasurement(UnitsOfMeasurement.BYTE[0],UnitsOfMeasurement.BYTE[1],void 0,1);
+        
+        UnitsOfMeasurement.DISTANCE = (
+            (e = {})['metric'] = ((n = {})[UnitsOfMeasurement.MILLIMETER[0]] = new UnitOfMeasurement(UnitsOfMeasurement.MILLIMETER[0],UnitsOfMeasurement.MILLIMETER[1],f,.001),
+            n[UnitsOfMeasurement.CENTIMETER[0]] = new UnitOfMeasurement(UnitsOfMeasurement.CENTIMETER[0],UnitsOfMeasurement.CENTIMETER[1],f,.01),
+            n[UnitsOfMeasurement.METER[0]] = f,
+            n[UnitsOfMeasurement.KILOMETER[0]] = new UnitOfMeasurement(UnitsOfMeasurement.KILOMETER[0],UnitsOfMeasurement.KILOMETER[1],f,1e3),
+            n),
+            e['imperial'] = ((i = {})[UnitsOfMeasurement.INCH[0]] = new UnitOfMeasurement(UnitsOfMeasurement.INCH[0],UnitsOfMeasurement.INCH[1],f,.0254),
+            i[UnitsOfMeasurement.FOOT[0]] = new UnitOfMeasurement(UnitsOfMeasurement.FOOT[0],UnitsOfMeasurement.FOOT[1],f,.3048),
+            i[UnitsOfMeasurement.MILE[0]] = new UnitOfMeasurement(UnitsOfMeasurement.MILE[0],UnitsOfMeasurement.MILE[1],f,1609.344),
+            i),
+            e);
+            
+        UnitsOfMeasurement.AREA = ((a = {})['metric'] = ((s = {})[UnitsOfMeasurement.SQUAREMETER[0]] = g,
+            s),
+            a['imperial'] = ((c = {})[UnitsOfMeasurement.SQUAREFOOT[0]] = new UnitOfMeasurement(UnitsOfMeasurement.SQUAREFOOT[0],UnitsOfMeasurement.SQUAREFOOT[1],g,.092903),
+            c),
+            a);
+            
+        
+        UnitsOfMeasurement.VOLUME = ((l = {})['metric'] = ((u = {})[UnitsOfMeasurement.CUBICMETER[0]] = m,
+            u),
+            l['imperial'] = ((d = {})[UnitsOfMeasurement.CUBICFOOT[0]] = new UnitOfMeasurement(UnitsOfMeasurement.CUBICFOOT[0],UnitsOfMeasurement.CUBICFOOT[1],m,.0283168),
+            d),
+            l);
+        
+        //数据大小
+        var y = ((p = {})[UnitsOfMeasurement.BYTE[0]] = v,
+            p[UnitsOfMeasurement.KILOBYTE[0]] = new UnitOfMeasurement(UnitsOfMeasurement.KILOBYTE[0],UnitsOfMeasurement.KILOBYTE[1],v,1e3),
+            p[UnitsOfMeasurement.MEGABYTE[0]] = new UnitOfMeasurement(UnitsOfMeasurement.MEGABYTE[0],UnitsOfMeasurement.MEGABYTE[1],v,1e6),
+            p[UnitsOfMeasurement.GIGABYTE[0]] = new UnitOfMeasurement(UnitsOfMeasurement.GIGABYTE[0],UnitsOfMeasurement.GIGABYTE[1],v,1e9),
+            p[UnitsOfMeasurement.TERABYTE[0]] = new UnitOfMeasurement(UnitsOfMeasurement.TERABYTE[0],UnitsOfMeasurement.TERABYTE[1],v,1e12),
+            p[UnitsOfMeasurement.PETABYTE[0]] = new UnitOfMeasurement(UnitsOfMeasurement.PETABYTE[0],UnitsOfMeasurement.PETABYTE[1],v,1e15),
+            p);
+            UnitsOfMeasurement.DATA = ((h = {})['metric'] = y,
+            h['imperial'] = y,
+            h)
+    }
+    ,
+    getUnitsOfMeasurementByDomain : function(e) {
+        
+        return this[e.toUpperCase()]
+        
+        /* switch (e.toUpperCase()) {
+        case a.DISTANCE:
+            return t.DISTANCE;
+        case a.AREA:
+            return t.AREA;
+        case a.VOLUME:
+            return t.VOLUME;
+        case a.DATA:
+            return t.DATA;
+        default:
+            console.error(e + " measurement domain is not supported.")
+        } */
+    }
+    ,
+    getUnitsOfMeasurementByDomainAndSystem : function(domain, system) {
+        var r = this.getUnitsOfMeasurementByDomain(domain);
+        if (r.hasOwnProperty(system.toLowerCase()))
+            return r[system.toLowerCase()];
+        console.error(n + " measurement system is not supported.")
+    }
+    ,
+    getDefaultUnitByDomainAndSystem : function(e, n) {
+        switch (e.toUpperCase()) {
+            case 'DISTANCE':
+                switch (n.toLowerCase()) {
+                case 'metric':
+                    return this.DISTANCE['metric'][this.METER[0]];
+                case 'imperial':
+                    return this.DISTANCE['imperial'][this.FOOT[0]];
+                default:
+                    console.error(n + " measurement system is not supported.")
+                }
+            case 'AREA':
+                switch (n.toLowerCase()) {
+                case 'metric':
+                    return this.AREA['metric'][this.SQUAREMETER[0]];
+                case 'imperial':
+                    return this.AREA['imperial'][this.SQUAREFOOT[0]];
+                default:
+                    console.error(n + " measurement system is not supported.")
+                }
+            case 'VOLUME':
+                switch (n.toLowerCase()) {
+                case 'metric':
+                    return this.VOLUME['metric'][this.CUBICMETER[0]];
+                case 'imperial':
+                    return this.VOLUME['imperial'][this.CUBICFOOT[0]];
+                default:
+                    console.error(n + " measurement system is not supported.")
+                }
+            case 'DATA':
+                switch (n.toLowerCase()) {
+                case 'metric':
+                    return this.DATA['metric'][this.BYTE[0]];
+                case 'imperial':
+                    return this.DATA['imperial'][this.BYTE[0]];
+                default:
+                    console.error(n + " measurement system is not supported.")
+                }
+            default:
+                console.error(e + " measurement domain is not supported.")
+        }
+    }
+    
+   
+}
+    
+    
+    
+
+ 
+
+
+
+
+
+class UnitService{
+    
+    constructor(/* e, n, i */){
+          //this.LanguageService = e,
+        //this.localStorageService = n,
+        //this.gettext = i,
+        //this.unitChanged = new r.Signal,
+        this.LOCAL_STORAGE_KEY = "iv_unit_key"//?
+        UnitsOfMeasurement.init()
+        this.unitSystems = ['metric', 'imperial']//[o.UoMSystem.METRIC, o.UoMSystem.IMPERIAL],
+        this.defaultSystem = 'metric'//'imperial'
+        //var a = this.LanguageService.getBrowserLocaleString().toLowerCase();
+        //this.defaultSystem =  t.isLocaleImperial(a) ? o.UoMSystem.IMPERIAL : o.UoMSystem.METRIC,
+        
+        //this.initUnit()
+        
+    }
+     
+    
+    /* initUnit() {
+        var t = this.localStorageService.get(this.LOCAL_STORAGE_KEY);
+        if (t)
+            for (var e = 0, n = this.unitSystems; e < n.length; e++) {
+                var i = n[e];
+                if (i === t)
+                    return void this.setUnit(i, !0)
+            }
+        this.setUnit(this.defaultSystem, !1)
+    }
+    setUnit(t, e) {
+        this.currentSystem !== t && (this.currentSystem = t,
+        this.unitChanged.emit()),
+        e && this.localStorageService.set(this.LOCAL_STORAGE_KEY, t)
+    } */
+    
+   
+    
+    /*isLocaleImperial(e) {
+        return t.IMPERIAL_LOCALES.indexOf(e.toLowerCase()) >= 0
+    }
+    ,
+     t.IMPERIAL_LOCALES = ["en_us"],
+    t.ɵfac(e) {
+        return new (e || t)(c.ɵɵinject(l.LanguageService),
+        c.ɵɵinject("localStorageService"),c.ɵɵinject("gettext"))
+    }
+     ,
+    t.ɵprov = c.ɵɵdefineInjectable({
+        token: t,
+        factory: t.ɵfac,
+        providedIn: "root"
+    }), */
+    
+} 
+
+
+
+    
+
+class UoMService{
+    constructor(/* UnitService */){
+        this.UnitService = new UnitService()/* UnitService */
+        
+    }
+    scopedConvert (t, n, precision = 2, r, minFactor) {
+        return  this.convert(t, n, precision, r, minFactor)
+    }
+    
+    convert(number, domain, precision = 2, system, minFactor, ifEighths = !1) { 
+        if (!number) return "";
+        var s = this.getMostRelevantMeasurement(domain, system || this.UnitService.currentSystem, number, minFactor);
+        return this.getFormattedMeasurementString(s[0], s[1], precision, ifEighths)
+    }
+    
+    convertBack(number, domain, precision = 2, fromSystem, minFactor ) { //从英制转到'metric'
+        if (!number) return "";
+        var d = UnitsOfMeasurement.getDefaultUnitByDomainAndSystem(domain,'metric')
+        
+        var s = this.getMostRelevantMeasurement2(domain, fromSystem, number, minFactor);
+        return this.getFormattedMeasurementString(s[0], d, precision ) 
+        
+        
+        /* 栗子:
+        viewer.unitConvert.convertBack(1, 'area', 5, 'imperial')
+        '0.09290 m²'
+        viewer.unitConvert.convertBack(1, 'Distance', 2, 'imperial')
+        '0.03 m' 
+        */
+    }
+    
+    getFormattedMeasurementString(number, unit, precision, ifEighths) {
+        var result 
+        if(ifEighths && unit.name === UnitsOfMeasurement.FOOT[0]){
+            result = this.formatImperialDistance(number * FEET_TO_INCHES_FACTOR)
+        }else if(ifEighths && unit.name === UnitsOfMeasurement.INCH[0]){
+            result = this.formatImperialDistance(number)
+             
+        }else{
+            result = number.toLocaleString(void 0, {
+                minimumFractionDigits: precision,
+                maximumFractionDigits: precision
+            }) + " " + unit.symbol
+        }
+        
+        return result
+    }
+    
+    formatImperialDistance(e) {
+        var n = Math.round(8 * e)
+          , i = Math.floor(n / 8)
+          , r = Math.floor(i / FEET_TO_INCHES_FACTOR)
+          , o = i - r * FEET_TO_INCHES_FACTOR
+          , a = EIGHTHS_SYMBOLS[n % 8]
+          , s = 0 === o && "" !== a ? "" : o;
+          
+        "" !== s && "" !== a && (a = " " + a)   
+        
+        return  0 !== r ? r + "' " + s + a + '"' : "" + s + a + '"'
+    }
+    
+    getMostRelevantMeasurement(domain, system, number, minFactor=0) {
+        /* var a = r.values(UnitsOfMeasurement.getUnitsOfMeasurementByDomainAndSystem(domain, system))
+          , s = r.filter(a, function(t) {  
+            return t.factor >= i
+        })
+          , c = r.reduce(s, function(t, e) {
+            return e.fromBase(number) < t.fromBase(number) && e.fromBase(number) >= 1 ? e : t
+        }); */
+        let a = []
+        let u = UnitsOfMeasurement.getUnitsOfMeasurementByDomainAndSystem(domain, system)
+        for(let i in u){a.push(u[i])}
+         
+        let s = a.filter(m=>m.factor >= minFactor) 
+         
+        
+       
+        let c = s.reduce(function(prev, currentValue) {//reduce最终值是最后一次return的值 ( 没看懂这句话作用) 
+            return currentValue.fromBase(number) < prev.fromBase(number) && currentValue.fromBase(number) >= 1 ? currentValue : prev
+        })
+        
+        return c ? [c.fromBase(number), c] : void 0
+    }
+    
+    getMostRelevantMeasurement2(domain, system, number, minFactor=0) {//add
+        let a = []
+        let u = UnitsOfMeasurement.getUnitsOfMeasurementByDomainAndSystem(domain, system)
+        for(let i in u){a.push(u[i])} 
+        let s = a.filter(m=>m.factor >= minFactor)  
+        let c = s.reduce(function(prev, currentValue) {//reduce最终值是最后一次return的值 ( 没看懂这句话作用) 
+            return currentValue.toBase(number) < prev.toBase(number) && currentValue.toBase(number) >= 1 ? currentValue : prev
+        }) 
+        return c ? [c.toBase(number), c] : void 0
+    }
+    /* ɵfac(e){
+        return new (e || t)(c.ɵɵinject(l.UnitService))
+    }
+    
+    ɵprov = c.ɵɵdefineInjectable({
+        token: t,
+        factory: t.ɵfac,
+        providedIn: "root"
+    }) */
+   
+}
+
+
+/* 
+使用方法:
+
+
+初始化:
+    var unitConvert = new UoMService();
+ 
+    要设置的类型,两种:var unitSystem = 'metric' || 'imperial'  
+ 
+
+使用:
+
+    面积:
+        let text = viewer.unitConvert.convert(area, 'area', void 0,  unitSystem,  )  
+
+    长度:
+        var text = viewer.unitConvert.convert(distance, 'distance', void 0, unitSystem, 0.1 , true)//distance要传0.1这个factor。 最后一个参数传true代表使用八分之一表示。
+
+ */
+
+
+
+export {UoMService}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 367 - 0
src/custom/utils/browser.js


+ 19 - 0
src/custom/utils/cameraLight.js

@@ -0,0 +1,19 @@
+import MathLight from './MathLight.js'
+
+var cameraLight = {
+    clampVFOV: function(currentFov, maxHFov, width, height) {//限制currentFov, 使之造成的横向fov不大于指定值maxHFov
+        var r = cameraLight.getHFOVFromVFOV(currentFov, width, height);
+        return r > maxHFov ? cameraLight.getVFOVFromHFOV(maxHFov, width, height) : currentFov
+    },
+    getHFOVForCamera: function(camera,  getRad) {
+        return cameraLight.getHFOVByScreenPrecent(camera.fov, camera.aspect, getRad)
+    }, 
+    //add
+    getHFOVByScreenPrecent: function(fov, percent, getRad) { //当fov为占比百分百时,percent代表在屏幕上从中心到边缘的占比
+        let rad = 2 * Math.atan(percent * Math.tan(fov * MathLight.RADIANS_PER_DEGREE / 2));
+        if(getRad)return rad 
+        else return rad * MathLight.DEGREES_PER_RADIAN;
+    }
+};
+
+export default cameraLight

+ 112 - 0
src/custom/utils/file.js

@@ -0,0 +1,112 @@
+//import { i18n } from "@/lang/index"
+// 媒体名称
+/* export const mediaTypes = {
+    image: i18n.t("common.photo"),
+    video: i18n.t("common.video"),
+    audio: i18n.t("common.voice"),
+} */
+
+// 媒体扩展类型
+export const mediaMimes = {
+    image: ["jpg", "png", "jpeg", "bmp", "gif"],
+    audio: ["mp3", "aac", "ogg", "wav" /* , "m4a" */],
+    video: ["mp4", "mov", "quicktime", "webm" /* "rmvb", "wmv" */], //ios:mov
+}
+
+// 媒体大小显示(MB)
+export const mediaMaxSize = {
+    image: 10,
+    video: 20,
+    audio: 5,
+}
+
+/**
+ * 获取媒体扩展类型
+ * @param {Stirng} filename 文件名称
+ */
+export const getMime = filename => {
+    if (!filename || filename.indexOf(".") === -1) {
+        return ""
+    }
+
+    return filename
+        .split(".")
+        .pop()
+        .toLowerCase()
+}
+
+/**
+ * 在路径中获取文件名
+ * @param {*} path
+ */
+export const getFilename = path => {
+    const segment = (path || "").split("/")
+    return segment[segment.length - 1]
+}
+
+/**
+ * 检测媒体文件是否超过预设限制
+ * @param {String} type 媒体类型
+ * @param {Number} size 文件大小
+ */
+export const checkSizeLimit = (type, size) => {
+    size = size / 1024 / 1024
+
+    return size <= mediaMaxSize[type]
+}
+
+export const checkSizeLimitFree = (size, limit) => {
+    size = size / 1024 / 1024
+
+    return size <= limit
+}
+
+/**
+ * 检测媒体类型
+ * @param {String} type 媒体类型
+ * @param {String} filename 文件名称
+ */
+export const checkMediaMime = (type, filename) => {
+    const mime = getMime(filename)
+    const find = mediaMimes[type]
+    if (!find) {
+        return false
+    }
+
+    return find.indexOf(mime) !== -1
+}
+
+export const checkMediaMimeByAccept = (accept, filename) => {
+    let mime = getMime(filename)
+    let type = accept
+    if (type && type.indexOf("jpg") == -1 && type.indexOf("jpeg") != -1) {
+        type += ",image/jpg"
+    }
+    return (type || "").indexOf(mime) != -1
+}
+
+export const base64ToBlob = base64 => {
+    let arr = base64.split(","),
+        mime = arr[0].match(/:(.*?);/)[1],
+        bstr = atob(arr[1]),
+        n = bstr.length,
+        u8arr = new Uint8Array(n)
+    while (n--) {
+        u8arr[n] = bstr.charCodeAt(n)
+    }
+    return new Blob([u8arr], { type: mime })
+}
+
+export const base64ToDataURL = base64 => {
+    return window.URL.createObjectURL(base64ToBlob(base64))
+}
+
+export const blobToBase64 = function(blob) {
+    return new Promise(resolve => {
+        var reader = new FileReader()
+        reader.onload = function() {
+            resolve(reader.result)
+        }
+        reader.readAsDataURL(blob)
+    })
+}

+ 608 - 0
src/custom/utils/math.js

@@ -0,0 +1,608 @@
+
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+import searchRings from "./searchRings.js";
+
+
+
+var math = {
+    getBaseLog(x, y) {//返回以 x 为底 y 的对数(即 logx y) .  Math.log 返回一个数的自然对数
+        return Math.log(y) / Math.log(x);
+    }
+    ,
+    convertVector : {
+        ZupToYup: function(e){//navvis -> 4dkk
+            return new THREE.Vector3(e.x,e.z,-e.y)
+        },
+        YupToZup: function(e){//4dkk -> navvis
+            return new THREE.Vector3(e.x,-e.z,e.y)
+        },
+        
+        
+    },
+    convertQuaternion: {
+        ZupToYup: function(e){//navvis -> 4dkk  //不同于convertVisionQuaternion
+            let rotation = new THREE.Euler(-Math.PI/2,0,0)   
+            let quaternion = new THREE.Quaternion().setFromEuler(rotation)
+            return e.clone().premultiply(quaternion)
+            //return new THREE.Quaternion(e.x,e.z,-e.y,e.w).multiply((new THREE.Quaternion).setFromAxisAngle(new THREE.Vector3(1,0,0), THREE.Math.degToRad(90)))
+        },
+        YupToZup: function(e){//4dkk -> navvis
+            let rotation = new THREE.Euler(Math.PI/2,0,0)
+            let quaternion = new THREE.Quaternion().setFromEuler(rotation)
+            return e.clone().premultiply(quaternion)
+        },
+        
+        
+    },
+	 
+    convertVisionQuaternion: function(e) {
+        return new THREE.Quaternion(e.x,e.z,-e.y,e.w).multiply((new THREE.Quaternion).setFromAxisAngle(new THREE.Vector3(0,1,0), THREE.Math.degToRad(90)))
+    },
+    invertVisionQuaternion : function(e) {//反转给算法部
+        var a = e.clone().multiply((new THREE.Quaternion).setFromAxisAngle(new THREE.Vector3(0,1,0), THREE.Math.degToRad(-90)))
+        return new THREE.Quaternion(a.x,-a.z,a.y,a.w)
+    },
+    //------------
+    
+    getVec2Angle : function(dir1,dir2){ 
+        return Math.acos( THREE.Math.clamp(this.getVec2Cos(dir1,dir2), -1,1) )  
+    },
+    getVec2Cos :  function(dir1,dir2){ 
+        return dir1.dot(dir2) / dir1.length() / dir2.length()    
+    },
+    getAngle:function(vec1, vec2, axis){//带方向的角度 vector3
+        var angle = vec1.angleTo(vec2)
+        var axis_ = vec1.clone().cross(vec2);
+        if(typeof axis == 'string'){
+            if(axis_[axis] < 0){
+                angle *= -1
+            }
+        }else{//vector3
+            if(axis_.dot(axis)< 0){
+                angle *= -1
+            }
+        }
+        
+        return angle
+    }, 
+    
+    closeTo : function(a,b, precision=1e-6){ 
+        let f = (a,b)=>{
+            return Math.abs(a-b) < precision;
+        } 
+          
+        if(typeof (a) == 'number'){
+            return f(a, b);
+        }else{
+            let judge = (name)=>{
+                if(a[name] == void 0)return true //有值就判断,没值就不判断
+                else return f(a[name],b[name])
+            }
+            return judge('x') && judge('y') && judge('z') && judge('w')  
+        } 
+        
+    }, 
+     
+
+    
+	toPrecision: function (e, t) {//xzw change 保留小数
+		var f = function (e, t) {
+			var i = Math.pow(10, t);
+			return Math.round(e * i) / i
+		}
+		if (e instanceof Array) {
+			for (var s = 0; s < e.length; s++) {
+				e[s] = f(e[s], t);
+			}
+			return e;
+		} else if (e instanceof Object) {
+			for (var s in e) {
+				e[s] = f(e[s], t);
+			}
+			return e;
+		} else return f(e, t)
+	},
+    isEmptyQuaternion: function(e) {
+        return 0 === Math.abs(e.x) && 0 === Math.abs(e.y) && 0 === Math.abs(e.z) && 0 === Math.abs(e.w)
+    },
+    projectPositionToCanvas: function(e, t, i) {
+        i = i || new THREE.Vector3,
+        i.copy(e);
+		var r = .5 * $('#player').width()
+			, o = .5 * $('#player').height();
+        return i.project(t),
+        i.x = i.x * r + r,
+        i.y = -(i.y * o) + o,
+        i
+    },
+     
+	
+	handelPadResize:false,
+	/* handelPadding : function () { //去除player左边和上面的宽高,因为pc的player左上有其他element  许钟文
+		
+		var pads = [];//记录下来避免反复计算
+		var index = [];
+		var resetPad = function(){
+			pads = []; 
+			index = [];
+			math.handelPadResize = false; //switchview时resized为true
+		}
+		
+		if(config.isEdit && !config.isMobile){
+			window.addEventListener('resize',resetPad);
+		}
+		return function(x, y, domE){
+			if(!config.isEdit || config.isMobile) {
+				return {
+					x: x,
+					y: y
+				}  
+			}
+			
+			if(this.handelPadResize)resetPad(); 
+			domE = domE || $('#player')[0];
+			var pad;
+			var i = index.indexOf(domE);
+			if (i == -1){ 
+				index.push(domE);
+				pad = {
+					x: this.getOffset("left", domE),
+					y: this.getOffset("top", domE)
+				}
+				pads.push(pad)  
+			}					
+			else pad = pads[i];
+			return {
+				x: x - pad.x,
+				y: y - pad.y
+			}
+		}
+		
+	}(),  */
+	
+	getOffset: function (type, element, parent) {//获取元素的边距 许钟文
+		var offset = (type == "left") ? element.offsetLeft : element.offsetTop;
+		if (!parent) parent = $("body")[0];
+		while (element = element.offsetParent) {
+			if (element == parent) break;
+			offset += (type == "left") ? element.offsetLeft : element.offsetTop;
+		}
+		return offset;
+	}
+
+	,
+    constrainedTurn: function(e) {
+        var t = e % (2 * Math.PI);
+        return t = t > Math.PI ? t -= 2 * Math.PI : t < -Math.PI ? t += 2 * Math.PI : t
+    },
+    getFOVDotThreshold: function(e) {
+        return Math.cos(THREE.Math.degToRad(e / 2))
+    },
+    transform2DForwardVectorByCubeFace: function(e, t, i, n) {
+        switch (e) {
+        case GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_X:
+            i.set(1, t.y, t.x);
+            break;
+        case GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
+            i.set(-1, t.y, -t.x);
+            break;
+        case GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
+            i.set(-t.x, 1, -t.y);
+            break;
+        case GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
+            i.set(-t.x, -1, t.y);
+            break;
+        case GLCubeFaces.GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
+            i.set(-t.x, t.y, 1);
+            break;
+        case GLCubeFaces.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
+            i.set(t.x, t.y, -1)
+        }
+        n && i.normalize()
+    },
+	
+	
+	
+	getFootPoint : function(oldPos, p1, p2, restricInline){ //找oldPos在线段p1, p2上的垂足
+		/* if(isWorld){//输出全局坐标 需要考虑meshGroup.position
+			p1 = p1.clone();
+			p2 = p2.clone();
+			p1.y += mainDesign.meshGroup.position.y;
+			p2.y += mainDesign.meshGroup.position.y;
+		} */
+        if(p1.equals(p2))return p1.clone()
+		var op1 = oldPos.clone().sub(p1); 
+		var p1p2 = p1.clone().sub(p2)
+		var p1p2Len = p1p2.length()
+		var leftLen = op1.dot(p1p2) / p1p2Len;
+		var pos = p1.clone().add(p1p2.multiplyScalar( leftLen/p1p2Len )); 
+         
+        if(restricInline && pos.clone().sub(p1).dot( pos.clone().sub(p2)   ) > 0){//foot不在线段上
+            if(pos.distanceTo(p1) < pos.distanceTo(p2)) pos = p1.clone();
+            else pos = p2.clone();
+        }
+        
+		return pos;
+	},
+
+	
+	
+	
+	
+	
+	/**
+   * 计算多边形的重心
+   * @param {*} points
+   */
+	getCenterOfGravityPoint : function(mPoints){
+		var area = 0.0;//多边形面积
+		var Gx = 0.0, Gy = 0.0;// 重心的x、y
+
+		for (var i = 1; i <= mPoints.length; i++) {
+			var ix = mPoints[i % mPoints.length].x;
+			var iy = mPoints[i % mPoints.length].y;
+			var nx = mPoints[i - 1].x;
+			var ny = mPoints[i - 1].y;
+			var temp = (ix * ny - iy * nx) / 2.0;
+			area += temp;
+			Gx += temp * (ix + nx) / 3.0;
+			Gy += temp * (iy + ny) / 3.0;
+		}
+		Gx = Gx / area;
+		Gy = Gy / area;
+		return { x: Gx, y: Gy };
+	},
+	
+	getBound : function(ring){
+		var bound = new THREE.Box2();
+		for(var j=0,len = ring.length; j<len; j++){
+			bound.expandByPoint(ring[j])
+		}
+		return bound;
+	},
+
+	isPointInArea : function(ring, holes, point, ifAtLine){//判断点是否在某个环内, 若传递了holes代表还要不能在内环内
+		var bound = this.getBound(ring);
+		if(point.x < bound.min.x || point.x > bound.max.x || point.y < bound.min.y || point.y > bound.max.y)return false;
+		
+		 
+		var inside = false;
+		var x = point.x,
+		  y = point.y;
+
+		for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) {
+		  var xi = ring[i].x,
+			yi = ring[i].y;
+		  var xj = ring[j].x,
+			yj = ring[j].y;
+ 
+		  if((xi - x)*(yj - y) == (xi - x)*(yi - y) && x>=Math.min(xi,xj) && x<=Math.max(xi,xj)//xzw add
+			  && y>=Math.min(yi,yj) && y<=Math.max(yi,yj)
+		  ){
+			  //return !!ifAtLine;//在线段上,则判断为…… (默认在外)
+              return {atLine:true}
+		  }
+		  
+		   if (((yi > y) != (yj > y)) &&
+			(x < (xj - xi) * (y - yi) / (yj - yi) + xi) 
+			) {
+			inside = !inside;
+		  } 
+		  
+		}
+        
+        if(inside && holes){
+            return !holes.some(ring=>this.isPointInArea(ring, null, point, ifAtLine) )   //不能存在于任何一个二级内环内
+        }else{
+            return inside;
+        }
+        
+
+		
+	},
+	
+	getArea : function (ring) { //求面积  顺时针为正  来自three shape
+		for (var t = ring.length, i = 0, n = t - 1, r = 0; r < t; n = r++)
+			i += ring[n].x * ring[r].y - ring[r].x * ring[n].y;
+		return -.5 * i
+	},
+	isInBetween : function(a, b, c, precision) {
+        // 如果b几乎等于a或c,返回false.为了避免浮点运行时两值几乎相等,但存在相差0.00000...0001的这种情况出现使用下面方式进行避免
+
+        /* if (Math.abs(a - b) < 0.000001 || Math.abs(b - c) < 0.000001) {
+            return false;
+        } 
+ 
+        return (a <= b && b <= c) || (c <= b && b <= a);*/
+        
+        
+        //更改:如果b和a或c中一个接近 就算在a和c之间
+        return (a <= b && b <= c) || (c <= b && b <= a) || this.closeTo(a,b,precision) || this.closeTo(b,c,precision); 
+        
+    },
+	 
+    
+    ifPointAtLineBound:function(point, linePoints, precision){
+        //待验证  横线和竖线比较特殊
+        return  math.isInBetween(linePoints[0].x, point.x, linePoints[1].x, precision) && math.isInBetween(linePoints[0].y, point.y, linePoints[1].y, precision) 
+    }
+    
+    , 
+	
+	isLineIntersect: function (line1, line2, notSegment, precision) {//线段和线段是否有交点.  notSegment代表是直线而不是线段
+		var a1 = line1[1].y - line1[0].y;
+		var b1 = line1[0].x - line1[1].x;
+		var c1 = a1 * line1[0].x + b1 * line1[0].y;
+		//转换成一般式: Ax+By = C
+		var a2 = line2[1].y - line2[0].y;
+		var b2 = line2[0].x - line2[1].x;
+		var c2 = a2 * line2[0].x + b2 * line2[0].y;
+		// 计算交点		
+		var d = a1 * b2 - a2 * b1;
+
+		// 当d==0时,两线平行
+		if (d == 0) {
+		  return false;
+		} else {
+		  var x = (b2 * c1 - b1 * c2) / d;
+		  var y = (a1 * c2 - a2 * c1) / d;
+
+		  // 检测交点是否在两条线段上
+		  /* if (notSegment || (isInBetween(line1[0].x, x, line1[1].x) || isInBetween(line1[0].y, y, line1[1].y)) &&
+			(isInBetween(line2[0].x, x, line2[1].x) || isInBetween(line2[0].y, y, line2[1].y))) {
+			return {x,y};
+		  } */
+          if (notSegment ||  math.ifPointAtLineBound({x,y}, line1, precision) && math.ifPointAtLineBound({x,y}, line2, precision)){ 
+                return {x,y};
+		  }
+		}  
+    },
+     
+	getNormal2d : function(o={} ){//获取二维法向量 方向向内
+		var x,y, x1,y1; 
+		//line2d的向量
+        if(o.vec){
+            x1 = o.vec.x;  y1 = o.vec.y
+        }else{
+            x1 = o.p1.x - o.p2.x;
+            y1 = o.p1.y - o.p2.y;
+        }
+		
+		//假设法向量的x或y固定为1或-1
+		if(y1 != 0){
+			x = 1;
+			y = - (x1 * x) / y1;
+		}else if(x1 != 0){//y如果为0,正常情况x不会是0
+			y = 1;
+			x = - (y1 * y) / x1;
+		}else{
+			console.log("两个点一样");
+			return null;
+		}
+		
+		//判断方向里或者外:
+		var vNormal = new THREE.Vector3(x, 0, y);
+		var vLine = new THREE.Vector3(x1, 0, y1);
+		var vDir = vNormal.cross(vLine);
+		if(vDir.y>0){
+			x *= -1;
+			y *= -1;
+		} 
+		return new THREE.Vector2(x, y).normalize();
+ 	},
+     
+    getQuaBetween2Vector:function(oriVec, newVec, upVec){ //获取从oriVec旋转到newVec可以应用的quaternion
+        var angle = oriVec.angleTo(newVec);
+        var axis = oriVec.clone().cross( newVec).normalize();//两个up之间
+        if(axis.length() == 0){//当夹角为180 或 0 度时,得到的axis为(0,0,0),故使用备用的指定upVec
+            return new THREE.Quaternion().setFromAxisAngle( upVec, angle );
+        }
+        return new THREE.Quaternion().setFromAxisAngle( axis, angle );
+    }
+    /* ,
+    getQuaBetween2Vector2 : function(oriVec, newVec   ){//not camera
+        var _ = (new THREE.Matrix4).lookAt( oriVec, new THREE.Vector3, new THREE.Vector3(0,1,0))
+        var aimQua = (new THREE.Quaternion).setFromRotationMatrix(_)
+        var _2 = (new THREE.Matrix4).lookAt( newVec, new THREE.Vector3, new THREE.Vector3(0,1,0))
+        var aimQua2 = (new THREE.Quaternion).setFromRotationMatrix(_2)
+        
+        return aimQua2.multiply(aimQua.clone().inverse()) 
+        
+    } */
+    
+        
+    ,
+    
+    getScaleForConstantSize : function(){ //获得规定二维大小的mesh的scale值。可以避免因camera的projection造成的mesh视觉大小改变。  来源:tag.updateDisc
+        var w;  
+        var i = new THREE.Vector3, o = new THREE.Vector3, l = new THREE.Vector3, c = new THREE.Vector3, h = new THREE.Vector3
+        return function(op={}){
+            if(op.width2d) w = op.width2d //如果恒定二维宽度
+            else{//否则考虑上距离,加一丢丢近大远小的效果
+                var currentDis, nearBound, farBound
+                if(op.camera.type == "OrthographicCamera"){
+                    currentDis = 200 / op.camera.zoom  //(op.camera.right - op.camera.left) / op.camera.zoom
+                }else{
+                    currentDis = op.position.distanceTo(op.camera.position);
+                } 
+                w = op.maxSize - ( op.maxSize -  op.minSize) * THREE.Math.smoothstep(currentDis,  op.nearBound,  op.farBound);
+                //maxSize : mesh要表现的最大像素宽度;   nearBound: 最近距离,若比nearBound近,则使用maxSize
+            }
+            i.copy(op.position).project(op.camera),  //tag中心在屏幕上的二维坐标
+            o.set(op.resolution.x / 2, op.resolution.y / 2, 1).multiply(i), //转化成px   -w/2 到 w/2的范围
+            l.set(w / 2, 0, 0).add(o),  //加上tag宽度的一半
+            c.set(2 / op.resolution.x, 2 / op.resolution.y, 1).multiply(l), //再转回  -1 到 1的范围
+            h.copy(c).unproject(op.camera);//再转成三维坐标,求得tag边缘的位置
+            var g = h.distanceTo(op.position)//就能得到tag的三维半径
+            //这里使用的都是resolution2, 好处是手机端不会太小, 坏处是pc更改网页显示百分比时显示的大小会变(或许可以自己算出设备真实的deviceRatio, 因window.screen是不会改变的),但考虑到用户可以自行调节字大小也许是好的
+            return g  //可能NAN  当相机和position重叠时
+        }
+    }()
+    ,
+    
+    //W , H, left, top分别是rect的宽、高、左、上
+    getCrossPointAtRect : function(p1, aim, W , H, left, top){//求射线p1-aim在rect边界上的交点,其中aim在rect范围内,p1则不一定(交点在aim这边的延长线上)
+        
+        var x,y, borderX;
+        var r = (aim.x - p1.x) / (aim.y - p1.y);//根据相似三角形原理先求出这个比值	
+        var getX = function(y){
+            return  r * (y-p1.y) + p1.x;
+        }
+        var getY = function(x){
+            return  1/r * (x-p1.x) + p1.y;
+        }
+        if(aim.x >= p1.x){ 
+            borderX = W+left;
+        }else{
+            borderX = left; 
+        }
+        x = borderX;
+        y = getY(x);
+        if(y < top || y > top+H){
+            if(y < top){
+                y = top; 
+            }else{
+                y = top+H; 
+            }
+            x = getX(y)			
+        }
+        return new THREE.Vector2(x, y);  
+    },
+    getDirFromUV : function(uv){ //获取dir   反向计算 - -  二维转三维比较麻烦 
+        var dirB; //所求 单位向量
+        
+        
+        
+        var y = Math.cos(uv.y * Math.PI);              //uv中纵向可以直接确定y,  根据上面getUVfromDir的反向计算 
+                            // 故 uv.y * Math.PI  就是到垂直线(向上)的夹角
+        var angle = 2 * Math.PI * uv.x - Math.PI       //x/z代表的是角度
+
+        var axisX, axisZ; //axis为1代表是正,-1是负数
+        if (-Math.PI <= angle && angle < 0) {
+            axisX = -1 //下半圆
+        } else {
+            axisX = 1 //上半圆
+        }
+        if (-Math.PI / 2 <= angle && angle < Math.PI / 2) {
+            axisZ = 1 //右半圆
+        } else {
+            axisZ = -1 //左半圆
+        }
+
+
+
+        var XDivideZ = Math.tan(angle);
+        var z = Math.sqrt((1 - y * y) / (1 + XDivideZ * XDivideZ));
+        var x = XDivideZ * z
+
+
+        if (z * axisZ < 0) { //异号
+            z *= -1;
+            x *= -1;
+            if (x * axisX < 0) {
+          //      console.log("wrong!!!!!??????????")
+            }
+        }
+
+        x *= -1 //计算完成后这里不能漏掉 *= -1	
+        dirB = this.convertVector.YupToZup(new THREE.Vector3(x, y, z))   
+
+        //理想状态下x和z和anotherDir相同
+        return dirB
+
+
+    },
+    
+    getUVfromDir : function(dir) { //获取UV  同shader里的计算 
+        var dir = this.convertVector.ZupToYup(dir)
+        dir.x *= -1; //计算前这里不能漏掉 *= -1  见shader
+        var tx = Math.atan2(dir.x, dir.z) / (Math.PI * 2.0) + 0.5; //atan2(y,x) 返回从 X 轴正向逆时针旋转到点 (x,y) 时经过的角度。区间是-PI 到 PI 之间的值
+        var ty = Math.acos(dir.y) / Math.PI;
+        return new THREE.Vector2(tx,  ty)  
+
+        //理想状态下tx相同
+    },
+     
+    getDirByLonLat : function(lon,lat){
+        var dir = new THREE.Vector3
+        var phi = THREE.Math.degToRad(90 - lat);
+        var theta = THREE.Math.degToRad(lon);
+        dir.x = Math.sin(phi) * Math.cos(theta);
+        dir.y = Math.cos(phi);
+        dir.z = Math.sin(phi) * Math.sin(theta);   
+        return dir
+    } //0,0 => (1,0,0)     270=>(0,0,-1)
+    ,    
+    projectPointAtPlane:function(o={}){//获取一个点在一个面上的投影 {facePoints:[a,b,c], point:}
+        var plane = new THREE.Plane().setFromCoplanarPoints(...o.facePoints)
+        return plane.projectPoint(o.point, new THREE.Vector3() )
+    }
+    ,
+    
+    getPolygonsMixedRings:function( polygons,  onlyGetOutRing){//{points:[vector2,...],holes:[[],[]]} 
+         
+         
+        let points = [] 
+        let lines = []
+        let i = 0 
+        
+        polygons.forEach(e=> points.push(...e.map(a=>new THREE.Vector2().copy(a) )) )   
+        polygons.forEach((ps,j)=>{
+            let length = ps.length;
+            let index = 0;
+            while(index<length){
+                lines.push({p1:index+i,p2:(index+1)%length+i});
+                index ++;
+            }
+            i+=length
+        })
+    
+             
+        points.forEach((p,j)=>{p.id = j}) 
+            
+        let rings = searchRings({
+            points,
+            lines,
+            onlyGetOutRing    
+        })
+        //console.log(rings)
+        
+        rings = rings.filter(e=>e.closetParent == void 0)// 子环不加,被外环包含了
+        
+        return rings 
+        
+    },
+
+    getQuaFromPosAim( position, target ){ 
+        let matrix = (new THREE.Matrix4).lookAt(position, target, new THREE.Vector3(0,0,1))
+        return (new THREE.Quaternion).setFromRotationMatrix(matrix)
+        
+    },
+    
+    
+    getBoundByPoints(points, minSize){ 
+        var bound = new THREE.Box3
+        points.forEach(point=>{
+            bound.expandByPoint(point)
+        }) 
+        let center = bound.getCenter(new THREE.Vector3)
+        if(minSize){
+            let minBound = (new THREE.Box3()).setFromCenterAndSize(center, minSize)
+            bound.union(minBound)
+        }
+        return {
+            bounding:bound,
+            size: bound.getSize(new THREE.Vector3),
+            center,
+        }
+    },
+
+
+
+};
+
+ 
+Potree.math = math
+
+
+export default math

+ 489 - 0
src/custom/utils/request.js

@@ -0,0 +1,489 @@
+/*
+ * @Author: Rindy
+ * @Date: 2019-08-06 16:25:08
+ * @LastEditors: Rindy
+ * @LastEditTime: 2021-08-27 12:33:49
+ * @Description: Request
+ */
+
+/** http请求模块
+ * @category utils
+ * @module utils/request
+ */
+
+ 
+ 
+import browser from "./browser.js"
+import { base64ToBlob } from "./file.js"
+//import { $alert, $confirm, $loginTips } from "@/components/shared/message"
+//import { $waiting } from "@/components/shared/loading"
+//import { checkLogin } from "@/api"
+//import { LoginDetector } from "@/core/starter"
+//import { i18n } from "@/lang"
+//import { password } from "@/utils/string"
+//import store from "../Store"
+
+// 空函数
+const noop = function() {}
+// 请求回调队列
+let postQueue = []
+
+/**
+ * @property {number} NEXT              - 继续执行
+ * @property {number} SUCCESS           - 成功
+ * @property {number} EXCEPTION         - 异常错误
+ * @property {number} FAILURE_CODE_3001 - 缺少必要参数
+ * @property {number} FAILURE_CODE_3002 - 访问异常
+ * @property {number} FAILURE_CODE_3003 - 非法访问
+ * @property {number} FAILURE_CODE_3004 - 用户未登录
+ * @property {number} FAILURE_CODE_3005 - 验证码已过期
+ * @property {number} FAILURE_CODE_3006 - 验证码错误
+ * @property {number} FAILURE_CODE_3007 - 昵称已存在
+ * @property {number} FAILURE_CODE_3008 - 该手机已被注册
+ * @property {number} FAILURE_CODE_3009 - 两次输入的密码不一致
+ * @property {number} FAILURE_CODE_3010 - 昵称长度错误
+ * @property {number} FAILURE_CODE_3011 - 密码长度错误
+ * @property {number} FAILURE_CODE_3012 - 昵称包含敏感词
+ * @property {number} FAILURE_CODE_3013 - 手机号码格式错误
+ * @property {number} FAILURE_CODE_3014 - 账号或密码不正确
+ * @property {number} FAILURE_CODE_3015 - 用户不存在
+ * @property {number} FAILURE_CODE_3016 - 您没有权限,请联系管理员
+ * @property {number} FAILURE_CODE_3017 - 空文件
+ * @property {number} FAILURE_CODE_3018 - 需要上传或使用的文件不存在
+ * @property {number} FAILURE_CODE_5010 - 找不到该场景对应的相机
+ * @property {number} FAILURE_CODE_5012 - 数据不正常
+ * @property {number} FAILURE_CODE_5014 - 无权操作该场景
+ * @property {number} FAILURE_CODE_5005 - 场景不存在
+ */
+export const statusCode = {
+    NEXT: -999,
+    SUCCESS: 0,
+    EXCEPTION: -1,
+    FAILURE_CODE_3001: 3001,
+    FAILURE_CODE_3002: 3002,
+    FAILURE_CODE_3003: 3003,
+    FAILURE_CODE_3004: 3004,
+    FAILURE_CODE_3005: 3005,
+    FAILURE_CODE_3006: 3006,
+    FAILURE_CODE_3007: 3007,
+    FAILURE_CODE_3008: 3008,
+    FAILURE_CODE_3009: 3009,
+    FAILURE_CODE_3010: 3010,
+    FAILURE_CODE_3011: 3011,
+    FAILURE_CODE_3012: 3012,
+    FAILURE_CODE_3013: 3013,
+    FAILURE_CODE_3014: 3014,
+    FAILURE_CODE_3015: 3015,
+    FAILURE_CODE_3016: 3016,
+    FAILURE_CODE_3017: 3017,
+    FAILURE_CODE_3018: 3018,
+    FAILURE_CODE_5010: 5010,
+    FAILURE_CODE_5012: 5012,
+    FAILURE_CODE_5014: 5014,
+    FAILURE_CODE_5005: 5005,
+}
+
+ 
+/**
+ * 获取Token
+ *  @function
+ */
+export function getToken() {
+    var urlToken = browser.urlHasValue("token", true)
+    if (urlToken) {
+        // 设置token共享给用户中心
+        localStorage.setItem("token", urlToken)
+    }
+    return urlToken || localStorage.getItem("token") || ""
+}
+
+/**
+ * 根据状态码的结果处理后续操作
+ * @function
+ * @param {number} code - 状态码
+ * @param {function} callback - 回调
+ */
+export function statusCodesHandler(code, callback) {
+    if (code == statusCode.EXCEPTION) {
+        return $alert({ content: i18n.t("tips.exception") })
+    }
+
+    if (
+        code == statusCode.FAILURE_CODE_3002 ||
+        code == statusCode.FAILURE_CODE_3003 ||
+        code == statusCode.FAILURE_CODE_3004
+    ) {
+        callback(code)
+        return showLoginTips()
+    }
+
+    if (code == statusCode.FAILURE_CODE_3001) {
+        callback(code)
+        return $alert({ content: i18n.t("tips.params_notfound") })
+    }
+
+    if (code == statusCode.FAILURE_CODE_3017) {
+        callback(code)
+        return $alert({ content: i18n.t("tips.file_notfound") })
+    }
+
+    if (code == statusCode.FAILURE_CODE_5005) {
+        /* if (!config.isEdit) {
+            return (location.href = config.pages.NotFound)
+        } */
+        callback(code)
+        return $alert({ content: i18n.t("tips.scene_notfound") })
+    }
+
+    if (code == statusCode.FAILURE_CODE_5010) {
+        callback(code)
+        return $alert({ content: i18n.t("tips.camera_notfound") })
+    }
+
+    if (code == statusCode.FAILURE_CODE_5012) {
+        callback(code)
+        return $alert({ content: i18n.t("tips.data_error") })
+    }
+
+    if (code == statusCode.FAILURE_CODE_5014) {
+        callback(code)
+        return $alert({ content: i18n.t("tips.auth_deny") })
+    }
+
+    return statusCode.NEXT
+}
+
+$.ajaxSetup({
+    headers: {},
+    beforeSend: function(xhr) {
+        const token = getToken()
+        if (token) {
+            xhr.setRequestHeader("token", token)
+        } else if (!token && this.url.indexOf("isLogin") != -1) {
+            showLoginTips()
+        }
+
+        /* if (config.oem == "localshow") {
+            // 本地版本兼容当前目录
+            if (this.url.indexOf("http") == -1 && this.url.indexOf("/") == 0) {
+                this.url = this.url.substr(1)
+            }
+        } */
+
+        // if(this.url.indexOf('http')==-1 && this.url.indexOf('/') !=0){
+        //     this.url = '/'+this.url
+        // }
+    },
+    error: function(xhr, status, error) {
+        // 出错时默认的处理函数
+        if (this.url.indexOf("/scene.json") != -1 && xhr.status == 404) {
+            return $alert({ content: i18n.t("tips.scene_notfound") })
+        } else if (this.type === "POST") {
+            return $alert({ content: i18n.t("tips.network_error") })
+        }
+    },
+    success: function(result) {},
+    complete: function() {
+        // Post类型请求无论成功或失败都关闭等待提示
+        if (this.type === "POST") {
+            http.__loading && $waiting.hide()
+        }
+
+        http.__loading = true
+    },
+})
+
+/**
+ * @namespace http
+ * @type {Object}
+ */
+export const http = {
+    statusCode,
+    __loading: true,
+    __request(xhr, method, url, data, done, fail) {
+        if (typeof done != "function") {
+            done = noop
+        }
+        if (typeof fail != "function") {
+            fail = noop
+        }
+
+        xhr.done(result => {
+            if (typeof result.code !== "undefined") {
+                const flag = statusCodesHandler(result.code, function(code) {
+                    // 需要登录的状态
+                    if (
+                        code == statusCode.FAILURE_CODE_3001 ||
+                        code == statusCode.FAILURE_CODE_3002 ||
+                        code == statusCode.FAILURE_CODE_3003 ||
+                        code == statusCode.FAILURE_CODE_3004
+                    ) {
+                        if (url.indexOf("isLogin") == -1 && url.indexOf("openSceneBykey") == -1) {
+                            postQueue.push(function() {
+                                http[method](url, data, done, fail)
+                            })
+                        }
+                    }
+                    fail()
+                })
+
+                if (flag === statusCode.NEXT) {
+                    done(result, result.code == 0)
+                }
+            } else {
+                done(result)
+            }
+        })
+
+        xhr.fail(fail)
+
+        xhr.always(() => (xhr = null))
+
+        return xhr
+    },
+    /**
+     * Get请求
+     * @param {String} url 请求地址
+     * @param {Object?} data 请求参数
+     * @param {Function?} done 成功回调
+     * @param {Function?} fail 失败回调
+     */
+    get(url, data = {}, done, fail) {
+        if (/\.json/.test(url)) {
+            // json文件格式自动调用getJson方法
+            return this.getJson(url, data, done, fail)
+        }
+        return this.__request($.get(url, data), "get", url, data, done, fail)
+    },
+    /**
+     * Get Blob请求
+     * @param {String} url 请求地址
+     * @param {Object?} data 请求参数
+     * @param {Function?} done 成功回调
+     * @param {Function?} fail 失败回调
+     */
+    getText(url, data = {}, done, fail) {
+        return this.__request(
+            $.ajax({
+                url: url,
+                dataType: "text",
+            }),
+            "getText",
+            url,
+            data,
+            done,
+            fail
+        )
+    },
+    /**
+     * GetJson请求 读取json文件数据
+     * @param {String} url 请求地址
+     * @param {Object?} data 请求参数
+     * @param {Function?} done 成功回调
+     * @param {Function?} fail 失败回调
+     */
+    getJson(url, data = {}, done, fail) {
+        return this.__request($.getJSON(url, data), "get", url, data, done, fail)
+    },
+    /**
+     * Get Blob请求
+     * @param {String} url 请求地址
+     * @param {Object?} data 请求参数
+     * @param {Function?} done 成功回调
+     * @param {Function?} fail 失败回调
+     */
+    getBlob(url, data = {}, done, fail) {
+        return this.__request(
+            $.ajax({
+                url: url,
+                dataType: "blob",
+            }),
+            "getBlob",
+            url,
+            data,
+            done,
+            fail
+        )
+    },
+    /**
+     * Get Arraybuffer请求
+     * @param {String} url 请求地址
+     * @param {Object?} data 请求参数
+     * @param {Function?} done 成功回调
+     * @param {Function?} fail 失败回调
+     */
+    getArraybuffer(url, data = {}, done, fail) {
+        return this.__request(
+            $.ajax({
+                url: url,
+                dataType: "arraybuffer",
+            }),
+            "getArraybuffer",
+            url,
+            data,
+            done,
+            fail
+        )
+    },
+    /**
+     * Post 请求
+     * @param {String} url 请求地址
+     * @param {Object?} data 请求参数
+     * @param {Function?} done 成功回调
+     * @param {Function?} fail 失败回调
+     */
+    post(url, data = {}, done, fail) {
+        if (url.indexOf("isLogin") == -1) {
+            http.__loading && $waiting.show()
+        }
+
+        return this.__request($.post(url, data), "post", url, data, done, fail)
+    },
+    /**
+     * PostJson 请求
+     * @param {String} url 请求地址
+     * @param {Object?} data 请求参数
+     * @param {Function?} done 成功回调
+     * @param {Function?} fail 失败回调
+     */
+    postJson(url, data = {}, done, fail) {
+        http.__loading && $waiting.show()
+        return this.__request(
+            $.ajax({
+                type: "POST",
+                url: url,
+                contentType: "application/json",
+                data: JSON.stringify(data),
+            }),
+            "postJson",
+            url,
+            data,
+            done,
+            fail
+        )
+    },
+    /**
+     * Post 表单 支持文件上传
+     * @param {String} url 请求地址
+     * @param {FormData?} formData 请求参数
+     * @param {Function?} done 成功回调
+     * @param {Function?} fail 失败回调
+     */
+    postForm(url, formData, done, fail, onProgress) {
+        if (typeof onProgress === "function") {
+            return this.__request(
+                $.ajax({
+                    type: "POST",
+                    url: url,
+                    processData: false,
+                    contentType: false,
+                    data: formData,
+                    xhr: function() {
+                        const xhr = new XMLHttpRequest()
+                        xhr.upload.addEventListener("progress", function(e) {
+                            onProgress((e.loaded / e.total) * 100 + "%")
+                        })
+                        return xhr
+                    },
+                }),
+                "postForm",
+                url,
+                formData,
+                done,
+                fail
+            )
+        } else {
+            http.__loading && $waiting.show()
+            return this.__request(
+                $.ajax({
+                    type: "POST",
+                    url: url,
+                    processData: false,
+                    contentType: false,
+                    data: formData,
+                }),
+                "postForm",
+                url,
+                formData,
+                done,
+                fail
+            )
+        }
+    },
+    /**
+     * 加载图片
+     * @param {String} url 请求地址
+     * @param {Number?} retry 重试次数,默认为3
+     */
+    loadImage(url, retry = 3) {
+        const def = $.Deferred()
+        const img = new Image()
+
+        /* if (process.env.VUE_APP_REGION == "AWS" && url.indexOf("x-oss-process=image") != -1) {
+            var arr = url.split("?")
+            url = arr[0] + encodeURIComponent("?" + arr[1].replace(/\//g, "@"))
+        } */
+
+        const load = () => {
+            console.warn("Retrying load image: " + url)
+            this.loadImage(url, retry - 1)
+                .done(def.resolve.bind(def))
+                .progress(def.notify.bind(def))
+                .fail(def.reject.bind(def))
+        }
+
+        img.onerror = function() {
+            retry > 0 ? setTimeout(() => load(), 1e3) : def.reject(`[${url}]加载失败`)
+        }
+
+        img.onload = function() {
+            def.resolve(img)
+        }
+
+        img.crossOrigin = "anonymous"
+        img.src = url
+
+        return def
+    },
+    /**
+     * 上传文件
+     * @param {String} url 请求地址
+     * @param {Object?} data 请求参数
+     * @param {Function?} done 成功回调
+     * @param {Function?} fail 失败回调
+     */
+    uploadFile(url, data = {}, done, fail, onProgress) {
+        const form = new FormData()
+        // if (file.needTransfer) { //ie和苹果都不支持dataURLtoFile得传送,所以只能用blob
+        //     form.append("file", common.dataURLtoBlob(file.file), file.name || file.file.name);
+        // } else {
+        //     form.append("file", file.file, file.name || file.file.name);
+        // }
+        for (let key in data) {
+            if (key == "file") {
+                form.append("file", data[key], data.filename || data[key].name)
+            } else if (key != "filename") {
+                form.append(key, data[key])
+            }
+        }
+        return this.postForm(url, form, done, fail, onProgress)
+    },
+    /**
+     * 上传文件
+     * @param {String} url 请求地址
+     * @param {Object?} data 请求参数 {file:'base64 string',filename:'image.jpg',...}
+     * @param {Function?} done 成功回调
+     * @param {Function?} fail 失败回调
+     */
+    uploadBlobFile(url, data = {}, done, fail) {
+        const form = new FormData()
+        for (let key in data) {
+            if (key === "file") {
+                form.append("file", base64ToBlob(data.file), data.filename)
+            } else if (key != "filename") {
+                form.append(key, data[key])
+            }
+        }
+        return this.postForm(url, form, done, fail)
+    },
+}

+ 879 - 0
src/custom/utils/searchRings.js

@@ -0,0 +1,879 @@
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+import math from "./math.js";
+
+var points = [];
+var lines = [];
+var rings = []; 
+ 
+var precision = 0.1 //容错精度 //正常是0.01 但是在编辑时容易出现交错的线看不出来,导致需要getSliceLines 然后多出新增点
+ 
+ 
+var getPoint = function(o, type){
+    var point;
+    if(typeof o == "string" || typeof o == "number")point = points.find(p=> p.ids.includes(o));
+    else{
+        point = points.find(p=> math.closeTo(p.x , o.x, precision) && math.closeTo(p.y , o.y, precision)   ) 
+        if(!point) point = new Point(o.x, o.y,{record:true, id:o.id}, type) 
+        else{ 
+            //console.log('addPoint', point, o)
+            point.addPoint(o.id)
+        }
+    }
+    if(!point){
+        console.log("no point!")
+    }
+    
+    
+    return point
+}
+
+var getLine = function(id){
+    return lines.find(line=> line.ids.includes(id));
+}
+
+
+
+var getAngleInfo = function(points){
+    var info = {}
+    info.angle = points[1].clone().sub(points[0]).angle(); 
+    if(math.closeTo(info.angle, Math.PI*2)){ //如360-0.01  
+        info.angle -= Math.PI*2;    //有可能得到负数-0.001
+    }else if(info.angle > Math.PI || math.closeTo(info.angle, Math.PI)){//如180+-0.01
+        info.angle -= Math.PI;
+        info.reverse = true
+    } 
+    return info  //结果大约是 0 - 3.14
+}
+
+class Point extends THREE.Vector2{
+    constructor(x, y, o={}){
+        super(x, y);
+        
+        if(o.record){   
+            this.id = o.id;
+            if(this.id == void 0) this.id = "add_"+points.length 
+            this.ids =  [this.id] ;//存储拥有该坐标的点原始数据的id
+            points.push(this)  
+        }  
+        
+        this.type = o.type || "" 
+        this.lines = [];
+          
+    }
+    
+    addPoint(id){
+        this.ids.push(id)
+    }
+    
+    searchLineByFactor(dir, type, comeLine){
+        
+        var lines = this.lines.filter(line=>line.searchTime<2)
+        
+        if(lines.length==0)return;
+        else if(lines.length==1)return lines[0];
+        else lines = lines.filter(line=>line!=comeLine)
+        
+        if(lines.length==1)return lines[0];
+        
+        var result; 
+        lines.forEach(line=>{
+            var vec = line.getVector();
+            if(line.points[1] == this) vec.negate();
+            var factor = math.getVec2Angle(dir, vec);
+            
+            if(new THREE.Vector3(dir.x, dir.y, 0).cross(new THREE.Vector3(vec.x, vec.y, 0)).z<0) factor*= -1  /////
+             
+            if(!result){
+                result = {line, factor}
+            } 
+            else{
+                if(type == "min" && factor<result.factor || type == "max" && factor>result.factor) result = {line, factor}
+            }
+        })
+        return result.line;
+    }
+} 
+ 
+var lineLen=0;
+class Line{
+    constructor(o){  
+        if(o.points[0] == o.points[1])return; 
+        this.points = o.points;
+        this.type = o.type || 'line'
+        
+        if(this.type == 'line'){
+            var oldLine = lines.find(line=>line.points.includes(o.points[0]) && line.points.includes(o.points[1]))
+            if(oldLine){
+                o.id != void 0 && oldLine.ids.push(o.id)
+                return oldLine;
+            }
+            this.id = o.id == void 0 ? ("line"+lineLen ++) : o.id
+            this.ids = [this.id]
+           
+            o.dontWriteToPoint || this.points.forEach((point)=>{point.lines.push(this)})
+            o.isChild || lines.push(this);
+            this.searchTime = 0 // 最多两次
+        } 
+         
+        this.children = [];//分割
+        this.parents = [];//分割
+        this.match = [];
+         
+        
+    }
+    
+    
+    getAngleInfo(){ 
+        var angleInfo = getAngleInfo(this.points)
+        this.angle = angleInfo.angle
+        this.reverse = angleInfo.reverse
+    }
+    
+    getIntersectWithLine(line, precision){
+        var joint = line.points.find(point=>this.points.includes(point))
+        if(joint)return {point:joint, type:"joint"};
+        
+        var intersect = math.isLineIntersect( line.points , this.points , false, precision );
+        if(intersect) return {point: intersect, type:"intersect"};
+        
+        
+    } 
+    
+    writeToPoint(){
+        this.points.forEach((point)=>{point.lines.includes(this) || point.lines.push(this)})
+    }
+    
+    checkIfParent(line){ 
+        if(this == line){ 
+            return true;//原因就是slice的点和端点很近  误差导致 
+        }
+        else return this.parents.find(e=>e.checkIfParent(line))
+        
+    }
+    
+    splitByPoint(point){
+        var line1 = new Line({points:[point, this.points[0]],  dontWriteToPoint:true, hasntsure:true})
+        var line2 = new Line({points:[point, this.points[1]],  dontWriteToPoint:true, hasntsure:true})
+        
+        if(!line1.points || !line2.points){//有至少一个是点相同的,没写到group.lines里
+            
+            console.warn('splitByPoint 线有点相同')
+            return;
+        }
+        
+        if(this.checkIfParent(line1)||this.checkIfParent(line2) || line1.checkIfParent(this) || line2.checkIfParent(this)){
+            console.warn("splitByPoint 发现parent和children一样")//,请检查getSliceWalls,尤其 if(math.closeTo(line1.angle,line2.angle)){ 处
+
+            return;
+        }
+        var deal = (line)=>{ 
+            this.children.push(line);
+            line.parents.push(this)  
+             
+            if(!lines.includes(line))lines.push(line)
+            line.writeToPoint()    
+            
+        }
+        deal(line1)
+        deal(line2)
+         
+        var index = this.points[0].lines.indexOf(this);
+        index > -1 && this.points[0].lines.splice(index,1)
+        var index = this.points[1].lines.indexOf(this);
+        index > -1 && this.points[1].lines.splice(index,1)
+         
+        
+        return [line1,line2]
+    }
+    splitByPoints(points){
+        points = points.map(point=>{return {dis:point.distanceTo(this.points[0]), point:point}})
+        points.sort((point1, point2)=>{return point1.dis - point2.dis})
+        var children = [];
+        
+        
+        points.forEach((point, index)=>{
+            var line1 = new Line({points:[point.point, index==0?this.points[0]:points[index-1].point ],group:this.group , dontWriteToPoint:true, hasntsure:true})
+            children.push(line1)  
+        })
+        var line2 = new Line({points:[points[points.length-1].point, this.points[1] ],group:this.group , dontWriteToPoint:true, hasntsure:true})
+        children.push(line2); 
+        
+        
+         
+        var a = children.find(line=> !line.points ||  this.checkIfParent(line) || line.checkIfParent(this))
+        if(a){
+            console.error("splitByPoints  return")
+            return;
+        }
+         
+        
+        children.forEach(line=>{  
+            this.children.push(line);
+            line.parents.push(this)   
+            
+            if(!lines.includes(line))lines.push(line)
+            line.writeToPoint()   
+            line.writeToPoint() 
+        })
+        
+        var index = this.points[0].lines.indexOf(this);
+        index > -1 && this.points[0].lines.splice(index,1)
+        var index = this.points[1].lines.indexOf(this);
+        index > -1 && this.points[1].lines.splice(index,1)
+        
+         
+    }  
+    
+    
+    getAllSlices(){//如果有被分割的片段 就返回片段,否则返回自身
+        var children = [];
+        var traverse = function(elem){
+            if(elem.children.length == 0) children.push(elem) 
+            else elem.children.forEach(traverse)
+        }
+        traverse(this)
+        return children
+    }
+    getVector(){
+        return this.points[1].clone().sub(this.points[0]);
+    }
+    
+    getLength(){
+        return this.points[0].distanceTo(this.points[1])
+    }
+    
+    getCenter(){
+        return this.points[1].clone().add(this.points[0]).multiplyScalar(.5);
+    }
+} 
+var getMixedSet = function(arr1, arr2){//交集
+    return arr1.filter(item=>arr2.includes(item));
+}
+var getUnionSet = function(arr1, arr2){//并集
+    return arr1.concat(arr2.filter(item=>!arr1.includes(item)))
+}
+var getDifferenceSet = function(arr1, arr2){//差集
+    var arr11 = arr1.filter(item=>!arr2.includes(item));
+    var arr22 = arr2.filter(item=>!arr1.includes(item));
+    return arr11.concat(arr22)
+}
+var getDifferenceSetMuti = function(arr){//收集绝对没有重复的元素,也就是判断出现次数=1的
+    var set = [];
+    arr.forEach(arr1=>{
+        arr1.forEach(item=>{
+            var index = set.indexOf(item)
+            if(index>-1){
+                set.splice(index, 1)
+            }else{
+                set.push(item)
+            }
+        })
+    })
+    return set;
+} 
+
+function DoorAtWhichLine(points, lines){
+    var mid = points[0].clone().add(points[1]).multiplyScalar(0.5)
+    lines = lines.filter(line=>math.ifPointAtLineBound(mid, line.points, precision))
+    if(lines.length == 0)return
+    var result = {line:null, dis:Infinity}
+    lines.forEach(line=>{
+        var foot = math.getFootPoint(mid, line.points[0], line.points[1] )
+        var dis = foot.distanceTo(mid)
+        if(dis<result.dis){
+            result.line = line; result.dis = dis
+        }
+    })
+    return result
+    
+}
+
+
+
+
+var ringLen = 0;
+class Ring{
+    constructor(o){
+        this.id = ringLen ++
+        this.type = o.type || 'normal';
+        this.points = o.points;
+        this.lines = o.lines;     
+        rings.push(this); 
+        this.child = []//包含的环
+        this.parent = []//被包含的环
+        this.smallNeibours = []//相邻最小环(存在和它有一个以上的相同边的最小环)
+        
+        var area = math.getArea(this.points);
+        this.area = Math.abs(area)
+        this.isClockwise = area<0//是否逆时针。一般都是逆时针得到的,如果是顺时针,可能是贪吃蛇的情况,可能不是最小环,需要去掉。
+    } 
+    
+}
+var findLine = function(p1,p2){
+    return lines.find(line=>line.points.includes(p1) && line.points.includes(p2)  )
+}
+var ifSamePart = function(checkPart , part){//checkPart中所包含的part片段是否和基准part的顺序一样(逆序也可以, 中间有其他数也可以,起始不同也行。比如 01234和204一样的)
+    var axis, startIndex, newCheckPart=[];
+     
+     
+    for(var j=0,len1 = checkPart.length; j<len1; j++){//将checkPart中比part多的数除去,使两个数组中包含的数完全相同。
+        if(part.indexOf(checkPart[j])>-1)newCheckPart.push(checkPart[j]);
+    }
+
+    for(var i=0,len = part.length; i<len; i++){
+        var index = newCheckPart.indexOf(part[i]);
+        if(index == -1)return false;
+        if(i == 0) startIndex = index;//标记第一个查找点对应的index 
+        else if(i == 1){//标记查找顺序是正还是逆
+            axis = index - startIndex;
+            if(axis == len - 1) axis = -1;//刚好是首和尾
+            else if(axis == 1- len) axis = 1;
+            
+            if(axis != -1 && axis != 1){
+                return false
+            }  
+        }else{//判断是否是按顺序的
+            if(index != ((startIndex+axis * i + len) % len) ) return false;
+        } 
+    }
+    return {sameAxis:axis>0};	//如果一样的话返回正逆是否相同		
+}
+
+//或者判断是否有相同边(但是相同点是可以组成不同环)
+var ifSameRing = function(ring1, ring2){//判断两个环是否相等。 除了可以逆向外顺序要对
+    if(ring1 instanceof Ring)ring1 = ring1.points;
+    if(ring2 instanceof Ring)ring2 = ring2.points;
+    if(ring1.length != ring2.length)return false;
+    if(ring1.lines && ring2.lines){
+        if(getDifferenceSet(ring1.lines , ring2.lines).length == 0)return true;//差集个数为0
+    }else{
+        if(ifSamePart(ring1, ring2))return true 
+    }
+    
+}
+  
+ 
+
+var atWhichChildLine = function(point, line, precision){
+    if(line.children.length == 0){//这里可能要放低精度 保证能找到
+        if(math.ifPointAtLineBound(point, line.points, precision)) return line;
+           
+    }else{
+        for(var i=0;i<line.children.length;i++){
+            var at = atWhichChildLine(point, line.children[i], precision)
+            if(at)return at
+        }
+    }
+}
+
+
+
+function getSliceLines(){
+    var len = lines.length;
+     
+     
+    var deal = function(line1,line2){
+        if(line1 == line2)return;
+        
+        if(line1.angle == void 0) line1.getAngleInfo()
+        if(line2.angle == void 0) line2.getAngleInfo()
+            
+        var intersect = line1.getIntersectWithLine(line2, precision);
+        if(intersect){
+            var point //得到交点
+            if(intersect.type == "intersect"){
+                point = getPoint(intersect.point, "whenGetSliceLines");
+         
+                var line1_ = atWhichChildLine(point, line1)
+                var line2_ = atWhichChildLine(point, line2)
+                //重合的情况还没考虑(平行)
+                if(!line1_) line1_ = atWhichChildLine(point, line1, precision)//降低精度
+                if(!line1_) line1_ = atWhichChildLine(point, line1, precision*2)//降低精度
+                if(!line2_) line2_ = atWhichChildLine(point, line2, precision)
+                if(!line2_) line2_ = atWhichChildLine(point, line2, precision*2)//降低精度
+                //拆分线条:
+                
+                //如果还报错,找不到ChildLine,就直接返回吧  或者搞个循环 逐渐降低精度
+                if(!line1_ || !line2_){
+                    console.warn("atWhichChildLine仍旧找不到 :" + line1.id + ',' + line2.id + ", pointId: "+point.id)
+                    line1_ || console.warn("找不到line1")
+                    line2_ || console.warn("找不到line2")
+                    return;
+                }
+                
+                if(line1_.points.find(p=>p == point) && line2_.points.find(p=>p == point)){//这个点是line1_、 line2_端点,不做处理
+                    //console.log("joint型  "+point.id)
+                }else if(line1_.points.find(p=>p == point)){//T型交叉 
+                    line2_.splitByPoint(point)//加入到母线中,之后还先用母线判断交点
+                    //console.log("T型交叉1 "+point.id)
+                }else if(line2_.points.find(p=>p == point)){//T型交叉 
+                    line1_.splitByPoint(point)
+                    //console.log("T型交叉2 "+point.id)
+                }else{//十字交叉 
+                    line1_.splitByPoint(point)
+                    line2_.splitByPoint(point)
+                }     
+            }else{
+                point = intersect.point//交点是端点 
+                if(math.closeTo(line1.angle,line2.angle)){ //重合一部分
+                    var children1 = line1.getAllSlices()
+                    var children2 = line2.getAllSlices();
+                    if(children1.length>1 || children2.length>1){  //使用最小分割片段来比较
+                        children1.forEach(child1=>{
+                            children2.forEach(child2=>{
+                                deal(child1, child2)
+                            }) 
+                        })
+                        return;
+                    }
+                
+                    var anotherPoint1 = line1.points.find(point_=>point_!=point)
+                    var anotherPoint2 = line2.points.find(point_=>point_!=point)
+                    if(math.ifPointAtLineBound(anotherPoint1, line2.points)){
+                        line2.splitByPoint(anotherPoint1)
+                    }else if(math.ifPointAtLineBound(anotherPoint2, line1.points)){
+                        line1.splitByPoint(anotherPoint2)
+                    }
+                } 
+                
+            }
+            
+        }else if(math.closeTo(line1.angle,line2.angle)){
+            var vec1 = line1.getVector()
+            var vec = line1.points[0].clone().sub(line2.points[0]);
+            var cos = math.getVec2Cos(vec1, vec);
+            if(math.closeTo(cos, -1, 1e-4) || math.closeTo(cos, 1, 1e-4)){ //共线
+            
+                var children1 = line1.getAllSlices()
+                var children2 = line2.getAllSlices();
+                if(children1.length>1 || children2.length>1){  //使用最小分割片段来比较
+                    children1.forEach(child1=>{
+                        children2.forEach(child2=>{
+                            deal(child1, child2)
+                        }) 
+                    })
+                    return;
+                }
+                
+                //判断是否重叠 
+                var A = line1.points[0]; 
+                var C = line1.reverse == line2.reverse ? line2.points[0] : line2.points[1]; 
+                 
+                var B = line1.points[1];
+                var D = line1.reverse == line2.reverse ? line2.points[1] : line2.points[0];
+                 
+                var BC = C.clone().sub(B) 
+                var AD = D.clone().sub(A)
+                if(BC.length()<AD.length()){
+                    var BA = A.clone().sub(B); 
+                    if(math.getVec2Angle(BC, BA) >= 1.57 )return;//没有重叠部分
+                }else{
+                    var AB = B.clone().sub(A)
+                    if(math.getVec2Angle(AD, AB) >= 1.57 )return;
+                }
+            
+            
+            
+                var f = function(line1,line2){
+                    var one = math.ifPointAtLineBound(line1.points[0], line2.points);
+                    var two = math.ifPointAtLineBound(line1.points[1], line2.points);
+                    if(one && two){//line1在line2上
+                        line2.splitByPoints( line1.points )
+                        return true
+                    }else if(one || two){//错开
+                        var point1 = one ? line1.points[0] : line1.points[1];
+                        var anotherPoint1 = one ? line1.points[1] : line1.points[0];
+                        var dis1 = line2.points[0].distanceTo(anotherPoint1);
+                        var dis2 = line2.points[1].distanceTo(anotherPoint1); 
+                        var point2 = dis1 < dis2 ? line2.points[0] : line2.points[1]
+                        line1.splitByPoint(point2) 
+                        line2.splitByPoint(point1)
+                        return true
+                    }
+                }
+                f(line1, line2) || f(line2, line1)
+            }
+        }
+    
+    }
+     
+    for(let i=0;i<len;i++){
+        let line1 = lines[i];
+        for(let j=i+1;j<len;j++){ 
+            let line2 = lines[j];
+            deal(line1,line2)
+           
+        }
+    }
+    
+    //console.log("原有线条个数:"+len)
+    
+    //lines = lines.filter((line)=>{return line.children.length == 0})
+    
+    //console.log("现有线条个数:"+lines.length)
+    
+}
+var bound = new THREE.Box2()
+var build = function(o){
+    //融合了相近点
+    //根据bound 处理precision
+    o.points.forEach(p=>{
+        bound.expandByPoint(new THREE.Vector2(p.x,p.y))
+    })
+     
+    
+    if(o.precision != void 0){
+        precision = o.precision
+    }else{
+        var boundSize = bound.getSize(new THREE.Vector2)
+        precision = THREE.Math.clamp(Math.max(boundSize.x, boundSize.y) / 70, 0.2, 2);
+    }
+    
+    
+    
+    
+    
+    
+    
+    o.points.forEach(point=>getPoint(point))//{x:..,y:..}
+     
+        
+    o.lines.forEach(line=>{ //{p1:id1. p2:id2}
+        new Line({points:[getPoint(line.p1), getPoint(line.p2)], id:line.id })
+    })
+    //注意:不能出现一条线的两个点坐标一致,否则寻路时方向出错。 所以手动融合下相近点。
+}
+  
+ 
+var searchRings = function(o={}){
+    points = [];
+    lines = [];
+    rings = [];
+    lineLen = ringLen = 0
+    o.points = o.points || []
+    o.lines = o.lines || []
+    
+     
+    build(o)
+    
+    if(!o.dontSliceLines){ 
+        getSliceLines()
+    }
+    
+    
+    
+    
+    
+    //查找最小回路:
+    //参考: 引入方向因子的最小回路、最大回路搜索算法.pdf 
+    //方法:  逆时针寻找(标记)最外层大环 -->从走过的点开始逆时针寻找最小环(直到所有可走的路被走过两次)-->逆时针寻找最外层大环(直到所有可走的路被走过两次)-->..
+    //其中找大环时选择方向因子最小的路, 而小环则相反(但只有开始第一条路是一样的, 都是选择最左边的点的因子最小的路)。  
+    //标记方法: 每条线需要被搜索两次才算完毕。搜索完毕的线退出搜索。(依据:搜索完全部最小回路后 , 在无向图中删除搜索过 2 次的边及孤立节点得到退化图 , 恰好构成最大回路。)  
+    var searchTime = 0;
+    var addRingJudgeCount = 0
+    var addRingJudge = function(ring, lines, connectedLines, type){// 处理拣出的片段 
+        addRingJudgeCount++;
+        
+        //console.log("addRingJudge points("+ type+"):"+  ring.map(point=>point.id) )
+        if(o.onlyGetOutRing && type == "small")return
+        
+        if(type == "small" || o.onlyGetOutRing){//挑出回路:
+            var newRings = []
+            while(ring.length){
+                var road = [];
+                var turnBack = false;
+                for(let i=0;i<ring.length;i++){
+                    if(road.includes(ring[i])){//如果走到方才的点,可能形成回路。 无论是不是回路都要摘去这段。
+                        var index = road.indexOf(ring[i])
+                        var pointArr = ring.slice(index, i);
+                        var linesArr = lines.slice(index, i);
+                        ring.splice(index,i-index);
+                        lines.splice(index,i-index);
+                        if(pointArr.length>2){// 如果只有两个数,代表原路返回, 如 1->2(->1) 
+                            if( !rings.find(ring_=>ifSameRing(pointArr, ring_))) newRings.push( new Ring({points: pointArr, lines:linesArr}) )
+                        }
+                        turnBack = true
+                        break;
+                    }else{
+                        road.push(ring[i])
+                        turnBack = false
+                    } 
+                }
+                if(!turnBack){//没有重复的点,那么就直接处理整条。
+                    if(ring.length>2){// 如果只有两个数,代表原路返回, 如 1->2(->1) 
+                        if( !rings.find(ring_=>ifSameRing(ring, ring_))) newRings.push( new Ring({points: ring, lines}) )
+                    }
+                    break;
+                }
+            } 
+            
+            if(type != 'small'){
+                newRings.forEach(e=>e.isOutRing = true)
+            }
+            
+            
+            //console.log(newRings)
+        }else{
+            return ring
+        }
+    }
+
+     
+   
+
+    var search = function(point2d, comeRoad, type, connectedLines){
+        searchTime++
+        var goLine; 
+        var direction;
+        if(type.includes("big")){  
+            if(!comeRoad){
+                if(type.includes("Left")){//逆时针
+                    direction = new THREE.Vector2(1,0);
+                }else{
+                    direction = new THREE.Vector2(-1,0);
+                }
+                goLine = point2d.searchLineByFactor(direction,"min") 
+            }else{
+                var lastPoint = comeRoad.points[comeRoad.points.length-1]
+                direction = point2d.clone().sub(lastPoint);
+                goLine = point2d.searchLineByFactor(direction,"min", findLine(point2d, lastPoint)) 
+            }
+            
+        }else{ 
+            if(!comeRoad){
+                //似乎找最小环时,第一条线也是找最小的因子,这样才能保证逆时针(除非只有顺时针一条路)
+                direction = new THREE.Vector2(1,0);
+                goLine = point2d.searchLineByFactor(direction,"min")  
+                
+            }else{
+                var lastPoint = comeRoad.points[comeRoad.points.length-1]
+                direction = point2d.clone().sub(lastPoint);
+                goLine = point2d.searchLineByFactor(direction,"max", findLine(point2d, lastPoint))
+            } 
+        }
+        if(!goLine)return 
+        
+        
+        goLine.searchTime++;
+        connectedLines.includes(goLine) || connectedLines.push(goLine)
+         
+             
+        var nextPoint = goLine.points.find( point => point2d!=point )
+        
+        //if( comeRoad && comeRoad.points[comeRoad.points.length - 1] == nextPoint ) return;//不能查找来时的方向(反方向)
+        //走不通就原路返回
+        
+        var roadPoints = comeRoad ? comeRoad.points.concat([point2d]) : [point2d];//每个分叉都能构成一条新的road
+        var roadLines = comeRoad ?  comeRoad.lines.concat([goLine]) : [goLine];
+        
+        
+        
+        
+        //走到第一个点就算停止,这时候可能得到一个环、或者一段走了两遍的线、或者一条线上带了些环。 
+        if(nextPoint == roadPoints[0]) return addRingJudge(roadPoints, roadLines, connectedLines, type) //形成环 
+        else{ 
+            /* var len = roadPoints.indexOf(nextPoint);
+            if( len > -1){ //走到走过的路的某一点 构成这段路的回路
+                var points = roadPoints.slice(len, roadPoints.length);
+                var lines = roadLines.slice(len, roadPoints.length);
+                addRingJudge(points, lines)    
+            }else{ */
+                return search(nextPoint,  {lines:roadLines, points:roadPoints}, type, connectedLines);//继续寻路
+            //}  
+        }
+         
+    }
+     
+
+    while(1){//搜寻一次大环
+        var connectedLines = [];//被搜寻过的且searchTime<2的线。一旦全部搜完就说明该连通区域搜寻完毕,继续查下一个连通区域。
+        var startPoint = null
+        points.forEach(point=>{//找出x最小的点
+            if(!point.lines.find(line=>line.searchTime<2))return;
+            if(!startPoint)startPoint = point
+            else if(point.x < startPoint.x)startPoint = point;
+        })
+        if(!startPoint)break; //说明全部找完
+        
+        
+        var ring = search(startPoint, null, "bigLeft", connectedLines)//逆时针  
+        //search(startPoint, null, "bigRight", connectedLines);//顺时针(为了防止最外层不是回路之前写了顺时针,但如果是回路就会走重复。后来发现只要逆时针即可,因为走完后剩下的可以再次找大环)
+        
+        connectedLines = connectedLines.filter(line=>line.searchTime<2)
+        
+        
+        
+        
+        while(connectedLines.length>0){//目标是顺着connectedLines把所有连通的小环都找到
+            
+            let points_ = [];//connectedLines中所有的点
+            connectedLines.forEach(line=>line.points.forEach(point=>{if(!points_.includes(point))points_.push(point) }))
+            var startPoint = null
+            points_.forEach(point=>{//找出x最小的点
+                if(!point.lines.find(line=>line.searchTime<2))return;
+                if(!startPoint)startPoint = point
+                else if(point.x < startPoint.x)startPoint = point;
+            })
+            if(!startPoint)break;
+            
+             
+            search(startPoint, null, "small", connectedLines)
+            
+            connectedLines = connectedLines.filter(line=>line.searchTime<2)
+        }   
+    } 
+    
+    
+    
+    /* if(o.onlyGetOutRing){
+        rings = rings.filter(e=>e.isOutRing)   
+    } */
+    
+    //console.log("searchTime "+searchTime + ", addRingJudgeCount " +addRingJudgeCount)
+    
+    
+   
+    
+    //找出所有的相邻关系,包括公共边 
+    var len = rings.length;
+    for(let i=0; i<len; i++){
+        let ring1 = rings[i] 
+        for(let j=i+1; j<len; j++){
+            let ring2 = rings[j]
+            var bothHasLines = getMixedSet(ring1.lines, ring2.lines) 
+            if(bothHasLines.length){//ring1oíring2?àáú
+                ring1.smallNeibours.push(ring2)
+                ring2.smallNeibours.push(ring1) 
+            }else{ 
+            }  
+        }
+    }
+    rings.forEach(ring1=>{
+        for(let i=0; i<len; i++){
+            var ring2 = rings[i];
+            if(ring1 == ring2 || ring1.smallNeibours.includes(ring2))continue;
+            
+            let inside
+            for(let u=0;u<ring1.points.length;u++){
+                inside = math.isPointInArea(ring2.points, null, ring1.points[u]);
+                if(!inside)break
+                else if(inside && !inside.atLine){
+                    break
+                }
+            } 
+             
+            
+            if(inside){  //只要其中一个点在ring2内,就说明ring1是内环          
+                if(inside.atLine){//(还是会存在点全在线上的情况,这时候判断中心点)  
+                    var center = math.getCenterOfGravityPoint(ring1.points)
+                    let inside1 = math.isPointInArea(ring2.points, null, center);
+                    if(!inside1){
+                        continue
+                    }
+                }
+                
+                ring2.child.push(ring1);
+                ring1.parent.push(ring2)
+            }
+        }
+    })
+    //去除非最小的ring  是否应该检测parent child?
+     /* 
+        like this: 
+        |———————————————————————|
+        |———|———————|———————|   |
+        |   |       |       |   |
+        |   |———————|———————|   |
+        |———————————————————————|
+    */
+    var wiseRings = rings.filter(r=>!r.isClockwise)//一般都是逆时针得到的,如果是顺时针,可能是贪吃蛇的情况,可能不是最小环,需要去掉。 
+    if(wiseRings.length > 0){
+        //console.log('%c存在非最小的ring! 进行处理:',"color:#00f"); 
+        wiseRings.forEach(ring=>{ //(此案例验证出smallNeibours就是它的最小构成,可以再看看别的案例)
+            if(ring.smallNeibours.length>0){//另:如果内部只有一个,说明它是最小环,不需要处理
+                var is = false
+                var difference = getDifferenceSet(ring.lines , getDifferenceSetMuti(ring.smallNeibours.concat(ring.child).map(ring=>ring.lines)))//获取所有smallNeibours和child的边中没有重复过的边(就是outline) 和该ring的线比较
+                 
+                
+                is = difference.every(line=> ring.child.find(r=>r.lines.includes(line))  )  //多出的线只能是child中的线
+                
+                if(is){
+                    console.log('%c删除非最小环 ring'+ring.id,"color:#00f");
+                    console.log(ring)
+                    rings.splice(rings.indexOf(ring), 1) 
+                    ring.child.forEach(c=>{var index = c.parent.indexOf(ring);index>-1 && c.parent.splice(index,1)})
+                    ring.parent.forEach(c=>{var index = c.child.indexOf(ring);index>-1 && c.child.splice(index,1)})
+                    ring.smallNeibours.forEach(c=>{var index = c.smallNeibours.indexOf(ring);index>-1 && c.smallNeibours.splice(index,1)})
+                }
+                
+            }
+            
+        })
+    }
+    
+    
+    
+    /* rings = rings.filter(ring=>{ 
+        rings = rings.filter(ring=>{ 
+            var enoughSize = ring.area > 0.5
+            if(!enoughSize){console.log('因面积过小去除ring '+ring.id + " , area: "+ring.area)}
+            return enoughSize
+        }) 
+        rings.forEach(ring=>{
+            if(ring.closetChilds){
+                ring.closetChilds = ring.closetChilds.filter(e=>rings.includes(e))
+            }
+        })
+        
+        return rings
+        
+    })  */ //在dealRings前不能随意删除rings,因为判断是否是最小环时需要全部的环  
+    
+    rings.forEach(ring=>{ //这里和cad中的不太一样, cad中双数个parent算外环,单数内环; 这里不分内外, 只看有无parent child
+        if(ring.parent.length){
+            ring.closetParent = ring.parent.find(ring_ => ring_.parent.length == ring.parent.length - 1)//最近一层的大环就是比它的parent个数少一的
+            ring.closetParent.closetChilds || (ring.closetParent.closetChilds = [])//内环可能多个
+            ring.closetParent.closetChilds.push(ring)
+        } 
+    })
+    
+    
+    
+    //console.log(rings)
+    
+    
+    
+   
+    
+    var _ring = rings.map(ring=>{ 
+        var data = {
+            id: ring.id,  
+            points: ring.points.map(point=>{return {id: point.ids[0], x:point.x, y:point.y}}),
+            /* doors : o.doors.filter(door=>{
+                if(ring.closetChilds){
+                    var childOutLines = getDifferenceSetMuti(ring.closetChilds.map(ring=>ring.lines)) //最近子环的外边
+                    return ring.lines.concat(childOutLines).includes(door.atLine)   
+                }else{
+                    return ring.lines.includes(door.atLine) 
+                } 
+            }), */ 
+            area:ring.area,
+            closetParent : ring.closetParent && ring.closetParent.id,
+            closetChilds : ring.closetChilds && ring.closetChilds.map(e=>e.id) 
+        } 
+        
+        return data
+    })
+        
+    //console.log(JSON.stringify(_ring))
+
+    return _ring
+    
+    
+    
+    
+    
+}
+
+ 
+export default searchRings

+ 486 - 0
src/custom/utils/transitions.js

@@ -0,0 +1,486 @@
+ 
+var easing = {};
+//渐变曲线函数,反应加速度的变化
+
+
+//currentTime:x轴当前时间(从0-到duration), startY:起始点, duration:总时长, wholeY:路程 (即endY-startY)
+//参数基本是 x, 0, 1, 1 
+
+
+/* 
+easeOut 基本是y= m * (x-dur)^k + n, 若k为偶数,m<0, 若k为奇数,m>0;  (因为偶数的话必须开口向下才能获得斜率递减的递增的那段,而奇数是对称的,单调递增. )
+根据x=0时y=0, x=dur时y=S , 得 n = S,m = -S/(-dur)^k
+
+*/
+ 
+easing.getEaseOut = function(k){// k 是>=2的整数. 越大变化率越大, 相同初始速度所需要时间越久 
+    let easeFun
+    k = Math.round(k)
+     
+    if(k<2){   
+        k = Math.PI / 2  
+        easeFun = easing.easeOutSine  
+    }else{
+        easeFun = function(currentTime, startY, wholeY, duration) {  
+            if(k>2){ 
+                console.log(k) 
+            }
+           return -wholeY/Math.pow(-duration, k) * Math.pow(currentTime-duration,  k) + wholeY 
+        }  
+    }
+      
+    return {
+        k,
+        easeFun  
+    }  
+}
+
+
+
+
+
+
+
+
+
+
+easing.linearTween = function(currentTime, startY, wholeY, duration) {
+    return wholeY * currentTime / duration + startY
+}
+,
+easing.easeInQuad = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration,
+    wholeY * currentTime * currentTime + startY
+}
+,
+easing.easeOutQuad = function(currentTime, startY, wholeY, duration) { // 如套上实际的距离S和时长dur, y = - S / dur *(x^2-2x) 当s为1,dur为1时,是  y = -(x-1)^2 + 1 , 在0-1中是斜率递减的递增函数.     导数- S / dur *(2x-2 )   可求出实时速度  故在0这一时刻,速度为 2S/dur  
+    return currentTime /= duration,
+    -wholeY * currentTime * (currentTime - 2) + startY
+}
+,
+easing.easeInOutQuad = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration / 2,
+    currentTime < 1 ? wholeY / 2 * currentTime * currentTime + startY : (currentTime--,
+    -wholeY / 2 * (currentTime * (currentTime - 2) - 1) + startY)
+}
+,
+easing.easeInCubic = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration,
+    wholeY * currentTime * currentTime * currentTime + startY
+}
+,
+easing.easeOutCubic = function(currentTime, startY, wholeY, duration) {// y = S / dur^3 *(x-dur)^3 + S,对称中心是(dur,S),从0-dur是 斜率递减的递增函数,导数为3S/dur^3 * (x-dur)^2, 0时速度为3S/dur
+    return currentTime /= duration,
+    currentTime--,
+    wholeY * (currentTime * currentTime * currentTime + 1) + startY
+}
+,
+easing.easeInOutCubic = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration / 2,
+    currentTime < 1 ? wholeY / 2 * currentTime * currentTime * currentTime + startY : (currentTime -= 2,
+    wholeY / 2 * (currentTime * currentTime * currentTime + 2) + startY)
+}
+,
+easing.easeInQuart = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration,
+    wholeY * currentTime * currentTime * currentTime * currentTime + startY
+}
+,
+easing.easeOutQuart = function(currentTime, startY, wholeY, duration) {//根据上面的计算,估计0时速度应该是4S/dur吧…… 
+    return currentTime /= duration,
+    currentTime--,
+    -wholeY * (currentTime * currentTime * currentTime * currentTime - 1) + startY
+}
+,
+easing.easeInOutQuart = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration / 2,
+    currentTime < 1 ? wholeY / 2 * currentTime * currentTime * currentTime * currentTime + startY : (currentTime -= 2,
+    -wholeY / 2 * (currentTime * currentTime * currentTime * currentTime - 2) + startY)
+}
+,
+easing.easeInQuint = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration,
+    wholeY * currentTime * currentTime * currentTime * currentTime * currentTime + startY
+}
+,
+easing.easeOutQuint = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration,
+    currentTime--,
+    wholeY * (currentTime * currentTime * currentTime * currentTime * currentTime + 1) + startY
+}
+,
+easing.easeInOutQuint = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration / 2,
+    currentTime < 1 ? wholeY / 2 * currentTime * currentTime * currentTime * currentTime * currentTime + startY : (currentTime -= 2,
+    wholeY / 2 * (currentTime * currentTime * currentTime * currentTime * currentTime + 2) + startY)
+}
+,
+easing.easeInSine = function(currentTime, startY, wholeY, duration) {
+    return -wholeY * Math.cos(currentTime / duration * (Math.PI / 2)) + wholeY + startY
+}
+,
+easing.easeOutSine = function(currentTime, startY, wholeY, duration) {// y' = S * PI / 2 / dur * cos(PI/2/dur * x)
+    console.log('easeOutSine')
+    return wholeY * Math.sin(currentTime / duration * (Math.PI / 2)) + startY
+}
+,
+easing.easeInOutSine = function(currentTime, startY, wholeY, duration) {
+    return -wholeY / 2 * (Math.cos(Math.PI * currentTime / duration) - 1) + startY
+}
+,
+easing.easeInExpo = function(currentTime, startY, wholeY, duration) {
+    return wholeY * Math.pow(2, 10 * (currentTime / duration - 1)) + startY
+}
+,
+easing.easeOutExpo = function(currentTime, startY, wholeY, duration) {
+    return wholeY * (-Math.pow(2, -10 * currentTime / duration) + 1) + startY
+}
+,
+easing.easeInOutExpo = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration / 2,
+    currentTime < 1 ? wholeY / 2 * Math.pow(2, 10 * (currentTime - 1)) + startY : (currentTime--,
+    wholeY / 2 * (-Math.pow(2, -10 * currentTime) + 2) + startY)
+}
+,
+easing.easeInCirc = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration,
+    -wholeY * (Math.sqrt(1 - currentTime * currentTime) - 1) + startY
+}
+,
+easing.easeOutCirc = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration,
+    currentTime--,
+    wholeY * Math.sqrt(1 - currentTime * currentTime) + startY
+}
+,
+easing.easeInOutCirc = function(currentTime, startY, wholeY, duration) {
+    return currentTime /= duration / 2,
+    currentTime < 1 ? -wholeY / 2 * (Math.sqrt(1 - currentTime * currentTime) - 1) + startY : (currentTime -= 2,
+    wholeY / 2 * (Math.sqrt(1 - currentTime * currentTime) + 1) + startY)
+}
+,
+easing.easeInElastic = function(currentTime, startY, wholeY, duration) {
+    var r = 1.70158
+      , o = 0
+      , a = wholeY;
+    return 0 === currentTime ? startY : 1 === (currentTime /= duration) ? startY + wholeY : (o || (o = .3 * duration),
+    a < Math.abs(wholeY) ? (a = wholeY,
+    r = o / 4) : r = o / (2 * Math.PI) * Math.asin(wholeY / a),
+    -(a * Math.pow(2, 10 * (currentTime -= 1)) * Math.sin((currentTime * duration - r) * (2 * Math.PI) / o)) + startY)
+}
+,
+easing.easeOutElastic = function(currentTime, startY, wholeY, duration) {
+    var r = 1.70158
+      , o = 0
+      , a = wholeY;
+    return 0 === currentTime ? startY : 1 === (currentTime /= duration) ? startY + wholeY : (o || (o = .3 * duration),
+    a < Math.abs(wholeY) ? (a = wholeY,
+    r = o / 4) : r = o / (2 * Math.PI) * Math.asin(wholeY / a),
+    a * Math.pow(2, -10 * currentTime) * Math.sin((currentTime * duration - r) * (2 * Math.PI) / o) + wholeY + startY)
+}
+,
+easing.easeInOutElastic = function(currentTime, startY, wholeY, duration) {
+    var r = 1.70158
+      , o = 0
+      , a = wholeY;
+    return 0 === currentTime ? startY : 2 === (currentTime /= duration / 2) ? startY + wholeY : (o || (o = duration * (.3 * 1.5)),
+    a < Math.abs(wholeY) ? (a = wholeY,
+    r = o / 4) : r = o / (2 * Math.PI) * Math.asin(wholeY / a),
+    currentTime < 1 ? -.5 * (a * Math.pow(2, 10 * (currentTime -= 1)) * Math.sin((currentTime * duration - r) * (2 * Math.PI) / o)) + startY : a * Math.pow(2, -10 * (currentTime -= 1)) * Math.sin((currentTime * duration - r) * (2 * Math.PI) / o) * .5 + wholeY + startY)
+}
+,
+easing.easeInBack = function(currentTime, startY, wholeY, duration, r) {
+    return void 0 === r && (r = 1.70158),
+    wholeY * (currentTime /= duration) * currentTime * ((r + 1) * currentTime - r) + startY
+}
+,
+easing.easeOutBack = function(currentTime, startY, wholeY, duration, r) {
+    return void 0 === r && (r = 1.70158),
+    wholeY * ((currentTime = currentTime / duration - 1) * currentTime * ((r + 1) * currentTime + r) + 1) + startY
+}
+,
+easing.easeInOutBack = function(currentTime, startY, wholeY, duration, r) {
+    return void 0 === r && (r = 1.70158),
+    (currentTime /= duration / 2) < 1 ? wholeY / 2 * (currentTime * currentTime * (((r *= 1.525) + 1) * currentTime - r)) + startY : wholeY / 2 * ((currentTime -= 2) * currentTime * (((r *= 1.525) + 1) * currentTime + r) + 2) + startY
+}
+,
+easing.easeOutBounce = function(currentTime, startY, wholeY, duration) {
+    return (currentTime /= duration) < 1 / 2.75 ? wholeY * (7.5625 * currentTime * currentTime) + startY : currentTime < 2 / 2.75 ? wholeY * (7.5625 * (currentTime -= 1.5 / 2.75) * currentTime + .75) + startY : currentTime < 2.5 / 2.75 ? wholeY * (7.5625 * (currentTime -= 2.25 / 2.75) * currentTime + .9375) + startY : wholeY * (7.5625 * (currentTime -= 2.625 / 2.75) * currentTime + .984375) + startY
+}
+,
+easing.easeInBounce = function(currentTime, startY, wholeY, r) {
+    return wholeY - easing.easeOutBounce(r - currentTime, 0, wholeY, r) + startY
+}
+,
+easing.easeInOutBounce = function(currentTime, startY, wholeY, r) {
+    return currentTime < r / 2 ? .5 * easing.easeInBounce(2 * currentTime, 0, wholeY, r) + startY : .5 * easing.easeOutBounce(x, 2 * currentTime - r, 0, wholeY, r) + .5 * wholeY + startY
+}
+
+ 
+
+
+
+
+
+
+
+
+
+
+
+
+var lerp = {
+	/* vector: function(currentTime, startY, f) {//xzw change, add f
+		var wholeY = currentTime.clone();
+		return startY = startY.clone(),
+		function(duration) {
+			currentTime.set(wholeY.x * (1 - duration) + startY.x * duration, wholeY.y * (1 - duration) + startY.y * duration, wholeY.z * (1 - duration) + startY.z * duration)
+			f && f(currentTime,duration);
+		}
+	},
+    quaternion: function(currentTime, startY, f) {//xzw change, add f
+        var wholeY = currentTime.clone();
+        return function(duration) {
+            currentTime.copy(wholeY).slerp(startY, duration);
+			f && f(currentTime,duration);
+        }
+    },
+    property: function(currentTime, startY, wholeY, duration) {
+        var r = currentTime[startY];
+        return function(o) {
+            currentTime[startY] = r * (1 - o) + wholeY * o,
+            duration && duration(currentTime[startY])
+        }
+    },
+    uniform: function(currentTime, startY, wholeY) {
+        var duration = currentTime.material.uniforms[startY].value;
+        return function(r) {
+            try{
+                currentTime.material.uniforms[startY] && (currentTime.material.uniforms[startY].value = duration * (1 - r) + wholeY * r)
+            }catch(currentTime){
+                console.log(1)
+            }
+            
+        }
+    },
+    matrix4: function(currentTime, startY) {
+        var wholeY = currentTime.clone();
+        return function(duration) {
+            for (var r = currentTime.elements, o = wholeY.elements, a = startY.elements, s = 0; s < 16; s++)
+                r[s] = o[s] * (1 - duration) + a[s] * duration
+        }
+    },
+    allUniforms: function(currentTime, startY, wholeY) {
+        var duration = currentTime.map(function(currentTime) {
+            return this.uniform(currentTime, startY, wholeY)
+        }
+        .bind(this));
+        return function(currentTime) {
+            duration.forEach(function(startY) {
+                startY(currentTime)
+            })
+        }
+    } */
+    
+    
+    
+    vector: function(t, i, f) {//xzw change, add f
+        var n = t.clone();
+        return i = i.clone(),
+        function(e) {
+            t.set(n.x * (1 - e) + i.x * e, n.y * (1 - e) + i.y * e, n.z * (1 - e) + i.z * e)
+            f && f(t,e);
+        }
+    },
+    quaternion: function(t, i, f) {//xzw change, add f
+        var n = t.clone();
+        return function(e) {
+            t.copy(n).slerp(i, e)
+            f && f(t,e);
+        }
+    },
+    property: function(t, i, n, r) {
+        var o = t[i];
+        return function(e) {
+            t[i] = o * (1 - e) + n * e,
+            r && r(t[i])     
+        }
+    },
+    uniform: function(t, i, n) {
+        var r = t.material.uniforms[i].value;
+        return function(e) {
+            t.material.uniforms[i] && (t.material.uniforms[i].value = r * (1 - e) + n * e)
+        }
+    },
+    matrix4: function(o, a) {
+        var s = o.clone();
+        return function(e) {
+            for (var t = o.elements, i = s.elements, n = a.elements, r = 0; r < 16; r++)
+                t[r] = i[r] * (1 - e) + n[r] * e
+        }
+    },
+    allUniforms: function(e, t, i) {
+        var n = e.map(function(e) {
+            return this.uniform(e, t, i)
+        }
+        .bind(this));
+        return function(t) {
+            n.forEach(function(e) {
+                e(t)
+            })
+        }
+    }
+};
+ 
+
+
+
+
+/* 
+    渐变
+    
+
+ */
+
+var transitions = {
+    globalDone: null,
+    funcs: [],
+    counter: 0,
+    uniqueID: 0,
+    start: function(func, duration, done, delay, ease, name, id, cancelFun) {
+        return delay = delay || 0,
+        this.funcs.push({
+            func: func,
+            current: -delay * Math.abs(duration),                      //当前时间
+            duration: (1 - Math.max(delay, 0)) * Math.abs(duration),   //总时长
+            done: done,
+            easing: ease || easing.linearTween,                //渐变曲线
+            cycling: duration < 0,
+            running: !0,
+            debug: delay < 0,
+            name: name || "T" + this.counter,
+            id: void 0 === id ? this.counter : id,
+            paused: !1,
+			cancelFun : cancelFun,   //取消时执行的函数
+        }),
+        func(0, 16),
+        this.counter += 1,
+        func
+    },
+    trigger: function(e) {
+        var t = void 0 === e.delayRatio ? 0 : e.delayRatio
+            , u = e.func || function() {}
+            , r = void 0 === e.duration ? 0 : e.duration;
+        void 0 !== e.cycling && e.cycling && (r = -Math.abs(r));
+        var o = e.done || null
+            , a = e.easing || easing.linearTween
+            , s = e.name || "R" + this.counter
+            , l = void 0 === e.id ? this.counter : e.id;
+        return this.start(u, r, o, t, a, s, l)
+    },
+    setTimeout: function(e, t, u) {
+        var duration = void 0 === u ? this.counter : u;
+        return this.trigger({
+            done: e,
+            duration: void 0 === t ? 0 : t,
+            name: "O" + this.counter,
+            id: duration
+        })
+    },
+    pause: function() {
+        this.paused = !0
+    },
+    resume: function() {
+        this.paused = !1
+    },
+    update: function(e) {
+        this.funcs.forEach(function(t) {
+            if (!(t.paused || (t.current += 1e3 * e,
+            t.current < 0)))
+                if (t.current >= t.duration && !t.cycling) {
+                    var u = t.easing(1, 0, 1, 1);
+                    t.func(u, 1e3 * e),
+                    t.done && t.done(),
+                    t.running = !1
+                } else {
+                    var duration = t.easing(t.current % t.duration / t.duration, 0, 1, 1)
+                        , r = t.func(duration, 1e3 * e) || !1;
+                    r && (t.done && t.done(),
+                    t.running = !1)
+                }
+        });
+        var t = this.funcs.length;
+        this.funcs = this.funcs.filter(function(e) {
+            return e.running
+        });
+        var u = this.funcs.length;
+        if (t > 0 && 0 === u && this.globalDone) {
+            var duration = this.globalDone;
+            this.globalDone = null,
+            duration()
+        }
+    },
+    adjustSpeed: function(e, t) {
+        for (var u = this.getById(e), duration = 0; duration < u.length; duration++) {
+            var r = u[duration];
+            r.duration /= t,
+            r.current /= t
+        }
+    },
+    getById: function(e) {
+        return this.funcs.filter(function(t) {
+            return e === t.id
+        })
+    },
+    get: function(e) {
+        for (var t = 0; t < this.funcs.length; t += 1)
+            if (this.funcs[t].func === e)
+                return this.funcs[t];
+        return null
+    },
+    isRunning: function(e) {
+        var t = this.get(e);
+        return null !== t && t.running
+    },
+    countActive: function() {
+        for (var e = 0, t = 0; t < this.funcs.length; t += 1)
+            e += this.funcs[t].running;
+        return e
+    },
+    listActive: function() {
+        for (var e = [], t = 0; t < this.funcs.length; t += 1)
+            this.funcs[t].running && e.push(this.funcs[t].name);
+        return e
+    },
+    done: function(e) {
+        this.globalDone = e
+    },
+    cancelById: function(e, dealCancelFun) { //xzw add dealDone
+        var t = void 0 === e ? 0 : e;
+		let cancels = []
+        this.funcs = this.funcs.filter(function(e) {
+			var is = e.id == t;
+			
+			if(is && dealCancelFun){
+                e.cancelFun && cancels.push(e.cancelFun)
+				//e.cancelFun && e.cancelFun()
+			} 
+            return !is
+        })
+        
+        cancels.forEach(e=>{e()}) //先从funcs中去除后再执行
+        
+        
+    },
+    cancel: function(e) {
+        this.funcs = this.funcs.filter(function(t) {
+            return t.func !== e
+        })
+    },
+    getUniqueId: function() {
+        return this.uniqueID -= 1,
+        this.uniqueID
+    } 
+};
+
+export    {transitions, lerp, easing}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 5160 - 0
src/custom/viewer/ExtendViewer - 原始.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 3796 - 0
src/custom/viewer/ExtendViewer.js


+ 91 - 0
src/custom/viewer/Viewport.js

@@ -0,0 +1,91 @@
+
+
+
+import * as THREE from "../../../libs/three.js/build/three.module.js";
+import Common from '../utils/Common.js'
+
+export default class Viewport{
+    
+    constructor( view, camera, prop={}){//目前不支持换camera
+        
+        this.left = prop.left;
+        this.bottom = prop.bottom;
+        this.width = prop.width;
+        this.height = prop.height;
+        this.name = prop.name 
+        this.view = view
+        this.camera = camera 
+        this.active = true 
+        this.unableChangePos = false
+        this.noPointcloud;
+        //this.keys = [...] firstPersonCtl....
+        this.resolution = new THREE.Vector2;
+        this.resolution2 = new THREE.Vector2;
+        this.offset = new THREE.Vector2; //viewportOffset 范围从0-整个画布的像素
+        this.extraEnableLayers = prop.extraEnableLayers || [];//额外可展示的层
+        this.cameraLayers = prop.cameraLayers 
+        this.pixelRatio = prop.pixelRatio  //如果规定pixelRatio的话要传,这样就覆盖devicePicelRatio, 如magnifier
+    }
+     
+    
+    clone(){ 
+        return Common.CloneClassObject(this)
+          
+    }
+    
+    getMoveSpeed(){
+        return this.moveSpeed
+    }
+    setMoveSpeed(e){
+        this.moveSpeed = e
+    }
+    
+    layersAdd(name){
+        this.extraEnableLayers.includes(name) || this.extraEnableLayers.push(name)
+        
+    }
+    layersRemove(name){
+        let index = this.extraEnableLayers.indexOf(name)
+        if(index > -1){
+            this.extraEnableLayers.splice(index, 1)
+        }
+    }
+
+
+    
+	cameraChanged() {
+		var copy = ()=>{
+            this.previousState = {
+                projectionMatrix: this.camera.projectionMatrix.clone(),//worldMatrix在this.control时归零了所以不用了吧,用position和qua也一样
+                position: this.camera.position.clone(),
+                quaternion: this.camera.quaternion.clone()
+                
+            }; 
+        }
+        let projectionChanged = true
+        let positionChanged = true
+        let quaternionChanged = true
+        let getChanged = ()=>{
+            return {
+                projectionChanged,positionChanged,quaternionChanged, 
+                changed:projectionChanged || positionChanged || quaternionChanged
+            }
+        }
+        if (this.previousState){ 
+            projectionChanged = !this.camera.projectionMatrix.equals(this.previousState.projectionMatrix) 
+            positionChanged = !this.camera.position.equals(this.previousState.position)  
+            quaternionChanged = !this.camera.quaternion.equals(this.previousState.quaternion) 
+        }   
+        copy() 
+        
+        return getChanged()
+	}
+    
+    setResolution(w,h, wholeW=0, wholeH=0){
+        this.resolution.set(w,h);//是client的width height
+        
+        this.resolution2.copy(this.resolution).multiplyScalar(this.pixelRatio || window.devicePixelRatio)
+         
+        this.offset.set(wholeW,wholeH).multiply(new THREE.Vector2(this.left,this.bottom)).multiplyScalar(window.devicePixelRatio) 
+    }
+}

+ 777 - 0
src/custom/viewer/map/Map.js

@@ -0,0 +1,777 @@
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+ 
+
+
+let texLoader = new THREE.TextureLoader() 
+    texLoader.crossOrigin = "anonymous" 
+
+
+let createErrorMaterial = function() {
+   var t = new THREE.MeshBasicMaterial({
+       transparent: !0,
+       depthWrite: !1,
+       depthTest: !0,
+       opacity: 1,
+       side: THREE.DoubleSide
+   });
+   return t.color = new THREE.Color(3355443),
+   t
+}
+let tempVector = new THREE.Vector3,  //sharedata
+    face1 = new THREE.Face3(0,1,2),
+    face2 = new THREE.Face3(2,3,0),
+    errorMaterial = createErrorMaterial(),
+    uv00 = new THREE.Vector2(0,0),
+    uv01 = new THREE.Vector2(0,1),
+    uv10 = new THREE.Vector2(1,0),
+    uv11 = new THREE.Vector2(1,1),
+    face1UV = [uv00, uv10, uv11],
+    face2UV = [uv11, uv01, uv00]
+
+
+
+const HALF_WORLD_SIZE = 21e6 
+const MAX_VERTICAL_DIST = 2 
+const MAX_VERTICAL_DIST_TO_BEST = 1  
+ 
+
+
+
+
+//高德坐标拾取工具 : https://lbs.amap.com/tools/picker
+
+
+export class MapLayer extends THREE.EventDispatcher{ // 包括了 MapLayerBase SceneLayer
+    constructor(viewer_, viewport){
+        super()
+        this.sceneGroup = new THREE.Object3D;
+        this.sceneGroup.name = "MapLayer" 
+         
+        
+        this.loadingInProgress = 0
+        this.maps = [] 
+        this.frustum = new THREE.Frustum 
+        this.frustumMatrix = new THREE.Matrix4 
+        this.tileColor = /* i && i.tileColor ? i.tileColor :  */new THREE.Color(16777215)
+        this.viewport = viewport
+        this.changeViewer(viewer_) 
+        //添加地图
+        var map = new TiledMapOpenStreetMap(this, this.tileColor )
+        this.addMap(map) 
+        
+
+        //map.setEnable(false)
+        
+        
+        
+        
+        /* this.on('needUpdate',()=>{ 
+            this.mapLayer.update()
+        }) 
+          */
+    }
+    
+    addMapEntity(data, datasetId){
+         
+        if(!data || !data[0]){ 
+            Potree.Log('平面图无数据','red')
+            return
+        }
+        
+        var floorplan = new TiledMapFromEntity(this, this.tileColor, data[0]    )//[0]?
+
+        if(floorplan){
+            floorplan.name += "_"+ datasetId  
+
+            this.addMap(floorplan)
+            floorplan.updateProjection()
+            floorplan.updateObjectGroup()
+             
+            let visible = false
+
+            if(datasetId in Potree.settings.floorplanEnables){
+                visible = Potree.settings.floorplanEnables[datasetId]
+            }else{
+                visible = Potree.settings.floorplanEnable
+            }
+
+            if(visible){
+                this.needUpdate = true
+            }else{
+                floorplan.setEnable(false)
+            }
+            
+            this.dispatchEvent({type:'floorplanLoaded', floorplan})
+        }
+        return floorplan
+    }
+    
+    
+    getFloorplan(datasetId){
+        return this.maps.find(e=>e.name == 'floorplan'+"_"+ datasetId  )
+    }
+    
+    
+    addMap(t){ 
+        this.maps.push(t)
+        //this.view.invalidateScene()
+        this.needUpdate = true
+    }
+     
+    removeMap(t){
+        var e = this.maps.indexOf(t);
+        if(e >= 0){
+            t.removeFromSceneGroup(this.sceneGroup) 
+            this.maps.splice(e, 1) 
+        }
+         
+        /* this.view.invalidateScene() */ 
+        this.needUpdate = true
+        this.viewer.dispatchEvent({
+            type:'content_changed'
+        }) 
+    }
+    
+     
+    
+    
+    changeViewer(viewer_){//add 
+        this.viewer = viewer_ 
+    }
+    
+    initProjection(){
+        this.maps.forEach(map=>{
+            map.updateProjection()
+            map.updateObjectGroup() 
+        })
+    }
+    
+    visibilityChanged(){
+        if (!this.visible)
+            for (var t = 0, e = this.maps; t < e.length; t++){
+                e[t].removeFromSceneGroup(this.sceneGroup)
+            }
+    }
+    
+    onAfterRenderViewport(e){
+        var n = this;
+        
+        /* this.isVisibleInViewport(e) && (this.updateTimer || this.loadingInProgress || (this.updateTimer = window.setTimeout((function(){
+            n.update(e).then((function  (t){
+                t && n.loadComplete.emit(!0)
+            }
+            )).catch(u.handleWarning)
+        }
+        ), 100))) */
+    }
+     
+     
+     
+    update(){
+        this.needUpdate = false
+        if(this.disabled || !this.maps.find(e=>!e.disabled) || !this.maps.find(e=>e.objectGroup.visible)   )return  //add
+        
+          
+        var e, n, i, r, o;
+        
+        this.updateTimer = void 0,
+        e = this.viewport.camera,
+        n = e.projectionMatrix.clone(),
+        
+        n.elements[0] /= 1.5,
+        n.elements[5] /= 1.5,
+        this.frustumMatrix.multiplyMatrices(n, e.matrixWorldInverse),
+        this.frustum.setFromProjectionMatrix(this.frustumMatrix),
+        this.frustum.planes[4].setComponents(0, 0, 0, 0),
+        this.frustum.planes[5].setComponents(0, 0, 0, 0),
+        i = !0 
+        
+       
+        for (r = 0; r < this.maps.length; r++){
+            var map = this.maps[r] 
+            i = map.update(this.frustum, this.sceneGroup) && i;
+        }
+            
+        return [2, i]
+        
+                 
+    }
+     
+    getAttributions(){
+        for (var t = {}, e = 0, n = this.maps; e < n.length; e++){
+            n[e].fillAttributions(t)
+        }
+        return t
+    }
+     
+    updateProjection(){
+        for (var t = 0, e = this.maps; t < e.length; t++){
+            var n = e[t];
+            n.clearProjection(),
+            n.updateObjectGroup()
+        }
+    }
+}
+ 
+    
+    
+    
+    
+    
+    
+    
+    
+   
+ 
+
+export class TiledMapBase extends THREE.EventDispatcher{
+    constructor(/* t,  */name, mapLayer, tileColor, projection){
+        super();
+        this.name = name
+        //this.TransformService = t,
+        this.mapLayer = mapLayer,
+        this.tileColor = tileColor,
+        this.bias = 0 
+        this.zIndex = -1  
+        this.objectGroup = new THREE.Object3D;
+        this.objectGroup.name = name
+        this.objectGroupAdded = !1,
+        this.baseTile = new MapTile(this,/*   this.mapLayer, */this.objectGroup,this.tileColor),
+        this.isTileVisibleBox = new THREE.Box3,
+        this.isTileVisibleVec = new THREE.Vector3
+        
+        
+        this.projection = projection
+        this._zoomLevel = 0;//1-20
+        
+    }
+    
+    get zoomLevel(){
+        return this._zoomLevel
+    }
+    set zoomLevel(zoomLevel){
+        if(this._zoomLevel != zoomLevel){
+            this._zoomLevel = zoomLevel
+            //this.dispatchEvent('zoomLevelChange',zoomLevel)
+             
+            //if(this.name == 'map')console.log(zoomLevel,viewer.mapViewer.camera.zoom)
+        }
+    }
+    
+    
+    updateObjectGroup(){
+        this.position && this.objectGroup.position.copy(this.position),
+        this.quaternion && this.objectGroup.quaternion.copy(this.quaternion),
+        this.objectGroup.updateMatrixWorld(!0)
+    }
+    
+    updateProjection(){
+        //this.transformMapToLocal || (this.transformMapToLocal = this.TransformService.getTransform(this.projection, this.TransformService.crsLocal))
+        if(!this.transformMapToLocal){
+            if(proj4.defs("NAVVIS:TMERC")){
+                if(this.projection == "EPSG:4550"){
+                    this.transformMapToLocal = {
+                            forward:(e)=>{
+                                var a = viewer.transform.lonlatTo4550.inverse(e)
+                                return viewer.transform.lonlatToLocal.forward(a)
+                            },
+                        }
+                        
+                   
+                }else{
+                    this.transformMapToLocal = proj4(this.projection, "NAVVIS:TMERC") 
+                }
+                    //this.transformMapToLocal = proj4(this.projection, "NAVVIS:TMERC") 
+             
+               
+            }
+        } 
+    
+    } 
+    
+    setEnable(enable){//add
+        if(!this.disabled == enable)return
+        if(enable){
+            console.log('setEnable',true)
+        }
+        this.disabled = !enable
+     
+        viewer.updateVisible(this.objectGroup, 'setEnable', enable)
+        
+        if(!enable){
+            this.baseTile.remove()
+        }else{
+            this.mapLayer.needUpdate = true
+        }
+         
+        this.mapLayer.viewer.dispatchEvent({
+            type:'content_changed'
+        })
+        
+        
+    }
+    
+    
+    
+    /* clearProjection(){
+        this.transformMapToLocal = void 0,
+        this.projection.name !== this.TransformService.NAVVIS_LOCAL && this.baseTile.remove()
+    } */
+    
+    update(e, n){
+        
+        var unavailable = (this.disabled || !this.objectGroup.visible)//地图即使不显示也要获得zoomlevel
+        if(this.name != 'map' && unavailable)return 
+        
+        this.updateProjection()
+        
+        if(!this.transformMapToLocal)return
+        
+        if (!this.isTileVisible(new THREE.Vector3(0,0,0), this.mapSizeM, e))
+            return this.removeFromSceneGroup(n), !0;
+        
+        let viewport = this.mapLayer.viewport
+        
+        
+        var i = new THREE.Vector3(-.5 * this.mapSizeM,0,0);
+        i.applyMatrix4(this.objectGroup.matrixWorld),
+        i.project(viewport.camera);
+        var o = new THREE.Vector3(.5 * this.mapSizeM,0,0);
+        o.applyMatrix4(this.objectGroup.matrixWorld),
+        o.project(viewport.camera);
+        var a = viewport.resolution.x 
+          , s = viewport.resolution.y 
+        if (a <= 0 || s <= 0 || isNaN(i.x) || isNaN(o.x))  return !1;
+        i.sub(o),
+        i.x *= a / 2,
+        i.y *= s / 2;
+        
+        var c = this.tileSizePx / i.length()
+          , level = Math.ceil(-Math.log(c) / Math.log(2) - this.bias);
+        level = Math.max(level, 0) 
+        level = Math.min(level, void 0 === this.maxDepth ? 1 / 0 : this.maxDepth) 
+        this.zoomLevel = level//add
+        //console.log(level)
+        if(!unavailable){
+            this.addToSceneGroup(n) 
+            return this.baseTile.update(this, e, level, this.mapSizeM, 0, 0, "")
+        }
+    }
+     
+     
+     
+     
+    isTileVisible(e, n, i){
+        if (n > HALF_WORLD_SIZE) return !0;
+        var r = .5 * n;
+        this.transformMapToLocal.forward(e) 
+        this.isTileVisibleBox.makeEmpty() 
+        this.isTileVisibleVec.set(e.x - r, e.y - r, e.z).applyMatrix4(this.objectGroup.matrixWorld)
+        this.isTileVisibleBox.expandByPoint(this.isTileVisibleVec)
+        this.isTileVisibleVec.set(e.x - r, e.y + r, e.z).applyMatrix4(this.objectGroup.matrixWorld)
+        this.isTileVisibleBox.expandByPoint(this.isTileVisibleVec)
+        this.isTileVisibleVec.set(e.x + r, e.y - r, e.z).applyMatrix4(this.objectGroup.matrixWorld)
+        this.isTileVisibleBox.expandByPoint(this.isTileVisibleVec)
+        this.isTileVisibleVec.set(e.x + r, e.y + r, e.z).applyMatrix4(this.objectGroup.matrixWorld)
+        this.isTileVisibleBox.expandByPoint(this.isTileVisibleVec)
+        return i.intersectsBox(this.isTileVisibleBox)
+    }
+     
+    addToSceneGroup(t){
+        this.objectGroupAdded || (t.add(this.objectGroup),
+        this.objectGroupAdded = !0)
+    }
+     
+    removeFromSceneGroup(t){
+        this.baseTile.remove(),
+        this.objectGroupAdded && (t.remove(this.objectGroup),
+        this.objectGroupAdded = !1)
+    }
+    
+    
+}
+
+export class MapTile{
+    constructor(map,/*  t, */ e, n){
+        this.map = map;
+        //this.mapLayer = t,
+        this.objectGroup = e,
+        this.tileColor = n,
+        this.meshAdded = !1,
+        this.textureLoaded = !1,
+        this.children = []
+    }
+    update(e, n, i, r, o, a, s){
+        return !!this.doesNotContainTilesToBeDisplayed(e) || (0 === i ? this.updateTile(e, r, o, a) : this.updateSubTiles(e, n, i, r, o, a, s))
+    }
+    
+    doesNotContainTilesToBeDisplayed(t){
+        return t.tilePresenceMap && t.tilePresenceMap.empty
+    }
+    
+    updateTile(t, e, n, i){ 
+        if(!this.mesh){
+            this.createTileObject(t, e, n, i)
+        }
+        if(!this.meshAdded){
+            this.objectGroup.add(this.mesh) 
+            this.meshAdded = !0
+        }
+        if(this.textureLoaded){
+            this.removeChildren() 
+        }
+        
+        return this.textureLoaded
+    }
+    
+    updateSubTiles(entity, n, level, o, a, s, c){
+        for (var l = !0, u = [-.25 * o, .25 * o, -.25 * o, .25 * o], d = [.25 * o, .25 * o, -.25 * o, -.25 * o], p = 0; p < 4; ++p){
+            var h = c + p.toString(10);
+            //一级(512):0 1 2 3分别为左上、右上、左下、右下。二级(1024)就是把一级的每一块分裂,如00 01 02 03分别是0的左上、右上、左下、右下……
+            /* if(entity.name == 'floorplan'){
+                console.log(1)
+            } */
+            
+            
+            if (!entity.tilePresenceMap || entity.tilePresenceMap[h]){
+                //去掉判断,直接显示
+                var f = a + u[p]
+                  , m = s + d[p];
+                tempVector.set(f, m, 0);
+                if (entity.isTileVisible(tempVector, .5 * o, n)){
+                     
+                    this.children[p] || (this.children[p] = new MapTile(this.map, this.objectGroup,this.tileColor))
+                    l = this.children[p].update(entity, n, level - 1, .5 * o, f, m, h) && l
+                } else {
+                    if (this.children[p]){
+                        this.children[p].remove()
+                        delete this.children[p]
+                    }
+
+                }
+                 
+            }
+        }
+        return l && this.removeObject3D(),
+        l
+
+    }
+    
+    createTileObject(t, e, n, a){
+        var s = this;
+        this.mesh = this.createMesh(t.transformMapToLocal, e, n, a),
+        this.textureLoaded = !1;
+        var c = t.mapSizeM / e
+          , l = Math.log(c) / Math.log(2)
+          , u = n / e + .5 * (c - 1)
+          , d = -a / e + .5 * (c - 1)
+          , p = t.getTileUrl(Math.round(l), Math.round(u), Math.round(d));
+        viewer.setObjectLayers(this.mesh, 'map' )
+        this.mesh.renderOrder = -(1e6 - l - 100 * (t.zIndex || 0));
+        var h = this.mesh.material;
+        
+        
+        var loadDone = ()=>{
+            this.map.mapLayer.loadingInProgress--
+            if(this.map.mapLayer.loadingInProgress == 0){
+                this.map.mapLayer.dispatchEvent('loadDone') 
+            }
+        }
+        
+        h.map = texLoader.load(p, (tex)=>{
+            if(this.mesh){//如果还要显示的话
+                this.textureLoaded = true
+                this.mesh.material.opacity = 1
+                //this.mapLayer.view.invalidateScene()
+                
+                this.map.mapLayer.viewer.dispatchEvent({
+                    type:'content_changed'
+                })
+                this.map.mapLayer.needUpdate = true //表示还要继续update(以removeChildren)
+            }else{
+                tex.dispose()   
+            } 
+            loadDone()
+        } , void 0, (()=>{//error
+            this.textureLoaded = !0
+            if(this.mesh){ 
+                this.mesh.material.dispose() //o.disposeMeshMaterial(this.mesh) 
+                this.mesh.material =  errorMaterial
+                //this.map.mapLayer.view.invalidateScene())
+                this.map.mapLayer.viewer.dispatchEvent({
+                    type:'content_changed'
+                }) 
+            } 
+            loadDone()
+        })) 
+        
+        h.map.anisotropy = 0,
+        h.map.generateMipmaps = !1,
+        h.map.minFilter = THREE.LinearFilter,
+        h.map.magFilter = THREE.LinearFilter,
+        this.map.mapLayer.loadingInProgress++
+    }
+    
+    createMesh(t, e, n, o){
+        var a = new THREE.Geometry;
+        return tempVector.set(n - e / 2, o - e / 2, 0),
+        a.vertices.push(new THREE.Vector3().copy(t.forward(tempVector))),
+        tempVector.set(n + e / 2, o - e / 2, 0),
+        a.vertices.push(new THREE.Vector3().copy(t.forward(tempVector))),
+        tempVector.set(n + e / 2, o + e / 2, 0),
+        a.vertices.push(new THREE.Vector3().copy(t.forward(tempVector))),
+        tempVector.set(n - e / 2, o + e / 2, 0),
+        a.vertices.push(new THREE.Vector3().copy(t.forward(tempVector))),
+        a.faces.push(face1),
+        a.faces.push(face2),
+        a.faceVertexUvs[0].push(face1UV),
+        a.faceVertexUvs[0].push(face2UV),
+        new THREE.Mesh(a,this.createMaterial())
+    }
+    
+    createMaterial(){
+        var t = new THREE.MeshBasicMaterial({
+            transparent: !0,
+            depthWrite: !1,
+            depthTest: !0,
+            opacity: 0,
+            side: THREE.DoubleSide
+        });
+        return t.color = this.tileColor ? this.tileColor : new THREE.Color(16777215),
+        t
+    }
+    
+    remove(){
+        this.removeObject3D(),
+        this.removeChildren()
+    }
+    
+    removeObject3D(){
+        if (this.mesh){
+            if (this.objectGroup.remove(this.mesh),
+            this.textureLoaded){
+                var t = this.mesh.material.map;
+                t && t.dispose()
+            } 
+            this.mesh.material.dispose() //o.disposeMeshMaterial(this.mesh),
+            this.mesh.geometry.dispose() 
+            this.mesh = void 0
+        }
+        this.meshAdded = !1,
+        this.textureLoaded = !1
+        
+    }
+    
+    removeChildren(){
+        for (var t = 0, e = this.children; t < e.length; t++){
+            var n = e[t];
+            n && (n.removeObject3D(),
+            n.removeChildren())
+        }
+        this.children.length = 0
+    }
+  
+}
+
+
+
+
+proj4.defs("EPSG:3857", "+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs")
+//这里地图世界的中心是不是lon:0,lat:0     
+
+export class TiledMapOpenStreetMap extends TiledMapBase{
+    constructor(mapLayer, tileColor){
+        super('map', mapLayer, tileColor, /* "EPSG:4550" */  "EPSG:3857"  ) //EPSG projection
+        //this.baseUrl = "https://wprd03.is.autonavi.com/appmaptile?style=7&x=${x}&y=${y}&z=${z}",
+        //this.baseUrl = "https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x=${x}&y=${y}&z=${z}" //最高只到18 level
+        this.baseUrl = "https://wprd04.is.autonavi.com/appmaptile?lang=zh_cn&style=7&x=${x}&y=${y}&z=${z}"    //最高只到19
+        
+        this.attribution = "© PopSmart,  © 高德地图",
+        this.tileSizePx = 256
+        this.mapSizeM = 40075017
+        this.maxDepth = 19//20
+        this.bias = 0.5 
+         
+  
+            
+    }
+    
+    getTileUrl(t, e, n){ 
+        return this.baseUrl.replace(/\${z}/, t.toString(10)).replace(/\${x}/, e.toString(10)).replace(/\${y}/, n.toString(10))
+    }
+     
+    fillAttributions(t){
+        t[this.attribution] = {
+            score: 50
+        }
+    }
+    
+    
+    
+}
+
+
+
+
+export class TiledMapFromEntity extends TiledMapBase{
+    constructor(mapLayer, tileColor, data){ 
+        super('floorplan', mapLayer, tileColor,  "NAVVIS:TMERC"   /* "EPSG:3857"  *//* "WGS84" */)  //直接就是本地坐标,没有projec 
+        
+         
+        let entity = this.tiledMapEntity = this.fillFromData(data)  
+        let time = entity.updateTime || entity.createTime  
+          
+        this.tileSizePx = entity.tileSizePx,
+        this.mapSizeM = entity.mapSizeM,
+        this.maxDepth = entity.maxDepth;
+        this.postStamp = time ? time.replace(/[^0-9]/ig,'') : (new Date).getTime()   
+        //this.projection = n.crsLocal,
+        this.zIndex = 0, 
+        this.tilePresenceMap = this.decodeBitStream(this.tiledMapEntity.quadtree) //包含tile分裂信息,如果写错了会造成tile显示不全
+         
+    }
+  
+    fillFromData(e){ 
+        let data = {}
+        
+        data.id = e.id
+        data.globalLocation = Potree.Utils.VectorFactory.fromArray3(e.location),
+        data.orientation = Potree.Utils.QuaternionFactory.fromArray(e.orientation) 
+        //if(Potree.fileServer){
+            data.filePath = `${Potree.settings.urls.prefix1}${e.file_path}`
+        //}else{
+        //    data.filePath = `${Potree.settings.urls.prefix}/data/${Potree.settings.number}/${e.file_path}`
+        //}
+         
+        //if(!data.filePath.includes('building_1'))data.filePath = data.filePath.replace('building','building_1')//暂时
+        data.fileName = '$DEPTH/$X/$Y.png' //e.file_name,
+        data.type = e.type,
+        data.mapSizeM = e.map_size_m,
+        data.tileSizePx = e.tile_size_px,
+        data.maxDepth = e.max_depth,
+        data.quadtree = e.quadtree,
+        data.floorId = e.floor_id,
+        data.bundleId = e.bundle_id  
+        //this.computeLocalCoordinates()
+        return data
+        
+    }
+    
+    
+    
+    computeLocalCoordinates(){
+        if(proj4.defs("NAVVIS:TMERC")){
+            this.tiledMapEntity.location = new THREE.Vector3().copy(viewer.transform.lonlatToLocal.forward(this.tiledMapEntity.globalLocation))
+        } 
+    }
+     
+    updateProjection() {
+        super.updateProjection()
+        if(!this.position){
+            this.computeLocalCoordinates()
+        } 
+        /* this.projection = this.TransformService.crsLocal,
+        t.prototype.updateProjection.call(this) */
+    } 
+    
+    
+    get position(){
+        return this.tiledMapEntity.location
+       /* enumerable: !0,
+       configurable: !0 */
+    }
+    get quaternion(){
+        return this.tiledMapEntity.orientation
+        /* enumerable: !0,
+        configurable: !0 */
+    }
+   
+    getTileUrl(t, e, n) {
+        var i = (this.tiledMapEntity.filePath + "/" + this.tiledMapEntity.fileName).replace(/\$DEPTH/g, t.toString(10)).replace(/\$X/g, e.toString(10)).replace(/\$Y/g, n.toString(10));
+        return i += "?t=" + this.postStamp  
+        //this.RestService.addAuthorizationQueryParameter(i) //????
+    }
+    
+    fillAttributions(t) {
+        t.NavVis = {
+            score: 100
+        }
+    }
+   
+    
+    
+    
+    decodeBitStream(t) {
+        if (!t)
+            return {
+                empty: !0
+            };
+        for (var e = {}, n = [e], i = 0; i < t.length; i++) {
+            var r = n.shift()
+              , o = parseInt(t.substr(i, 1), 16);
+            if (1 & o) {
+                var a = {};
+                r[0] = a,
+                n.push(a)
+            }
+            2 & o && (a = {},
+            r[1] = a,
+            n.push(a)),
+            4 & o && (a = {},
+            r[2] = a,
+            n.push(a)),
+            8 & o && (a = {},
+            r[3] = a,
+            n.push(a))
+        }
+        var s = {
+            empty: !0
+        };
+        return this.computeHashes(s, e, ""),
+        s
+    }
+   
+    computeHashes(t, e, n) {
+        for (var i = 0; i < 4; i++)
+            e[i] && (t[n + i.toString(10)] = !0,
+            t.empty = !1,
+            this.computeHashes(t, e[i], n + i.toString(10)))
+    }
+    
+ 
+
+}
+
+
+/* { 
+    "bundle_id": 1,                         //t-CwfhfqJ
+    "file_name": "$DEPTH/$X/$Y.png",
+    "file_path": "data/bundle_t-CwfhfqJ/building_1/map_tiles/11",
+    "floor_id": 11,
+    "id": 1,
+    "location": [
+        113.5957510575092,
+        22.366605927999239,
+        0.0
+    ],
+    "map_size_m": 61.44,
+    "max_depth": 3,
+    "orientation": [
+        0.7071067811865476,
+        0.0,
+        0.0,
+        0.7071067811865475
+    ],
+    "quadtree": "fe5f7c7fcffff7f53",
+    "sceneCode": "t-CwfhfqJ",
+    "tile_size_px": 256,
+    "type": "TILED_PYRAMID"
+
+
+}
+ */
+
+
+
+ 
+

+ 649 - 0
src/custom/viewer/map/MapViewer.js

@@ -0,0 +1,649 @@
+
+import * as THREE from "../../../../libs/three.js/build/three.module.js";
+import {MapLayer} from './Map.js'
+import {FirstPersonControls} from '../../../navigation/FirstPersonControlsNew.js'
+import {InputHandler} from "../../../navigation/InputHandlerNew.js";
+
+import {ViewerBase} from "../viewerBase.js"
+import {ExtendView} from "../../../viewer/ExtendView.js";
+import Viewport from "../Viewport.js"; 
+
+
+import math from "../../utils/math.js";
+//import CursorDeal from '../utils/CursorDeal.js' 
+import {Images360} from '../../modules/panos/Images360.js'
+import Common from '../../utils/Common.js' 
+import {transitions, easing, lerp} from "../../utils/transitions.js";
+import {config } from "../../settings.js";
+/* var centerCross = new THREE.Mesh(new THREE.SphereGeometry(1, 4, 4),new THREE.MeshBasicMaterial({
+    transparent:true,  color:"#ff0000", opacity:0.5
+})); */
+
+
+/* const mapHeight = -1000;//要比点云低。最低
+const cameraHeight = 1000;  //最高 */
+const panosHeight = config.map.mapHeight + 100 //要比点云低  (marker)
+const cursorHeight = 0;//比地图高就行  
+const routeLayerHeight = config.map.mapHeight + 105
+
+const texLoader = new THREE.TextureLoader()
+const planeGeo = new THREE.PlaneBufferGeometry(1,1)
+
+const markerSize = 1;
+
+
+var initCameraFeildWidth = 50
+var panoMarkerMats  
+ 
+
+export class MapViewer extends ViewerBase{
+    constructor(dom){
+        super(dom, {
+            clearColor: Potree.config.mapBG,
+            name: 'mapViewer'
+        })
+        this.visible = true
+        this.initScene()
+         
+         
+        this.mapLayer = new MapLayer(this, this.viewports[0])
+        this.scene.add(this.mapLayer.sceneGroup)
+        this.mapLayer.sceneGroup.position.setZ(Potree.config.map.mapHeight)
+        this.mapRatio = 0.5
+        this.splitDir = 'leftRight'
+        
+        
+        //this.scene.add(centerCross)
+        
+     
+        viewer.addEventListener("camera_changed", (e)=>{
+            if(e.viewport == viewer.mainViewport) this.updateCursor()
+            else this.updateWhenAtViewer() 
+        })
+        
+         
+        //viewer.addEventListener("global_mousemove", this.updateWhenAtViewer.bind(this))  //鼠标移动时reticule也动,所以直接就needRender
+        viewer.reticule.addEventListener('update',(e)=>{
+            if(this.attachedToViewer)this.needRender = true 
+        })
+         
+        
+        viewer.scene.addEventListener("360_images_added", this.addPanos.bind(this))
+        
+        
+        viewer.addEventListener("loadPointCloudDone",  this.initProjection.bind(this))
+        
+        this.addEventListener('global_click',(e)=>{
+            if(!e.isTouch && e.button != THREE.MOUSE.LEFT)return
+            this.updateClosestPano(e.intersect)
+        })
+        
+        
+        
+        this.addEventListener('add',(e)=>{//添加其他mesh
+            this.scene.add(e.object)
+            if(e.name == 'route'){
+                e.object.position.z = routeLayerHeight 
+            } 
+            viewer.setObjectLayers(e.object, 'mapObjects' )
+        })
+        
+        
+        
+        
+        
+        if(!Potree.settings.isOfficial){
+            let domRoot = viewer.renderer.domElement.parentElement; 
+            let elAttach = $("<input type='button' value='map绑定'></input>");
+            elAttach.css({
+                position : "absolute",
+                right : '80%',
+                bottom: '20px',
+                zIndex: "10000",
+                fontSize:'1em', color:"black",
+                background:'rgba(255,255,255,0.8)',
+            })
+            let state = false
+            elAttach.on("click", () => {
+                state = !state
+                this.attachToMainViewer(state, 'measure')
+                elAttach.val(state ? 'map分离':'map绑定')
+            }); 
+            domRoot.appendChild(elAttach[0]);
+            
+            
+        }
+        
+    }
+    
+    
+    
+    
+    
+    waitLoadDone(callback){//确保加载完后执行
+        if(this.mapLayer.loadingInProgress == 0){
+            callback()
+        }else{
+            var f = ()=>{
+                callback()
+                this.mapLayer.removeEventListener('loadDone', f)
+            } 
+            this.mapLayer.addEventListener('loadDone', f)  
+        }
+        
+    }
+    
+    addListener(images360){
+        images360.addEventListener('flyToPano',e=>{
+            let toPano = e.toPano
+            if(toPano.dontMoveMap) return
+            
+            /* transitions.start(lerp.vector(this.view.position, toPano.pano.position.clone().setZ(cameraHeight), 
+            
+            (pos, progress)=>{
+                  
+            }), toPano.duration, null, 0, easing[toPano.easeName]  );  */ 
+            let boundSize// = new THREE.Vector2(10,10)
+            
+            this.moveTo(toPano.pano.position.clone().setZ(Potree.config.map.cameraHeight),  boundSize, toPano.duration, toPano.easeName) 
+        })
+        
+        
+    }
+    
+    
+    
+    initProjection(){
+        this.started = true
+        this.mapLayer.initProjection()
+        
+    }
+     
+    initScene(){
+        let w = initCameraFeildWidth
+        let width = this.renderArea.clientWidth;
+		let height = this.renderArea.clientHeight; 
+        //let aspect = width / height
+        this.camera = new THREE.OrthographicCamera(-width/2,width/2,height/2,-height/2/* -w/2, w/2, w/2/aspect, -w/2/aspect */, 0.01, 10000);
+        this.camera.zoom = width / w  //zoom越大视野越小
+        //this.camera.position.set(0,0,10);
+        this.camera.up.set(0,0,1)
+        //this.camera.lookAt(new THREE.Vector3())
+        //this.camera.updateMatrixWorld()
+         
+        this.view = new ExtendView();
+        this.view.position.set(0,0,Potree.config.map.cameraHeight);
+        this.view.lookAt(0,0,0)
+        this.setViewLimit('standard')
+            
+        let viewport = new Viewport( this.view, this.camera, {
+            left:0, bottom:0, width:1, height: 1, name:'mapViewport'  
+        })
+        viewport.axis = ["x","y"]
+        viewport.axisSign = [1,1]
+        
+        viewport.noPointcloud = true; //不要渲染点云
+        viewport.render = this.render.bind(this)//标志给mainView渲染
+        //viewport.unableDepth = true //depthBasicMaterial等在此viewport中不开启depth
+        
+        
+        
+        this.viewports = [viewport]
+        
+               
+                
+                
+        this.controls = new FirstPersonControls(this, this.viewports[0]);
+        this.controls.setEnable(true)
+        this.scene = new THREE.Scene();  
+  
+        
+        
+        this.inputHandler = new InputHandler(this, this.scene);
+        this.inputHandler.name = 'mapInputHandler'
+        //this.inputHandler.addInputListener(this.controls);
+        this.inputHandler.registerInteractiveScene(this.scene);//interactiveScenes
+       
+        this.viewports[0].interactiveScenes = this.inputHandler.interactiveScenes;//供viewer的inputHandler使用
+         
+        var cursor = new THREE.Mesh(planeGeo, new THREE.MeshBasicMaterial({
+            transparent:true,
+            opacity:0.9,
+            depthTest : false, //防止透明冲突
+            map: texLoader.load(Potree.resourcePath+'/textures/pic_location128.png' )
+        }))
+            cursor.position.set(0,0,cursorHeight);
+            
+            
+        this.cursor = cursor
+       
+        this.scene.add(cursor)
+        viewer.setObjectLayers(this.scene, 'mapObjects' )
+    }
+    
+    setViewLimit(state){//设置边界,当编辑空间模型等时要解禁
+        let setting = Potree.config.OrthoCameraLimit[state]
+        if(setting){
+            this.view.limitBound = new THREE.Box3().copy(setting.posBound)
+            this.camera.zoomLimit = $.extend({},setting.zoom);
+        }else{  
+            this.view.limitBound = null
+            this.camera.zoomLimit  = null
+        }
+    } 
+    
+    updateCursor(){
+         
+        var scale = math.getScaleForConstantSize( {//规定下最小最大像素
+            minSize : 80,  maxSize : 200,   nearBound : initCameraFeildWidth*0.1 , farBound : initCameraFeildWidth*2,
+            camera:this.camera , position: this.cursor.getWorldPosition(new THREE.Vector3()),
+            resolution: this.viewports[0].resolution//2
+        })
+        this.cursor.scale.set(scale, scale, scale);//当地图缩放时
+        this.cursor.position.copy(viewer.mainViewport.camera.position).setZ(cursorHeight)//当场景镜头旋转移动时
+        this.cursor.rotation.z = viewer.mainViewport.view.yaw
+        this.needRender = true
+    }
+    
+    addPanos(e){
+        var panosGroup = new THREE.Object3D;
+        panoMarkerMats = {
+            default: new THREE.MeshBasicMaterial({
+                transparent:true,
+                opacity: 0.5,
+                map: texLoader.load(Potree.resourcePath+'/textures/map_marker.png' ),
+            }),
+            selected: new THREE.MeshBasicMaterial({
+                transparent:true,
+                opacity: 1,
+                map: texLoader.load(Potree.resourcePath+'/textures/map_marker.png' ),
+            })
+        }
+        
+         
+        e.images.panos.forEach(pano=>{
+            pano.mapMarker = new THREE.Mesh(planeGeo, panoMarkerMats.default);
+            pano.mapMarker.position.copy(pano.position).setZ(0)   
+            pano.mapMarker.scale.set(markerSize,markerSize,markerSize)
+            pano.mapMarker.name = 'mapMarker'
+            panosGroup.add(pano.mapMarker); 
+            
+            
+            let mouseover = (e)=>{ 
+                if(!e.byMap){
+                    pano.mapMarker.material = panoMarkerMats.selected
+                    if(!e.byMainView) pano.dispatchEvent({type: "hoverOn", byMap:true})
+                    this.needRender = true    
+                }
+            }
+            
+            let mouseleave = (e)=>{
+                if(!e.byMap){
+                    pano.mapMarker.material = panoMarkerMats.default
+                    if(!e.byMainView) pano.dispatchEvent({type: "hoverOff", byMap:true})
+                    this.needRender = true
+                }
+            }
+             
+            
+            pano.mapMarker.addEventListener('mouseover', mouseover);  
+			pano.mapMarker.addEventListener('mouseleave', mouseleave);
+            
+            pano.addEventListener('hoverOn', mouseover)
+            pano.addEventListener('hoverOff', mouseleave)
+            
+            let onclick = (e)=>{
+                viewer.images360.flyToPano(pano)
+            }
+            pano.mapMarker.addEventListener('click', onclick); 
+            
+            pano.addEventListener('isVisible',(e)=>{//是否显示该点的mesh(不显示也能走)   
+                //console.log('panoMarker isVisible', pano.id, e.visible)
+                viewer.updateVisible(pano.mapMarker, 'panoVisible', e.visible )
+                this.needRender = true
+                
+            })
+            pano.addEventListener('rePos',(e)=>{
+                pano.mapMarker.position.copy(pano.position).setZ(0)
+            })
+        })
+        this.scene.add(panosGroup)
+        panosGroup.position.z = panosHeight
+        this.panosGroup = panosGroup
+        viewer.setObjectLayers(panosGroup, 'mapObjects' )
+        
+        
+        /* e.images.on('markersDisplayChange', (show)=>{
+            panosGroup.visible = show
+            this.needRender = true
+        }) */
+        
+        //-------
+        
+        //this.fitPanosToViewport()
+
+
+        this.initFitView()
+
+    }
+    
+    
+    
+    updateClosestPano(intersect){
+        if(viewer.images360.flying)return; 
+        intersect = intersect && intersect.orthoIntersect
+        if(!intersect)return
+        
+        intersect = intersect.clone().setZ(0)
+        
+        const minDis = 20 //距离鼠标不能太远
+
+        
+        var filterFuncs = [
+            (pano)=>{
+                return pano.position.clone().setZ(0).distanceTo(intersect) < minDis 
+            },
+            Images360.filters.isEnabled(),
+            
+            Images360.filters.isVisible(),//只走显示的点,否则会走到别的层 
+        ]; 
+      
+        
+         
+    
+        
+        
+        var pano = Common.find(viewer.images360.panos,  filterFuncs, [Images360.sortFunctions.floorDisSquaredToPoint(intersect)]);
+        if (pano && pano != viewer.images360.currentPano ) {
+           viewer.images360.flyToPano(pano)
+            
+        }   
+          
+    }
+    
+    
+    fitPanosToViewport(){//使所有漫游点占满viewport 
+        
+        //var w = viewer.bound.boundSize.x; 
+        var boundSize = viewer.images360.bound.size.clone().multiplyScalar(1.1);
+        boundSize.max(new THREE.Vector3(4,4,4))
+        let endPosition = viewer.images360.bound.center.clone()
+        this.moveTo(endPosition, boundSize, 0) 
+    }
+
+    fitToPointcloud(pointcloud, duration=400){
+        var boundSize = pointcloud.bound.getSize(new THREE.Vector3)/* .multiplyScalar(1.1); */
+            boundSize.max(new THREE.Vector3(4,4,4))
+        let endPosition = pointcloud.bound.getCenter(new THREE.Vector3)
+        this.moveTo(endPosition, boundSize, duration) //给点duration去变化 否则地图放大后所占的还是很小
+    }
+
+    initFitView(){ 
+        let dis = 5 , px = 70 //地图上px像素长度代表的距离为dis  //px是手动缩放到5m后发现是这个长度
+        let zoom = px / dis;
+        this.camera.zoom = zoom
+        this.moveTo(viewer.images360.position/* viewer.images360.bound.center */) 
+        this.camera.updateProjectionMatrix()
+
+    }
+    
+    fitToDatasets(datasets){
+        let bound = new THREE.Box3; 
+        datasets.forEach(e=>bound.union(e.bound))
+        let center = bound.getCenter(new THREE.Vector3)
+        let size = bound.getSize(new THREE.Vector3)
+        
+        this.moveTo(center, size, 200 ) //给duration是为了顺应视口大小改变,缓冲
+    }
+    
+    moveTo(endPosition, boundSize, duration=0, easeName){//前两个参数有xy即可
+        endPosition = new THREE.Vector3(endPosition.x,endPosition.y,Potree.config.map.cameraHeight)
+        this.view.moveOrthoCamera(this.viewports[0],  {endPosition, boundSize },   duration,  easeName)
+        
+       
+        
+        
+        /* let endZoom, startZoom = this.camera.zoom 
+        
+        //修改相机为bound中心,这样能看到全部(宽度范围内)
+        
+        this.view.setView({ position:endPosition,  duration, 
+            callback:()=>{//done
+                 
+            },
+            onUpdate:(progress)=>{ 
+                if(boundSize){ 
+                    let aspect = boundSize.x / boundSize.y
+                    let w, h; 
+                    
+                    if(this.camera.aspect > aspect){//视野更宽则用bound的纵向来决定
+                        h = boundSize.y
+                        //w = h * this.camera.aspect
+                        endZoom = this.viewports[0].resolution.y / h
+                    }else{
+                        w = boundSize.x; 
+                        //h = w / this.camera.aspect
+                        endZoom = this.viewports[0].resolution.x / w
+                    }  
+                    //onUpdate时更新endzoom是因为画布大小可能更改
+                    
+                    
+                    this.camera.zoom = endZoom * progress + startZoom * (1 - progress)
+                    this.camera.updateProjectionMatrix() 
+                } 
+            },
+            
+            Easing:easeName
+        
+        }) */
+          
+            
+    }
+    
+    
+    
+    
+    updateWhenAtViewer(e){
+        if(this.attachedToViewer){ 
+            if(this.started) this.mapLayer.update()  
+            
+            this.updateCursor() 
+            this.needRender = true
+        } 
+    }
+    
+    update(delta, areaSize ){
+        if(!this.visible && !this.attachedToViewer )return
+        
+        
+        if(this.attachedToViewer){
+            if(this.mapLayer.needUpdate){//必须更新。(较少触发)
+                this.updateWhenAtViewer()
+            }
+            return
+        }            
+         
+         
+        this.updateScreenSize()
+         
+        this.controls.update(delta);
+        this.view.applyToCamera(this.camera) 
+        
+            
+        let changed = this.cameraChanged() 
+        
+        
+        if(this.started && (changed || this.mapLayer.needUpdate))this.mapLayer.update()
+        
+        if(changed /*||   || this.needRender */){ 
+            this.dispatchEvent({
+                type: "camera_changed", 
+                camera: this.camera,
+                viewport : this.viewports[0]
+            })
+                 
+            this.needRender = true
+            this.updateCursor()//更改大小
+        }
+        this.render()
+        
+    }
+    
+    attachToMainViewer(state, desc, mapRatio=0.5, options={}){//转移到viewer中。测量时展示or截图需要
+        
+        if(!Potree.settings.isOfficial)this.renderArea.style.display = state ? 'none':'block'
+        
+        if(state){
+            this.enabledOld = this.enabled
+            this.enabled = true
+            
+            if(mapRatio != 'dontSet'){
+                this.changeSplitScreenDir(options.dir, mapRatio)
+                 
+                if(this.attachedToViewer){ 
+                    //this.fitPanosToViewport() 
+                    viewer.updateScreenSize({forceUpdateSize:true})        
+                    return
+                }                
+                viewer.viewports = [viewer.mainViewport, viewer.mapViewer.viewports[0] ];//因为mainViewer的相机变化会触发map的变化,所以先渲染mainViewer
+            }
+              
+            
+            if(desc == 'measure') this.inputHandler.registerInteractiveScene(viewer.measuringTool.scene);//虽然用的是viewer的inputHandler,但借用了this.inputHandler的interactiveScenes
+            else if(desc == 'split4Screens') {
+                this.inputHandler.registerInteractiveScene(viewer.scene.scene);
+            }
+            
+        }else{
+            if(!this.attachedToViewer)return
+             
+            viewer.mainViewport.left = 0;
+            viewer.mainViewport.bottom = 0;
+            viewer.mainViewport.width = 1;
+            viewer.mainViewport.height = 1;
+             
+            this.viewports[0].width = 1;
+            this.viewports[0].bottom = 0;
+            this.viewports[0].height = 1;
+            this.viewports[0].left = 0;
+            
+            this.inputHandler.unregisterInteractiveScene(viewer.measuringTool.scene);
+            this.inputHandler.unregisterInteractiveScene(viewer.scene.scene);
+            viewer.viewports = [viewer.mainViewport]
+            this.updateScreenSize({forceUpdateSize:true}) //更新相机projectionMatrix
+        }
+        
+        //if(desc == 'measure')this.renderMeasure = state 
+        this.attachedToViewer = state
+        
+        
+        
+        
+         
+        viewer.updateScreenSize({forceUpdateSize:true})
+        
+        
+        //mapRatio != 'dontSet' && !options.dontFit && this.moveTo(...)//要写在updateScreenSize后面,因为要根据视图大小来fit
+        if(options.moveToCurrentPos){
+            let boundSize = new THREE.Vector2(10,10)
+            let duration = 1000
+            this.moveTo(viewer.images360.position.clone(), boundSize, duration)
+        }    
+        this.needRender = true
+    }
+    
+    changeSplitScreenDir(dir, mapRatio){//左右 | 上下
+        //if(!dir || dir == this.dir)return
+        if(dir )this.splitDir = dir
+        this.updateSplitSize(mapRatio)
+        /* if(this.attachedToViewer){ //如果已经分屏了,中途修改方向的话……
+            this.updateSplitSize()
+        } */ 
+    }
+    updateSplitSize(mapRatio){
+        if(mapRatio != void 0) this.mapRatio = mapRatio
+        
+        //console.log(this.mapRatio)
+        
+        if(this.splitDir == 'leftRight'){//地图在左方
+            viewer.mainViewport.left = this.mapRatio
+            viewer.mainViewport.width = 1-this.mapRatio
+            this.viewports[0].width = this.mapRatio; 
+            
+            viewer.mainViewport.bottom = this.viewports[0].bottom = 0
+            viewer.mainViewport.height = this.viewports[0].height = 1
+             
+        }else if(this.splitDir == 'upDown'){ //地图在下方
+            viewer.mainViewport.bottom = this.mapRatio
+            viewer.mainViewport.height = 1-this.mapRatio
+            this.viewports[0].height = this.mapRatio;  
+            
+            viewer.mainViewport.left = this.viewports[0].left = 0
+            viewer.mainViewport.width = this.viewports[0].width = 1
+            
+        }
+        if(this.attachedToViewer){ 
+            viewer.updateScreenSize({forceUpdateSize:true})
+        }
+    }
+    
+    render(params={}){
+       
+        if(!this.visible && !this.attachedToViewer || !this.needRender && !params.force)return
+        
+        let renderer = params.renderer || this.renderer
+        
+        if(params.target){
+            renderer.setRenderTarget(params.target)
+        } 
+        /* if(params.resize){
+            this.emitResizeMsg(new THREE.Vector2(params.width,params.height, viewport:params.viewport))
+        }  */
+        params.clear ? params.clear() : renderer.clear(); 
+        
+        if(!this.attachedToViewer){
+            viewer.dispatchEvent({type: "render.begin",  viewer: this, viewport:this.viewports[0], params }); 
+        }
+        
+        viewer.setCameraLayers(this.camera, ['map','mapObjects'  , 'bothMapAndScene'  ])
+        
+        if(this.mapGradientBG){//渲染背景
+            viewer.scene.cameraBG.layers.set(Potree.config.renderLayers.bg);
+            renderer.render(viewer.scene.scene, viewer.scene.cameraBG);
+        }
+        
+        renderer.render(this.scene, this.camera);
+        renderer.render(viewer.scene.scene, this.camera);
+        
+        //测量线等
+        //params.renderOverlay && params.renderOverlay({camera:this.camera, isMap:true, viewport: this.viewports[0] })//其余如reticule 等场景层
+        params.renderOverlay && params.renderOverlay( $.extend({}, params, { isMap:true }))
+        
+        
+        renderer.setRenderTarget(null)
+        
+        this.needRender = false
+        
+    }
+    
+    
+    /* render(){
+        
+        let camera =  viewer.scene.getActiveCamera();
+
+        viewer.scene.view.position.x += 0.01    
+
+        viewer.setCameraLayers(camera, ['sceneObjects','marker','reticule','skybox' ]) 
+        this.renderer.render(viewer.scene.scene, camera);  
+
+
+    } */
+    
+    
+    
+}
+
+
+
+//本地调试地图白屏是因为代码自动更新了 但没刷新
+

+ 361 - 0
src/custom/viewer/viewerBase.js

@@ -0,0 +1,361 @@
+
+import * as THREE from "../../../libs/three.js/build/three.module.js"; 
+
+import Common from '../utils/Common.js'
+ 
+
+export class ViewerBase extends THREE.EventDispatcher{
+    constructor(domElement, args = {}){
+        super()
+        this.name = args.name
+        this.renderArea = domElement 
+        this.oldResolution = new THREE.Vector2()
+        
+        this.screenSizeInfo = {
+            W:0, H:0, pixelRatio:1 , windowWidth:0, windowHeight:0
+        }
+        
+        this.initContext(args);
+        
+         
+        this.addEventListener('content_changed', ()=>{//画面改变,需要渲染
+            this.needRender = true
+        })
+        
+        
+    }
+    
+    
+    
+    
+    
+    initContext(args){
+        //console.log(`initializing three.js ${THREE.REVISION}`);
+
+        let width = this.renderArea.clientWidth;
+        let height = this.renderArea.clientHeight;
+
+        let contextAttributes = {
+            alpha: true,
+            depth: true,
+            stencil: false,
+            antialias: true, 
+            preserveDrawingBuffer: true,
+            powerPreference: "high-performance",
+        }; 
+
+        let canvas = document.createElement("canvas");
+                                                             
+
+        let context = canvas.getContext('webgl', contextAttributes );
+
+        this.renderer = new THREE.WebGLRenderer({
+            alpha: true, //支持透明
+            premultipliedAlpha: false, 
+            canvas: canvas,
+            context: context, 
+        }); 
+        
+        this.renderer.sortObjects = true; //原先false 打开了renderOrder才奏效
+        //this.renderer.setSize(width, height);
+        this.renderer.autoClear = args.autoClear || false;
+
+
+        //args.clearColor = args.clearColor || '#aa0033'
+        args.clearColor && this.renderer.setClearColor(args.clearColor)
+        this.renderArea.appendChild(this.renderer.domElement);
+        this.renderer.domElement.tabIndex = '2222';
+        this.renderer.domElement.style.position = 'absolute';
+        this.renderer.domElement.addEventListener('mousedown', () => {
+            this.renderer.domElement.focus();
+        });
+        //this.renderer.domElement.focus();
+
+        // NOTE: If extension errors occur, pass the string into this.renderer.extensions.get(x) before enabling
+        // enable frag_depth extension for the interpolation shader, if available
+        let gl = this.renderer.getContext();
+        
+         
+        gl.getExtension('EXT_frag_depth');
+        gl.getExtension('WEBGL_depth_texture');
+        gl.getExtension('WEBGL_color_buffer_float'); 	// Enable explicitly for more portability, EXT_color_buffer_float is the proper name in WebGL 2
+        
+        if(gl.createVertexArray == null){
+            let extVAO = gl.getExtension('OES_vertex_array_object');
+
+            if(!extVAO){
+                throw new Error("OES_vertex_array_object extension not supported");
+            }
+
+            gl.createVertexArray = extVAO.createVertexArrayOES.bind(extVAO);
+            gl.bindVertexArray = extVAO.bindVertexArrayOES.bind(extVAO);
+        }
+
+                                                            
+                           
+ 
+                                                
+                                      
+                               
+                                                                            
+                  
+       /*  let oldClear = gl.clear; 
+        gl.clear = (bits)=>{
+             console.error('clear')
+        }   
+ */
+        
+    }
+    
+    
+    
+    
+    updateScreenSize(o={}) { //有可能需要让viewport来判断,当窗口大小不变但viewport大小变时 
+           
+        var render = false, ratio, w, h;
+        //记录应当render的大小
+        if (o.width != void 0 && o.height != void 0) {
+            w = o.width
+            h = o.height
+            render = true
+            ratio = 1
+        }else { 
+
+            w = this.renderArea.clientWidth;
+            h = this.renderArea.clientHeight
+            
+
+             
+            if(w !== this.screenSizeInfo.W || h !== this.screenSizeInfo.H || o.forceUpdateSize || this.screenSizeInfo.pixelRatio != window.devicePixelRatio){
+                this.screenSizeInfo.W = w 
+                this.screenSizeInfo.H = h 
+                render = true 
+                this.screenSizeInfo.pixelRatio = window.devicePixelRatio  //如果player放在小窗口了,也要监测devicePixelRatio,因为缩放时client宽高不会改变
+                //config.isMobile ? (ratio = Math.min(window.devicePixelRatio, 2)) : (ratio = window.devicePixelRatio)
+                ratio = window.devicePixelRatio
+            }     
+        }
+        if (render) { 
+            this.setSize(w, h, ratio); 
+        } 
+    }   
+
+
+    /* updateScreenSize(o={}) { //容易出错。。
+           
+        var render = false, ratio, w, h;
+        //记录应当render的大小
+        if (o.width != void 0 && o.height != void 0) {
+            w = o.width
+            h = o.height
+            render = true
+            ratio = 1
+        }else { 
+            w = this.renderArea.clientWidth;
+            h = this.renderArea.clientHeight 
+
+            let refreshWin = ()=>{
+                this.screenSizeInfo.windowWidth = window.innerWidth
+                this.screenSizeInfo.windowHeight = window.innerHeight
+            }
+ 
+            let prepareToRender = ()=>{
+                w = this.renderArea.clientWidth;
+                h = this.renderArea.clientHeight
+                this.screenSizeInfo.W = w 
+                this.screenSizeInfo.H = h 
+                refreshWin() //render后才refreshWin
+                render = true   
+                this.screenSizeInfo.pixelRatio = window.devicePixelRatio  //如果player放在小窗口了,也要监测devicePixelRatio,因为缩放时client宽高不会改变
+                //config.isMobile ? (ratio = Math.min(window.devicePixelRatio, 2)) : (ratio = window.devicePixelRatio)
+                ratio = window.devicePixelRatio
+                //console.log('setSize11', w) 
+                o.forceUpdateSize = false ;//防止再次render
+            }
+
+            let ifNeedUpdate = ()=>{
+                w = this.renderArea.clientWidth;
+                h = this.renderArea.clientHeight
+                return o.forceUpdateSize || w !== this.screenSizeInfo.W || h !== this.screenSizeInfo.H || this.screenSizeInfo.pixelRatio != window.devicePixelRatio
+            }
+            
+            if(ifNeedUpdate()){
+                //当只有一个有效viewport时,且因为改变整个窗口大小而触发的话,延时
+                let canInterval = !o.forceUpdateSize && this.viewports.filter(e=>e.active).length==1 && (this.screenSizeInfo.windowWidth != window.innerWidth || this.screenSizeInfo.windowHeight != window.innerHeight  ) 
+                  
+                if(canInterval){
+                    Common.intervalTool.isWaiting('updateScreenSize', ()=>{ //延时update,防止崩溃 , 未到时间就拦截(第一次直接执行)
+                         if(ifNeedUpdate()){
+                            prepareToRender()
+                            return true
+                         }
+                    }, 500) 
+                }else{  
+                    //console.log('soon', window.innerWidth, this.screenSizeInfo.windowWidth)
+                    prepareToRender() 
+                } 
+                 
+            }    
+            
+        }
+        if (render) { 
+            this.setSize(w, h, ratio); 
+        } 
+    }  */
+
+     
+    setSize(width, height, devicePixelRatio, onlyForTarget){ 
+        //console.log('setSize', width) 
+        if(!onlyForTarget){//onlyForTarget表示不更改当前renderer,只是为了rendertarget才要改变viewport
+                       
+            this.renderer.setSize(width, height, null, devicePixelRatio); // resize之后会自动clear(似乎因为setScissor ),所以一定要立刻绘制,所以setSize要在cameraChanged、update之前
+              
+                                                     
+                                                                                                                                                                                 
+              
+                                                                        
+        }
+       
+        this.composer && this.composer.setSize(width, height);
+        
+        if(this.viewports){
+            this.viewports.forEach((view,i)=>{
+                if(!view.active)return
+                
+                var width_ = width * view.width
+                var height_ = height * view.height
+                
+                if(height_ == 0)return  //avoid NAN
+                
+                view.setResolution(Math.ceil(width_), Math.ceil(height_), width, height ) //本来应该是floor,但是这样奇数时会少一个像素,导致向左移一个像素且宽度少1。现在则多绘制1个像素,超出的1个像素应该不会绘制出来(但不知道其他地方是否有偏差,比如pick时)
+                let aspect = width_ / height_;  //camera的参数精确些,不用视口的归整的resolution像素值,否则hasChange无法为true, 导致canvasResize了但map没update从而闪烁
+                view.camera.aspect = aspect;
+                
+                if(view.camera.type == "OrthographicCamera"){
+                     
+                    /* //不改宽度 同4dkk
+                    var heightHalf = view.camera.right / aspect 
+                    view.camera.top = heightHalf 
+                    view.camera.bottom = -heightHalf  */
+                    //高宽都改 使大小不随视口大小改变  navvis (直接和视口大小一致即可,通过zoom来定大小)
+                     
+                    view.camera.left = -width_/2 
+                    view.camera.right = width_/2
+                    view.camera.bottom = -height_/2;
+                    view.camera.top = height_/2
+                    
+                    
+                }else{   
+                
+                
+                }
+                
+                view.camera.updateProjectionMatrix();
+            })
+        }
+        
+        
+        if(!onlyForTarget){//因为onlyForTarget不传递devicePixelRatio所以不发送了
+            this.emitResizeMsg({viewport:this.viewports[0],  deviceRatio:devicePixelRatio})
+        }
+         
+    } 
+    
+    emitResizeMsg(e){//切换viewport渲染时就发送一次, 通知一些材质更新resolution。  
+        if(!e.viewport.resolution.equals(this.oldResolution)){ 
+            this.dispatchEvent($.extend(e, {type:'resize'})) 
+            this.oldResolution.copy(e.viewport.resolution)
+        }
+        
+        
+    }
+    
+    
+    
+    cameraChanged() {//判断相机是否改变
+        var changed = false;
+        /* if(this.needRender){
+            this.needRender = false
+            return true
+        } */
+        for(let i=0,j=this.viewports.length;i<j;i++){
+            let changeInfo = this.viewports[i].cameraChanged()
+            if(changeInfo.changed){
+                changed = true
+                //if(!this.changeTime ||this.changeTime<100){ 
+                    this.dispatchEvent({
+                        type: "camera_changed", 
+                        camera: this.viewports[i].camera,
+                        viewport : this.viewports[i],
+                        changeInfo 
+                    }) 
+                    //this.changeTime = (this.changeTime || 0) +1
+                //} 
+            }                
+        }
+        return changed
+    }
+    
+    
+    
+    
+    
+    makeScreenshot( size,  viewports, compressRatio){//暂时不要指定viewports渲染,但也可以
+        
+       
+        let {width, height} = size;
+        
+        
+        
+        
+        /* let oldBudget = Potree.pointBudget;
+        Potree.pointBudget = Math.max(10 * 1000 * 1000, 2 * oldBudget);
+        let result = Potree.updatePointClouds(this.scene.pointclouds, camera, size );
+        Potree.pointBudget = oldBudget;
+         
+         
+		this.dispatchEvent({ //resize everything  such as lines  targets
+            type: 'resize', 
+            resolution: new THREE.Vector2(width,height), 
+        });*/
+       
+		let target = new THREE.WebGLRenderTarget(width, height, {
+			format: THREE.RGBAFormat,
+		});
+ 
+	 
+		this.setSize(width, height,1,true) 
+        
+		this.render({
+            target ,
+            //camera ,
+            viewports: viewports || this.viewports,
+            screenshot : true, 
+            width ,
+            height, 
+            resize :true //需要resize
+        });
+        let dataUrl = Potree.Utils.renderTargetToDataUrl(target, width, height, this.renderer, compressRatio)
+        
+        
+		/* let pixelCount = width * height;
+		let buffer = new Uint8Array(4 * pixelCount);
+
+		this.renderer.readRenderTargetPixels(target, 0, 0, width, height, buffer);
+
+        let dataUrl = Potree.Utils.pixelsArrayToDataUrl(buffer, width, height, compressRatio) */
+
+ 
+		target.dispose();
+        
+        //resize back 
+        //this.updateScreenSize({forceUpdateSize:true})   
+        
+		return {
+			width, 
+			height,
+			dataUrl 
+		};
+	}
+    
+}

+ 1 - 1
src/navigation/FirstPersonControls.js

@@ -19,7 +19,7 @@ import {Utils} from "../utils.js";
 import {EventDispatcher} from "../EventDispatcher.js";
 
 
-export class FirstPersonControls extends EventDispatcher {
+export class FirstPersonControls extends EventDispatcher {//old
 	constructor (viewer) {
 		super();
 

+ 6 - 4
src/navigation/FirstPersonControlsNew.js

@@ -114,11 +114,13 @@ export class FirstPersonControls extends THREE.EventDispatcher {
                     let distance = camera.position.distanceTo(this.intersectStart.location)                                               //不按下ctrl的话                 
                       
                     //按照orbitControl的方式旋转:
-                    let rotationSpeed = 2.5;  
-                    
-                    this.yawDelta -= e.drag.pointerDelta.x * rotationSpeed;
-                    this.pitchDelta += e.drag.pointerDelta.y * rotationSpeed;
+                    /* let rotationSpeed = 1//2.5;   
+                    this.yawDelta += e.drag.pointerDelta.x * rotationSpeed;
+                    this.pitchDelta -= e.drag.pointerDelta.y * rotationSpeed; */
 
+                    //旋转方向和偏移量尽量和在漫游点处旋转方式的一样
+                    this.yawDelta += e.drag.pointerDelta.x / 700 * viewport.resolution.x
+                    this.pitchDelta -= e.drag.pointerDelta.y / 700 * viewport.resolution.y
                    
                     //先更新一下相机:    
                     this.update() 

+ 1 - 1
src/utils/TransformationTool.js

@@ -2,7 +2,7 @@
 import * as THREE from "../../libs/three.js/build/three.module.js";
 import {Utils} from "../utils.js";
 
-export class TransformationTool {
+export class TransformationTool {//old
 	constructor(viewer) {
 		this.viewer = viewer;
 

+ 943 - 0
src/utils/TransformationToolNew.js

@@ -0,0 +1,943 @@
+
+import * as THREE from "../../libs/three.js/build/three.module.js";
+import {Utils} from "../utils.js";
+
+
+
+//add-------------------------------------
+const OpaWhenNotSelect = 0.6
+const ScaleRatio = 8
+const OutlineColor = 0x666666
+//----------------------------------------
+const hideFocusHandles = true//add
+
+
+export class TransformationTool {
+	constructor(viewer) {
+		this.viewer = viewer;
+
+		this.scene = new THREE.Scene();
+        this.modesEnabled = {scale:true,translation:true,rotation:true}//add
+		this.selection = [];
+		this.pivot = new THREE.Vector3();
+		this.dragging = false;
+		this.showPickVolumes = false;
+
+		this.viewer.inputHandler.registerInteractiveScene(this.scene);
+		this.viewer.inputHandler.addEventListener('selection_changed', (e) => {
+			for(let selected of this.selection){
+				this.viewer.inputHandler.blacklist.delete(selected);
+			}
+
+			this.selection = e.selection;
+
+			for(let selected of this.selection){
+				this.viewer.inputHandler.blacklist.add(selected);
+			}
+
+		});
+
+
+        this.viewer.addEventListener('global_touchstart',(e)=>{ //add
+            this.update()
+        })
+
+
+
+		let red = Potree.config.axis.x.color
+		let green = Potree.config.axis.y.color
+		let blue = Potree.config.axis.z.color
+		
+		this.activeHandle = null;
+		this.scaleHandles = {
+			"scale.x+": {name: "scale.x+", node: new THREE.Object3D(), color: red, alignment: [+1, +0, +0]},
+			"scale.x-": {name: "scale.x-", node: new THREE.Object3D(), color: red, alignment: [-1, +0, +0]},
+			"scale.y+": {name: "scale.y+", node: new THREE.Object3D(), color: green, alignment: [+0, +1, +0]},
+			"scale.y-": {name: "scale.y-", node: new THREE.Object3D(), color: green, alignment: [+0, -1, +0]},
+			"scale.z+": {name: "scale.z+", node: new THREE.Object3D(), color: blue, alignment: [+0, +0, +1]},
+			"scale.z-": {name: "scale.z-", node: new THREE.Object3D(), color: blue, alignment: [+0, +0, -1]},
+		};
+		this.focusHandles = {
+			"focus.x+": {name: "focus.x+", node:  new THREE.Object3D(), color: red, alignment: [+1, +0, +0]},
+			"focus.x-": {name: "focus.x-", node:  new THREE.Object3D(), color: red, alignment: [-1, +0, +0]},
+			"focus.y+": {name: "focus.y+", node:  new THREE.Object3D(), color: green, alignment: [+0, +1, +0]},
+			"focus.y-": {name: "focus.y-", node:  new THREE.Object3D(), color: green, alignment: [+0, -1, +0]},
+			"focus.z+": {name: "focus.z+", node:  new THREE.Object3D(), color: blue, alignment: [+0, +0, +1]},
+			"focus.z-": {name: "focus.z-", node:  new THREE.Object3D(), color: blue, alignment: [+0, +0, -1]},
+		};
+		this.translationHandles = {
+			"translation.x": {name: "translation.x", node:  new THREE.Object3D(), color: red, alignment: [1, 0, 0]},
+			"translation.y": {name: "translation.y", node:  new THREE.Object3D(), color: green, alignment: [0, 1, 0]},
+			"translation.z": {name: "translation.z", node:  new THREE.Object3D(), color: blue, alignment: [0, 0, 1]},
+		};
+		this.rotationHandles = {
+			"rotation.x": {name: "rotation.x", node:  new THREE.Object3D(), color: red, alignment: [1, 0, 0]},
+			"rotation.y": {name: "rotation.y", node:  new THREE.Object3D(), color: green, alignment: [0, 1, 0]},
+			"rotation.z": {name: "rotation.z", node:  new THREE.Object3D(), color: blue, alignment: [0, 0, 1]},
+		};
+		this.handles = Object.assign({}, this.scaleHandles,    hideFocusHandles?{}:this.focusHandles, this.translationHandles, this.rotationHandles);
+		this.pickVolumes = [];
+
+		this.initializeScaleHandles();
+		this.initializeFocusHandles();
+		this.initializeTranslationHandles();
+		this.initializeRotationHandles();
+
+
+		let boxFrameGeometry = new THREE.Geometry();
+		{
+			// bottom
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, 0.5));
+			// top
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, 0.5));
+			// sides
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, 0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(0.5, 0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, -0.5, -0.5));
+			boxFrameGeometry.vertices.push(new THREE.Vector3(-0.5, 0.5, -0.5));
+		}
+		this.frame = new THREE.LineSegments(boxFrameGeometry, new THREE.LineBasicMaterial({color: 0xffff00}));
+		this.scene.add(this.frame);
+        viewer.setObjectLayers(this.scene, 'transformationTool' )
+		
+        this.scene.traverse(e=>{
+            e.pickDontCheckDis = true; //pick时不需要识别是否在点云之上
+        })
+        
+        
+	}
+  
+  
+    setModeEnable(mode,enable){//xzw add
+        let handels = this[mode + 'Handles']
+        if(!handels)return
+        for(let o in handels){
+            handels[o].node.visible = !!enable
+        }  
+        this.modesEnabled[mode] = !!enable
+    }
+  
+  
+	initializeScaleHandles(){
+		let sgSphere = new THREE.SphereGeometry(1, 32, 32);
+		let sgLowPolySphere = new THREE.SphereGeometry(1, 16, 16);
+
+		for(let handleName of Object.keys(this.scaleHandles)){
+			let handle = this.scaleHandles[handleName];
+			let node = handle.node;
+			this.scene.add(node);
+			node.position.set(...handle.alignment).multiplyScalar(0.5);
+
+			let material = new THREE.MeshBasicMaterial({
+				color: handle.color,
+				opacity: OpaWhenNotSelect,
+				transparent: true
+				});
+
+			let outlineMaterial = new THREE.MeshBasicMaterial({
+				color: OutlineColor, 
+				side: THREE.BackSide,
+				opacity: OpaWhenNotSelect,
+				transparent: true});
+
+			let pickMaterial = new THREE.MeshNormalMaterial({
+				opacity: 0.2,
+				transparent: true,
+				visible: this.showPickVolumes});
+
+			let sphere = new THREE.Mesh(sgSphere, material);
+			sphere.scale.set(2, 2, 2 );
+			sphere.name = `${handleName}.handle`;
+			node.add(sphere);
+			
+			let outline = new THREE.Mesh(sgSphere, outlineMaterial);
+			outline.scale.set(1.1, 1.1, 1.1);
+			outline.name = `${handleName}.outline`;
+			sphere.add(outline);
+
+			let pickSphere = new THREE.Mesh(sgLowPolySphere, pickMaterial);
+			pickSphere.name = `${handleName}.pick_volume`;
+			pickSphere.scale.set(2, 2, 2);
+			sphere.add(pickSphere);
+			pickSphere.handle = handleName;
+			this.pickVolumes.push(pickSphere);
+
+			node.setOpacity = (target) => {
+				let opacity = {x: material.opacity};
+				let t = new TWEEN.Tween(opacity).to({x: target}, 100);
+				t.onUpdate(() => {
+					sphere.visible = opacity.x > 0;
+					pickSphere.visible = opacity.x > 0;
+					material.opacity = opacity.x;
+					outlineMaterial.opacity = opacity.x;
+					pickSphere.material.opacity = opacity.x * 0.5;
+				});
+				t.start();
+			};
+
+			pickSphere.addEventListener("drag", (e) => this.dragScaleHandle(e));
+			pickSphere.addEventListener("drop", (e) => this.dropScaleHandle(e));
+
+			pickSphere.addEventListener("mouseover", e => {
+				//node.setOpacity(1);
+			});
+
+			pickSphere.addEventListener("click", e => {
+				//e.consume();
+			});
+
+			pickSphere.addEventListener("mouseleave", e => {
+				//node.setOpacity(OpaWhenNotSelect);
+			});
+		}
+	}
+
+	initializeFocusHandles(){
+        if(hideFocusHandles)return//add
+		//let sgBox = new THREE.BoxGeometry(1, 1, 1);
+		let sgPlane = new THREE.PlaneGeometry(4, 4, 1, 1);
+		let sgLowPolySphere = new THREE.SphereGeometry(1, 16, 16);
+
+		let texture = new THREE.TextureLoader().load(`${exports.resourcePath}/icons/eye_2.png`);
+
+		for(let handleName of Object.keys(this.focusHandles)){
+			let handle = this.focusHandles[handleName];
+			let node = handle.node;
+			this.scene.add(node);
+			let align = handle.alignment;
+
+			//node.lookAt(new THREE.Vector3().addVectors(node.position, new THREE.Vector3(...align)));
+			node.lookAt(new THREE.Vector3(...align));
+
+			let off = 0.8;
+			if(align[0] === 1){
+				node.position.set(1, off, -off).multiplyScalar(0.5);
+				node.rotation.z = Math.PI / 2;
+			}else if(align[0] === -1){
+				node.position.set(-1, -off, -off).multiplyScalar(0.5);
+				node.rotation.z = Math.PI / 2;
+			}else if(align[1] === 1){
+				node.position.set(-off, 1, -off).multiplyScalar(0.5);
+				node.rotation.set(Math.PI / 2, Math.PI, 0.0);
+			}else if(align[1] === -1){
+				node.position.set(off, -1, -off).multiplyScalar(0.5);
+				node.rotation.set(Math.PI / 2, 0.0, 0.0);
+			}else if(align[2] === 1){
+				node.position.set(off, off, 1).multiplyScalar(0.5);
+			}else if(align[2] === -1){
+				node.position.set(-off, off, -1).multiplyScalar(0.5);
+			}
+
+			let material = new THREE.MeshBasicMaterial({
+				color: handle.color,
+				opacity: 0,
+				transparent: true,
+				map: texture
+			});
+
+			//let outlineMaterial = new THREE.MeshBasicMaterial({
+			//	color: 0x000000, 
+			//	side: THREE.BackSide,
+			//	opacity: 0,
+			//	transparent: true});
+
+			let pickMaterial = new THREE.MeshNormalMaterial({
+				//opacity: 0,
+				transparent: true,
+				visible: this.showPickVolumes});
+
+			let box = new THREE.Mesh(sgPlane, material);
+			box.name = `${handleName}.handle`;
+			box.scale.set(1.5, 1.5, 1.5);
+			box.position.set(0, 0, 0);
+			box.visible = false;
+			node.add(box);
+			//handle.focusNode = box;
+			
+			//let outline = new THREE.Mesh(sgPlane, outlineMaterial);
+			//outline.scale.set(1.4, 1.4, 1.4);
+			//outline.name = `${handleName}.outline`;
+			//box.add(outline);
+
+			let pickSphere = new THREE.Mesh(sgLowPolySphere, pickMaterial);
+			pickSphere.name = `${handleName}.pick_volume`;
+			pickSphere.scale.set(2, 2, 2);
+			box.add(pickSphere);
+			pickSphere.handle = handleName;
+			this.pickVolumes.push(pickSphere);
+
+			node.setOpacity = (target) => {
+				let opacity = {x: material.opacity};
+				let t = new TWEEN.Tween(opacity).to({x: target}, 100);
+				t.onUpdate(() => {
+					pickSphere.visible = opacity.x > 0;
+					box.visible = opacity.x > 0;
+					material.opacity = opacity.x;
+					//outlineMaterial.opacity = opacity.x;
+					pickSphere.material.opacity = opacity.x * 0.5;
+				});
+				t.start();
+			};
+
+			//pickSphere.addEventListener("drag", e => {});
+
+			pickSphere.addEventListener("mouseup", e => {
+				//e.consume();
+			});
+
+			pickSphere.addEventListener("mousedown", e => {
+				//e.consume();
+			});
+
+			pickSphere.addEventListener("click", e => {
+				//e.consume();
+
+				let selected = this.selection[0];
+				let maxScale = Math.max(...selected.scale.toArray());
+				let minScale = Math.min(...selected.scale.toArray());
+				let handleLength = Math.abs(selected.scale.dot(new THREE.Vector3(...handle.alignment)));
+				let alignment = new THREE.Vector3(...handle.alignment).multiplyScalar(2 * maxScale / handleLength);
+				alignment.applyMatrix4(selected.matrixWorld);
+				let newCamPos = alignment;
+				let newCamTarget = selected.getWorldPosition(new THREE.Vector3());
+
+				Utils.moveTo(this.viewer.scene, newCamPos, newCamTarget);
+			});
+
+			pickSphere.addEventListener("mouseover", e => {
+				//box.setOpacity(1);
+			});
+
+			pickSphere.addEventListener("mouseleave", e => {
+				//box.setOpacity(OpaWhenNotSelect);
+			});
+		}
+	}
+
+	initializeTranslationHandles(){
+
+		let boxGeometry = new THREE.BoxGeometry(1, 1, 1);
+
+		for(let handleName of Object.keys(this.translationHandles)){
+			let handle = this.handles[handleName];
+			let node = handle.node;
+			this.scene.add(node);
+
+			let material = new THREE.MeshBasicMaterial({
+				color: handle.color,
+				opacity: OpaWhenNotSelect,
+				transparent: true});
+
+			let outlineMaterial = new THREE.MeshBasicMaterial({
+				color: OutlineColor, 
+				side: THREE.BackSide,
+				opacity: OpaWhenNotSelect,
+				transparent: true});
+
+			let pickMaterial = new THREE.MeshNormalMaterial({
+				opacity: 0.2,
+				transparent: true,
+				visible: this.showPickVolumes
+			});
+
+			let box = new THREE.Mesh(boxGeometry, material);
+			box.name = `${handleName}.handle`;
+			box.scale.set(1, 1, 72);
+			box.lookAt(new THREE.Vector3(...handle.alignment));
+			box.renderOrder = 10;
+			node.add(box);
+			handle.translateNode = box;
+
+			let outline = new THREE.Mesh(boxGeometry, outlineMaterial);
+			outline.name = `${handleName}.outline`;
+			outline.scale.set(1.3, 1.3, 1.01);
+			outline.renderOrder = 0;
+			box.add(outline);
+
+			let pickVolume = new THREE.Mesh(boxGeometry, pickMaterial);
+			pickVolume.name = `${handleName}.pick_volume`;
+			pickVolume.scale.set(4, 4, 1.1);
+			pickVolume.handle = handleName;
+			box.add(pickVolume);
+			this.pickVolumes.push(pickVolume);
+
+			node.setOpacity = (target) => {
+				let opacity = {x: material.opacity};
+				let t = new TWEEN.Tween(opacity).to({x: target}, 100);
+				t.onUpdate(() => {
+					box.visible = opacity.x > 0;
+					pickVolume.visible = opacity.x > 0;
+					material.opacity = opacity.x;
+					outlineMaterial.opacity = opacity.x;
+					pickMaterial.opacity = opacity.x * 0.5;
+				});
+				t.start();
+			};
+
+			pickVolume.addEventListener("drag", (e) => {this.dragTranslationHandle(e)});
+			pickVolume.addEventListener("drop", (e) => {this.dropTranslationHandle(e)});
+		}
+	}
+
+	initializeRotationHandles(){
+		let adjust = 1.5;
+		let torusGeometry = new THREE.TorusGeometry(1, adjust * 0.015, 8, 64, Math.PI / 2);
+		let outlineGeometry = new THREE.TorusGeometry(1, adjust * 0.018, 8, 64, Math.PI / 2);
+		let pickGeometry = new THREE.TorusGeometry(1, adjust * 0.04, 6, 4, Math.PI / 2);
+
+		for(let handleName of Object.keys(this.rotationHandles)){
+			let handle = this.handles[handleName];
+			let node = handle.node;
+			this.scene.add(node);
+
+			let material = new THREE.MeshBasicMaterial({
+				color: handle.color,
+				opacity: OpaWhenNotSelect,
+				transparent: true
+            });
+
+			let outlineMaterial = new THREE.MeshBasicMaterial({
+				color: OutlineColor, 
+				side: THREE.BackSide,
+				opacity: OpaWhenNotSelect,
+				transparent: true
+            });
+
+			let pickMaterial = new THREE.MeshNormalMaterial({
+				opacity: 0.2,
+				transparent: true,
+				visible: this.showPickVolumes
+			});
+
+			let box = new THREE.Mesh(torusGeometry, material);
+			box.name = `${handleName}.handle`;
+			box.scale.set(30, 30, 30);
+			box.lookAt(new THREE.Vector3(...handle.alignment));
+			node.add(box);
+			handle.translateNode = box;
+
+			let outline = new THREE.Mesh(outlineGeometry, outlineMaterial);
+			outline.name = `${handleName}.outline`;
+			outline.scale.set(1, 1, 1);
+			outline.renderOrder = 0;
+			box.add(outline);
+
+			let pickVolume = new THREE.Mesh(pickGeometry, pickMaterial);
+			pickVolume.name = `${handleName}.pick_volume`;
+			pickVolume.scale.set(1, 1, 1);
+			pickVolume.handle = handleName;
+			box.add(pickVolume);
+			this.pickVolumes.push(pickVolume);
+
+			node.setOpacity = (target) => {
+				let opacity = {x: material.opacity};
+				let t = new TWEEN.Tween(opacity).to({x: target}, 100);
+				t.onUpdate(() => {
+					box.visible = opacity.x > 0;
+					pickVolume.visible = opacity.x > 0;
+					material.opacity = opacity.x;
+					outlineMaterial.opacity = opacity.x;
+					pickMaterial.opacity = opacity.x * 0.5;
+				});
+				t.start();
+			};
+
+
+			//pickVolume.addEventListener("mouseover", (e) => {
+			//	//let a = this.viewer.scene.getActiveCamera().getWorldDirection(new THREE.Vector3()).dot(pickVolume.getWorldDirection(new THREE.Vector3()));
+			//	console.log(pickVolume.getWorldDirection(new THREE.Vector3()));
+			//});
+			
+			pickVolume.addEventListener("drag", (e) => {this.dragRotationHandle(e)});
+			pickVolume.addEventListener("drop", (e) => {this.dropRotationHandle(e)});
+		}
+	}
+
+	dragRotationHandle(e){
+		let drag = e.drag;
+		let handle = this.activeHandle;
+		let camera = this.viewer.scene.getActiveCamera();
+
+		if(!handle){
+			return
+		};
+
+		let localNormal = new THREE.Vector3(...handle.alignment);
+		let n = new THREE.Vector3();
+		n.copy(new THREE.Vector4(...localNormal.toArray(), 0).applyMatrix4(handle.node.matrixWorld));
+		n.normalize();
+
+		if (!drag.intersectionStart){
+
+			//this.viewer.scene.scene.remove(this.debug);
+			//this.debug = new THREE.Object3D();
+			//this.viewer.scene.scene.add(this.debug);
+			//Utils.debugSphere(this.debug, drag.location, 3, 0xaaaaaa);
+			//let debugEnd = drag.location.clone().add(n.clone().multiplyScalar(20));
+			//Utils.debugLine(this.debug, drag.location, debugEnd, 0xff0000);
+
+			drag.intersectionStart = drag.location;
+			drag.objectStart = drag.object.getWorldPosition(new THREE.Vector3());
+			drag.handle = handle;
+
+			let plane = new THREE.Plane().setFromNormalAndCoplanarPoint(n, drag.intersectionStart);
+
+			drag.dragPlane = plane;
+			drag.pivot = drag.intersectionStart;
+		}else{
+			handle = drag.handle;
+		}
+
+		this.dragging = true;
+
+		let pointer = this.viewer.inputHandler.pointer
+		let domElement = this.viewer.renderer.domElement;
+		let ray = Utils.mouseToRay(pointer, camera, domElement.clientWidth, domElement.clientHeight);
+		
+		let I = ray.intersectPlane(drag.dragPlane, new THREE.Vector3());
+
+		if (I) {
+			let center = this.scene.getWorldPosition(new THREE.Vector3());
+			let from = drag.pivot;
+			let to = I;
+
+			let v1 = from.clone().sub(center).normalize();
+			let v2 = to.clone().sub(center).normalize();
+
+			let angle = Math.acos(v1.dot(v2));
+			let sign = Math.sign(v1.cross(v2).dot(n));
+			angle = angle * sign;
+			if (Number.isNaN(angle)) {
+				return;
+			}
+
+			let normal = new THREE.Vector3(...handle.alignment);
+			for (let selection of this.selection) {
+				selection.rotateOnAxis(normal, angle);
+				selection.dispatchEvent({
+					type: "orientation_changed",
+					object: selection
+				});
+			}
+
+			drag.pivot = I;
+		}
+	}
+
+	dropRotationHandle(e){
+		this.dragging = false;
+		this.setActiveHandle(null);
+	}
+
+	dragTranslationHandle(e){
+		let drag = e.drag;
+		let handle = this.activeHandle;
+		let camera = this.viewer.scene.getActiveCamera();
+			
+		if(!drag.intersectionStart && handle){
+			drag.intersectionStart = drag.location;
+			drag.objectStart = drag.object.getWorldPosition(new THREE.Vector3());
+
+			let start = drag.intersectionStart;
+			let dir = new THREE.Vector4(...handle.alignment, 0).applyMatrix4(this.scene.matrixWorld);
+			let end = new THREE.Vector3().addVectors(start, dir);
+			let line = new THREE.Line3(start.clone(), end.clone());
+			drag.line = line;
+
+			let camOnLine = line.closestPointToPoint(camera.position, false, new THREE.Vector3());
+			let normal = new THREE.Vector3().subVectors(camera.position, camOnLine);
+			let plane = new THREE.Plane().setFromNormalAndCoplanarPoint(normal, drag.intersectionStart);
+			drag.dragPlane = plane;
+			drag.pivot = drag.intersectionStart;
+		}else{
+			handle = drag.handle;
+		}
+
+		this.dragging = true;
+
+		{
+			let pointer = this.viewer.inputHandler.pointer
+			let domElement = this.viewer.renderer.domElement;
+			let ray = Utils.mouseToRay(pointer, camera, domElement.clientWidth, domElement.clientHeight);
+			let I = ray.intersectPlane(drag.dragPlane, new THREE.Vector3());
+
+			if (I) {
+				let iOnLine = drag.line.closestPointToPoint(I, false, new THREE.Vector3());
+
+				let diff = new THREE.Vector3().subVectors(iOnLine, drag.pivot);
+
+				for (let selection of this.selection) {
+					selection.position.add(diff);
+					selection.dispatchEvent({
+						type: "position_changed",
+						object: selection
+					});
+				}
+
+				drag.pivot = drag.pivot.add(diff);
+			}
+		}
+	}
+
+	dropTranslationHandle(e){
+		this.dragging = false;
+		this.setActiveHandle(null);
+	}
+
+	dropScaleHandle(e){
+		this.dragging = false;
+		this.setActiveHandle(null);
+	}
+
+	dragScaleHandle(e){
+		let drag = e.drag;
+		let handle = this.activeHandle;
+		let camera = this.viewer.scene.getActiveCamera();
+
+		if(!drag.intersectionStart){
+			drag.intersectionStart = drag.location;
+			drag.objectStart = drag.object.getWorldPosition(new THREE.Vector3());
+			drag.handle = handle;
+
+			let start = drag.intersectionStart;
+			let dir = new THREE.Vector4(...handle.alignment, 0).applyMatrix4(this.scene.matrixWorld);
+			let end = new THREE.Vector3().addVectors(start, dir);
+			let line = new THREE.Line3(start.clone(), end.clone());
+			drag.line = line;
+
+			let camOnLine = line.closestPointToPoint(camera.position, false, new THREE.Vector3());
+			let normal = new THREE.Vector3().subVectors(camera.position, camOnLine);
+			let plane = new THREE.Plane().setFromNormalAndCoplanarPoint(normal, drag.intersectionStart);
+			drag.dragPlane = plane;
+			drag.pivot = drag.intersectionStart;
+
+			//Utils.debugSphere(viewer.scene.scene, drag.pivot, 0.05);
+		}else{
+			handle = drag.handle;
+		}
+
+		this.dragging = true;
+
+		{
+			let pointer = this.viewer.inputHandler.pointer
+			let domElement = this.viewer.renderer.domElement;
+			let ray = Utils.mouseToRay(pointer, camera, domElement.clientWidth, domElement.clientHeight);
+			let I = ray.intersectPlane(drag.dragPlane, new THREE.Vector3());
+
+			if (I) {
+				let iOnLine = drag.line.closestPointToPoint(I, false, new THREE.Vector3());
+				let direction = handle.alignment.reduce( (a, v) => a + v, 0);
+
+				let toObjectSpace = this.selection[0].matrixWorld.clone().invert();
+				let iOnLineOS = iOnLine.clone().applyMatrix4(toObjectSpace);
+				let pivotOS = drag.pivot.clone().applyMatrix4(toObjectSpace);
+				let diffOS = new THREE.Vector3().subVectors(iOnLineOS, pivotOS);
+				let dragDirectionOS = diffOS.clone().normalize();
+				if(iOnLine.distanceTo(drag.pivot) === 0){
+					dragDirectionOS.set(0, 0, 0);
+				}
+				let dragDirection = dragDirectionOS.dot(new THREE.Vector3(...handle.alignment));
+
+				let diff = new THREE.Vector3().subVectors(iOnLine, drag.pivot);
+				let diffScale = new THREE.Vector3(...handle.alignment).multiplyScalar(diff.length() * direction * dragDirection);
+				let diffPosition = diff.clone().multiplyScalar(0.5);
+   
+ 
+ 
+				for (let selection of this.selection) {
+                    //xzw 改:否则不跟手
+                    let diffScale_ = diffScale.clone().divide(selection.boundingBox.getSize(new THREE.Vector3))
+					selection.scale.add(diffScale_);
+                    //selection.scale.add(diffScale);
+					selection.scale.x = Math.max(0.1, selection.scale.x);
+					selection.scale.y = Math.max(0.1, selection.scale.y);
+					selection.scale.z = Math.max(0.1, selection.scale.z);
+					selection.position.add(diffPosition);
+                    
+                    
+					selection.dispatchEvent({
+						type: "position_changed",
+						object: selection
+					});
+					selection.dispatchEvent({
+						type: "scale_changed",
+						object: selection
+					});
+				}
+
+				drag.pivot.copy(iOnLine);
+				//Utils.debugSphere(viewer.scene.scene, drag.pivot, 0.05);
+			}
+		}
+	}
+
+	setActiveHandle(handle){
+		if(this.dragging){
+			return;
+		}
+
+		if(this.activeHandle === handle){
+			return;
+		}
+
+		this.activeHandle = handle;
+
+		if(handle === null){
+			for(let handleName of Object.keys(this.handles)){
+				let handle = this.handles[handleName];
+				handle.node.setOpacity(0);
+			}
+		}
+
+		if(!hideFocusHandles){
+            for(let handleName of Object.keys(this.focusHandles)){
+                let handle = this.focusHandles[handleName];
+
+                if(this.activeHandle === handle){
+                    handle.node.setOpacity(1.0);
+                }else{
+                    handle.node.setOpacity(OpaWhenNotSelect)
+                }
+            }
+		}
+
+		for(let handleName of Object.keys(this.translationHandles)){
+			let handle = this.translationHandles[handleName];
+
+			if(this.activeHandle === handle){
+				handle.node.setOpacity(1.0);
+			}else{
+				handle.node.setOpacity(OpaWhenNotSelect)
+			}
+		}
+
+		for(let handleName of Object.keys(this.rotationHandles)){
+			let handle = this.rotationHandles[handleName];
+
+			//if(this.activeHandle === handle){
+			//	handle.node.setOpacity(1.0);
+			//}else{
+			//	handle.node.setOpacity(OpaWhenNotSelect)
+			//}
+
+			handle.node.setOpacity(OpaWhenNotSelect);
+		}
+
+		for(let handleName of Object.keys(this.scaleHandles)){
+			let handle = this.scaleHandles[handleName];
+
+			if(this.activeHandle === handle){
+				handle.node.setOpacity(1.0);
+                if(!hideFocusHandles){
+                    let relatedFocusHandle = this.focusHandles[handle.name.replace("scale", "focus")];
+                    let relatedFocusNode = relatedFocusHandle.node;
+                    relatedFocusNode.setOpacity(OpaWhenNotSelect);
+                }
+				for(let translationHandleName of Object.keys(this.translationHandles)){
+					let translationHandle = this.translationHandles[translationHandleName];
+					translationHandle.node.setOpacity(OpaWhenNotSelect);
+				}
+
+				//let relatedTranslationHandle = this.translationHandles[
+				//	handle.name.replace("scale", "translation").replace(/[+-]/g, "")];
+				//let relatedTranslationNode = relatedTranslationHandle.node;
+				//relatedTranslationNode.setOpacity(OpaWhenNotSelect);
+
+
+			}else{
+				handle.node.setOpacity(OpaWhenNotSelect)
+			}
+		}
+
+		
+
+
+
+		if(handle){
+			handle.node.setOpacity(1.0);
+		}
+
+		
+	}
+
+	update () {
+
+		if(this.selection.length === 1){ 
+			this.scene.visible = true;
+
+			this.scene.updateMatrix();
+			this.scene.updateMatrixWorld();
+
+			let selected = this.selection[0];
+			let world = selected.matrixWorld;
+			let camera = this.viewer.scene.getActiveCamera();
+			let domElement = this.viewer.renderer.domElement;
+			let pointer = this.viewer.inputHandler.pointer;
+
+			let center = selected.boundingBox.getCenter(new THREE.Vector3()).clone().applyMatrix4(selected.matrixWorld);
+
+			this.scene.scale.copy(selected.boundingBox.getSize(new THREE.Vector3()).multiply(selected.scale));
+			this.scene.position.copy(center);
+			this.scene.rotation.copy(selected.rotation);
+
+			this.scene.updateMatrixWorld();
+
+			{ 
+				// adjust rotation handles
+				if(!this.dragging){
+					let tWorld = this.scene.matrixWorld;
+					let tObject = tWorld.clone().invert();
+					let camObjectPos = camera.getWorldPosition(new THREE.Vector3()).applyMatrix4(tObject);
+
+					let x = this.rotationHandles["rotation.x"].node.rotation;
+					let y = this.rotationHandles["rotation.y"].node.rotation;
+					let z = this.rotationHandles["rotation.z"].node.rotation;
+
+					x.order = "ZYX";
+					y.order = "ZYX";
+
+					let above = camObjectPos.z > 0;
+					let below = !above;
+					let PI_HALF = Math.PI / 2;
+
+					if(above){
+						if(camObjectPos.x > 0 && camObjectPos.y > 0){
+							x.x = 1 * PI_HALF;
+							y.y = 3 * PI_HALF;
+							z.z = 0 * PI_HALF;
+						}else if(camObjectPos.x < 0 && camObjectPos.y > 0){
+							x.x = 1 * PI_HALF;
+							y.y = 2 * PI_HALF;
+							z.z = 1 * PI_HALF;
+						}else if(camObjectPos.x < 0 && camObjectPos.y < 0){
+							x.x = 2 * PI_HALF;
+							y.y = 2 * PI_HALF;
+							z.z = 2 * PI_HALF;
+						}else if(camObjectPos.x > 0 && camObjectPos.y < 0){
+							x.x = 2 * PI_HALF;
+							y.y = 3 * PI_HALF;
+							z.z = 3 * PI_HALF;
+						} 
+					}else if(below){
+						if(camObjectPos.x > 0 && camObjectPos.y > 0){
+							x.x = 0 * PI_HALF;
+							y.y = 0 * PI_HALF;
+							z.z = 0 * PI_HALF;
+						}else if(camObjectPos.x < 0 && camObjectPos.y > 0){
+							x.x = 0 * PI_HALF;
+							y.y = 1 * PI_HALF;
+							z.z = 1 * PI_HALF;
+						}else if(camObjectPos.x < 0 && camObjectPos.y < 0){
+							x.x = 3 * PI_HALF;
+							y.y = 1 * PI_HALF;
+							z.z = 2 * PI_HALF;
+						}else if(camObjectPos.x > 0 && camObjectPos.y < 0){
+							x.x = 3 * PI_HALF;
+							y.y = 0 * PI_HALF;
+							z.z = 3 * PI_HALF;
+						} 
+                        
+                    }  
+                    
+				}
+
+
+                // adjust scale of components
+				for(let handleName of Object.keys(this.handles)){
+					let handle = this.handles[handleName];
+					let node = handle.node;
+
+					let handlePos = node.getWorldPosition(new THREE.Vector3());
+					let distance = handlePos.distanceTo(camera.position);
+					let pr = Utils.projectedRadius(1, camera, distance, domElement.clientWidth, domElement.clientHeight);
+
+					let ws = node.parent.getWorldScale(new THREE.Vector3());
+
+					let s = (ScaleRatio / pr);
+					let scale = new THREE.Vector3(s, s, s).divide(ws);
+                
+					let rot = new THREE.Matrix4().makeRotationFromEuler(node.rotation);     //需要使用到旋转,所以我把设置scale的移到旋转后了,否则在视图上下旋转的分界线处rotateHandel会被拉长从而闪烁。
+					let rotInv = rot.clone().invert();
+                
+					scale.applyMatrix4(rotInv);
+					scale.x = Math.abs(scale.x);
+					scale.y = Math.abs(scale.y);
+					scale.z = Math.abs(scale.z);
+
+					node.scale.copy(scale);
+				}
+
+
+
+				{
+					let ray = Utils.mouseToRay(pointer, camera, domElement.clientWidth, domElement.clientHeight);
+					let raycaster = new THREE.Raycaster(ray.origin, ray.direction);
+					raycaster.layers.enableAll()//add
+                    
+                    
+                    let pickVolumes = this.pickVolumes.filter(v=>{
+                        let mode = v.handle.split('.')[0]; 
+                        return this.modesEnabled[mode]
+                    })
+                    let intersects = raycaster.intersectObjects(pickVolumes, true);
+                    
+                     
+					if(intersects.length > 0){
+						let I = intersects[0];
+						let handleName = I.object.handle;
+                        console.log(handleName)
+						this.setActiveHandle(this.handles[handleName]);
+					}else{
+						this.setActiveHandle(null);
+					}
+				}
+
+				// 
+				for(let handleName of Object.keys(this.scaleHandles)){
+					let handle = this.handles[handleName];
+					let node = handle.node;
+					let alignment = handle.alignment;
+
+					
+
+				}
+			}
+
+		}else{
+			this.scene.visible = false;
+		}
+		
+	}
+
+};
+
+
+/* 
+    note:
+      
+    transformationTool.scene会跟随选中物体,其scale就是boundingbox的大小。因此transformationTool.frame这个框也会跟着缩放
+    
+    
+
+
+
+
+ */

+ 1 - 1
src/viewer/View.js

@@ -55,7 +55,7 @@ export class View{//base
 		
 	}
 
-	lookAt(t){
+	lookAt(t){//setPivot 
 		let V;
 		if(arguments.length === 1){
 			V = new THREE.Vector3().subVectors(t, this.position);