xzw 1 месяц назад
Родитель
Сommit
f39ea5e7f3
44 измененных файлов с 19467 добавлено и 0 удалено
  1. 6 0
      package-lock.json
  2. 1 0
      package.json
  3. 1228 0
      src/modelViewer/InputHandler.js
  4. 517 0
      src/modelViewer/ModelManager.js
  5. 424 0
      src/modelViewer/OrbitControls.js
  6. 566 0
      src/modelViewer/View.js
  7. 544 0
      src/modelViewer/Viewer.js
  8. 104 0
      src/modelViewer/Viewport.js
  9. 27 0
      src/modelViewer/app.js
  10. 379 0
      src/modelViewer/index.html
  11. 1373 0
      src/modelViewer/libs/BufferGeometryUtils.js
  12. 76 0
      src/modelViewer/libs/ColorSpaces.js
  13. 564 0
      src/modelViewer/libs/DRACOLoader.js
  14. 4812 0
      src/modelViewer/libs/GLTFLoader.js
  15. 1083 0
      src/modelViewer/libs/KTX2Loader.js
  16. 102 0
      src/modelViewer/libs/WorkerPool.js
  17. 46 0
      src/modelViewer/libs/basis/README.md
  18. 21 0
      src/modelViewer/libs/basis/basis_transcoder.js
  19. BIN
      src/modelViewer/libs/basis/basis_transcoder.wasm
  20. 1 0
      src/modelViewer/libs/basis/ver146.txt
  21. 52 0
      src/modelViewer/libs/draco/draco_decoder.js
  22. BIN
      src/modelViewer/libs/draco/draco_decoder.wasm
  23. 33 0
      src/modelViewer/libs/draco/draco_encoder.js
  24. 104 0
      src/modelViewer/libs/draco/draco_wasm_wrapper.js
  25. 1 0
      src/modelViewer/libs/ktx-parse.module.js
  26. 113 0
      src/modelViewer/libs/meshopt_decoder.module.js
  27. 6 0
      src/modelViewer/libs/three.core.min.js
  28. 6 0
      src/modelViewer/libs/three.module.min.js
  29. 115 0
      src/modelViewer/libs/zstddec.module.js
  30. 138 0
      src/modelViewer/material/BasicMaterial.js
  31. 0 0
      src/modelViewer/modelConfig.json
  32. 19 0
      src/modelViewer/objects/fatline/Line2.js
  33. 60 0
      src/modelViewer/objects/fatline/LineGeometry.js
  34. 982 0
      src/modelViewer/objects/fatline/LineMaterial.js
  35. 355 0
      src/modelViewer/objects/fatline/LineSegments2.js
  36. 241 0
      src/modelViewer/objects/fatline/LineSegmentsGeometry.js
  37. 538 0
      src/modelViewer/utils/Common.js
  38. 131 0
      src/modelViewer/utils/CursorDeal.js
  39. 489 0
      src/modelViewer/utils/DrawUtil.js
  40. 2033 0
      src/modelViewer/utils/TransformControls.js
  41. 1381 0
      src/modelViewer/utils/TransformationTool.js
  42. 58 0
      src/modelViewer/utils/math.js
  43. 491 0
      src/modelViewer/utils/transitions.js
  44. 247 0
      src/modelViewer/viewerBase.js

+ 6 - 0
package-lock.json

@@ -9,6 +9,7 @@
       "version": "0.0.0",
       "dependencies": {
         "swiper": "^14.0.5",
+        "three": "^0.184.0",
         "vue": "^3.5.39",
         "vue-router": "^4.6.4",
         "vuex": "^4.1.0"
@@ -1397,6 +1398,11 @@
         "node": ">= 4.7.0"
       }
     },
+    "node_modules/three": {
+      "version": "0.184.0",
+      "resolved": "https://registry.npmmirror.com/three/-/three-0.184.0.tgz",
+      "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg=="
+    },
     "node_modules/tinyglobby": {
       "version": "0.2.17",
       "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz",

+ 1 - 0
package.json

@@ -15,6 +15,7 @@
   },
   "dependencies": {
     "swiper": "^14.0.5",
+    "three": "^0.184.0",
     "vue": "^3.5.39",
     "vue-router": "^4.6.4",
     "vuex": "^4.1.0"

Разница между файлами не показана из-за своего большого размера
+ 1228 - 0
src/modelViewer/InputHandler.js


+ 517 - 0
src/modelViewer/ModelManager.js

@@ -0,0 +1,517 @@
+import * as THREE from 'three';  
+import {GLTFLoader} from  "./libs/GLTFLoader.js";  
+import {Common} from './utils/Common.js'
+ 
+let viewer
+export default class ModelManager extends THREE.EventDispatcher{
+    constructor(viewer_){
+        super()
+        
+        viewer = viewer_
+        this.setLoaders()
+    } 
+    
+    
+    setLoaders(){
+        this.fileManager = new THREE.LoadingManager(); //整体的load manager
+        this.fileManager.onLoad = () => {
+            console.log('All resources have been loaded');
+            this.fileManager.loading = false
+            this.dispatchEvent('managerOnLoad')
+            // 在这里可以执行模型渲染、动画等操作
+        };
+
+        // 设置加载进度的回调函数(可选)
+        this.fileManager.onProgress = (item, loaded, total) => {
+            if(loaded < total) this.fileManager.loading = true
+            console.log(`Loading ${item}: ${loaded} of ${total}`);
+        };
+
+         
+        this.loaders = {
+            //objLoaders : [],//new OBJLoader( this.fileManager ),
+            //mtlLoader : new MTLLoader( this.fileManager ),
+            glbLoader : new GLTFLoader(undefined, viewer.renderer,  './lib/' ),
+            /* plyLoader : new PLYLoader( this.fileManager ),
+            dxfLoader : new DxfLoader(),
+            shapeLoader: new ShapefileLoader() */
+        }
+    }
+
+
+    getObjLoader(){
+        let loader = this.loaders.objLoaders.find(e=>!e.inUse)
+        if(!loader){
+            loader = new OBJLoader( this.fileManager )   
+                                
+            this.loaders.objLoaders.push(loader)
+        }
+        loader.inUse = true 
+        return loader 
+    }
+
+
+
+
+
+
+
+    modelLoaded(object, fileInfo_={}, done){//普通模型加载完以后
+        object.isModel = true
+        let boundingBox = new THREE.Box3()
+        if(fileInfo_.parentInfo){
+            object.name = fileInfo_.name   
+            fileInfo_.parentInfo.loadedCount ++
+            fileInfo_.parentInfo.modelGroup.add(object) 
+            if(fileInfo_.parentInfo.loadedCount == fileInfo_.parentInfo.url.length){ 
+                return this.modelLoaded(fileInfo_.parentInfo.modelGroup, fileInfo_.parentInfo, done)
+            }else{ 
+                return
+            }   
+        }
+        
+        object.name = fileInfo_.name != void 0 ? fileInfo_.name :  Common.getNameFromURL(fileInfo_.url,true)  // fileInfo_.fileType
+        object.fileType = fileInfo_.fileType
+        object.boundingBox = boundingBox  //未乘上matrixWorld的本地boundingBox
+        //fileInfo_.parentInfo || object.scale.set(1,1,1);//先获取原始的大小时的boundingBox 
+        object.opacity = 1 //初始化 记录
+        object.updateMatrixWorld()
+        
+        if(fileInfo_.id != void 0)object.dataset_id = fileInfo_.id
+        
+        
+        fileInfo_.loadCostTime = Date.now() - fileInfo_.loadStartTime
+        /* let weight = Math.round((total / 1024 / 1024) * 100) / 100;*/
+        console.log( '加载完毕:', fileInfo_.name, Common.getNameFromURL(fileInfo_.url), '耗时(ms)', fileInfo_.loadCostTime, /* 模型数据量:' + weight + 'M' */)
+          
+           
+        if(fileInfo_.fileType == '3dTiles'){
+            let isGroup = !object.runtime  
+            let children = object.runtime ? [object] : object.children
+            
+            
+            children.forEach(object =>{
+                let boundingBox_ = new THREE.Box3()
+                
+                
+                let tileset = object.runtime.getTileset()
+           
+                //TileHeader: tileset.root 
+                //参见另一个工程 TileRenderer.js  preprocessNode //这个坐标位置几万…… let data = boundingVolume.halfAxes  //但这个似乎是premultiply( transform );过后的,可能需还原下
+                
+                let json = tileset.tileset   
+                let box = json.root.boundingVolume.box
+                 
+                if(box){
+                    let center = new THREE.Vector3(box[0],box[1],box[2])
+                    let boundSize = new THREE.Vector3( )  
+                     
+                    // get the extents of the bounds in each axis
+                    let vecX = new THREE.Vector3( box[ 3 ], box[ 4 ], box[ 5 ] )
+                    let vecY = new THREE.Vector3( box[ 6 ], box[ 7 ], box[ 8 ] );
+                    let vecZ = new THREE.Vector3( box[ 9 ], box[ 10 ], box[ 11 ] );
+
+                    const scaleX = vecX.length();
+                    const scaleY = vecY.length();
+                    const scaleZ = vecZ.length(); 
+                    
+                    boundingBox_.min.set( - scaleX, - scaleY, - scaleZ );
+                    boundingBox_.max.set( scaleX, scaleY, scaleZ );
+                    
+                    if(isGroup){//如果是多个拼成的,每个都保留原先在parent中的offset。 如果是外层,作为独立个体,不用理会位置信息,直接放中央。
+                        object.position.copy(center)
+                        object.position.z *= -1
+                        boundingBox_.translate(object.position) //由于是内层,其位移会改变整体的boundingbox
+                    }
+                    
+                }else if(json.root.boundingVolume.sphere){
+                    let sphereData = json.root.boundingVolume.sphere
+                    let center = new THREE.Vector3(...sphereData)
+                    let radius = sphereData[3] / 2 
+                     
+                    boundingBox_.min.set( - radius, - radius, - radius );
+                    boundingBox_.max.set( radius, radius, radius );
+                    
+                    
+                    
+                }else{
+                    return console.error('json boundingVolume 缺少信息') 
+                }
+                
+                //中心点居然没用。可能是漏用了什么信息,也许这和LVBADUI_qp是散的有关。
+                //console.log('3d tiles json',json)
+                
+                json.root.refine = 'ADD';
+                json.refine = 'ADD';
+                    
+                
+                boundingBox.union(boundingBox_)
+                
+            })
+            
+            
+            
+            
+        }else { 
+            //Common.setObjectLayers(object,'model')  //先于3dgs渲染 透明部分会有问题吧
+            
+            
+            object.traverse( ( child )=>{ 
+                let is = child.isMesh || child instanceof THREE.Points || child.isLine
+                
+                if (is){ 
+                    //child.renderOrder = config.renderOrders.model; 
+                    let boundingBox_  
+                    if(child instanceof THREE.SkinnedMesh){//animation
+                        child.computeBoundingBox();
+                        boundingBox_  = child.boundingBox
+                    }else{
+                        child.geometry.computeBoundingBox()
+                        boundingBox_ = child.geometry.boundingBox
+                    }
+                    
+                    //获取在scale为1时,表现出的大小
+                    boundingBox.union(boundingBox_.clone().applyMatrix4(child.matrixWorld)) //但感觉如果最外层object大小不为1,要还原下scale再乘
+                 
+                    if(child instanceof THREE.SkinnedMesh){ 
+                        child.boundingBox = null    //delete 动画会导致bound改变, raycast干脆不用boundingBox了,否则之后要实时重计算 
+                    } 
+                    
+                    let changeMat = (oldMat)=>{
+                        let mat = oldMat
+                        if(fileInfo_.unlit && (!(oldMat instanceof THREE.MeshBasicMaterial) /* || object.fileType == 'glb' */)){ //注释掉是因为已经写入到loader文件里了
+                            mat = new THREE.MeshBasicMaterial({name:oldMat.name, map : oldMat.map, opacity: oldMat.opacity, color: oldMat.color, skinning:oldMat.skinning})  
+                        }  
+                        
+                        if(fileInfo_.useStandandMat && !(oldMat instanceof THREE.MeshStandardMaterial)){
+                            mat = new THREE.MeshStandardMaterial()
+                            mat.roughness = 0.7
+                            mat.metalness = 0.5
+                        }   
+                        fileInfo_.metalness != void 0 && (mat.metalness = fileInfo_.metalness)
+                        fileInfo_.roughness != void 0 && (mat.roughness = fileInfo_.roughness)
+                        if(mat != oldMat)oldMat.dispose()
+                        //纯色的还是不能用BasicMaterial
+                        return mat
+                    }
+                    if(child.material instanceof Array){//obj
+                        child.material = child.material.map(m=>changeMat(m))
+                    }else{
+                        child.material = changeMat(child.material)
+                    }
+                   
+                   
+                    if(fileInfo_.prop?.is4dkkModel){
+                    
+                        child.material.color.set(1,1,1); //看到有obj不是白色
+                    }
+                    
+                } 
+            } );
+        }
+        viewer.objs.add(object) 
+        {
+            let boundSize = new THREE.Vector3 
+            object.boundingBox.getSize(boundSize) 
+            let max = Math.max(boundSize.x, boundSize.y,boundSize.z)
+            object.initialScale = 100/max
+            object.scale.set(object.initialScale,object.initialScale,object.initialScale )
+            
+        }
+        
+        if(fileInfo_.transform){
+            let setTransfrom = (name)=>{
+                let value = fileInfo_.transform[name]
+                if(value == void 0)return
+                if(value instanceof Array){
+                    object[name].fromArray(value)
+                }else{ 
+                    object[name].copy(value)
+                }
+            } 
+            setTransfrom('position')
+            setTransfrom('rotation')
+            setTransfrom('scale')
+              
+        }
+         
+        
+        if(fileInfo_.moveWithPointcloud){
+            object.updateMatrix();
+            object.matrixAutoUpdate = false
+            object.matrix.premultiply(viewer.scene.pointclouds[0].transformMatrix) //默认跟随第一个数据集
+            object.matrixWorldNeedsUpdate = true 
+        }
+        object.updateMatrixWorld()
+        this.getBoundCenter(object) //初始化  
+        fileInfo_.objLoader && (fileInfo_.objLoader.inUse = false)        
+        done && done(object, fileInfo_)
+        
+        this.dispatchEvent({type:'modelLoaded',model:object})
+        
+        
+        
+        //如果需要点击出现transform工具需要它有select事件 如 viewer.objs.children[1].addEventListener('select',()=>{})
+    }
+    
+    
+    
+    
+    async loadModel(fileInfo, done, onProgress_, onError){ 
+        console.log('开始加载', fileInfo.name, Common.getNameFromURL(fileInfo.url) )
+    
+        let boundingBox = new THREE.Box3()
+        
+        if(fileInfo.objurl){ 
+            fileInfo.fileType = 'obj'   //兼容最早的 
+        } 
+        
+        
+        if(fileInfo.url instanceof Array){
+            if(fileInfo.url.length == 1){
+                fileInfo.url =  fileInfo.url[0]
+            }else{
+                fileInfo.loadedCount = 0  
+                fileInfo.modelGroup = new THREE.Object3D; //parentGroup.name = fileInfo.title
+                fileInfo.url.forEach((url,i)=>{
+                    let fileInfoS = Common.CloneObject(fileInfo)
+                    fileInfoS.url = url  
+                    fileInfoS.name = 'child-'+i
+                    fileInfoS.parentInfo = fileInfo
+                    this.loadModel(fileInfoS, done, onProgress_, onError)
+                })  
+                return
+            }
+        }
+        fileInfo.url = Common.dealURL(fileInfo.url) //去除'+'
+        fileInfo.loadStartTime = Date.now()   
+        //let fileType =  fileInfo.tilesUrl ? '3dTiles' :  fileInfo.objurl ? 'obj' : 'glb'
+         
+        
+        let loadDone = (object,   fileInfo_   )=>{ 
+            this.modelLoaded(object,   fileInfo_ || fileInfo  , done)
+        }
+        
+        
+        let onProgress = function ( xhr ) {
+            if ( xhr.lengthComputable ) {
+                let percentComplete = xhr.loaded / xhr.total * 100;
+                //console.log( Math.round(percentComplete, 2) + '% downloaded' ); 
+                onProgress_ && onProgress_(percentComplete)
+            }  
+        };
+    
+    
+    
+        if(fileInfo.fileType == 'obj' && !fileInfo.objurl){
+            let a = fileInfo.url.split('/') 
+            let tails = a.pop().split('.') 
+            let head = a.join('/') + '/'
+            let name = tails[0],  fileType = tails[1]
+            if(fileType == 'obj'){
+                fileInfo.objurl || (fileInfo.objurl = fileInfo.url)
+                fileInfo.mtlurl || (fileInfo.mtlurl = head + name + '.mtl')
+            }else{
+                fileInfo.fileType = 'glb'
+            }
+        }
+    
+    
+        if(fileInfo.fileType == 'obj'){ //暂时不支持数组
+            let objLoader = fileInfo.objLoader = this.getObjLoader()
+            let loadobj = ()=>{
+                objLoader.load(fileInfo.objurl, (object, total)=>{  
+                    loadDone(object/* , total, fileInfo.objurl */)
+                }, onProgress,  onError )
+            }
+            if(fileInfo.mtlurl){ 
+                this.loaders.mtlLoader.load( fileInfo.mtlurl , (materials)=>{ 
+                     
+                    objLoader.setMaterials( materials )  //因为这句所以不同obj不能用同一个objLoader,否则材质紊乱,模型变白
+                    materials.preload(); 
+                    loadobj()                                      
+                                             
+                } , onProgress,  ()=>{
+                    //console.log('mtl load failed, load obj directly')
+                    loadobj()
+                });  
+            }else{
+                loadobj()
+                                    
+                                        
+            } 
+        }else if(fileInfo.fileType == 'glb'){ 
+            this.loaders.glbLoader.unlitMat = true//!!fileInfo.unlit
+            this.loaders.glbLoader.load(fileInfo.url,  ( gltf, total )=>{    
+                console.log('loadGLTF', gltf, 'aniCount:',gltf.animations.length)
+                let model = gltf.scene  
+                model.gltf = gltf 
+                this.gltfAddAnimation(model)
+                loadDone(model) 
+            }, onProgress, onError)
+            
+        }else if(fileInfo.fileType == 'ply'){
+            this.loaders.plyLoader.load( fileInfo.url, (geometry) => {
+                let object
+                console.log('ply加载完毕', geometry)
+                if(!geometry.index){//点云 
+                    object = new THREE.Points(geometry, new THREE.PointsMaterial({vertexColors:true, size:0.02})) 
+                    //141M的点云,intersect费时300ms以上
+                }else{//mesh
+                    object = new THREE.Mesh(geometry)    
+                }       
+                loadDone(object)
+            })
+            
+        }else if(fileInfo.fileType == '3dTiles'){ 
+            let result 
+            try{ 
+                 result = await Loader3DTiles.load({
+                    url: fileInfo.url, 
+                    gltfLoader : this.loaders.glbLoader,   
+                    options: {       
+                        maximumScreenSpaceError: fileInfo.maximumScreenSpaceError || 80 ,  //越小越清晰。           如果本身tiles很密很小这个值就不能很大。
+                        //maxDepth: 100, 
+                        //maximumMemoryUsage: 100, //缓存大小,见tiles3DMaxMemory。单位M(但实际结果是 2.5*maximumMemoryUsage + 750  。超过2G会崩, 所以应该小于540) 若太小,密集的tile反复加载很卡. (任务管理器刷新网页后若内存不掉就要结束进程否则虚高)
+                        debug: browser.urlHasValue('tilesBox'),  //show box  
+                        parent: this.scene.scene, 
+                        is4dkkModel: fileInfo.is4dkkModel, //是否是4dkk中的模型. 通常maximumScreenSpaceError需要10
+                        updateTime: fileInfo.updateTime, //加后缀防止缓存
+                        //cesiumIONToken:  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI5OTc4MTFiYS1hYzhlLTQ3ZjYtYWJmMi1hODMwMWMxZGRiYTQiLCJpZCI6ODU1NDksImlhdCI6MTY1Mzc5NDc5N30.ldTi8bF3XvSOgnZrMITokRW4kE3i8Mwbarhk5OQbPsI',
+                    },  
+                })
+                    
+                //console.log(result)
+                result.model.runtime = result.runtime
+     
+                   
+                let loaded = false
+                let tileset = result.runtime.getTileset()
+                tileset.addEventListener('endTileLoading', function (data) {//Tileset3D
+                    if (data.loadingCount == 0 && !loaded) {
+                        loaded = true; 
+                        //console.log('loaded!!!!!!!!!!!!!')
+                    }
+                });
+                tileset.addEventListener('tileLoaded',(e)=>{ //每一个tile加载完要更改透明度
+                    let master = result.model.parent == viewer.objs ? result.model : result.model.parent//最多两层 
+                    //MergeEditor.changeOpacity(e.tileContent,  master.opacity)
+                    if(master.panos) viewer.images360.judgeModelMat(e.tileContent)
+                    //set Layers ?
+                    //Utils.setObjectLayers(e.tileContent, 'model')  
+                    fileInfo.side && e.tileContent.traverse(e=>e.material && (e.material.side = fileInfo.side))//新软件导出的带坐标的box型模型要反面才看的到,干脆双面
+                })
+                
+                { 
+                    let vi = true
+                    Object.defineProperty( result.model, "visible", {
+                        get: function() {
+                            return vi 
+                        },
+                        set: function(v) { 
+                            vi = v 
+                            result.model.visiChangeCallback()
+                        }  
+                    })  
+                }
+                let v = true
+                result.model.visiChangeCallback = (force)=>{
+                    let visi = result.model.realVisible()
+                    tileset.visible = visi;  //同步,使不加载 
+                    if(force || v != visi){
+                        tileset.nextForceUpdate = true
+                        v = visi
+                    } 
+                }
+                loadDone(result.model/* , null, fileInfo.url */) 
+                   
+            }catch(e){ 
+                //debugger  // Error: Failed to fetch resource
+                
+                onError ? onError(e) : console.error(e)
+            }            
+            
+            
+        } 
+        
+        
+         
+    }
+    
+    
+    
+    
+    gltfAddAnimation(model){ 
+        
+        if(model.gltf?.animations.length){
+            /* let skeleton = new THREE.SkeletonHelper( model ); 
+            viewer.scene.add(skeleton)
+            model.skeletonHelper = skeleton //注意:不能覆盖model.skeleton,因其另有 
+            Utils.updateVisible(skeleton,'hide',false)   */
+           
+            let mixer = new THREE.AnimationMixer( model);
+            model.actions = []
+            model.gltf.animations.forEach(ani=>{
+                if(ani.tracks.filter(e=>e instanceof THREE.QuaternionKeyframeTrack).length > 1){ //>一帧的
+                    model.actions.push(mixer.clipAction( ani )); 
+                } 
+            })
+            model.mixer = mixer 
+           
+        }
+        
+    }
+     
+    
+    setAllTilesets(){//让所有tileset执行fun。    objs里每个model最多两层tileset
+        let models = arguments.length == 2 ? [arguments[0]] : viewer.objs.children //如果要设定某个model的tileset,就传第一个参数
+        let fun = arguments[1] || arguments[0]
+        
+        models.forEach(e=>{
+            if(e.fileType == '3dTiles'){
+                e.traverse(child=>{
+                    if(child.runtime){
+                        fun(child)
+                        return {stopContinue:true}
+                    }
+                })
+                
+            }
+        }) 
+    }
+    
+    removeModel(model){
+        model.parent.remove(model)
+        let dispose = (e)=>{
+            e.geometry && e.geometry.dispose() 
+            e.dealMaterial(a=>a.dispose())  
+        } 
+        model.traverse(e=>{
+            dispose(e) 
+        })
+           
+        /* if(settings.boundAddObjs && model.isChildOf(viewer.objs)){
+            this.updateModelBound() 
+        } */
+    }
+    
+    
+    getBoundCenter(model){
+        if(!model.boundCenter) {
+            model.boundCenter = new THREE.Vector3
+            model.boundSize = new THREE.Vector3
+        }
+        let bound = model.boundingBox.clone().applyMatrix4(model.matrixWorld) 
+        bound.getCenter(model.boundCenter)
+        bound.getSize(model.boundSize)
+        //model.boundingBox.getCenter(model.boundCenter).applyMatrix4(model.matrixWorld) 
+    } 
+    
+    moveBoundCenterTo(model,pos){ //使boundCenter在所要的位置 
+        let diff = new THREE.Vector3().subVectors(pos, model.boundCenter) 
+        model.position.add(diff); 
+    } 
+    
+     
+    
+}

+ 424 - 0
src/modelViewer/OrbitControls.js

@@ -0,0 +1,424 @@
+
+import * as THREE from 'three'; 
+import {Common,browser,defines} from './utils/Common.js'
+const standartMinRadius = 2
+ 
+
+ 
+export class OrbitControls extends THREE.EventDispatcher{
+	
+	constructor(viewer, viewport){
+		super();
+		this.isOrbitControls = true
+		this.viewer = viewer;
+		this.renderer = viewer.renderer;
+        
+		this.scene = null;
+		this.sceneControls = new THREE.Scene();
+
+		this.rotationSpeed =  browser.isMobile() ? 0.006 : 0.002;   //旋转速度
+         
+        viewport = viewport || viewer.viewports[0]
+        
+        this.setCurrentViewport({hoverViewport:viewport, force:true}) //this.currentViewport = viewport
+        
+        this.moveSpeed = 0.02
+		this.fadeFactor = 20;
+		this.yawDelta = 0;
+		this.pitchDelta = 0;
+		this.panDelta = new THREE.Vector2(0, 0);
+		this.radiusDelta = 0;
+
+		//this.doubleClockZoomEnabled = true;
+
+		this.tweens = [];
+        this.dollyStart = new THREE.Vector2
+        this.dollyEnd = new THREE.Vector2
+        this.minRadius = standartMinRadius
+        this.maxRadius = 300
+        this.constantlyForward = 0//true
+        
+        this.progression = 1
+        
+        this.keys = {
+            FORWARD: ['W'.charCodeAt(0), 38],
+            BACKWARD: ['S'.charCodeAt(0), 40],
+            LEFT: ['A'.charCodeAt(0), 37],
+            RIGHT: ['D'.charCodeAt(0), 39],
+            UP: ['Q'.charCodeAt(0)],
+            DOWN: ['E'.charCodeAt(0)], 
+        };
+        
+        
+		let drag = (e) => {
+            if(!this.enabled)return
+            
+            let viewport = e.dragViewport;
+            if(!viewport /* || viewport.camera.type == "OrthographicCamera"  */)return
+             
+            let mode
+            let view = viewport.view//this.currentViewport.view
+            if(e.isTouch){ 
+                if(e.touches.length == 1){
+                    mode = 'rotate'  
+                }else{  
+                    mode = 'scale' //'scale-pan'
+                }  
+            }else{
+                mode = 'rotate' //e.buttons === defines.Buttons.LEFT ? 'rotate' : 'pan'
+            } 
+            
+			if (e.drag.startHandled === undefined) {
+				e.drag.startHandled = true; 
+				this.dispatchEvent({type: 'start'});
+			}
+ 
+			if (mode == 'rotate') {
+                let ndrag = e.drag.mouseDelta.clone() 
+                if(ndrag.x || ndrag.y){
+                    view.cancelFlying('rotate')
+                }
+				this.yawDelta -= ndrag.x * this.rotationSpeed;
+				this.pitchDelta -= ndrag.y * this.rotationSpeed;
+                
+			} else if(mode == 'pan'){
+                //if(!this.dragStarted) this.updateRadius('startPan')
+				this.panDelta.x += e.drag.pointerDelta.x;
+				this.panDelta.y -= e.drag.pointerDelta.y;
+                 
+			}else if(mode == 'scale-pan'){ //add
+                this.dollyEnd.subVectors(e.touches[0].pointer, e.touches[1].pointer); 
+                var scale = this.dollyEnd.length() / this.dollyStart.length() 
+                  
+                this.dollyStart.copy(this.dollyEnd); 
+                this.radiusDelta = (1-scale) * view.radius 
+			  
+                //------------------------
+                //平移
+                let pointer = new THREE.Vector2().addVectors(e.touches[0].pointer, e.touches[1].pointer).multiplyScalar(0.5);//两个指头的中心点
+                 
+                let delta = new THREE.Vector2().subVectors(pointer, this.lastScalePointer)
+                delta.y *= -1
+                this.panDelta.add(delta)
+                
+                this.lastScalePointer = pointer.clone()
+                  
+                //console.log('scale ',scale, this.radiusDelta  )
+                
+            }
+            
+            this.stopTweens();
+            this.dragStarted = true
+            
+		};
+        
+         
+        
+		let drop = e => {
+            if(!this.enabled)return
+            this.dragStarted = false
+			this.dispatchEvent({type: 'end'});
+		};
+
+		let scroll = (e) => {
+            if(!this.enabled)return
+			let resolvedRadius = this.currentViewport.view.radius + this.radiusDelta;
+             
+			this.radiusDelta += -e.delta * resolvedRadius * 0.08; 
+             
+			this.stopTweens();
+            //this.updateRadius('scroll')
+		};
+
+		let dblclick = (e) => {
+            if(!this.enabled)return
+			if(this.doubleClockZoomEnabled){
+				this.zoomToLocation(e.mouse);
+			}
+		};
+
+		let previousTouch = null;
+		let touchStart = e => {
+			previousTouch = e;
+		};
+
+		let touchEnd = e => {
+			previousTouch = e;
+		};
+
+		let touchMove = e => {
+            if(!this.enabled)return
+			if (e.touches.length === 2 && previousTouch.touches.length === 2){
+				let prev = previousTouch;
+				let curr = e;
+
+				let prevDX = prev.touches[0].pageX - prev.touches[1].pageX;
+				let prevDY = prev.touches[0].pageY - prev.touches[1].pageY;
+				let prevDist = Math.sqrt(prevDX * prevDX + prevDY * prevDY);
+
+				let currDX = curr.touches[0].pageX - curr.touches[1].pageX;
+				let currDY = curr.touches[0].pageY - curr.touches[1].pageY;
+				let currDist = Math.sqrt(currDX * currDX + currDY * currDY);
+
+				let delta = currDist / prevDist;
+				let resolvedRadius = this.currentViewport.view.radius + this.radiusDelta;
+				let newRadius = resolvedRadius / delta;
+				this.radiusDelta = newRadius - resolvedRadius;
+
+				this.stopTweens();
+			}else if(e.touches.length === 3 && previousTouch.touches.length === 3){
+				let prev = previousTouch;
+				let curr = e;
+
+				let prevMeanX = (prev.touches[0].pageX + prev.touches[1].pageX + prev.touches[2].pageX) / 3;
+				let prevMeanY = (prev.touches[0].pageY + prev.touches[1].pageY + prev.touches[2].pageY) / 3;
+
+				let currMeanX = (curr.touches[0].pageX + curr.touches[1].pageX + curr.touches[2].pageX) / 3;
+				let currMeanY = (curr.touches[0].pageY + curr.touches[1].pageY + curr.touches[2].pageY) / 3;
+
+				let delta = {
+					x: (currMeanX - prevMeanX) / this.renderer.domElement.clientWidth,
+					y: (currMeanY - prevMeanY) / this.renderer.domElement.clientHeight
+				};
+
+				this.panDelta.x += delta.x;
+				this.panDelta.y += delta.y;
+
+				this.stopTweens();
+			}
+
+			previousTouch = e;
+		};
+
+		this.addEventListener('touchstart', touchStart);
+		this.addEventListener('touchend', touchEnd);
+		this.addEventListener('touchmove', touchMove);
+		this.viewer.addEventListener('global_drag', drag);
+		this.viewer.addEventListener('global_drop', drop);
+		this.viewer.addEventListener('global_mousewheel', scroll);
+		this.viewer.addEventListener('global_dblclick', dblclick);
+        /* this.viewer.addEventListener('global_touchmove', (e)=>{ 
+            if(e.touches.length>1){//单指的就触发上一句 
+                //console.log('global_touchmove' )
+                drag(e)
+            }
+        }); */
+        let prepareScale = (e)=>{//触屏的scale
+            this.dollyStart.subVectors(e.touches[0].pointer, e.touches[1].pointer);
+            this.lastScalePointer = new THREE.Vector2().addVectors(e.touches[0].pointer, e.touches[1].pointer).multiplyScalar(0.5);//两个指头的中心点
+              
+        }
+         
+        this.viewer.addEventListener('global_touchstart', (e)=>{
+            if(this.enabled && e.touches.length==2){//只监听开头两个指头
+                prepareScale(e)
+            }
+        })
+        /* this.viewer.addEventListener('global_touchend', (e)=>{
+            if(!this.enabled)return
+            if(e.touches.length==1){//停止scale,开始rotate
+                prepareRotate(e)
+                //this.pointerDragStart = null
+                //console.log('只剩一个', e.pointer.toArray())
+            }
+        }) */
+        
+        
+        /* this.viewer.addEventListener('focusOnObject',(o)=>{
+            if(o.position && o.CamTarget){
+                let distance = o.position.distanceTo(o.CamTarget)
+                //if(distance < minRadius) minRadius = distance * 0.5 //融合页面当focus一个很小的物体时,需要将minRadius也调小
+                this.minRadius = Math.min(standartMinRadius, distance * 0.5)
+                //console.log('focus dis', distance) 
+            }
+        }) */
+         
+	}
+
+    
+
+	setScene (scene) {
+		this.scene = scene;
+	}
+    
+    setCurrentViewport(o={}){//add
+        if(!this.enabled && !o.force )return
+        if(o.hoverViewport && this.currentViewport != o.hoverViewport ){
+            this.currentViewport = o.hoverViewport  
+			 
+        } 
+    }
+    
+    setEnable(enabled){
+        this.enabled = enabled
+    }
+	stop(){
+        if(!this.progression){
+            this.yawDelta = 0;
+            this.pitchDelta = 0;
+        }
+		this.radiusDelta = 0;
+		this.panDelta.set(0, 0);
+	}
+ 
+    
+    
+   
+
+	stopTweens () {
+		this.tweens.forEach(e => e.stop());
+		this.tweens = [];
+	}
+
+	update (delta) {
+        if(!this.enabled)return
+		let view = this.currentViewport.view//this.currentViewport.view;
+        let camera = this.currentViewport.camera
+
+
+
+        { // accelerate while input is given
+			let ih = this.viewer.inputHandler;
+
+			let moveForward = this.keys.FORWARD.some(e => ih.pressedKeys[e]);
+			let moveBackward = this.keys.BACKWARD.some(e => ih.pressedKeys[e]);
+			let moveLeft = this.keys.LEFT.some(e => ih.pressedKeys[e]);
+			let moveRight = this.keys.RIGHT.some(e => ih.pressedKeys[e]);
+			let moveUp = this.keys.UP.some(e => ih.pressedKeys[e]);
+			let moveDown = this.keys.DOWN.some(e => ih.pressedKeys[e]);
+            
+             
+            let moveSpeed = this.moveSpeed/* this.currentViewport.getMoveSpeed() */ / 100;
+            let px = 0 , py = 0, pz = 0
+            if(moveForward){
+                py = 1 * moveSpeed 
+            }else if(moveBackward){
+                py = -1 * moveSpeed 
+            }
+            
+            if(moveLeft){
+                px = -1 * moveSpeed 
+            }else if(moveRight){
+                px = 1 * moveSpeed 
+            }
+            if(moveUp){
+                pz = 1 * moveSpeed 
+            }else if(moveDown){
+                pz = -1 * moveSpeed 
+            }
+            
+            if(px!=0 || py!=0 || pz!=0){
+                //console.log(px,py,px)
+                view.translate(px, py, pz, {forceHorizon:!settings.orbitCtlMoveFree  }); 
+            }
+             
+        }
+                
+ 
+
+
+		 /*  { // apply rotation
+			let progression = Math.min(1, this.fadeFactor * delta);
+
+			let yaw = view.yaw;
+			let pitch = view.pitch;
+			let pivot = view.getPivot();
+
+			yaw -= progression * this.yawDelta;
+			pitch -= progression * this.pitchDelta;
+
+			view.yaw = yaw;
+			view.pitch = pitch;
+
+			let V = this.currentViewport.view.direction.multiplyScalar(-view.radius);
+			let position = new THREE.Vector3().addVectors(pivot, V);
+
+			view.position.copy(position);
+		}   */
+        
+        
+       { // apply rotation
+			let yaw = view.yaw;
+			let pitch = view.pitch;  
+            let pivot = view.getPivot();
+            
+            this.targetBound && pivot.clamp(this.targetBound.min,this.targetBound.max)
+            
+            let change = this.progression ? 0.15 : 1 //Math.min(0.95, 0.3 * deltaRatio)  //限制min画面会跳跃
+            
+            yaw += this.yawDelta * change 
+            pitch += this.pitchDelta * change 
+           //this.yawDelta > 0.001 && console.log('yaw', this.yawDelta )
+            //Math.abs(this.yawDelta * change ) > 0.0001 && console.log( 'change', change)
+			view.yaw = yaw;
+			view.pitch = pitch;
+            /* if(this.yawDelta || this.pitchDelta){
+                view.cancelFlying('rotate')
+            }
+             */
+            //剩余的下次再转
+            this.yawDelta = this.yawDelta * (1-change)
+            this.pitchDelta = this.pitchDelta * (1-change) 
+            
+            let V = this.currentViewport.view.direction.multiplyScalar(-view.radius);
+			let position = new THREE.Vector3().addVectors(pivot, V);
+
+			view.position.copy(position);
+		} 
+
+		if(camera.type != 'OrthographicCamera'){ // apply pan 平移 
+            let panDistance = view.radius * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2));//参照4dkk  只要radius设置正确就完全跟手(见updateRadius)  
+            //计算了下确实是这么算的。 平移pivot。  
+            
+			let px = -this.panDelta.x * panDistance * camera.aspect 
+			let py = this.panDelta.y * panDistance 
+
+			view.pan(px, py);
+		}
+
+		{ // apply zoom
+			let progression =  Math.min(1, this.fadeFactor * delta);
+             
+            
+            
+			// let radius = view.radius + progression * this.radiusDelta * view.radius * 0.1;
+			let radius = view.radius + progression * this.radiusDelta;
+                      
+			let V = view.direction.multiplyScalar(-radius);
+			let position = new THREE.Vector3().addVectors(view.getPivot(), V);
+            
+			radius = Math.min(radius, this.maxRadius)
+            /* if(this.constantlyForward) {// 到达中心点后还能继续向前移动,也就是能推进中心点 
+                radius = Math.max(radius, this.minRadius)
+            } */
+            
+            
+            view.radius = radius;            
+			view.position.copy(position);
+		}
+
+		{
+			let speed = view.radius;
+			//this.viewer.setMoveSpeed && this.viewer.setMoveSpeed(speed);
+            this.moveSpeed = speed
+		}
+
+		  { // decelerate over time
+			let progression = Math.min(1, this.fadeFactor * delta);
+			let attenuation = Math.max(0, 1 - this.fadeFactor * delta);
+
+			/* this.yawDelta *= attenuation;
+			this.pitchDelta *= attenuation;
+			this.panDelta.multiplyScalar(attenuation); */
+			// this.radiusDelta *= attenuation;
+            
+            this.panDelta.set(0,0)
+            
+            
+			this.radiusDelta -= progression * this.radiusDelta;
+            
+            
+            
+		}  
+	}
+};

+ 566 - 0
src/modelViewer/View.js

@@ -0,0 +1,566 @@
+import * as THREE from 'three';  
+import {transitions, easing, lerp} from './utils/transitions.js'
+import math from './utils/math.js'
+import {Common } from './utils/Common.js' 
+
+let sid = 0
+export class View extends THREE.EventDispatcher{//base
+	constructor () {
+        super()
+		this.position = new THREE.Vector3(0, 0, 0);
+
+		this.yaw = Math.PI / 4; //偏航角
+		this._pitch = -Math.PI / 4;//俯仰角
+        this.roll = 0 //滚转角 歪头  xzw add
+        
+		this.radius = 1;
+
+		this.maxPitch = Math.PI / 2;
+		this.minPitch = -Math.PI / 2;
+        
+        this.sid = sid++
+        this.LookTransition = 'LookTransition'+this.sid
+        this.FlyTransition = 'FlyTransition'+this.sid
+        
+        
+        
+	}
+    
+    
+    copy(a){
+        Common.CopyClassObject(this, a, {ignoreList: ['_listeners']})
+    }
+    
+	clone () {  
+        return Common.CloneClassObject(this, {ignoreList: ['_listeners']}) 
+	}
+	 
+    
+    applyToCamera(camera){
+        camera.position.copy(this.position);
+         if(this.rollFree){ //不受相机旋转模式限定,可以歪着,任意角度
+            camera.quaternion.copy(this.quaternion) 
+        }else{ 
+            camera.rotation.copy(this.rotation) 
+        }
+         
+        camera.updateMatrix();
+        camera.updateMatrixWorld();
+    
+    }
+    
+   
+    
+	get pitch () {
+		return this._pitch;
+	}
+
+	set pitch (angle) { 
+        if(!this.rollFree){
+            this._pitch = Math.max(Math.min(angle, this.maxPitch), this.minPitch);
+        }else{
+            this._pitch = angle
+        }
+	}
+
+	get direction () {
+        if(this.rollFree){//xzw add 不知为啥当roll不为0时必须这么算才对,好复杂。可能roll也影响了另外两个
+            return new THREE.Vector3(0,0,-1).applyQuaternion(this.quaternion)
+        }else{
+            let dir = new THREE.Vector3(0, 1, 0);
+
+            dir.applyAxisAngle(new THREE.Vector3(1, 0, 0), this.pitch);
+            dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), this.yaw);
+            //不考虑roll,因滚转角不影响视线方向向量。 所以根据direction也无法获知roll信息。甚至当xy都为0时无法获知yaw
+            return dir;
+        }
+	}
+
+	set direction (dir) {
+        dir = dir.clone().normalize()//add
+        
+		if(dir.x === 0 && dir.y === 0){
+			this.pitch = Math.PI / 2 * Math.sign(dir.z); 
+            //this.yaw = 0   //add:还是要指定一下, 否则不统一
+            
+		}else{
+			let yaw = Math.atan2(dir.y, dir.x) - Math.PI / 2;
+			let pitch = Math.atan2(dir.z, Math.sqrt(dir.x * dir.x + dir.y * dir.y));
+
+			this.yaw = yaw;
+			this.pitch = pitch;
+		} 
+	}
+
+    get rotation(){
+        var rotation = new THREE.Euler;
+        rotation.order = "ZXY";
+        rotation.x = Math.PI / 2 + this.pitch;
+        rotation.z = this.yaw
+        rotation.y = this.roll 
+        return rotation
+    }
+     
+    set rotation(rotation){ 
+        //this.direction = new THREE.Vector3(0,0,-1).applyEuler(rotation)  
+        
+        if(this.rollFree){
+            if(rotation.order != 'ZXY'){
+                return this.quaternion = new THREE.Quaternion().setFromEuler(rotation)
+            } 
+            this.yaw = rotation.z
+            this.rollFree && (this.roll = rotation.y)  //add  一般是极小的数字
+            this.pitch = rotation.x - Math.PI / 2 
+        }else{
+            if(rotation.y != 0){//因为 rotation的y不一定是0 , 所以不能直接逆着get rotation写。 
+                //console.error('set rotation y不为0!!!!?', rotation ) //过渡时因为quaternion lerp所以不为0。没办法了orz
+                this.direction = new THREE.Vector3(0,0,-1).applyEuler(rotation)  //转回direction有损耗,在俯视时的(dir.x==dir.y==0), 丢失yaw信息从而 yaw无法获取(希望不要遇到这种情况,如果有的话,考虑先计算yaw,似乎好像可以算)
+            }else{
+                this.pitch = rotation.x - Math.PI / 2
+                this.yaw = rotation.z
+            }
+        } 
+           
+    }
+    setRollFree(state){
+        this.rollFree = state
+        //if(!state)this.roll = 0 下次生效
+    }
+    get quaternion(){
+        /* if(this.rotMode == 'free'){
+            return this.freeQuaternion
+        }else{ */
+            return new THREE.Quaternion().setFromEuler(this.rotation)
+        //} 
+    }
+    
+    set quaternion(q){ 
+        /* if(this.rotMode == 'free'){
+            this.freeQuaternion.copy(q)
+        } */ 
+        this.rotation = new THREE.Euler().setFromQuaternion(q, this.rollFree && 'ZXY')
+         //不知为何非rollFree时不能用ZXY,会突然转向
+    }
+    
+    
+    
+    
+    
+	lookAt(t){//setPivot 
+		let V;
+		if(arguments.length === 1){
+			V = new THREE.Vector3().subVectors(t, this.position);
+		}else if(arguments.length === 3){
+			V = new THREE.Vector3().subVectors(new THREE.Vector3(...arguments), this.position);
+		}
+
+		let radius = V.length();
+		let dir = V.normalize();
+
+		this.radius = radius;
+		this.direction = dir;
+	}
+
+	getPivot () {
+		return new THREE.Vector3().addVectors(this.position, this.direction.multiplyScalar(this.radius));
+	}
+
+	getSide () {
+		let side = new THREE.Vector3(1, 0, 0);
+		side.applyAxisAngle(new THREE.Vector3(0, 0, 1), this.yaw);
+
+		return side;
+	}
+ 
+
+    pan (x, y) { //发现pan其实就是translate
+		this.translate(x, 0, y)
+	}
+    
+	translate (x, y, z, {forceHorizon, onlyGetVec}={}) {
+        //相机方向
+		let dir = new THREE.Vector3(0, 1, 0);
+		dir.applyAxisAngle(new THREE.Vector3(1, 0, 0), forceHorizon ? 0 : this.pitch); //上下角度
+		dir.applyAxisAngle(new THREE.Vector3(0, 0, 1), this.yaw);//水平角度  
+        
+        
+		let side = new THREE.Vector3(1, 0, 0);
+		side.applyAxisAngle(new THREE.Vector3(0, 0, 1), this.yaw);  //垂直于相机当前水平朝向的 左右方向
+
+		let up = side.clone().cross(dir); //垂直于相机当前水平朝向的 向上方向
+
+		let shift = side.multiplyScalar(x)   //x影响 左右分量
+			.add(dir.multiplyScalar(y))  //y影响 前后分量
+			.add(up.multiplyScalar(z));  //z影响 上下分量
+             
+        if(onlyGetVec)return shift
+        
+		this.position = this.position.add(shift);
+         
+        if(!math.closeTo({x,y,z}, 0, 1e-2)){ 
+            this.cancelFlying('pos')
+        }
+        
+        this.restrictPos()
+	}
+
+	translateWorld(x, y, z) { 
+		this.position.x += x;
+		this.position.y += y;
+		this.position.z += z;
+        
+        if(!math.closeTo({x,y,z}, 0, 1e-2)){ 
+            this.cancelFlying('pos')
+        }
+        
+        this.restrictPos()
+	}
+    restrictPos(position){//add
+        if(this.limitBound){
+            (position || this.position).clamp(this.limitBound.min, this.limitBound.max)
+        }
+    }
+ 
+    isFlying(type='all'){
+           
+        let a = transitions.getById(this.FlyTransition).length > 0
+        let b = transitions.getById(this.LookTransition).length > 0 
+       
+        return type == 'pos' ? a : type == 'rotate' ? b :  (a || b)
+    }
+    
+    cancelFlying(type='all', dealCancel=true){//外界只能通过这个来cancel
+         
+        type == 'pos' ? transitions.cancelById(this.FlyTransition, dealCancel )
+         : type == 'rotate' ? transitions.cancelById(this.LookTransition, dealCancel )    
+         : (transitions.cancelById(this.FlyTransition, dealCancel ), transitions.cancelById(this.LookTransition, dealCancel ))
+         //console.warn('cancelFlying ' , this.sid,  type)
+    }
+    
+    setView( info = {}){  
+        //console.log('setview', info)
+        this.cancelFlying()
+        let posWaitDone,  rotWaitDone , dir
+        
+        let posDone = ()=>{ 
+            rotWaitDone || done()
+            posWaitDone = false
+        }
+        let rotDone = ()=>{
+            if(endTarget){
+                this.lookAt(endTarget); //compute radius for orbitcontrol 
+            }else if(endQuaternion){
+                this.rotation = new THREE.Euler().setFromQuaternion(endQuaternion)
+            } 
+            if(endYaw != void 0){//前面两种在正俯视仰视时不准,故额外加一个这个
+                this.yaw = endYaw,  this.pitch = endPitch
+            }            
+            //if(dir.x == 0 && dir.y == 0)this.yaw = 0 //统一一下 朝上的话是正的。朝下的一般不是0,会保留一个接近0的小数所以不用管
+           
+            posWaitDone || done()
+            rotWaitDone = false 
+        }
+        
+        let done = ()=>{ //一定要旋转和位移都结束了才能执行
+            
+            let f = ()=>{ 
+                this.position.copy(endPosition)  //因为延时 后control的update会导致位置改变
+                info.callback && info.callback()   
+                this.dispatchEvent('flyingDone')  
+            }
+            if(info.duration){
+                setTimeout(f,10)//延迟是为了使isFlying先为false  1有概率不够
+            }else{
+                f()  //有的需要迅速执行回调
+            }
+            
+        }
+        
+        let endPosition = new THREE.Vector3().copy(info.position)
+        let startPosition = this.position.clone();
+		let startQuaternion, endQuaternion, endTarget = info.target && new THREE.Vector3().copy(info.target)  ,  
+            endYaw, startYaw, endPitch, startPitch, endRadius ;
+        
+        
+        this.restrictPos(endPosition)
+         
+         
+         
+         
+        if(info.endYaw == void 0){
+            if(info.target ){ 
+                endQuaternion = math.getQuaFromPosAim(endPosition,endTarget) //若为垂直,会自动偏向x负的方向
+                endRadius = endPosition.distanceTo(info.target)
+            }else if(info.quaternion){
+                endQuaternion = new THREE.Quaternion().copy(info.quaternion)
+            }   
+            
+            if(this.rotMode != 'free' && endQuaternion && math.closeTo(Math.abs(this.direction.z), 1, 1e-4)){ //在垂直的视角下的quaternion刚开始突变的厉害,这时候可能渐变yaw比较好(如俯视时点击测量线)
+                let a = this.clone();
+                a.quaternion = endQuaternion;
+                info.endYaw = a.yaw; info.endPitch = a.pitch;
+                //console.log('turn to yaw')
+            }
+        }
+        
+ 
+        if(info.endYaw != void 0) { 
+            startPitch = this.pitch
+            endPitch = info.endPitch;
+            startYaw = this.yaw
+            endYaw = info.endYaw 
+            if(Math.abs(startYaw - endYaw)>Math.PI){//如果差距大于半个圆,就要反个方向转(把大的那个数字减去360度)
+                startYaw > endYaw ? (startYaw -= Math.PI*2) : (endYaw -= Math.PI*2) 
+            }
+            //console.log('startYaw', startYaw, 'endYaw', endYaw)
+		}  
+         
+        if(endQuaternion){ 
+            startQuaternion = this.quaternion 
+        }
+        
+        
+        
+		if(!info.duration){
+			this.position.copy(endPosition);
+            this.restrictPos()
+			posWaitDone = true, rotWaitDone = true 
+            info.onUpdate && info.onUpdate(1)
+            posDone()
+            rotDone()
+		}else{
+            info.onUpdate && info.onUpdate(0) //初始化progress
+            let ease = info.Easing ? easing[info.Easing] : easing.easeInOutSine
+            if(endRadius && this.radius !== endRadius){
+                transitions.start(lerp.property(this, "radius", endRadius), info.duration, null, 0, ease, null, this.FlyTransition, null, info.ignoreFirstFrame )
+                    
+            }
+            let posChange = !this.position.equals(endPosition)
+            if(posChange){
+                posWaitDone = true 
+                transitions.start(lerp.vector(this.position, endPosition, (pos, progress, delta)=>{
+                    
+                    info.onUpdate && info.onUpdate(progress, delta)  
+                    
+                }), info.duration, posDone , 0, ease ,null, this.FlyTransition, ()=>{
+                    //中途取消 
+                    if(rotWaitDone ){
+                        /* endPosition = new THREE.Vector3().copy(this.position)//更改旋转的endQuaternion 
+                        endQuaternion = math.getQuaFromPosAim(endPosition,endTarget)  */
+                        //直接改变endQuaternion会突变,所以还是cancel吧
+                        this.cancelFlying('rotate')
+                    }else{
+                        this.dispatchEvent('flyCancel')
+                    }
+                    posWaitDone = false 
+                    info.cancelFun && info.cancelFun()
+                    
+                }, info.ignoreFirstFrame);  
+            } 
+            
+            if(endQuaternion || endYaw != void 0){
+                rotWaitDone = true 
+                transitions.start( (progress, delta )=>{
+                    if(endYaw != void 0){
+                        this.yaw = startYaw * (1-progress) + endYaw * progress
+                        this.pitch = startPitch * (1-progress) + endPitch * progress
+                    }else{ 
+                        let quaternion = (new THREE.Quaternion()).copy(startQuaternion) 
+                        lerp.quaternion(quaternion, endQuaternion)(progress)  //在垂直的视角下的角度突变的厉害,这时候可能渐变yaw比较好
+                         
+                        this.quaternion = quaternion
+                        //console.log(quaternion,this.yaw)
+                    }
+                    posChange || info.onUpdate && info.onUpdate(progress, delta)  
+                    
+                }, info.duration, rotDone , 0, ease ,null, this.LookTransition, ()=>{
+                    //中途取消
+                    rotWaitDone = false
+                    info.cancelFun && info.cancelFun()
+                    this.dispatchEvent('flyCancel')
+                }, info.ignoreFirstFrame); 
+                  
+            }      
+         
+            if(!posWaitDone && !rotWaitDone){//已经到达目标
+                info.onUpdate && info.onUpdate(1)
+                done()
+            } 
+        } 
+
+    }
+    
+    
+    //平移Ortho相机
+    moveOrthoCamera(viewport,  info, duration,  easeName){//boundSize优先于endZoom。
+        let camera = info.camera || viewport.camera
+        
+        let startZoom = camera.zoom 
+        let endPosition = info.endPosition 
+        let boundSize = info.boundSize
+        let endZoom = info.endZoom
+        let margin = info.margin || {x:0,y:0}/* 200 */ //像素
+        let onUpdate = info.onUpdate 
+        
+        
+        
+        if(info.bound){//需要修改boundSize以适应相机的旋转,当相机不在xy水平面上朝向z时
+            endPosition = endPosition || info.bound.getCenter(new THREE.Vector3())
+            
+            let matrixRot = new THREE.Matrix4().makeRotationFromEuler(this.rotation).invert() 
+            let boundingBox = info.bound.clone().applyMatrix4(matrixRot) 
+            boundSize = boundingBox.getSize(new THREE.Vector3())
+            
+        }           
+        
+        if(boundSize && boundSize.x == 0 && boundSize.y == 0){
+            boundSize.set(1,1)  //避免infinity
+        }
+         
+        this.setView( Object.assign(info,  { position:endPosition,  duration, 
+            
+            onUpdate:(progress, delta)=>{ 
+                if(boundSize || endZoom){ 
+                    if(boundSize){
+                        let aspect = boundSize.x / boundSize.y
+                        let w, h; 
+                        
+                        if(camera.aspect > aspect){//视野更宽则用bound的纵向来决定
+                            h = boundSize.y 
+                            endZoom = (viewport.resolution.y - margin.y) / h    //注意,要在resolution不为0时执行 
+                        }else{
+                            w = boundSize.x;  
+                            endZoom = (viewport.resolution.x - margin.x) / w
+                        }  
+                        //onUpdate时更新endzoom是因为画布大小可能更改
+                    }  
+                    
+                    this.zoom = camera.zoom = endZoom * progress + startZoom * (1 - progress)    //view里也加一下,有些地方需要记录,如截图
+                    camera.updateProjectionMatrix() 
+                    onUpdate && onUpdate(progress, delta)
+                } 
+            },
+            
+            Easing:easeName
+         
+        }))
+          
+        
+    }
+    
+    
+    
+    zoomOrthoCamera(camera, endZoom, pointer, duration, onProgress){//定点缩放
+         
+        let startZoom = camera.zoom
+      
+        let pointerPos = new THREE.Vector3(pointer.x, pointer.y,0.5); 
+        
+       
+        transitions.start(( progress)=>{ 
+            let oldPos = pointerPos.clone().unproject(camera);
+            
+            this.zoom = camera.zoom = endZoom * progress + startZoom * (1 - progress)
+            camera.updateProjectionMatrix() 
+            
+            
+            let newPos = pointerPos.clone().unproject(camera);
+            
+            //定点缩放, 恢复一下鼠标所在位置的位置改变量
+            let moveVec = new THREE.Vector3().subVectors(newPos, oldPos) 
+             
+            camera.position.sub(moveVec)
+            this.position.copy(camera.position)
+            
+            onProgress && onProgress()
+            
+        } , duration, null/* done */, 0,  easing.easeInOutSine, null, "zoomInView"/* , info.cancelFun */); 
+
+     
+        
+    } 
+    
+    
+    tranCamera(viewport,  info, duration,  easeName){
+        viewport.camera = info.midCamera
+        //viewport.camera.matrixWorld = info.endCamera.matrixWorld
+        
+        
+        //viewer.setCameraMode(CameraMode.ORTHOGRAPHIC) 
+        info.midCamera.projectionMatrix.copy(info.startCamera.projectionMatrix)
+        
+        let onUpdate = info.onUpdate
+        info.onUpdate = (progress, delta)=>{ 
+            lerp.matrix4(info.midCamera.projectionMatrix, info.endCamera.projectionMatrix)(progress) 
+             
+            onUpdate && onUpdate(progress, delta)
+        }
+        
+        let callback = info.callback
+        info.callback = ()=>{ 
+            viewport.camera = info.endCamera 
+            viewer.scene.measurements.forEach((e)=>{ 
+                Potree.Utils.updateVisible(e, 'tranCamera', true) 
+            }) 
+            this.applyToCamera(viewport.camera)
+            viewer.dispatchEvent({type:'camera_changed', viewport:viewer.mainViewport, changeInfo:{}})//update sprite
+             
+            callback && callback()
+        } 
+        //info.forbitCancel = true 
+        
+        info.camera = info.endCamera
+        
+        if(info.camera.type == "OrthographicCamera"){
+            this.moveOrthoCamera(viewport,  info, duration,  easeName)
+        }else{
+            this.setView( Object.assign(info,  { duration}) )
+        }
+        
+    }
+    
+    getJson(){
+        let json = {
+            yaw: this.yaw,  pitch: this.pitch,  position: this.position.clone(), radius: this.radius
+        }
+        return json  //JSON.stringify(json)
+    } 
+    
+    applyJson(json){
+        typeof json == 'string' &&  (json = JSON.parse(json))
+        this.position.copy(json.position)
+      
+        this.yaw = json.yaw,  this.pitch = json.pitch, this.radius = json.radius ?? this.radius
+    }
+    
+    
+    setCubeView(dir) {
+		 
+		switch(dir) {
+			case "front":
+				this.yaw = 0;
+                this.pitch = 0;
+				break;
+			case "back":
+				this.yaw =  Math.PI;  
+                this.pitch = 0;
+				break;
+			case "left":
+				this.yaw = -Math.PI / 2;
+                this.pitch = 0;
+				break;
+			case "right":
+				this.yaw = Math.PI / 2;
+                this.pitch = 0;
+				break;
+			case "top":
+				this.yaw = 0;
+                this.pitch = -Math.PI / 2;
+				break;
+			case "bottom":
+				this.yaw = -Math.PI;
+                this.pitch = Math.PI / 2;
+				break;
+		}
+	}
+};

+ 544 - 0
src/modelViewer/Viewer.js

@@ -0,0 +1,544 @@
+ 
+import * as THREE from 'three'; 
+import { ViewerBase } from './viewerBase.js'    
+import math from './utils/math.js'
+import { View } from './View.js' 
+import Viewport from './Viewport.js' 
+import { InputHandler } from './InputHandler.js'
+import ModelManager from './ModelManager.js'  
+import {OrbitControls} from './OrbitControls.js' 
+import {Common} from './utils/Common.js'
+import {transitions, easing, lerp} from './utils/transitions.js'
+
+import {TransformationTool} from './utils/TransformationTool.js'
+
+import {LineMaterial} from "./objects/fatline/LineMaterial.js"; 
+import CursorDeal from './utils/CursorDeal.js'
+
+
+
+const texLoader = new THREE.TextureLoader()
+const raycaster = new THREE.Raycaster() 
+
+export class Viewer extends ViewerBase {
+    constructor( args = {}) {
+        super( 
+            Object.assign(args, {
+                name: 'mainViewer',
+                antialias: true,
+                preserveDrawingBuffer: false,
+            })
+        ) //分屏局部刷新要preserveDrawingBuffer
+        
+        
+        if(this.renderer.capabilities.isWebGL2){
+            settings.isWebgl2 = true  //是否启用webgl2
+        }
+          
+        
+        this.clock = new THREE.Timer()
+        this.visible = true
+        //this.background = new THREE.Color(config.background)
+        CursorDeal.init(this, [this])
+        LineMaterial.registerViewer(this)
+        Common.registerViewer(this)
+        
+        let view = new View()
+        let fov = settings.fov || 45
+        this.cameraPerspect = new THREE.PerspectiveCamera(
+            fov,
+            1,
+            0.01,
+            1000
+        )
+        this.setFOV(fov) 
+        this.mainViewport = new Viewport(view, this.cameraPerspect, {
+            left: 0,
+            bottom: 0,
+            width: 1,
+            height: 1,
+            name: 'MainView',
+        })
+        this.viewports.push(this.mainViewport)
+        
+        
+
+        this.inputHandler = new InputHandler(this, this)
+        this.inputHandler.containsMouse = true //初始化,使键盘事件在mainViewer有效
+        this.inputHandler.registerInteractiveScene(this.scene) 
+ 
+        this.orbitControls = new OrbitControls(this) 
+         
+        this.controls = this.orbitControls
+        this.controls.setEnable(true) 
+ 
+        this.modelManager = new ModelManager(this)
+
+        ;((this.objs = new THREE.Object3D()), (this.objs.name = 'objs'))
+        this.objs.addEventListener('isVisible', () => {
+            this.setAllTilesets((model) => model.visiChangeCallback())
+        })
+        this.scene.add(this.objs)
+  
+        this.renderer.setAnimationLoop(this.loop.bind(this))
+  
+  
+        setTimeout(() => {
+            this.inputHandler.addEventListener('keydown', (e) => {
+                try {
+                    if (e.event.ctrlKey) {
+                        if (e.event.key.toLowerCase() == 'c') {
+                            let info = this.mainViewport.view.getJson() 
+                             
+                            info = JSON.stringify(info)
+                            ;(console.log(`Copy view params: ${info}`),
+                                navigator.clipboard.writeText(info)) //need https  似乎又不用了,会弹出是否允许粘贴
+                        } else if (e.event.key.toLowerCase() == 'v') {
+                            navigator.clipboard.readText().then((A) => {
+                                this.mainViewport.view.applyJson(A)
+                                console.log(`pasteViewParams ${A}`)
+                            })
+                        }
+                    }
+                } catch (e) {
+                    console.log(e)
+                }
+            })
+        }, 10)
+
+        let lights = new THREE.Group()
+        let light1 = new THREE.AmbientLight( 16777215, 2.5 ); 
+        lights.add(light1)
+        let light2 = new THREE.DirectionalLight( 16777215, 2 );  
+        light2.position.set(8.144397967842998, -12.29943471041463, -4.045120249012417);
+		light2.lookAt( new THREE.Vector3(0, 0, 0)); 
+        lights.add(light2)  
+        let light3 = new THREE.DirectionalLight( 16777215, 1);  
+        light3.position.set(-9.307866730425975, 35.775724620743354, 34.59199145571955);
+		light3.lookAt( new THREE.Vector3(0, 0, 0));
+        lights.add(light3) 
+        
+        if(settings.isTest){ 
+            let g = new THREE.SphereGeometry(1, 4, 4),
+                m = new THREE.MeshBasicMaterial({color:'#f02'})
+             
+            let helpers = [];
+            ([light2, light3].forEach(light=>{
+                let helper = new THREE.DirectionalLightHelper(light)
+                lights.add(helper) 
+                helpers.push(helper)
+                const sphere = new THREE.Mesh(g, m) 
+                sphere.position.copy(light.position)
+                sphere.addEventListener('select',()=>{})
+                sphere.boundingBox = new THREE.Box3
+                sphere.addEventListener('position_changed',()=>{
+                    light.position.copy(sphere.position)
+                    helper.update() 
+                })  
+                lights.add(sphere)
+            }))
+            
+            helpers[1].traverse(e=>e.material && e.material.color.set('#999'))
+            
+            
+            this.reportLightPos = ()=>{
+                console.log('light2', light2.position.toArray())
+                console.log('light3', light3.position.toArray())
+            }
+            this.getTranTool()
+             
+        }
+        
+        this.scene.add(lights)  
+  
+  
+    }
+
+    loop(timestamp) {
+        
+       
+        this.clock.update()
+        let delta = this.clock.getDelta() 
+        if (this.paused) return
+         
+        this.stats?.begin()
+
+        this.dispatchEvent({ type: 'loopStart', delta })
+  
+        transitions.update(delta) //写在开头,因为这时候最为固定,计时准确
+        this.updateScreenSize() //判断是否改变canvas大小
+        this.controls.update(delta)
+        this.viewports.forEach((viewport) => {
+            if (!viewport.active) return
+            viewport.view.applyToCamera(viewport.camera)
+        })
+        this.lastFrameChanged = this.cameraChanged() //判断camera画面是否改变
+       
+        //this.setAllTilesets(model=>model.runtime.update(deltaTime, this.renderer, this.mainViewport.camera))
+        /* {
+            let hasAnimation
+            window.pauseAni ||
+                this.objs.children.forEach((model) => {
+                    if (
+                        model.visible &&
+                        model.mixer &&
+                        (model.clipChanged ||
+                            model.actions.some((a) => a._mixer._isActiveAction(a) && !a.paused))
+                    ) {
+                        //播放中或者动作状态改变
+                        hasAnimation = true
+                        model.clipChanged = false
+                        model.mixer.update(delta)
+                        //console.log('mixer update', model.name)
+                    }
+                }) //以后有空的话用frust判断是否在画面内,不在的话即使有动画也不要 update 和 render, 如果paused的话是不是也可以不update
+            hasAnimation && this.dispatchEvent('content_changed')
+        }  */
+        this.transformationTool?.update()
+        this.render()
+    
+    }
+    
+    
+
+    //渲染顺序:  渲染不透明物体  ->   splatter ->   ClearDepth ->  渲染overlay,最上层物体, 透明物体只能放这渲染
+
+    render(params_ = {}) {
+        if (!this.visible) return
+
+        let viewports = params_.viewports || this.viewports
+
+        if (!this.needRender && !settings.renderAllViewports) {
+            viewports = viewports.filter((v) => v.needRender) //可以渲染的条件是viewer或viewport的needRender为true
+        }
+        viewports = viewports.filter((v) => v.active)
+        if (viewports.length == 0) return
+
+        let renderer = params_.renderer || this.renderer
+
+        let renderSize
+        if (params_.target) {
+            renderSize = new THREE.Vector2(params_.target.width, params_.target.height) //是画布大小
+        } else {
+            renderSize = renderer.getSize(new THREE.Vector2()) //是client大小
+        }
+
+        for (let i = 0; i < viewports.length; i++) {
+            let viewport = viewports[i]
+
+            let params = Object.assign({}, params_)
+            params.viewport = viewport
+            params.camera = params.camera || viewport.camera
+            params.extraEnableLayers = viewport.extraEnableLayers
+            params.cameraLayers = viewport.cameraLayers
+ 
+            viewport.noOverlay && (params.noOverlay = true)
+
+            var left, bottom, width, height
+            {
+                left = Math.ceil(renderSize.x * viewport.left)
+                bottom = Math.ceil(renderSize.y * viewport.bottom)
+
+                if (params_.target) {
+                    //有target时最好viewport是专门建出来的
+                    width = renderSize.x * viewport.width //target的大小可能和viewport不同,比如截图,这时会更改viewport大小
+                    height = renderSize.y * viewport.height
+                } else {
+                    width = viewport.resolution.x // 用的是client的width和height
+                    height = viewport.resolution.y
+                }
+                if (width == 0 || height == 0) continue //return
+
+                width = Math.ceil(width) //使用ceil,当原本有小数时,会重叠一个像素,无所谓
+                height = Math.ceil(height)
+
+                let scissorTest = viewport.width < 1 || viewport.height < 1 //不设置这个会把别的viewport clear掉
+                if (params_.target) {
+                    params_.target.viewport.set(left, bottom, width, height)
+                    scissorTest && params_.target.scissor.set(left, bottom, width, height)
+                    params_.target.scissorTest = scissorTest
+                    renderer.setRenderTarget(params_.target)
+                 } else {
+                    renderer.setViewport(left, bottom, width, height) //规定视口,影响图形变换(画布的使用范围)
+                    scissorTest && renderer.setScissor(left, bottom, width, height) //规定渲染范围
+                    renderer.setScissorTest(scissorTest) //开启WebGL剪裁测试功能,如果不开启,.setScissor方法设置的范围不起作用 | width==1且height==1时开启会只有鼠标的地方刷新,很奇怪
+                }
+            }
+
+            this.ifEmitResize({ viewport })
+             
+            viewport.beforeRender && viewport.beforeRender()
+
+            this.clear(params)
+            
+                
+            this.dispatchEvent({ type: 'render.begin', viewer: this, viewport, params })
+            renderer.render(this.scene, params.camera) 
+            renderer.clearDepth()
+            this.transformationTool &&  renderer.render(this.transformationTool.scene, params.camera)  
+            viewport.afterRender && viewport.afterRender()
+
+            this.dispatchEvent({ type: 'render.end', viewer: this, viewport })
+            viewport.needRender = false 
+        }
+
+        this.renderer.setRenderTarget(null)
+
+        this.needRender = false
+    }
+
+    
+    setFOV(fov) {
+        let oldFov = this.cameraPerspect.fov
+        this.fov = fov
+        if (settings.keepMinFov) {
+            this.cameraPerspect.setMinFov(this.fov)
+        } else {
+            this.cameraPerspect.fov = this.fov //add
+        }
+        if (oldFov != this.cameraPerspect.fov) {
+            this.cameraPerspect.updateProjectionMatrix() //add 
+        }
+    }
+
+     
+    focusOnObject(object, o = {}) {
+      
+        let resolve, reject, type
+        let promise = new Promise((resolve_, reject_)=>{resolve = resolve_, reject = reject_})
+         
+              
+        const result = {}
+        let target = new THREE.Vector3(), //相机focus的位置
+            position = new THREE.Vector3(), //相机最终位置
+            dis
+        if (object instanceof THREE.Vector3) {
+            ;((object = { position: object }), (type = 'point'))
+        } else if (object instanceof THREE.Box3) {
+            ;((object = { boundingBox: object }), (type = 'boundingBox'))
+        }
+        o.duration = 0  //o.duration ?? 1200  //暂时没加transition
+        let viewport = o.viewport || this.mainViewport
+        let camera = o.endCamera || viewport.camera
+        let cameraPos = camera.position.clone()
+        let boundSize
+
+        if (o.dontChangeCamDir && (o.endYaw == void 0 || o.endPitch == void 0)) {
+            //在俯视时仅靠dir来算不准
+            o.endYaw = viewport.view.yaw
+            o.endPitch = viewport.view.pitch
+        }
+
+        let getPosWithFullBound = (points, boundingBox, target, cameraPos) => {
+            //使boundingBox差不多占满屏幕时的相机到target的距离
+            // points 和 boundingBox 至少有一个
+
+            let scale
+
+            if (o.dontChangeCamDir) {
+                var inv = camera.matrixWorldInverse
+            } else {
+                var cameraTemp = camera.clone()
+                let view = viewer.mainViewport.view.clone()
+                view.position.copy(cameraPos)
+                view.lookAt(target)
+                if (o.endPitch != void 0) {
+                    view.pitch = o.endPitch
+                    view.yaw = o.endYaw
+                }
+                view.applyToCamera(cameraTemp)
+
+                //对镜头的bound
+                var inv = cameraTemp.matrixWorldInverse
+            }
+            var bound = new THREE.Box3()
+            if (points) {
+                //使用points得到的bound更小  //如果points和boundingbox的差别较大,尤其使target和points中心不一致,那么points不一定会刚好在boundingbox内
+                points.forEach((e) => {
+                    var p = e.clone().applyMatrix4(inv)
+                    bound.expandByPoint(p)
+                })
+                scale = 1.2
+            } else {
+                bound = boundingBox.applyMatrix4(inv)
+                scale = 1 //0.9;
+            }
+            boundSize = bound.getSize(new THREE.Vector3())
+
+            if (o.boundScale) {
+                scale = o.boundScale
+            }
+
+            {
+                boundSize.x *= scale //稍微放大一些,不然会靠到屏幕边缘
+                boundSize.y *= scale
+                let min = o.minBound || 1
+                boundSize.x = Math.max(min, boundSize.x)
+                boundSize.y = Math.max(min, boundSize.y)
+            }
+            if (camera.type == 'OrthographicCamera') {
+                dis = boundSize.length() / 2
+            } else {
+                let aspect = boundSize.x / boundSize.y
+                if (camera.aspect > aspect) {
+                    //视野更宽则用bound的纵向来决定
+                    dis =
+                        boundSize.y / 2 / Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) +
+                        boundSize.z / 2
+                } else {
+                    let hfov = cameraLight.getHFOVForCamera(camera, true)
+                    dis = boundSize.x / 2 / Math.tan(hfov / 2) + boundSize.z / 2
+                }
+                dis += camera.near
+            }
+            dis = Math.max(0.1, dis)
+
+            //三个顶点以上的由于measure的中心不等于bound的中心,所以点会超出bound外。 且由于视椎近大远小,即使是两个点的,bound居中后线看上去仍旧不居中.
+
+            //获得相机最佳位置
+            let dir
+            if (o.dontChangeCamDir) {
+                dir = viewport.view.direction.negate()
+            } else {
+                dir = new THREE.Vector3().subVectors(cameraPos, target).normalize()
+            }
+            if (o.dontLookUp && dir.z < 0) {
+                dir.negate()
+            }
+            position.copy(target).add(dir.multiplyScalar(dis))
+
+            if (false) {
+                //打开以检查box
+                if (!this.boundBox) {
+                    //调试
+                    this.boundBox = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1, 1))
+                    this.boundBox.material.wireframe = true
+                    this.boundBox.up.set(0, 0, 1)
+                    this.scene.scene.add(this.boundBox)
+                }
+                this.boundBox.position.copy(target)
+                this.boundBox.scale.copy(boundSize)
+                this.boundBox.lookAt(position)
+            }
+
+            return position
+        }
+
+        if (type == 'point') {
+            //dimension = 1
+            target.copy(object.position)
+            let bestDistance = o.distance || 2
+ 
+            if (o.dontChangePos) {
+                position.copy(cameraPos)
+            } else {
+                if (o.maxDis) {
+                    let disNow = cameraPos.distanceTo(target)
+                    dis = THREE.MathUtils.clamp(disNow, 1, o.maxDis)
+                } else {
+                    dis = bestDistance
+                } 
+                let dir_ = o.direction 
+                o.maxDis && (dis = Math.max(dis, o.maxDis)  ) 
+                position = target.clone().addScaledVector(dir_, dis); 
+                result.distance = dis
+                result.direction = dir_
+                result.position = position.clone() 
+                o.posCallback && o.posCallback(result) 
+            }
+            if(o.speed != void 0){ 
+                let moveDis = position.distanceTo(viewport.view.position)
+                //console.log('movedis', moveDis)
+                if(o.Easing == 'linearTween'){
+                    o.duration = moveDis / o.speed 
+                }else{// 'easeOutSine'   
+                    o.duration = ((Math.PI / 2) * moveDis) / o.speed  //speed是初始速度  
+                }
+            }
+           
+        } else if (object.boundingBox || type == 'boundingBox') {
+            //使屏幕刚好看全boundingBox
+
+            target = object.boundingBox.getCenter(new THREE.Vector3())
+            if (o.dir) {
+                //指定方向
+                cameraPos.copy(target).sub(o.dir)
+            }
+            position = getPosWithFullBound(
+                object.points,
+                object.boundingBox.clone(),
+                target,
+                cameraPos
+            )
+        }
+        let duration = o.duration
+        if (o.startCamera && o.endCamera) {
+            viewport.view.tranCamera(
+                viewport,
+                {
+                    endPosition: position,
+                    target,
+                    boundSize,
+                    callback: () => {
+                        //console.log('focusOnObjectSuccess: '+object.name,  type)
+                        resolve()
+                    },
+                    startCamera: o.startCamera,
+                    endCamera: o.endCamera,
+                    midCamera: this.scene.cameraBasic,
+                    endYaw: o.endYaw,
+                    endPitch: o.endPitch,
+                },
+                duration
+            )
+        } else if (camera.type == 'OrthographicCamera') {
+            viewport.view.moveOrthoCamera(
+                viewport,
+                {
+                    endPosition: position,
+                    target,
+                    boundSize,
+                    endYaw: o.endYaw,
+                    endPitch: o.endPitch,
+                    callback: () => {
+                        //console.log('focusOnObjectSuccess: '+object.name,  type)
+                         resolve()
+                    },
+                },
+                duration
+            )
+        } else {
+            viewport.view.setView({
+                position,
+                target,
+                duration,
+                endYaw: o.endYaw,
+                endPitch: o.endPitch,
+                Easing: o.Easing,
+                onUpdate: o.onUpdate,
+                callback: () => {
+                    //console.log('focusOnObjectSuccess: '+object.name,  type)
+                    
+                    resolve()
+                },
+            })
+        }
+
+        this.dispatchEvent({ type: 'focusOnObject', CamTarget: target, position }) //给controls发送信息
+        
+        result.duration = duration
+        result.promise = promise
+        return result
+         
+    }
+
+    getTranTool() {
+        if (!this.transformationTool) {
+            this.transformationTool = new TransformationTool(this)
+        }
+        return this.transformationTool
+    }
+ 
+}
+
+ 

+ 104 - 0
src/modelViewer/Viewport.js

@@ -0,0 +1,104 @@
+
+
+import * as THREE from 'three';
+import {Common} from './utils/Common.js'
+import math from './utils/math.js'
+
+
+export default class Viewport extends THREE.EventDispatcher{
+    
+    constructor( view, camera, prop={}){//目前不支持换camera
+        super()
+        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
+        this.needRender_ = false
+    }
+     
+    set needRender(s){
+        this.needRender_ = s
+    } 
+    
+    get needRender(){
+        return this.needRender_
+    } 
+    
+    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 = ()=>{ 
+            projectionChanged && (this.previousState.projectionMatrix = this.camera.projectionMatrix.clone())
+            positionChanged && (this.previousState.position = this.camera.position.clone())
+            quaternionChanged && (this.previousState.quaternion = this.camera.quaternion.clone())
+            resolutionChanged && (this.previousState.resolution = this.resolution.clone(), this.previousState.resolution2 = this.resolution2.clone())
+            this.previousState.active = this.active
+            
+        }
+        let projectionChanged = true, positionChanged = true, quaternionChanged = true, activeChanged = true, resolutionChanged = true
+        let getChanged = ()=>{
+            return {
+                projectionChanged,positionChanged,quaternionChanged, activeChanged, resolutionChanged,
+                changed:projectionChanged || positionChanged || quaternionChanged || activeChanged || resolutionChanged
+            }
+        }
+        if (this.previousState){ 
+            projectionChanged = !this.camera.projectionMatrix.equals(this.previousState.projectionMatrix) 
+            positionChanged = !this.camera.position.equals(this.previousState.position)  
+            quaternionChanged = !math.closeTo(this.camera.quaternion,this.previousState.quaternion)   //!this.camera.quaternion.equals(this.previousState.quaternion)//改为close是因为controls加了缓动会一直变 
+            activeChanged = this.active != this.previousState.active
+            resolutionChanged = !this.resolution.equals(this.previousState.resolution) || !this.resolution2.equals(this.previousState.resolution2)
+        }else{
+            this.previousState = {}
+        }   
+        copy() 
+        
+        return getChanged()
+	}
+    
+    setResolution(w,h, wholeW=0, wholeH=0, devicePixelRatio = window.devicePixelRatio){
+        this.resolution.set(w,h);//是client的width height
+        
+        this.resolution2.copy(this.resolution).multiplyScalar(this.pixelRatio || devicePixelRatio )
+         
+        this.offset.set(wholeW,wholeH).multiply(new THREE.Vector2(this.left,this.bottom))//.multiplyScalar(window.devicePixelRatio) 
+    
+        this.dispatchEvent({type:'resize'}) 
+    }
+}

+ 27 - 0
src/modelViewer/app.js

@@ -0,0 +1,27 @@
+
+import Viewer from './Viewer.js'
+import {browser} from './utils/Common.js' 
+
+ 
+window.settings = {
+    pauseIntersect:true,
+    isTest: true,
+    fov:  15 , //八猴是这个值
+    aniso: browser.isMobile() ? 4 : 8 //八猴是4
+}
+
+
+const sdk = {
+    init(dom, id){
+        
+    },
+    load(){
+        
+        
+    }
+    
+}
+
+
+
+export {sdk}

+ 379 - 0
src/modelViewer/index.html

@@ -0,0 +1,379 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="utf-8">
+	<meta name="description" content="">
+	<meta name="author" content="">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+	<title>Model Viewer</title>
+ 
+</head>
+<style>
+    <!-- #render_area{ 
+        position: absolute; 
+        top: 0px;
+        bottom: 0px;
+        left: 0px;
+        right: 0px;
+        overflow: hidden;
+        z-index: 1;  
+    } -->
+    canvas { 
+        width: 100%; 
+        height: 100% 
+    }
+    /* 全局样式 */  
+        * {
+            box-sizing: border-box;
+            margin: 0;
+            padding: 0;
+        }
+
+        body {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            min-height: 100vh;
+            background: #f0f4f8;
+            font-family: 'Segoe UI', Roboto, system-ui, sans-serif;
+            padding: 20px;
+        }
+
+        .progressContainer {
+            z-index: 999;
+            border-radius: 32px;
+            padding: 40px 48px; 
+            width: 100%;
+            max-width: 520px;
+            transition: box-shadow 0.2s;
+        }
+ 
+
+        /* 进度条容器 */
+        .progress-wrapper {
+            margin-bottom: 30px;
+        }
+
+        .progress-label {
+            display: flex;
+            justify-content: space-between;
+            font-size: 0.9rem;
+            font-weight: 500;
+            color: #2c3e50;
+            margin-bottom: 8px;
+        }
+
+        .progress-label .percentage {
+            color: #1e6f9f;
+            font-weight: 600;
+        }
+
+        /* 轨道 (背景) */
+        .progress-track {
+            background: #e9edf2;
+            border-radius: 40px;
+            height: 12px;
+            overflow: hidden;
+            box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.06);
+            position: relative;
+        }
+
+        /* 填充条 (动画) */
+        .progress-fill {
+            width: 0%;
+            height: 100%;
+            background: linear-gradient(90deg, #3b8fc2, #2a7da8);
+            border-radius: 40px;
+            transition: width 0.35s cubic-bezier(0.22, 0.61, 0.36, 1);
+            box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15) inset;
+            position: relative;
+        }
+
+        /* 光泽效果 (装饰) */
+        .progress-fill::after {
+            content: '';
+            position: absolute;
+            top: 2px;
+            left: 6px;
+            right: 20%;
+            height: 4px;
+            background: rgba(255, 255, 255, 0.3);
+            border-radius: 20px;
+            filter: blur(1px);
+            opacity: 0.7;
+        } 
+
+        /* 响应式小屏 */
+        @media (max-width: 480px) {
+            .progressContainer {
+                padding: 28px 20px;
+            }
+          
+        }
+
+</style>
+<body>
+  
+	<div id="render_area" style="position: absolute; width: 100%; height: 100%; left: 0px; top: 0px; ">
+		 
+	</div> 
+    <div class="progressContainer">  
+        <div class="progress-wrapper">
+            <div class="progress-label"> 
+                <span class="percentage" id="percentDisplay">0%</span>
+            </div>
+            <div class="progress-track">
+                <div class="progress-fill" id="progressFill" style="width: 0%;"></div>
+            </div>
+        </div>
+         
+    </div>
+
+     
+</head>
+<body>
+
+  
+ 
+    <script type="importmap">
+    {
+        "imports": {
+            "three": "/examples/tomcat/modelViewer2/libs/three.module.min.js"  
+        } 
+        
+    }
+     </script>   
+     <script>
+//let ftCanvas = document.createElement('canvas');
+
+
+window.Features = (function () {
+ 
+    let gl , webgl2Support
+    
+    return {
+        webgl2RealSupport(){
+            if(webgl2Support != void 0){
+                return webgl2Support
+            }
+        
+            try {
+                var canvas = document.createElement('canvas')
+                if(window.WebGL2RenderingContext){ //遇到有设备(iphone8 plus ios14.1 型号MQ8F2CH/A)直接获取webgl2后会点云和全景图闪烁,WebGL2RenderingContext和得到的context是undefined。但是为何4dkk不会闪烁
+                     gl = canvas.getContext('webgl2') //麒麟系统chromium 128 到这一步才获取失败 如果直接对最终的canvas获取webgl2,会造成多viewport无法单独渲染以及clearAlpha透明失败
+                }
+            }catch (e) {
+                console.log(e)  
+            }
+            webgl2Support = !!gl  
+            return webgl2Support 
+        },
+         
+        }
+    }());
+
+
+    
+   
+    
+    
+    </script>
+	<script type="module">
+        import * as THREE from 'three';
+        import {Viewer} from '/examples/tomcat/modelViewer2/Viewer.js' 
+        import {browser} from '/examples/tomcat/modelViewer2/utils/Common.js' 
+        import {transitions,easing} from '/examples/tomcat/modelViewer2/utils/transitions.js' 
+        import math  from '/examples/tomcat/modelViewer2/utils/math.js'
+        
+        import BasicMaterial from '/examples/tomcat/modelViewer2/material/BasicMaterial.js'
+        
+        window.settings = {
+            pauseIntersect:true,
+            isTest: true,
+            fov:  15 , //八猴是这个值
+            aniso: browser.isMobile() ? 4 : 8 //八猴是4
+        }
+        
+        window.viewer = new Viewer( {renderArea:  document.querySelector('#render_area')})
+        
+        const bound = 1000
+        viewer.controls.targetBound = { min: new THREE.Vector3(-bound,-bound,-bound),
+                                        max: new THREE.Vector3(bound,bound,bound)}
+        viewer.controls.maxRadius = 4000
+        viewer.background = '#999'
+        //viewer.backgroundOpacity = 0
+        
+        
+        //progressbar
+        const fill = document.getElementById('progressFill');
+        const percentDisplay = document.getElementById('percentDisplay');
+        const slider = document.getElementById('slider');
+        const sliderValue = document.getElementById('sliderValue');
+        const resetBtn = document.getElementById('resetBtn');
+        const halfBtn = document.getElementById('halfBtn');
+        const statusTag = document.getElementById('statusTag');
+        const statusMessage = document.getElementById('statusMessage');
+        const stateBadge = document.getElementById('stateBadge');
+
+        // 当前进度值 (0-100)
+        let currentValue = 0;
+
+        // 更新 UI (进度条、数字、辅助状态)
+        function updateProgress(value) {
+            // 钳制 0-100
+            //因解析还要时间,所以加载完时设置为一个最大值
+            let loadedValue = 90
+            let clamped = Math.min(100, Math.max(0, (loadedValue * value / 100).toFixed(1)));
+            currentValue = clamped;
+             
+            // 进度条宽度
+            fill.style.width = clamped + '%';
+            // 百分比数字
+            percentDisplay.textContent = clamped + '%'; 
+        }
+ 
+        
+        const mapNames = ['map','aoMap', 'normalMap','metalnessMap','roughnessMap']
+        window.materials = []
+        let done = (model)=>{
+            viewer.mainViewport.view.applyJson( {"yaw":-12.55357706351913,"pitch":-0.04359457986378778,"position":{"x":5.183429158209428,"y":-555.5368304352811,"z":25.216363971031676},"radius":556.1378226265584}) //setTimeout(()=>{viewer.focusOnObject(model)},10)   
+            document.querySelector('.progressContainer').style.display = 'none'
+            model.traverse(e=>{
+                if(e.material){ 
+                    materials.push(e.material) 
+                    
+                    mapNames.forEach(m=>{
+                        e.material[m].anisotropy = settings.aniso
+                        //e.material[m].minFilter = THREE.linearFilter 
+                    })
+                     
+                    e.material.aoMapIntensity = 5
+                    e.material.normalScale?.set(1.5,1.5)
+                     
+                     
+                    e.material.onBeforeCompile = function ( shader ) {
+                         
+                        //console.log(shader.vertexShader)
+                        console.log(shader.fragmentShader)
+                        shader.fragmentShader = shader.fragmentShader.replace(  //替换最后一个大括号
+                            /(void\s+main\s*\(.*?\)\s*\{[\s\S]*?)\}/,
+                            '$1' + `
+                             
+                            #ifdef Fresnel
+                                 // ===== 1. 标准漫反射光照(保留玉的立体感) =====
+                                /*vec3 lightDir = normalize(vec3(1.0, 2.0, 1.0)); // 固定主光源方向
+                                vec3 baseColor = vec3(1.0,1.0,1.0);
+                                float diff = max(dot(normal, lightDir), 0.0);
+                                vec3 diffuse = baseColor * (0.5 + 0.5 * diff);
+                                 */
+                                 
+                                 
+                                float rimPower = 20.; 
+                                vec3 viewDirN = normalize(vViewPosition);
+                                //normal is from normal_fragment_maps, 算上贴图后的法线
+                                 // ===== 3. 菲涅尔边缘光 =====
+                                float fresnel = 1.0 - max(dot(normal, viewDirN), 0.0);
+                                // 高次幂压缩:值越大,边缘光带越窄、越锐利
+                                float rim = pow(fresnel, rimPower);
+                                // 可选:阈值截断,去除中心微弱杂光,让轮廓更干净
+                                // rim = max(rim - 0.05, 0.0) / (1.0 - 0.05);
+                                //vec3 rimGlow = rimColor * rim * rimIntensity;
+
+                            
+                                // ===== 4. 合成最终颜色 =====
+                                
+                                gl_FragColor.a *= (0.1 + rim * 20.);//rim;
+                                gl_FragColor.rgb += vec3(1.,1.,1.) * rim;
+                                //gl_FragColor.rgb = mix( gl_FragColor.rgb, diffuse, 0.4);
+                            #endif   
+                            ` + '\n}'
+                        );  
+                        
+                    }
+                            
+                }
+            })
+        }
+        //为何半透明镯会有斑马阴影纹?用BasicMaterial材质不带map也会. 说是深度问题,好难啊
+        
+        
+        let onProgress_ = (v)=>{
+            console.log('onProgress_',v)
+            updateProgress(v);
+        }
+        let onError = ()=>{}
+        window.viewer.modelManager.loadModel({
+            fileType : 'glb',
+            url : 'models/lz076.glb',
+            
+            
+            //url : 'models/1/lz080-'+ (browser.urlHasValue('full') ? 'full' : 'high') + '.glb',
+             
+            metalness: 1, 
+            roughness:0.9,
+            transform:{
+                rotation : [Math.PI / 2, 0, 0]
+            },
+            //unlit:true
+        }, done, onProgress_, onError)
+        
+		window.THREE = THREE 
+        
+        
+        let texLoader = new THREE.TextureLoader()
+        
+        /*window.switchMap = function(){
+            let texLoader = new THREE.TextureLoader()
+            texLoader.load('/examples/tomcat/modelViewer2/models/1/2909_albedo.jpg',(tex)=>{
+                materials[0].map = tex
+                viewer.needRender = true 
+            }) 
+        }*/
+        window.showStructure = function(state){
+            if(state){
+                materials[0].defines.Fresnel = 1 
+                materials[0].transparent = true
+                materials[0].side = 2
+            }else{
+                delete materials[0].defines.Fresnel
+                materials[0].transparent = false
+                materials[0].side = 0
+            }
+            
+            materials[0].needsUpdate = true 
+            viewer.dispatchEvent('content_changed')
+        }
+        
+        let duration = 4000, minAlpha = 0.1, maxAlpha = 0.8 
+        
+        
+        window.addEmissive = function(){
+            
+            texLoader.load('/examples/tomcat/modelViewer2/models/1/lz080-up-light.png',(tex)=>{
+                //let plane = new THREE.Mesh(new THREE.PlaneGeometry(10,10), new THREE.MeshBasicMaterial({transparent:false}))
+                //viewer.scene.add(plane)
+                tex.premultiplyAlpha = true  //因透明部分不一定是黑色导致颜色不对,因emissive不读a通道所以预乘下 emissivemap_fragment
+                
+                tex.needsUpdate = true
+                materials[0].emissiveMap = tex  
+                materials[0].emissive.set('#fff')
+                materials[0].needsUpdate = true  //使加入   USE_EMISSIVEMAP
+                //materials[0].map = tex
+                //plane.material.map = tex   
+                
+                
+                viewer.dispatchEvent('content_changed')
+                
+                transitions.start((percent)=>{
+                    percent = percent * 2
+                    percent > 1 && (percent = 2 - percent )
+                    materials[0].emissiveIntensity = math.linearClamp(percent,[0,1], [minAlpha, maxAlpha]) 
+                    viewer.dispatchEvent('content_changed') 
+                }, -duration, null, 0, easing.easeInOutQuad, 'highlight'  )
+            }) 
+        }
+	</script>
+	
+	
+  </body>
+</html>

Разница между файлами не показана из-за своего большого размера
+ 1373 - 0
src/modelViewer/libs/BufferGeometryUtils.js


+ 76 - 0
src/modelViewer/libs/ColorSpaces.js

@@ -0,0 +1,76 @@
+import { LinearTransfer, Matrix3, SRGBTransfer } from 'three';;
+
+// Reference: http://www.russellcottrell.com/photo/matrixCalculator.htm
+
+const P3_PRIMARIES = [ 0.680, 0.320, 0.265, 0.690, 0.150, 0.060 ];
+const P3_LUMINANCE_COEFFICIENTS = [ 0.2289, 0.6917, 0.0793 ];
+const REC2020_PRIMARIES = [ 0.708, 0.292, 0.170, 0.797, 0.131, 0.046 ];
+const REC2020_LUMINANCE_COEFFICIENTS = [ 0.2627, 0.6780, 0.0593 ];
+const D65 = [ 0.3127, 0.3290 ];
+
+/******************************************************************************
+ * Display P3 definitions
+ */
+
+const LINEAR_DISPLAY_P3_TO_XYZ = /*@__PURE__*/ new Matrix3().set(
+	0.4865709, 0.2656677, 0.1982173,
+	0.2289746, 0.6917385, 0.0792869,
+	0.0000000, 0.0451134, 1.0439444
+);
+
+const XYZ_TO_LINEAR_DISPLAY_P3 = /*@__PURE__*/ new Matrix3().set(
+	2.4934969, - 0.9313836, - 0.4027108,
+	- 0.8294890, 1.7626641, 0.0236247,
+	0.0358458, - 0.0761724, 0.9568845
+);
+
+export const DisplayP3ColorSpace = 'display-p3';
+export const LinearDisplayP3ColorSpace = 'display-p3-linear';
+
+export const DisplayP3ColorSpaceImpl = {
+	primaries: P3_PRIMARIES,
+	whitePoint: D65,
+	transfer: SRGBTransfer,
+	toXYZ: LINEAR_DISPLAY_P3_TO_XYZ,
+	fromXYZ: XYZ_TO_LINEAR_DISPLAY_P3,
+	luminanceCoefficients: P3_LUMINANCE_COEFFICIENTS,
+	outputColorSpaceConfig: { drawingBufferColorSpace: DisplayP3ColorSpace }
+};
+
+export const LinearDisplayP3ColorSpaceImpl = {
+	primaries: P3_PRIMARIES,
+	whitePoint: D65,
+	transfer: LinearTransfer,
+	toXYZ: LINEAR_DISPLAY_P3_TO_XYZ,
+	fromXYZ: XYZ_TO_LINEAR_DISPLAY_P3,
+	luminanceCoefficients: P3_LUMINANCE_COEFFICIENTS,
+	workingColorSpaceConfig: { unpackColorSpace: DisplayP3ColorSpace },
+	outputColorSpaceConfig: { drawingBufferColorSpace: DisplayP3ColorSpace }
+};
+
+/******************************************************************************
+ * Rec. 2020 definitions
+ */
+
+const LINEAR_REC2020_TO_XYZ = /*@__PURE__*/ new Matrix3().set(
+	0.6369580, 0.1446169, 0.1688810,
+	0.2627002, 0.6779981, 0.0593017,
+	0.0000000, 0.0280727, 1.0609851
+);
+
+const XYZ_TO_LINEAR_REC2020 = /*@__PURE__*/ new Matrix3().set(
+	1.7166512, - 0.3556708, - 0.2533663,
+	- 0.6666844, 1.6164812, 0.0157685,
+	0.0176399, - 0.0427706, 0.9421031
+);
+
+export const LinearRec2020ColorSpace = 'rec2020-linear';
+
+export const LinearRec2020ColorSpaceImpl = {
+	primaries: REC2020_PRIMARIES,
+	whitePoint: D65,
+	transfer: LinearTransfer,
+	toXYZ: LINEAR_REC2020_TO_XYZ,
+	fromXYZ: XYZ_TO_LINEAR_REC2020,
+	luminanceCoefficients: REC2020_LUMINANCE_COEFFICIENTS,
+};

+ 564 - 0
src/modelViewer/libs/DRACOLoader.js

@@ -0,0 +1,564 @@
+//2022.11.11 copyfrom : https://unpkg.com/three@0.146.0/examples/jsm/loaders/DRACOLoader.js
+
+
+import {
+	BufferAttribute,
+	BufferGeometry,
+	FileLoader,
+	Loader
+} from 'three';
+
+const _taskCache = new WeakMap();
+
+class DRACOLoader extends Loader {
+
+	constructor( manager ) {
+
+		super( manager );
+
+		this.decoderPath = '';
+		this.decoderConfig = {};
+		this.decoderBinary = null;
+		this.decoderPending = null;
+
+		this.workerLimit = 4;
+		this.workerPool = [];
+		this.workerNextTaskID = 1;
+		this.workerSourceURL = '';
+
+		this.defaultAttributeIDs = {
+			position: 'POSITION',
+			normal: 'NORMAL',
+			color: 'COLOR',
+			uv: 'TEX_COORD'
+		};
+		this.defaultAttributeTypes = {
+			position: 'Float32Array',
+			normal: 'Float32Array',
+			color: 'Float32Array',
+			uv: 'Float32Array'
+		};
+
+	}
+
+	setDecoderPath( path ) {
+
+		this.decoderPath = path;
+
+		return this;
+
+	}
+
+	setDecoderConfig( config ) {
+
+		this.decoderConfig = config;
+
+		return this;
+
+	}
+
+	setWorkerLimit( workerLimit ) {
+
+		this.workerLimit = workerLimit;
+
+		return this;
+
+	}
+
+	load( url, onLoad, onProgress, onError ) {
+
+		const loader = new FileLoader( this.manager );
+
+		loader.setPath( this.path );
+		loader.setResponseType( 'arraybuffer' );
+		loader.setRequestHeader( this.requestHeader );
+		loader.setWithCredentials( this.withCredentials );
+
+		loader.load( url, ( buffer ) => {
+
+			this.decodeDracoFile( buffer, onLoad ).catch( onError );
+
+		}, onProgress, onError );
+
+	}
+
+	decodeDracoFile( buffer, callback, attributeIDs, attributeTypes ) {
+
+		const taskConfig = {
+			attributeIDs: attributeIDs || this.defaultAttributeIDs,
+			attributeTypes: attributeTypes || this.defaultAttributeTypes,
+			useUniqueIDs: !! attributeIDs
+		};
+
+		return this.decodeGeometry( buffer, taskConfig ).then( callback );
+
+	}
+
+	decodeGeometry( buffer, taskConfig ) {
+
+		const taskKey = JSON.stringify( taskConfig );
+
+		// Check for an existing task using this buffer. A transferred buffer cannot be transferred
+		// again from this thread.
+		if ( _taskCache.has( buffer ) ) {
+
+			const cachedTask = _taskCache.get( buffer );
+
+			if ( cachedTask.key === taskKey ) {
+
+				return cachedTask.promise;
+
+			} else if ( buffer.byteLength === 0 ) {
+
+				// Technically, it would be possible to wait for the previous task to complete,
+				// transfer the buffer back, and decode again with the second configuration. That
+				// is complex, and I don't know of any reason to decode a Draco buffer twice in
+				// different ways, so this is left unimplemented.
+				throw new Error(
+
+					'THREE.DRACOLoader: Unable to re-decode a buffer with different ' +
+					'settings. Buffer has already been transferred.'
+
+				);
+
+			}
+
+		}
+
+		//
+
+		let worker;
+		const taskID = this.workerNextTaskID ++;
+		const taskCost = buffer.byteLength;
+
+		// Obtain a worker and assign a task, and construct a geometry instance
+		// when the task completes.
+		const geometryPending = this._getWorker( taskID, taskCost )
+			.then( ( _worker ) => {
+
+				worker = _worker;
+
+				return new Promise( ( resolve, reject ) => {
+
+					worker._callbacks[ taskID ] = { resolve, reject };
+
+					worker.postMessage( { type: 'decode', id: taskID, taskConfig, buffer }, [ buffer ] );
+
+					// this.debug();
+
+				} );
+
+			} )
+			.then( ( message ) => this._createGeometry( message.geometry ) );
+
+		// Remove task from the task list.
+		// Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416)
+		geometryPending
+			.catch( () => true )
+			.then( () => {
+
+				if ( worker && taskID ) {
+
+					this._releaseTask( worker, taskID );
+
+					// this.debug();
+
+				}
+
+			} );
+
+		// Cache the task result.
+		_taskCache.set( buffer, {
+
+			key: taskKey,
+			promise: geometryPending
+
+		} );
+
+		return geometryPending;
+
+	}
+
+	_createGeometry( geometryData ) {
+
+		const geometry = new BufferGeometry();
+
+		if ( geometryData.index ) {
+
+			geometry.setIndex( new BufferAttribute( geometryData.index.array, 1 ) );
+
+		}
+
+		for ( let i = 0; i < geometryData.attributes.length; i ++ ) {
+
+			const attribute = geometryData.attributes[ i ];
+			const name = attribute.name;
+			const array = attribute.array;
+			const itemSize = attribute.itemSize;
+
+			geometry.setAttribute( name, new BufferAttribute( array, itemSize ) );
+
+		}
+
+		return geometry;
+
+	}
+
+	_loadLibrary( url, responseType ) {
+
+		const loader = new FileLoader( this.manager );
+		loader.setPath( this.decoderPath );
+		loader.setResponseType( responseType );
+		loader.setWithCredentials( this.withCredentials );
+
+		return new Promise( ( resolve, reject ) => {
+
+			loader.load( url, resolve, undefined, reject );
+
+		} );
+
+	}
+
+	preload() {
+
+		this._initDecoder();
+
+		return this;
+
+	}
+
+	_initDecoder() {
+
+		if ( this.decoderPending ) return this.decoderPending;
+
+		const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js';
+		const librariesPending = [];
+
+		if ( useJS ) {
+
+			librariesPending.push( this._loadLibrary( 'draco_decoder.js', 'text' ) );
+
+		} else {
+
+			librariesPending.push( this._loadLibrary( 'draco_wasm_wrapper.js', 'text' ) );
+			librariesPending.push( this._loadLibrary( 'draco_decoder.wasm', 'arraybuffer' ) );
+
+		}
+
+		this.decoderPending = Promise.all( librariesPending )
+			.then( ( libraries ) => {
+
+				const jsContent = libraries[ 0 ];
+
+				if ( ! useJS ) {
+
+					this.decoderConfig.wasmBinary = libraries[ 1 ];
+
+				}
+
+				const fn = DRACOWorker.toString();
+
+				const body = [
+					'/* draco decoder */',
+					jsContent,
+					'',
+					'/* worker */',
+					fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) )
+				].join( '\n' );
+
+				this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) );
+
+			} );
+
+		return this.decoderPending;
+
+	}
+
+	_getWorker( taskID, taskCost ) {
+
+		return this._initDecoder().then( () => {
+
+			if ( this.workerPool.length < this.workerLimit ) {
+
+				const worker = new Worker( this.workerSourceURL );
+
+				worker._callbacks = {};
+				worker._taskCosts = {};
+				worker._taskLoad = 0;
+
+				worker.postMessage( { type: 'init', decoderConfig: this.decoderConfig } );
+
+				worker.onmessage = function ( e ) {
+
+					const message = e.data;
+
+					switch ( message.type ) {
+
+						case 'decode':
+							worker._callbacks[ message.id ].resolve( message );
+							break;
+
+						case 'error':
+							worker._callbacks[ message.id ].reject( message );
+							break;
+
+						default:
+							console.error( 'THREE.DRACOLoader: Unexpected message, "' + message.type + '"' );
+
+					}
+
+				};
+
+				this.workerPool.push( worker );
+
+			} else {
+
+				this.workerPool.sort( function ( a, b ) {
+
+					return a._taskLoad > b._taskLoad ? - 1 : 1;
+
+				} );
+
+			}
+
+			const worker = this.workerPool[ this.workerPool.length - 1 ];
+			worker._taskCosts[ taskID ] = taskCost;
+			worker._taskLoad += taskCost;
+			return worker;
+
+		} );
+
+	}
+
+	_releaseTask( worker, taskID ) {
+
+		worker._taskLoad -= worker._taskCosts[ taskID ];
+		delete worker._callbacks[ taskID ];
+		delete worker._taskCosts[ taskID ];
+
+	}
+
+	debug() {
+
+		console.log( 'Task load: ', this.workerPool.map( ( worker ) => worker._taskLoad ) );
+
+	}
+
+	dispose() {
+
+		for ( let i = 0; i < this.workerPool.length; ++ i ) {
+
+			this.workerPool[ i ].terminate();
+
+		}
+
+		this.workerPool.length = 0;
+
+		return this;
+
+	}
+
+}
+
+/* WEB WORKER */
+
+function DRACOWorker() {
+
+	let decoderConfig;
+	let decoderPending;
+
+	onmessage = function ( e ) {
+
+		const message = e.data;
+
+		switch ( message.type ) {
+
+			case 'init':
+				decoderConfig = message.decoderConfig;
+				decoderPending = new Promise( function ( resolve/*, reject*/ ) {
+
+					decoderConfig.onModuleLoaded = function ( draco ) {
+
+						// Module is Promise-like. Wrap before resolving to avoid loop.
+						resolve( { draco: draco } );
+
+					};
+
+					DracoDecoderModule( decoderConfig ); // eslint-disable-line no-undef
+
+				} );
+				break;
+
+			case 'decode':
+				const buffer = message.buffer;
+				const taskConfig = message.taskConfig;
+				decoderPending.then( ( module ) => {
+
+					const draco = module.draco;
+					const decoder = new draco.Decoder();
+					const decoderBuffer = new draco.DecoderBuffer();
+					decoderBuffer.Init( new Int8Array( buffer ), buffer.byteLength );
+
+					try {
+
+						const geometry = decodeGeometry( draco, decoder, decoderBuffer, taskConfig );
+
+						const buffers = geometry.attributes.map( ( attr ) => attr.array.buffer );
+
+						if ( geometry.index ) buffers.push( geometry.index.array.buffer );
+
+						self.postMessage( { type: 'decode', id: message.id, geometry }, buffers );
+
+					} catch ( error ) {
+
+						console.error( error );
+
+						self.postMessage( { type: 'error', id: message.id, error: error.message } );
+
+					} finally {
+
+						draco.destroy( decoderBuffer );
+						draco.destroy( decoder );
+
+					}
+
+				} );
+				break;
+
+		}
+
+	};
+
+	function decodeGeometry( draco, decoder, decoderBuffer, taskConfig ) {
+
+		const attributeIDs = taskConfig.attributeIDs;
+		const attributeTypes = taskConfig.attributeTypes;
+
+		let dracoGeometry;
+		let decodingStatus;
+
+		const geometryType = decoder.GetEncodedGeometryType( decoderBuffer );
+
+		if ( geometryType === draco.TRIANGULAR_MESH ) {
+
+			dracoGeometry = new draco.Mesh();
+			decodingStatus = decoder.DecodeBufferToMesh( decoderBuffer, dracoGeometry );
+
+		} else if ( geometryType === draco.POINT_CLOUD ) {
+
+			dracoGeometry = new draco.PointCloud();
+			decodingStatus = decoder.DecodeBufferToPointCloud( decoderBuffer, dracoGeometry );
+
+		} else {
+
+			throw new Error( 'THREE.DRACOLoader: Unexpected geometry type.' );
+
+		}
+
+		if ( ! decodingStatus.ok() || dracoGeometry.ptr === 0 ) {
+
+			throw new Error( 'THREE.DRACOLoader: Decoding failed: ' + decodingStatus.error_msg() );
+
+		}
+
+		const geometry = { index: null, attributes: [] };
+
+		// Gather all vertex attributes.
+		for ( const attributeName in attributeIDs ) {
+
+			const attributeType = self[ attributeTypes[ attributeName ] ];
+
+			let attribute;
+			let attributeID;
+
+			// A Draco file may be created with default vertex attributes, whose attribute IDs
+			// are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively,
+			// a Draco file may contain a custom set of attributes, identified by known unique
+			// IDs. glTF files always do the latter, and `.drc` files typically do the former.
+			if ( taskConfig.useUniqueIDs ) {
+
+				attributeID = attributeIDs[ attributeName ];
+				attribute = decoder.GetAttributeByUniqueId( dracoGeometry, attributeID );
+
+			} else {
+
+				attributeID = decoder.GetAttributeId( dracoGeometry, draco[ attributeIDs[ attributeName ] ] );
+
+				if ( attributeID === - 1 ) continue;
+
+				attribute = decoder.GetAttribute( dracoGeometry, attributeID );
+
+			}
+
+			geometry.attributes.push( decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) );
+
+		}
+
+		// Add index.
+		if ( geometryType === draco.TRIANGULAR_MESH ) {
+
+			geometry.index = decodeIndex( draco, decoder, dracoGeometry );
+
+		}
+
+		draco.destroy( dracoGeometry );
+
+		return geometry;
+
+	}
+
+	function decodeIndex( draco, decoder, dracoGeometry ) {
+
+		const numFaces = dracoGeometry.num_faces();
+		const numIndices = numFaces * 3;
+		const byteLength = numIndices * 4;
+
+		const ptr = draco._malloc( byteLength );
+		decoder.GetTrianglesUInt32Array( dracoGeometry, byteLength, ptr );
+		const index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice();
+		draco._free( ptr );
+
+		return { array: index, itemSize: 1 };
+
+	}
+
+	function decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) {
+
+		const numComponents = attribute.num_components();
+		const numPoints = dracoGeometry.num_points();
+		const numValues = numPoints * numComponents;
+		const byteLength = numValues * attributeType.BYTES_PER_ELEMENT;
+		const dataType = getDracoDataType( draco, attributeType );
+
+		const ptr = draco._malloc( byteLength );
+		decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dataType, byteLength, ptr );
+		const array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice();
+		draco._free( ptr );
+
+		return {
+			name: attributeName,
+			array: array,
+			itemSize: numComponents
+		};
+
+	}
+
+	function getDracoDataType( draco, attributeType ) {
+
+		switch ( attributeType ) {
+
+			case Float32Array: return draco.DT_FLOAT32;
+			case Int8Array: return draco.DT_INT8;
+			case Int16Array: return draco.DT_INT16;
+			case Int32Array: return draco.DT_INT32;
+			case Uint8Array: return draco.DT_UINT8;
+			case Uint16Array: return draco.DT_UINT16;
+			case Uint32Array: return draco.DT_UINT32;
+
+		}
+
+	}
+
+}
+
+export { DRACOLoader };

Разница между файлами не показана из-за своего большого размера
+ 4812 - 0
src/modelViewer/libs/GLTFLoader.js


Разница между файлами не показана из-за своего большого размера
+ 1083 - 0
src/modelViewer/libs/KTX2Loader.js


+ 102 - 0
src/modelViewer/libs/WorkerPool.js

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

+ 46 - 0
src/modelViewer/libs/basis/README.md

@@ -0,0 +1,46 @@
+# Basis Universal GPU Texture Compression
+
+Basis Universal is a "[supercompressed](http://gamma.cs.unc.edu/GST/gst.pdf)"
+GPU texture and texture video compression system that outputs a highly
+compressed intermediate file format (.basis) that can be quickly transcoded to
+a wide variety of GPU texture compression formats.
+
+[GitHub](https://github.com/BinomialLLC/basis_universal)
+
+## Transcoders
+
+Basis Universal texture data may be used in two different file formats:
+`.basis` and `.ktx2`, where `ktx2` is a standardized wrapper around basis texture data.
+
+For further documentation about the Basis compressor and transcoder, refer to
+the [Basis GitHub repository](https://github.com/BinomialLLC/basis_universal).
+
+The folder contains two files required for transcoding `.basis` or `.ktx2` textures:
+
+* `basis_transcoder.js` — JavaScript wrapper for the WebAssembly transcoder.
+* `basis_transcoder.wasm` — WebAssembly transcoder.
+
+Both are dependencies of `THREE.KTX2Loader` and `THREE.BasisTextureLoader`:
+
+```js
+var ktx2Loader = new THREE.KTX2Loader();
+ktx2Loader.setTranscoderPath( 'examples/js/libs/basis/' );
+ktx2Loader.detectSupport( renderer );
+ktx2Loader.load( 'diffuse.ktx2', function ( texture ) {
+
+	var material = new THREE.MeshStandardMaterial( { map: texture } );
+
+}, function () {
+
+	console.log( 'onProgress' );
+
+}, function ( e ) {
+
+	console.error( e );
+
+} );
+```
+
+## License
+
+[Apache License 2.0](https://github.com/BinomialLLC/basis_universal/blob/master/LICENSE)

Разница между файлами не показана из-за своего большого размера
+ 21 - 0
src/modelViewer/libs/basis/basis_transcoder.js


BIN
src/modelViewer/libs/basis/basis_transcoder.wasm


+ 1 - 0
src/modelViewer/libs/basis/ver146.txt

@@ -0,0 +1 @@
+ktx2Loader 使用

Разница между файлами не показана из-за своего большого размера
+ 52 - 0
src/modelViewer/libs/draco/draco_decoder.js


BIN
src/modelViewer/libs/draco/draco_decoder.wasm


Разница между файлами не показана из-за своего большого размера
+ 33 - 0
src/modelViewer/libs/draco/draco_encoder.js


Разница между файлами не показана из-за своего большого размера
+ 104 - 0
src/modelViewer/libs/draco/draco_wasm_wrapper.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/modelViewer/libs/ktx-parse.module.js


Разница между файлами не показана из-за своего большого размера
+ 113 - 0
src/modelViewer/libs/meshopt_decoder.module.js


Разница между файлами не показана из-за своего большого размера
+ 6 - 0
src/modelViewer/libs/three.core.min.js


Разница между файлами не показана из-за своего большого размера
+ 6 - 0
src/modelViewer/libs/three.module.min.js


Разница между файлами не показана из-за своего большого размера
+ 115 - 0
src/modelViewer/libs/zstddec.module.js


+ 138 - 0
src/modelViewer/material/BasicMaterial.js

@@ -0,0 +1,138 @@
+import * as THREE from 'three'; 
+import {Common} from '../utils/Common.js' 
+ 
+
+
+let vs = ` 
+varying vec2 vUv;
+#ifdef HasMap
+    #ifdef UV_Transform  
+        uniform mat3 uvTransform;
+    #endif
+#endif
+out vec4 vPos;
+
+void main() {
+    #ifdef HasMap
+        #ifdef UV_Transform           
+            vUv = ( uvTransform * vec3( uv, 1 ) ).xy;               //include <uv_vertex> 
+        #else
+            vUv = uv;
+        #endif
+    #endif
+    vPos = vec4(position, 1.0);
+    gl_Position = projectionMatrix * modelViewMatrix * vPos;
+  
+} 
+  
+`  
+
+
+
+
+let fs = `  
+varying vec2 vUv; 
+uniform float opacity;
+
+#ifdef HasMap
+    uniform sampler2D map;   
+#endif
+#ifdef HasColor
+    uniform vec3 color;
+#endif
+
+void main() {
+    vec4 color_;
+     
+    
+    #ifdef UV_Transform 
+        if(vUv.x <= 0.0 || vUv.x >= 1.0 || vUv.y <= 0.0 || vUv.y >= 1.0) discard; //为什么measure.marker边缘会拉伸,只能这样了
+    #endif
+    
+    #ifdef HasColor
+        color_ = vec4(color, opacity); 
+    #else
+        color_ = vec4(1.0,1.0,1.0, opacity);
+    #endif
+    
+    #ifdef HasMap
+        vec4 texColor = texture2D(map, vUv); 
+        gl_FragColor = texColor * color_;
+         
+    #else
+        gl_FragColor = color_;
+    #endif
+    #include <colorspace_fragment>
+   
+}
+ 
+
+ ` 
+
+class BasicMaterial  extends THREE.ShaderMaterial{ 
+    constructor(o={}){
+       
+       super( Object.assign({},{ 
+            uniforms:{
+                color:  {type:'v3',   value:  new THREE.Color( o.color || "#FFF" )} ,
+                map:    {type: 't',    value: o.map },
+                opacity : {type:'f',    value : o.opacity == void 0 ? 1 : o.opacity  },
+                 
+            },
+            vertexShader: vs,   
+            fragmentShader: fs, 
+            defines:{HasColor:'' }
+        },o))
+        //this.opacity = o.opacity == void 0 ? 1 : o.opacity 
+    } 
+     
+    
+    copy(source){
+        super.copy(source) 
+        
+        this.map = source.map
+          
+        return this
+    }
+    
+    set opacity(o){
+        this.uniforms && (this.uniforms.opacity.value = o) 
+    }
+    get opacity(){
+        return this.uniforms.opacity.value  
+    }
+    
+ 
+    
+    set map(map){  
+        let oldDefines = Common.CloneObject(this.defines)
+        this.uniforms.map.value = map;         
+        if(map){ 
+            this.defines.HasMap = '' 
+            if(map.repeat.x != 1 || map.repeat.y != 1){
+                this.setUV()
+            }
+        }else{ 
+            delete this.defines.HasMap 
+            delete this.defines.UV_Transform
+        }
+        Common.ifSame(oldDefines,this.defines) || (this.needsUpdate = true)
+    }
+     
+    
+    get map(){
+        return this.uniforms.map.value  
+    }
+     
+    setUV(){
+        if(!this.map)return
+        this.map.updateMatrix()
+        this.uniforms.uvTransform = { value: this.map.matrix.clone() } 
+        this.defines.UV_Transform = true 
+    }
+    
+}
+
+
+
+export default BasicMaterial

+ 0 - 0
src/modelViewer/modelConfig.json


+ 19 - 0
src/modelViewer/objects/fatline/Line2.js

@@ -0,0 +1,19 @@
+import { LineSegments2 } from './LineSegments2.js';
+import { LineGeometry } from './LineGeometry.js';
+import { LineMaterial } from './LineMaterial.js';
+
+class Line2 extends LineSegments2 {
+
+	constructor( geometry = new LineGeometry(), material = new LineMaterial( { color: Math.random() * 0xffffff } ) ) {
+
+		super( geometry, material );
+
+		this.isLine2 = true;
+
+		this.type = 'Line2';
+
+	}
+
+}
+
+export { Line2 };

+ 60 - 0
src/modelViewer/objects/fatline/LineGeometry.js

@@ -0,0 +1,60 @@
+import { LineSegmentsGeometry } from './LineSegmentsGeometry.js';
+
+class LineGeometry extends LineSegmentsGeometry {
+
+	constructor() {
+
+		super();
+
+		this.isLineGeometry = true;
+
+		this.type = 'LineGeometry';
+
+	}
+ 
+    setPositions( array  ) { //xzw改成类似LineSegments的多段线  (第二个点和第三个点之间是没有线段的, 所以不用在意线段顺序)
+        const points = new Float32Array( array ); 
+        LineSegmentsGeometry.prototype.setPositions.call(this, points ); 
+        return this; 
+    }  
+
+	setColors( array ) {
+
+		// converts [ r1, g1, b1,  r2, g2, b2, ... ] to pairs format
+
+		const length = array.length - 3;
+		const colors = new Float32Array( 2 * length );
+
+		for ( let i = 0; i < length; i += 3 ) {
+
+			colors[ 2 * i ] = array[ i ];
+			colors[ 2 * i + 1 ] = array[ i + 1 ];
+			colors[ 2 * i + 2 ] = array[ i + 2 ];
+
+			colors[ 2 * i + 3 ] = array[ i + 3 ];
+			colors[ 2 * i + 4 ] = array[ i + 4 ];
+			colors[ 2 * i + 5 ] = array[ i + 5 ];
+
+		}
+
+		super.setColors( colors );
+
+		return this;
+
+	}
+
+	fromLine( line ) {
+
+		const geometry = line.geometry;
+
+		this.setPositions( geometry.attributes.position.array ); // assumes non-indexed
+
+		// set colors, maybe
+
+		return this;
+
+	}
+
+}
+
+export { LineGeometry };

+ 982 - 0
src/modelViewer/objects/fatline/LineMaterial.js

@@ -0,0 +1,982 @@
+/**
+ * parameters = {
+ *  color: <hex>,
+ *  lineWidth: <float>,
+ *  dashed: <boolean>,
+ *  dashScale: <float>,
+ *  dashSize: <float>,
+ *  dashOffset: <float>,
+ *  gapSize: <float>,
+ *  resolution: <Vector2>, // to be set by renderer
+ * }
+ */
+
+import {
+	ShaderLib,
+	ShaderMaterial,
+	UniformsLib,
+	UniformsUtils,
+	Vector2,
+    Color
+} from 'three';
+
+let viewer
+UniformsLib.line = {
+/* 
+	worldUnits: { value: 1 },
+	lineWidth: { value: 1 },
+	resolution: { value: new Vector2( 1, 1 ) },
+	dashOffset: { value: 0 },
+	dashScale: { value: 1 },
+	dashSize: { value: 1 },
+	gapSize: { value: 1 } // todo FIX - maybe change to totalSize
+ */
+    worldUnits: { value: 1 },
+	lineWidth: { value: 1 },
+	resolution: { value: new Vector2( 1, 1 ) },
+    viewportOffset: { value: new Vector2(0, 0 ) }, //left, top    
+    devicePixelRatio:{ value:window.devicePixelRatio},
+	dashScale: { value: 1 },
+	dashSize: { value: 1 },
+	dashOffset: { value: 0 },
+	gapSize: { value: 1 }, 
+	opacity: { value: 1 },
+     
+    backColor:     {type:'v3',   value: new Color("#ddd")},
+    clipDistance :          { type: 'f', 	value:  4}, //消失距离
+    occlusionDistance :     { type: 'f', 	value:  1 }, //变为backColor距离
+    maxClipFactor :         { type: 'f', 	value:  1 },  //0-1
+    maxOcclusionFactor :    { type: 'f', 	value:  1 },  //0-1 
+    startClipDis:  { type: 'f', 	value: 0 },  //开始逐渐消失的距离
+    startOcclusDis:  { type: 'f', 	value: 0 },  //开始逐渐褪色的距离
+    
+    depthTexture:{ value: null },
+    nearPlane:{value: 0.1},
+    farPlane:{value: 100000},
+    //uUseOrthographicCamera:{ type: "b", value: false },
+
+
+    fadeFar:{value:10},//渐变消失
+
+};
+
+ShaderLib[ 'line' ] = {
+
+	uniforms: UniformsUtils.merge( [
+		UniformsLib.common,
+		UniformsLib.fog,
+		UniformsLib.line
+	] ),
+
+	vertexShader:
+	/* glsl */`
+		#include <common>
+		#include <color_pars_vertex>
+		#include <fog_pars_vertex>
+		#include <logdepthbuf_pars_vertex>
+		#include <clipping_planes_pars_vertex>
+
+		uniform float lineWidth;
+		uniform vec2 resolution;
+        uniform float devicePixelRatio;  //add
+        
+        
+		attribute vec3 instanceStart;
+		attribute vec3 instanceEnd;
+
+		attribute vec3 instanceColorStart;
+		attribute vec3 instanceColorEnd;
+
+		#ifdef WORLD_UNITS
+
+			varying vec4 worldPos;
+			varying vec3 worldStart;
+			varying vec3 worldEnd;
+
+			#ifdef USE_DASH
+
+				varying vec2 vUv;
+
+			#endif
+
+		#else
+
+			varying vec2 vUv;
+
+		#endif
+
+		#ifdef USE_DASH
+
+			uniform float dashScale;
+			attribute float instanceDistanceStart;
+			attribute float instanceDistanceEnd;
+			varying float vLineDistance;
+
+		#endif
+
+		void trimSegment( const in vec4 start, inout vec4 end ) {
+
+			// trim end segment so it terminates between the camera plane and the near plane
+
+			// conservative estimate of the near plane
+			float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column
+			float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column
+			float nearEstimate = - 0.5 * b / a;
+
+			float alpha = ( nearEstimate - start.z ) / ( end.z - start.z );
+
+			end.xyz = mix( start.xyz, end.xyz, alpha );
+
+		}
+
+		void main() {
+
+			#ifdef USE_COLOR
+
+				vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd;
+
+			#endif
+
+			#ifdef USE_DASH
+
+				vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd;
+				vUv = uv;
+
+			#endif
+
+			float aspect = resolution.x / resolution.y;
+
+			// camera space
+			vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 );
+			vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );
+
+			#ifdef WORLD_UNITS
+
+				worldStart = start.xyz;
+				worldEnd = end.xyz;
+
+			#else
+
+				vUv = uv;
+
+			#endif
+
+			// special case for perspective projection, and segments that terminate either in, or behind, the camera plane
+			// clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
+			// but we need to perform ndc-space calculations in the shader, so we must address this issue directly
+			// perhaps there is a more elegant solution -- WestLangley
+
+			bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column
+
+			if ( perspective ) {
+
+				if ( start.z < 0.0 && end.z >= 0.0 ) {
+
+					trimSegment( start, end );
+
+				} else if ( end.z < 0.0 && start.z >= 0.0 ) {
+
+					trimSegment( end, start );
+
+				}
+
+			}
+
+			// clip space
+			vec4 clipStart = projectionMatrix * start;
+			vec4 clipEnd = projectionMatrix * end;
+
+			// ndc space
+			vec3 ndcStart = clipStart.xyz / clipStart.w;
+			vec3 ndcEnd = clipEnd.xyz / clipEnd.w;
+
+			// direction
+			vec2 dir = ndcEnd.xy - ndcStart.xy;
+
+			// account for clip-space aspect ratio
+			dir.x *= aspect;
+			dir = normalize( dir );
+
+			#ifdef WORLD_UNITS
+
+				// get the offset direction as perpendicular to the view vector
+				vec3 worldDir = normalize( end.xyz - start.xyz );
+				vec3 offset;
+				if ( position.y < 0.5 ) {
+
+					offset = normalize( cross( start.xyz, worldDir ) );
+
+				} else {
+
+					offset = normalize( cross( end.xyz, worldDir ) );
+
+				}
+
+				// sign flip
+				if ( position.x < 0.0 ) offset *= - 1.0;
+
+				float forwardOffset = dot( worldDir, vec3( 0.0, 0.0, 1.0 ) );
+
+				// don't extend the line if we're rendering dashes because we
+				// won't be rendering the endcaps
+				#ifndef USE_DASH
+
+					// extend the line bounds to encompass  endcaps
+					start.xyz += - worldDir * lineWidth * 0.5;
+					end.xyz += worldDir * lineWidth * 0.5;
+
+					// shift the position of the quad so it hugs the forward edge of the line
+					offset.xy -= dir * forwardOffset;
+					offset.z += 0.5;
+
+				#endif
+
+				// endcaps
+				if ( position.y > 1.0 || position.y < 0.0 ) {
+
+					offset.xy += dir * 2.0 * forwardOffset;
+
+				}
+
+				// adjust for lineWidth
+				offset *= lineWidth * 0.5;
+
+				// set the world position
+				worldPos = ( position.y < 0.5 ) ? start : end;
+				worldPos.xyz += offset;
+
+				// project the worldpos
+				vec4 clip = projectionMatrix * worldPos;
+
+				// shift the depth of the projected points so the line
+				// segments overlap neatly
+				vec3 clipPose = ( position.y < 0.5 ) ? ndcStart : ndcEnd;
+				clip.z = clipPose.z * clip.w;
+
+			#else
+
+				vec2 offset = vec2( dir.y, - dir.x );
+				// undo aspect ratio adjustment
+				dir.x /= aspect;
+				offset.x /= aspect;
+
+				// sign flip
+				if ( position.x < 0.0 ) offset *= - 1.0;
+
+				// endcaps
+				if ( position.y < 0.0 ) {
+
+					offset += - dir;
+
+				} else if ( position.y > 1.0 ) {
+
+					offset += dir;
+
+				}
+
+				// adjust for lineWidth
+				offset *= lineWidth;
+
+				// adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
+				offset /= resolution.y; //* devicePixelRatio;
+
+				// select end
+				vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd;
+
+				// back to clip space
+				offset *= clip.w;
+
+				clip.xy += offset;
+
+			#endif
+
+			gl_Position = clip;
+
+			vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation
+
+			#include <logdepthbuf_vertex>
+			#include <clipping_planes_vertex>
+			#include <fog_vertex>
+
+		}
+		`,
+
+	fragmentShader:
+	/* glsl */`
+		uniform vec3 diffuse;
+		uniform float opacity;
+		uniform float lineWidth;
+        uniform bool uUseOrthographicCamera; 
+		#ifdef USE_DASH
+
+			uniform float dashOffset;
+			uniform float dashSize;
+			uniform float gapSize; 
+		#endif
+ 
+        //加
+        #if /* defined(GL_EXT_frag_depth) && */ defined(useDepth)    
+            uniform sampler2D depthTexture;
+             
+            uniform vec2 resolution;
+            uniform vec2 viewportOffset;
+            uniform vec3 backColor;
+            uniform float occlusionDistance;
+            uniform float clipDistance;
+            uniform float startClipDis;
+            uniform float startOcclusDis;
+            uniform float maxClipFactor;
+            uniform float maxOcclusionFactor; 
+        #endif
+        #if defined(FadeFar)
+            uniform float fadeFar;
+        #endif
+        
+
+		varying float vLineDistance;
+
+		#ifdef WORLD_UNITS
+
+			varying vec4 worldPos;
+			varying vec3 worldStart;
+			varying vec3 worldEnd;
+
+			#ifdef USE_DASH
+
+				varying vec2 vUv;
+
+			#endif
+
+		#else
+
+			varying vec2 vUv;
+
+		#endif
+
+		#include <common>
+		#include <color_pars_fragment>
+		#include <fog_pars_fragment>
+		#include <logdepthbuf_pars_fragment>
+		#include <clipping_planes_pars_fragment>
+
+
+        #if/*  defined(GL_EXT_frag_depth) &&  */defined(useDepth) || defined(FadeFar)  
+            uniform float nearPlane;
+            uniform float farPlane;
+            float convertToLinear(float zValue)
+            {
+                //if(uUseOrthographicCamera){
+                //    return zValue*(farPlane-nearPlane)+nearPlane;
+                //}else{ 
+                    float z = zValue * 2.0 - 1.0;
+                    return (2.0 * nearPlane * farPlane) / (farPlane + nearPlane - z * (farPlane - nearPlane));
+                //} 
+            }
+        #endif
+
+
+
+		vec2 closestLineToLine(vec3 p1, vec3 p2, vec3 p3, vec3 p4) {
+
+			float mua;
+			float mub;
+
+			vec3 p13 = p1 - p3;
+			vec3 p43 = p4 - p3;
+
+			vec3 p21 = p2 - p1;
+
+			float d1343 = dot( p13, p43 );
+			float d4321 = dot( p43, p21 );
+			float d1321 = dot( p13, p21 );
+			float d4343 = dot( p43, p43 );
+			float d2121 = dot( p21, p21 );
+
+			float denom = d2121 * d4343 - d4321 * d4321;
+
+			float numer = d1343 * d4321 - d1321 * d4343;
+
+			mua = numer / denom;
+			mua = clamp( mua, 0.0, 1.0 );
+			mub = ( d1343 + d4321 * ( mua ) ) / d4343;
+			mub = clamp( mub, 0.0, 1.0 );
+
+			return vec2( mua, mub );
+
+		}
+
+		void main() {
+
+			#include <clipping_planes_fragment>
+ 
+			#ifdef USE_DASH
+
+				if ( vUv.y < - 1.0 || vUv.y > 1.0 ) discard; // discard endcaps
+                
+                
+                bool unvisible = mod( vLineDistance + dashOffset, dashSize + gapSize ) > dashSize;
+                //加
+                #ifdef DASH_with_depth
+                    
+                #else  
+                    if (unvisible) discard; // todo - FIX
+               
+                #endif
+			#endif
+            
+
+			float alpha = opacity;
+
+			#ifdef WORLD_UNITS
+
+				// Find the closest points on the view ray and the line segment
+				vec3 rayEnd = normalize( worldPos.xyz ) * 1e5;
+				vec3 lineDir = worldEnd - worldStart;
+				vec2 params = closestLineToLine( worldStart, worldEnd, vec3( 0.0, 0.0, 0.0 ), rayEnd );
+
+				vec3 p1 = worldStart + lineDir * params.x;
+				vec3 p2 = rayEnd * params.y;
+				vec3 delta = p1 - p2;
+				float len = length( delta );
+				float norm = len / lineWidth;
+
+				#ifndef USE_DASH
+
+					#ifdef USE_ALPHA_TO_COVERAGE
+
+						float dnorm = fwidth( norm );
+						alpha = 1.0 - smoothstep( 0.5 - dnorm, 0.5 + dnorm, norm );
+
+					#else
+
+						if ( norm > 0.5 ) {
+
+							discard;
+
+						}
+
+					#endif
+
+				#endif
+
+			#else
+
+				#ifdef USE_ALPHA_TO_COVERAGE
+
+					// artifacts appear on some hardware if a derivative is taken within a conditional
+					float a = vUv.x;
+					float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;
+					float len2 = a * a + b * b;
+					float dlen = fwidth( len2 );
+
+					if ( abs( vUv.y ) > 1.0 ) {
+
+						alpha = 1.0 - smoothstep( 1.0 - dlen, 1.0 + dlen, len2 );
+
+					}
+
+				#else
+
+					if ( abs( vUv.y ) > 1.0 ) {
+
+						float a = vUv.x;
+						float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;
+						float len2 = a * a + b * b;
+
+						if ( len2 > 1.0 ) discard;
+
+					}
+
+				#endif
+
+			#endif
+
+			vec4 diffuseColor = vec4( diffuse, alpha );
+
+            //加
+            #if /* defined(GL_EXT_frag_depth) && */ defined(useDepth) || defined(FadeFar)  
+               
+                float fragDepth = convertToLinear(gl_FragCoord.z);
+                #if defined(FadeFar) 
+                    float fadeOutFar = fadeFar * 1.3; //完全消失距离
+                    if(fragDepth > fadeOutFar){
+                        discard;
+                    }else if(fragDepth > fadeFar){
+                        alpha *= (fadeOutFar - fragDepth) / (fadeOutFar - fadeFar);
+                        diffuseColor.a = alpha;
+                    }  
+                #endif
+                
+                #if /* defined(GL_EXT_frag_depth) &&  */defined(useDepth)
+                
+                    float mixFactor = 0.0;
+                    float clipFactor = 0.0;
+                    //gl_FragCoord大小为 viewport client大小 
+                    vec2 depthTxtCoords = vec2(gl_FragCoord.x - viewportOffset.x,  gl_FragCoord.y - viewportOffset.y) / resolution;
+
+                    float textureDepth = convertToLinear(texture2D(depthTexture, depthTxtCoords).r);
+
+                    float delta = fragDepth - textureDepth;
+
+                    if (delta > 0.0)
+                    {
+                        
+                        /* mixFactor = clamp(delta / occlusionDistance, 0.0, maxOcclusionFactor);
+                        clipFactor = clamp(delta / clipDistance, 0.0, maxClipFactor); */
+                        mixFactor = clamp((delta - startOcclusDis) / (occlusionDistance - startOcclusDis), 0.0, maxOcclusionFactor);
+                        clipFactor = clamp((delta - startClipDis) / (clipDistance - startClipDis), 0.0, maxClipFactor);
+                    }
+                     
+                    if (clipFactor == 1.0)
+                    {
+                        discard;
+                    }
+                    
+                    vec4 backColor_ = vec4(backColor, alpha /* opacity */); //vec4(0.8,0.8,0.8, 0.8*opacity);
+                     
+                    #ifdef DASH_with_depth  
+                        // 只在被遮住的部分显示虚线, 所以若同时是虚线不可见部分和被遮住时, a为0
+                        if(unvisible) backColor_.a = 0.0;
+                    #endif 
+                    
+                    //vec4 diffuseColor = vec4(mix(diffuse, backColor_, mixFactor), opacity*(1.0 - clipFactor));
+                   
+                   
+                   
+                    diffuseColor = mix(diffuseColor, backColor_ , mixFactor);   
+                   
+                   
+                    diffuseColor.a *= (1.0 - clipFactor);  
+                
+                #endif
+            #endif
+
+			#include <logdepthbuf_fragment>
+			#include <color_fragment>
+
+			//gl_FragColor = vec4( diffuseColor.rgb, alpha );
+			gl_FragColor =  diffuseColor;   
+            
+			#include <tonemapping_fragment>
+			#include <colorspace_fragment>
+			#include <fog_fragment>
+			#include <premultiplied_alpha_fragment>
+
+
+		}
+		`
+};
+
+class LineMaterial extends ShaderMaterial {
+
+	constructor( parameters ) {
+
+        let [vs,fs] = [ShaderLib[ 'line' ].vertexShader, ShaderLib[ 'line' ].fragmentShader]
+
+		super( {
+
+			type: 'LineMaterial',
+
+			uniforms: UniformsUtils.clone( ShaderLib[ 'line' ].uniforms ),
+
+			vertexShader: vs,
+			fragmentShader: fs,
+
+			clipping: true // required for clipping support
+
+		} );
+
+		this.isLineMaterial = true;
+        this.lineWidth_ = 0 
+        this.supportExtDepth = parameters.supportExtDepth 
+        this.depthTestWhenPick = false //pick时是否识别点云等
+        
+        
+        if(parameters.color){
+            this.color = new Color(parameters.color)  
+        }
+        if(parameters.backColor){
+            this.uniforms.backColor.value = new Color(parameters.backColor)
+        }
+        if(parameters.clipDistance){
+            this.uniforms.clipDistance.value = parameters.clipDistance
+        }
+        if(parameters.occlusionDistance){
+            this.uniforms.occlusionDistance.value = parameters.occlusionDistance
+        }
+        if(parameters.maxClipFactor){
+            this.uniforms.maxClipFactor.value = parameters.maxClipFactor
+        }
+        
+		Object.defineProperties( this, {
+
+			color: {
+
+				enumerable: true,
+
+				get: function () {
+
+					return this.uniforms.diffuse.value;
+
+				},
+
+				set: function ( value ) {
+
+					this.uniforms.diffuse.value = value;
+
+				}
+
+			},
+
+			worldUnits: {
+
+				enumerable: true,
+
+				get: function () {
+
+					return 'WORLD_UNITS' in this.defines;
+
+				},
+
+				set: function ( value ) {
+
+					if ( value === true ) {
+
+						this.defines.WORLD_UNITS = '';
+
+					} else {
+
+						delete this.defines.WORLD_UNITS;
+
+					}
+
+				}
+
+			},
+
+			lineWidth: {
+
+				enumerable: true,
+
+				get: function () {
+
+					return this.lineWidth_;  //this.uniforms.lineWidth.value;
+
+				},
+
+				set: function ( value ) {
+
+					this.uniforms.lineWidth.value = value //* pixelRatio_;  //pixelRatio_暂时都是1,就不写了
+                    this.lineWidth_ = value
+				}
+
+			},
+
+			dashed: {
+
+				enumerable: true,
+
+				get: function () {
+
+					return Boolean( 'USE_DASH' in this.defines );
+
+				},
+
+				set( value ) {
+
+					if ( Boolean( value ) !== Boolean( 'USE_DASH' in this.defines ) ) {
+
+						this.needsUpdate = true;
+
+					}
+
+					if ( value === true ) {
+
+						this.defines.USE_DASH = '';
+
+					} else {
+
+						delete this.defines.USE_DASH;
+
+					}
+
+				}
+
+			},
+
+			dashScale: {
+
+				enumerable: true,
+
+				get: function () {
+
+					return this.uniforms.dashScale.value;
+
+				},
+
+				set: function ( value ) {
+
+					this.uniforms.dashScale.value = value;
+
+				}
+
+			},
+
+			dashSize: {
+
+				enumerable: true,
+
+				get: function () {
+
+					return this.uniforms.dashSize.value;
+
+				},
+
+				set: function ( value ) {
+
+					this.uniforms.dashSize.value = value;
+
+				}
+
+			},
+
+			dashOffset: {
+
+				enumerable: true,
+
+				get: function () {
+
+					return this.uniforms.dashOffset.value;
+
+				},
+
+				set: function ( value ) {
+
+					this.uniforms.dashOffset.value = value;
+
+				}
+
+			},
+
+			gapSize: {
+
+				enumerable: true,
+
+				get: function () {
+
+					return this.uniforms.gapSize.value;
+
+				},
+
+				set: function ( value ) {
+
+					this.uniforms.gapSize.value = value;
+
+				}
+
+			},
+
+			opacity: {
+
+				enumerable: true,
+
+				get: function () {
+
+					return this.uniforms.opacity.value;
+
+				},
+
+				set: function ( value ) {
+
+					this.uniforms.opacity.value = value;
+
+				}
+
+			},
+
+			resolution: {
+
+				enumerable: true,
+
+				get: function () {
+
+					return this.uniforms.resolution.value;
+
+				},
+
+				set: function ( value ) {
+
+					this.uniforms.resolution.value.copy( value );
+
+				}
+
+			},
+
+			alphaToCoverage: {
+
+				enumerable: true,
+
+				get: function () {
+
+					return Boolean( 'USE_ALPHA_TO_COVERAGE' in this.defines );
+
+				},
+
+				set: function ( value ) {
+
+					if ( Boolean( value ) !== Boolean( 'USE_ALPHA_TO_COVERAGE' in this.defines ) ) {
+
+						this.needsUpdate = true;
+
+					}
+
+					if ( value === true ) {
+
+						this.defines.USE_ALPHA_TO_COVERAGE = '';
+						this.extensions.derivatives = true;
+
+					} else {
+
+						delete this.defines.USE_ALPHA_TO_COVERAGE;
+						this.extensions.derivatives = false;
+
+					}
+
+				}
+
+			},
+            
+            
+            dashWithDepth:{//add 
+                enumerable: true,
+
+                get: function () {
+
+                    return 'DASH_with_depth' in this.defines 
+
+                },
+
+                set: function ( value ) {
+                    
+                    value = value && !!this.supportExtDepth
+                     
+                    if(value != this.dashWithDepth){ 
+                        if(value){
+                            this.defines.DASH_with_depth = '' 
+                        }else{
+                            delete this.defines.DASH_with_depth
+                        }
+                        this.needsUpdate = true
+                    }
+                }  
+            }, 
+
+		} ); 
+         
+        
+        this.events = {
+            setSize:(e)=>{//如果出现横条状的异常,往往是viewportOffset出错  //地图不需要
+                let viewport = e.viewport 
+                //console.log(viewport.name, viewport.resolution2)
+                this.uniforms.resolution.value.copy(viewport.resolution2)  
+                //this.uniforms.devicePixelRatio.value = window.devicePixelRatio 
+                //this.lineWidth = this.lineWidth_ //update
+                if(!this.realUseDepth || !e.viewport)return
+                let viewportOffset = viewport.offset || new THREE.Vector2() 
+                this.uniforms.viewportOffset.value.copy(viewportOffset)
+                
+            },
+            render:(e)=>{//before render  如果有大于两个viewport的话,不同viewport用不同的depthTex
+                this.useDepth && this.updateDepthParams(e)
+                
+                var viewport = e.viewport || viewer.mainViewport;
+                if(viewport != this.lastViewport){              //当mapViewer要渲染测量线后,就需要变viewport
+                    this.events.setSize({viewport})
+                }
+                this.lastViewport = viewport
+            } 
+        }
+            
+        this.setValues( parameters );
+        
+        let viewport = viewer.mainViewport; 
+        this.events.setSize({viewport})  
+        
+        viewer.addEventListener('resize', this.events.setSize)   
+        viewer.addEventListener("render.begin",  this.events.render)   
+            
+        
+	}
+    
+    
+    get useDepth(){ 
+        return this.useDepth_
+    } 
+    
+      
+    
+    set useDepth(value){
+        value = value && this.supportExtDepth  //如果不支持 EXT_DEPTH 的话会失效  
+        
+        if(this.useDepth_ != value){
+            
+            this.setRealDepth(value) 
+            this.useDepth_ = value 
+             
+            
+        }
+        
+    } 
+    
+    
+    setRealDepth(useDepth, viewport){//确实使用到depthTex
+        if(!this.realUseDepth != !useDepth){
+            if(useDepth ){
+                this.defines.useDepth = ''  
+            }else{
+                delete this.defines.useDepth 
+            }
+            this.realUseDepth = useDepth
+            //if(this.autoDepthTest)this.depthWrite = this.depthTest = !useDepth  //如果useDepth = false,使用原始的depthTest
+            this.needsUpdate = true
+            if(!viewport)viewport = viewer.mainViewport //暂时这么设置
+            useDepth && this.events.setSize({viewport})
+        }
+    }  
+    
+    
+    set fadeFar(far){
+        let needsUpdate = (!this.fadeFar ) != (!far)  //null为全部范围可见
+        //console.log('fadeFar needsUpdate', needsUpdate)
+        if(far){
+            this.defines.FadeFar = true
+            this.uniforms.fadeFar.value = far 
+        }else{
+            delete this.defines.FadeFar 
+        }
+        needsUpdate && (this.needsUpdate = true)
+    }
+    
+    get fadeFar(){
+        return 'FadeFar' in this.defines && this.uniforms.fadeFar.value
+    }
+    updateDepthParams(e={}){//主要用于点云遮住mesh
+        var viewport = e.viewport || viewer.mainViewport;
+        var camera = viewport.camera;
+        
+        let hasDepth = this.useDepth && camera.isPerspectiveCamera && viewer.collider.depthTarget
+         
+        this.setRealDepth(hasDepth, viewport)
+        
+        if(hasDepth){ 
+            this.uniforms.depthTexture.value = viewer.collider.depthTarget.depthTexture    //其实只赋值一次就行
+        } 
+        //hasDepth  or  FadeFar:
+        this.uniforms.nearPlane.value = camera.near;
+        this.uniforms.farPlane.value = camera.far; 
+        //this.uniforms.uUseOrthographicCamera.value = !camera.isPerspectiveCamera
+    }
+   
+}
+LineMaterial.registerViewer = function(viewer_){viewer = viewer_}
+export { LineMaterial };

+ 355 - 0
src/modelViewer/objects/fatline/LineSegments2.js

@@ -0,0 +1,355 @@
+import {
+	Box3,
+	InstancedInterleavedBuffer,
+	InterleavedBufferAttribute,
+	Line3,
+	MathUtils,
+	Matrix4,
+	Mesh,
+	Sphere,
+	Vector3,
+	Vector4
+} from 'three'; 
+import { LineSegmentsGeometry } from './LineSegmentsGeometry.js';
+import { LineMaterial } from './LineMaterial.js';
+
+const _start = new Vector3();
+const _end = new Vector3();
+
+const _start4 = new Vector4();
+const _end4 = new Vector4();
+
+const _ssOrigin = new Vector4();
+const _ssOrigin3 = new Vector3();
+const _mvMatrix = new Matrix4();
+const _line = new Line3();
+const _closestPoint = new Vector3();
+
+const _box = new Box3();
+const _sphere = new Sphere();
+const _clipToWorldVector = new Vector4();
+
+let _ray, _instanceStart, _instanceEnd, _lineWidth;
+
+// Returns the margin required to expand by in world space given the distance from the camera,
+// line width, resolution, and camera projection
+function getWorldSpaceHalfWidth( camera, distance, resolution ) {
+
+	// transform into clip space, adjust the x and y values by the pixel width offset, then
+	// transform back into world space to get world offset. Note clip space is [-1, 1] so full
+	// width does not need to be halved.
+	_clipToWorldVector.set( 0, 0, - distance, 1.0 ).applyMatrix4( camera.projectionMatrix );
+	_clipToWorldVector.multiplyScalar( 1.0 / _clipToWorldVector.w );
+	_clipToWorldVector.x = _lineWidth / resolution.width;
+	_clipToWorldVector.y = _lineWidth / resolution.height;
+	_clipToWorldVector.applyMatrix4( camera.projectionMatrixInverse );
+	_clipToWorldVector.multiplyScalar( 1.0 / _clipToWorldVector.w );
+
+	return Math.abs( Math.max( _clipToWorldVector.x, _clipToWorldVector.y ) );
+
+}
+
+function raycastWorldUnits( lineSegments, intersects ) {
+
+	for ( let i = 0, l = _instanceStart.count; i < l; i ++ ) {
+
+		_line.start.fromBufferAttribute( _instanceStart, i );
+		_line.end.fromBufferAttribute( _instanceEnd, i );
+
+		const pointOnLine = new Vector3();
+		const point = new Vector3();
+
+		_ray.distanceSqToSegment( _line.start, _line.end, point, pointOnLine );
+		const isInside = point.distanceTo( pointOnLine ) < _lineWidth * 0.5;
+
+		if ( isInside ) {
+
+			intersects.push( {
+				point,
+				pointOnLine,
+				distance: _ray.origin.distanceTo( point ),
+				object: lineSegments,
+				face: null,
+				faceIndex: i,
+				uv: null,
+				uv2: null,
+			} );
+
+		}
+
+	}
+
+}
+
+function raycastScreenSpace( lineSegments, camera, intersects ) {
+
+	const projectionMatrix = camera.projectionMatrix;
+	const material = lineSegments.material;
+	const resolution = material.resolution;
+	const matrixWorld = lineSegments.matrixWorld;
+
+	const geometry = lineSegments.geometry;
+	const instanceStart = geometry.attributes.instanceStart;
+	const instanceEnd = geometry.attributes.instanceEnd;
+
+	const near = - camera.near;
+
+	//
+
+	// pick a point 1 unit out along the ray to avoid the ray origin
+	// sitting at the camera origin which will cause "w" to be 0 when
+	// applying the projection matrix.
+	_ray.at( 1, _ssOrigin );
+
+	// ndc space [ - 1.0, 1.0 ]
+	_ssOrigin.w = 1;
+	_ssOrigin.applyMatrix4( camera.matrixWorldInverse );
+	_ssOrigin.applyMatrix4( projectionMatrix );
+	_ssOrigin.multiplyScalar( 1 / _ssOrigin.w );
+
+	// screen space
+	_ssOrigin.x *= resolution.x / 2;
+	_ssOrigin.y *= resolution.y / 2;
+	_ssOrigin.z = 0;
+
+	_ssOrigin3.copy( _ssOrigin );
+
+	_mvMatrix.multiplyMatrices( camera.matrixWorldInverse, matrixWorld );
+
+	for ( let i = 0, l = instanceStart.count; i < l; i ++ ) {
+
+		_start4.fromBufferAttribute( instanceStart, i );
+		_end4.fromBufferAttribute( instanceEnd, i );
+
+		_start4.w = 1;
+		_end4.w = 1;
+
+		// camera space
+		_start4.applyMatrix4( _mvMatrix );
+		_end4.applyMatrix4( _mvMatrix );
+
+		// skip the segment if it's entirely behind the camera
+		const isBehindCameraNear = _start4.z > near && _end4.z > near;
+		if ( isBehindCameraNear ) {
+
+			continue;
+
+		}
+
+		// trim the segment if it extends behind camera near
+		if ( _start4.z > near ) {
+
+			const deltaDist = _start4.z - _end4.z;
+			const t = ( _start4.z - near ) / deltaDist;
+			_start4.lerp( _end4, t );
+
+		} else if ( _end4.z > near ) {
+
+			const deltaDist = _end4.z - _start4.z;
+			const t = ( _end4.z - near ) / deltaDist;
+			_end4.lerp( _start4, t );
+
+		}
+
+		// clip space
+		_start4.applyMatrix4( projectionMatrix );
+		_end4.applyMatrix4( projectionMatrix );
+
+		// ndc space [ - 1.0, 1.0 ]
+		_start4.multiplyScalar( 1 / _start4.w );
+		_end4.multiplyScalar( 1 / _end4.w );
+
+		// screen space
+		_start4.x *= resolution.x / 2;
+		_start4.y *= resolution.y / 2;
+
+		_end4.x *= resolution.x / 2;
+		_end4.y *= resolution.y / 2;
+
+		// create 2d segment
+		_line.start.copy( _start4 );
+		_line.start.z = 0;
+
+		_line.end.copy( _end4 );
+		_line.end.z = 0;
+
+		// get closest point on ray to segment
+		const param = _line.closestPointToPointParameter( _ssOrigin3, true );
+		_line.at( param, _closestPoint );
+
+		// check if the intersection point is within clip space
+		const zPos = MathUtils.lerp( _start4.z, _end4.z, param );
+		const isInClipSpace = zPos >= - 1 && zPos <= 1;
+
+		const isInside = _ssOrigin3.distanceTo( _closestPoint ) < _lineWidth * 0.5;
+
+		if ( isInClipSpace && isInside ) {
+
+			_line.start.fromBufferAttribute( instanceStart, i );
+			_line.end.fromBufferAttribute( instanceEnd, i );
+
+			_line.start.applyMatrix4( matrixWorld );
+			_line.end.applyMatrix4( matrixWorld );
+
+			const pointOnLine = new Vector3();
+			const point = new Vector3();
+
+			_ray.distanceSqToSegment( _line.start, _line.end, point, pointOnLine );
+
+			intersects.push( {
+				point: point,
+				pointOnLine: pointOnLine,
+				distance: _ray.origin.distanceTo( point ),
+				object: lineSegments,
+				face: null,
+				faceIndex: i,
+				uv: null,
+				uv2: null,
+			} );
+
+		}
+
+	}
+
+}
+
+class LineSegments2 extends Mesh {
+
+	constructor( geometry = new LineSegmentsGeometry(), material = new LineMaterial( { color: Math.random() * 0xffffff } ) ) {
+
+		super( geometry, material );
+
+		this.isLineSegments2 = true;
+
+		this.type = 'LineSegments2';
+
+	}
+
+	// for backwards-compatibility, but could be a method of LineSegmentsGeometry...
+
+	computeLineDistances() {
+
+		const geometry = this.geometry;
+
+		const instanceStart = geometry.attributes.instanceStart;
+		const instanceEnd = geometry.attributes.instanceEnd;
+		const lineDistances = new Float32Array( 2 * instanceStart.count );
+
+		for ( let i = 0, j = 0, l = instanceStart.count; i < l; i ++, j += 2 ) {
+
+			_start.fromBufferAttribute( instanceStart, i );
+			_end.fromBufferAttribute( instanceEnd, i );
+
+			lineDistances[ j ] = ( j === 0 ) ? 0 : lineDistances[ j - 1 ];
+			lineDistances[ j + 1 ] = lineDistances[ j ] + _start.distanceTo( _end );
+
+		}
+
+		const instanceDistanceBuffer = new InstancedInterleavedBuffer( lineDistances, 2, 1 ); // d0, d1
+
+		geometry.setAttribute( 'instanceDistanceStart', new InterleavedBufferAttribute( instanceDistanceBuffer, 1, 0 ) ); // d0
+		geometry.setAttribute( 'instanceDistanceEnd', new InterleavedBufferAttribute( instanceDistanceBuffer, 1, 1 ) ); // d1
+
+		return this;
+
+	}
+
+	raycast( raycaster, intersects ) {
+
+		const worldUnits = this.material.worldUnits;
+		const camera = raycaster.camera;
+
+		if ( camera === null && ! worldUnits ) {
+
+			console.error( 'LineSegments2: "Raycaster.camera" needs to be set in order to raycast against LineSegments2 while worldUnits is set to false.' );
+
+		}
+
+		const threshold = ( raycaster.params.Line2 !== undefined ) ? raycaster.params.Line2.threshold || 0 : 0;
+
+		_ray = raycaster.ray;
+
+		const matrixWorld = this.matrixWorld;
+		const geometry = this.geometry;
+		const material = this.material;
+
+		_lineWidth = material.lineWidth + threshold;
+
+		_instanceStart = geometry.attributes.instanceStart;
+		_instanceEnd = geometry.attributes.instanceEnd;
+
+		// check if we intersect the sphere bounds
+		if ( geometry.boundingSphere === null ) {
+
+			geometry.computeBoundingSphere();
+
+		}
+
+		_sphere.copy( geometry.boundingSphere ).applyMatrix4( matrixWorld );
+
+		// increase the sphere bounds by the worst case line screen space width
+		let sphereMargin;
+		if ( worldUnits ) {
+
+			sphereMargin = _lineWidth * 0.5;
+
+		} else {
+
+			const distanceToSphere = Math.max( camera.near, _sphere.distanceToPoint( _ray.origin ) );
+			sphereMargin = getWorldSpaceHalfWidth( camera, distanceToSphere, material.resolution );
+
+		}
+
+		_sphere.radius += sphereMargin;
+
+		if ( _ray.intersectsSphere( _sphere ) === false ) {
+
+			return;
+
+		}
+
+		// check if we intersect the box bounds
+		if ( geometry.boundingBox === null ) {
+
+			geometry.computeBoundingBox();
+
+		}
+
+		_box.copy( geometry.boundingBox ).applyMatrix4( matrixWorld );
+
+		// increase the box bounds by the worst case line width
+		let boxMargin;
+		if ( worldUnits ) {
+
+			boxMargin = _lineWidth * 0.5;
+
+		} else {
+
+			const distanceToBox = Math.max( camera.near, _box.distanceToPoint( _ray.origin ) );
+			boxMargin = getWorldSpaceHalfWidth( camera, distanceToBox, material.resolution );
+
+		}
+
+		_box.expandByScalar( boxMargin );
+
+		if ( _ray.intersectsBox( _box ) === false ) {
+
+			return;
+
+		}
+
+		if ( worldUnits ) {
+
+			raycastWorldUnits( this, intersects );
+
+		} else {
+
+			raycastScreenSpace( this, camera, intersects );
+
+		}
+
+	}
+
+}
+
+export { LineSegments2 };

+ 241 - 0
src/modelViewer/objects/fatline/LineSegmentsGeometry.js

@@ -0,0 +1,241 @@
+import {
+	Box3,
+	Float32BufferAttribute,
+	InstancedBufferGeometry,
+	InstancedInterleavedBuffer,
+	InterleavedBufferAttribute,
+	Sphere,
+	Vector3,
+	WireframeGeometry
+} from 'three';  
+
+const _box = new Box3();
+const _vector = new Vector3();
+
+class LineSegmentsGeometry extends InstancedBufferGeometry {
+
+	constructor() {
+
+		super();
+
+		this.isLineSegmentsGeometry = true;
+
+		this.type = 'LineSegmentsGeometry';
+
+		const positions = [ - 1, 2, 0, 1, 2, 0, - 1, 1, 0, 1, 1, 0, - 1, 0, 0, 1, 0, 0, - 1, - 1, 0, 1, - 1, 0 ];
+		const uvs = [ - 1, 2, 1, 2, - 1, 1, 1, 1, - 1, - 1, 1, - 1, - 1, - 2, 1, - 2 ];
+		const index = [ 0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5 ];
+
+		this.setIndex( index );
+		this.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
+		this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
+
+	}
+
+	applyMatrix4( matrix ) {
+
+		const start = this.attributes.instanceStart;
+		const end = this.attributes.instanceEnd;
+
+		if ( start !== undefined ) {
+
+			start.applyMatrix4( matrix );
+
+			end.applyMatrix4( matrix );
+
+			start.needsUpdate = true;
+
+		}
+
+		if ( this.boundingBox !== null ) {
+
+			this.computeBoundingBox();
+
+		}
+
+		if ( this.boundingSphere !== null ) {
+
+			this.computeBoundingSphere();
+
+		}
+
+		return this;
+
+	}
+
+	setPositions( array ) {
+
+		let lineSegments;
+
+		if ( array instanceof Float32Array ) {
+
+			lineSegments = array;
+
+		} else if ( Array.isArray( array ) ) {
+
+			lineSegments = new Float32Array( array );
+
+		}
+
+		const instanceBuffer = new InstancedInterleavedBuffer( lineSegments, 6, 1 ); // xyz, xyz
+
+		this.setAttribute( 'instanceStart', new InterleavedBufferAttribute( instanceBuffer, 3, 0 ) ); // xyz
+		this.setAttribute( 'instanceEnd', new InterleavedBufferAttribute( instanceBuffer, 3, 3 ) ); // xyz
+
+		//
+
+		this.computeBoundingBox();
+		this.computeBoundingSphere();
+
+		return this;
+
+	}
+
+	setColors( array ) {
+
+		let colors;
+
+		if ( array instanceof Float32Array ) {
+
+			colors = array;
+
+		} else if ( Array.isArray( array ) ) {
+
+			colors = new Float32Array( array );
+
+		}
+
+		const instanceColorBuffer = new InstancedInterleavedBuffer( colors, 6, 1 ); // rgb, rgb
+
+		this.setAttribute( 'instanceColorStart', new InterleavedBufferAttribute( instanceColorBuffer, 3, 0 ) ); // rgb
+		this.setAttribute( 'instanceColorEnd', new InterleavedBufferAttribute( instanceColorBuffer, 3, 3 ) ); // rgb
+
+		return this;
+
+	}
+
+	fromWireframeGeometry( geometry ) {
+
+		this.setPositions( geometry.attributes.position.array );
+
+		return this;
+
+	}
+
+	fromEdgesGeometry( geometry ) {
+
+		this.setPositions( geometry.attributes.position.array );
+
+		return this;
+
+	}
+
+	fromMesh( mesh ) {
+
+		this.fromWireframeGeometry( new WireframeGeometry( mesh.geometry ) );
+
+		// set colors, maybe
+
+		return this;
+
+	}
+
+	fromLineSegments( lineSegments ) {
+
+		const geometry = lineSegments.geometry;
+
+		this.setPositions( geometry.attributes.position.array ); // assumes non-indexed
+
+		// set colors, maybe
+
+		return this;
+
+	}
+
+	computeBoundingBox() {
+
+		if ( this.boundingBox === null ) {
+
+			this.boundingBox = new Box3();
+
+		}
+
+		const start = this.attributes.instanceStart;
+		const end = this.attributes.instanceEnd;
+
+		if ( start !== undefined && end !== undefined ) {
+
+			this.boundingBox.setFromBufferAttribute( start );
+
+			_box.setFromBufferAttribute( end );
+
+			this.boundingBox.union( _box );
+
+		}
+
+	}
+
+	computeBoundingSphere() {
+
+		if ( this.boundingSphere === null ) {
+
+			this.boundingSphere = new Sphere();
+
+		}
+
+		if ( this.boundingBox === null ) {
+
+			this.computeBoundingBox();
+
+		}
+
+		const start = this.attributes.instanceStart;
+		const end = this.attributes.instanceEnd;
+
+		if ( start !== undefined && end !== undefined ) {
+
+			const center = this.boundingSphere.center;
+
+			this.boundingBox.getCenter( center );
+
+			let maxRadiusSq = 0;
+
+			for ( let i = 0, il = start.count; i < il; i ++ ) {
+
+				_vector.fromBufferAttribute( start, i );
+				maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector ) );
+
+				_vector.fromBufferAttribute( end, i );
+				maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector ) );
+
+			}
+
+			this.boundingSphere.radius = Math.sqrt( maxRadiusSq );
+
+			if ( isNaN( this.boundingSphere.radius ) ) {
+
+				console.error( 'THREE.LineSegmentsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', this );
+
+			}
+
+		}
+
+	}
+
+	toJSON() {
+
+		// todo
+
+	}
+
+	applyMatrix( matrix ) {
+
+		console.warn( 'THREE.LineSegmentsGeometry: applyMatrix() has been renamed to applyMatrix4().' );
+
+		return this.applyMatrix4( matrix );
+
+	}
+
+}
+
+export { LineSegmentsGeometry };

Разница между файлами не показана из-за своего большого размера
+ 538 - 0
src/modelViewer/utils/Common.js


+ 131 - 0
src/modelViewer/utils/CursorDeal.js

@@ -0,0 +1,131 @@
+
+import {Common} from './Common.js' 
+
+
+//处理cursor优先级
+
+
+var CursorDeal = {
+    priorityEvent : [//在前面的优先级高
+        
+        { pen_delPoint: `url({basePath}/images/polygon_mark/pic_pen_sub.png),auto`},
+        { pen_addPoint: `url({basePath}/images/polygon_mark/pic_pen_add.png),auto`},
+        { pen: `url({basePath}/images/polygon_mark/pic_pen.png),auto`},
+        
+        {hoverFirstMarker: "pointer"} ,
+        {"polygon_isIntersectSelf":'not-allowed'},
+        {"polygon_AtWrongPlace":'not-allowed'},
+        {'grabbing':'grabbing'},//通用
+        {'hoverGrab':'grab'},//通用
+        {'move':'move'},//通用 
+        {'pointer':'pointer'},//通用
+        
+        {polygonMark_move:'move'},
+        {polygonMark_hover: 'pointer'},
+        
+    
+        
+        {'zoomInCloud':'zoom-in'},
+        {'hoverPano':'pointer'}, 
+        {"notAllowed-default":'not-allowed'},   
+        //{'connectPano':`url({basePath}/images/connect.png),auto`},
+        //{'disconnectPano':`url({basePath}/images/connect-dis.png),auto`},
+         
+        //{'hoverLine':'pointer'},
+        {'hoverTranHandle':'grab'},
+          
+         
+        {"movePointcloud":'move'}, 
+        
+        {'delPoint':'url("https://4dkk.4dage.com/v4-test/www/sdk/images/polygon_mark/pic_pen_sub.png"), auto'},
+        {"markerMove":'grab'},
+        {'addPoint':'url("https://4dkk.4dage.com/v4-test/www/sdk/images/polygon_mark/pic_pen_add.png"), auto'},
+        { addOverlay: 'url(https://4dkk.4dage.com/v3-test/img/box_video.png),auto' },
+        {'rotatePointcloud':`url({basePath}/images/rotate-cursor.png),auto`}, 
+        {'hoverTag':'pointer'}, 
+         
+        {'addSth':'cell'},//or  crosshair
+        {'paint':'crosshair'}//'none'
+    ], 
+    list:[], //当前存在的cursor状态
+    currentCursorIndex:null,
+    
+    init : function(viewer, viewers){ 
+        this.priorityEvent.forEach(e=>{//刚开始basePath没值,现在换
+            for(let i in e){
+                e[i] = Common.replaceAll(e[i],'{basePath}','.'/* basePath */)
+            }
+        })
+         
+        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;

+ 489 - 0
src/modelViewer/utils/DrawUtil.js

@@ -0,0 +1,489 @@
+
+ 
+import * as THREE from 'three';  
+import math from './math.js';
+import {Line2} from "../objects/fatline/Line2.js";
+import {LineGeometry} from "../objects/fatline/LineGeometry.js";
+import {LineMaterial} from "../objects/fatline/LineMaterial.js"; 
+ 
+import {Common, config} from './Common.js' 
+
+
+
+
+
+var defaultColor = new THREE.Color(1,1,1);//config.applicationName == "zhiHouse" ? Colors.zhiBlue : Colors.lightGreen;
+
+function dealPosArr(points){//识别是否每个点都不一样,把连续点变为不连续的片段连接 
+    let add = (points)=>{
+        let points2 = [] , len = points.length 
+        for(let i=0;i<len-1;i++){
+            points2.push(points[i], points[i+1])
+        } 
+        return points2  
+    }
+    if(points[0] && points[0] instanceof Array){//多组,每组间连续,但组之间不连续
+        let points2 = []
+        points.forEach(ps=>points2.push(...add(ps)))
+        return points2   
+    }else if(points.length > 2 && !points[2].equals(points[1])){
+        return add(points)
+    }else return points
+    
+} 
+
+let center = new THREE.Vector3 
+function extractPos(posArr){//尽量让所有点都靠近原点 
+ 
+    //console.log('extractPos', posArr.map(e=>e.clone()))
+    let bound = new THREE.Box3
+    posArr.forEach(e=>bound.expandByPoint(e))
+    bound.getCenter(center)
+    
+    posArr.forEach(e=>e.sub(center))
+     
+    return center.clone()
+}
+
+
+var LineDraw = {
+    
+	createLine: function (posArr, o={}) {
+        //多段普通线  (第二个点和第三个点之间是没有线段的, 所以不用在意线段顺序)
+        var mat
+        if(o.mat){
+            mat = o.mat
+            if(mat instanceof LineMaterial)return LineDraw.createFatLine(posArr, o)
+        }else{
+            let prop = Object.assign({
+                lineWidth: o.lineWidth || 1,
+                //windows无效。 似乎mac/ios上粗细有效 ? 
+                color: o.color || defaultColor ,
+                transparent:true
+            },o)
+            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 || config.renderOrders.line
+  
+        this.moveLine(line, posArr)
+        
+		return line;  
+
+	},
+    
+	moveLine: function (line, posArr) {
+        //if(posArr.length == 0)return
+        if(!line.uncontinuous || posArr[0] && posArr[0] instanceof Array)posArr = dealPosArr(posArr)
+        let position = []
+        posArr.forEach(e=>position.push(e.x,e.y,e.z))
+        if(line.avoidBigNumber){  
+            let moveVec = extractPos(posArr)
+            line.position.copy(moveVec)//无视原本的position!
+        }
+        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 = false //!!Features.EXT_DEPTH.isSupported()  
+        
+        let params = Object.assign({}, {
+            //默认
+            lineWidth : 1,  
+            color:0xffffff,
+            transparent : true, // depthWrite:t,  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)
+          
+		return 	mat;			
+	},
+    
+    /* 
+        创建可以改变粗细的线。 
+     */
+	createFatLine : function(posArr, o){  
+		var geometry = new LineGeometry(); 
+		geometry.setColors( o.color || [1,1,1]);
+
+		var matLine = o.mat || this.createFatLineMat(o);
+		var line = new Line2( geometry, matLine );
+		//line.computeLineDistances();
+        line.uncontinuous = o.uncontinuous //线不连续,由线段组成
+		line.scale.set( 1, 1, 1 );
+		//line.renderOrder = config.renderOrders.line;
+        
+        this.moveFatLine(line, posArr)
+        
+		return line;
+
+	},
+    
+    
+    
+	moveFatLine: function(line, posArr ){
+        posArr = Common.CloneObject(posArr)
+        
+		var geometry = line.geometry;
+        var positions = [];
+        if(!line.uncontinuous || posArr[0] && posArr[0] instanceof Array) posArr = dealPosArr(posArr) 
+        
+        if(line.avoidBigNumber){  
+            let moveVec = extractPos(posArr)
+            line.position.copy(moveVec)//无视原本的position!
+        }  
+        
+        
+        posArr.forEach(e=>{positions.push(...e.toArray())})
+         
+		 
+        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(); 
+        } 
+         
+        
+        
+	},
+    
+    updateLine: function(line, posArr){
+        if(line instanceof Line2){
+            LineDraw.moveFatLine(line,posArr) 
+        }else{
+            LineDraw.moveLine(line,posArr)
+        }  
+    },
+   
+}
+
+var MeshDraw = { 
+
+    getShape: function(shapes, holes){  
+        //不一定闭合 暂时所有shapes共享holes。如果要单独的话, shapes改为[{shape:[],holes:[]},{}]的形式
+        if(shapes[0] && !(shapes[0] instanceof Array) ){//仅是一个shape的点
+            shapes = [shapes]
+        }
+        
+        let holesArr = []
+        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 ) 
+                } 
+                holesArr.push( holePath );
+            })  
+        }
+        
+        let shapesArr = shapes.map(points=>{
+            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 ) 
+            }
+            shape.holes.push(...holesArr)
+            shape.dontClose = points.dontClose //add 有的shape不需要闭合
+            return shape
+        })
+         
+        
+        return shapesArr        
+    },  
+
+ 
+    getShapeGeo: function(shapes, holes){//获取任意形状(多边形或弧形)的形状面  //quadraticCurveTo() 这是弧形的含函数
+		
+		var geometry = new THREE.ShapeGeometry( this.getShape(shapes, holes) );  //ShapeGeometry
+          
+		return geometry;
+		
+		
+	},
+    
+    
+    lessCurvePoints: function(points, oldCount, minRad=0.03, UtoTMapArr){//减少点数(拐弯的部分紧凑些,直线部分宽松些):
+        
+        let count = points.length  
+        let newUtoTMapArr = [] 
+        let newPoints = [] 
+        let pointIndexs = [] 
+        let lastVec
+        let startTime = performance.now()
+        /* if(UtoTMapArr){
+            for(let n=1;n<oldCount-1;n++){ 
+                pointIndexs.push(  UtoTMapArr.findIndex(e=>e>= n / (oldCount-1) ) )
+            }   
+        } */   
+        //console.log('cost dur:', performance.now() - startTime )    
+        let nextUtoTIndex = 1 
+        for(let i=0;i<count;i++){
+            let point = points[i];
+            let last = points[i-1]
+            let next = points[i+1]
+            
+            if(i == 0 || i == count-1 ) {
+                newPoints.push(point) //直接加入
+                UtoTMapArr && newUtoTMapArr.push(i == 0 ? 0 : 1) 
+            }else{ 
+                let curVec = new THREE.Vector3().subVectors(next,point)
+                if(!lastVec) lastVec = curVec
+                if(i>1){// 和上一个加入点的vec之间的夹角如果过大就加入 
+                     
+                    let reachNextUToT //找出新点中对应原先控制点的index,这些点必须加入拐点,否则会出现控制点偏移path(当它所在部分接近直线时)
+                    while(UtoTMapArr[i] > nextUtoTIndex / (oldCount-1)){//可能多个控制点对应一个点,当控制点很近时
+                        reachNextUToT = true
+                        nextUtoTIndex ++ 
+                    } 
+                
+                    if(/* pointIndexs.includes(i) || */  reachNextUToT ||  curVec.angleTo(lastVec) > minRad){//最小角度    (注意原始点不能太稀疏)
+                        newPoints.push(point)
+                        UtoTMapArr && newUtoTMapArr.push(UtoTMapArr[i])
+                        lastVec = curVec
+                       
+                    }  
+                }
+            }  
+        }   
+            
+        return {newUtoTMapArr, newPoints}
+    },
+     
+    getExtrudeGeo:  function(shapes, holes, options={openEnded:false, shapeDontClose:false}){//获得挤出棱柱,可以选择传递height,或者extrudePath
+        var shape = this.getShape(shapes, holes) //points是横截面 [vector2,...]
+        
+        if(options.extrudePath ){// 路径 :[vector3,...]
+             
+            var length = options.extrudePath.reduce((total, currentValue, currentIndex, arr)=>{
+                if(currentIndex == 0)return 0
+                return total + currentValue.distanceTo(arr[currentIndex-1]);
+            },0)
+            //options.extrudePath = new THREE.CatmullRomCurve3(options.extrudePath)
+            if(options.extrudePath.length == 2){
+                options.tension = 0 ;//否则一端扭曲
+                options.steps = 1
+            }
+              
+            {//去掉重复的点 
+                let path = []
+                const minDis = options.dontSmooth ? 0 : 0.2 //CatmullRomCurve3 经常扭曲,如果两个点靠得很近可能会扭曲,这里去除靠的太近的点。但去除后依旧会出现一定扭曲.   
+                options.extrudePath.forEach((p,i)=>{
+                    if(i==0 || i== options.extrudePath.length-1)return path.push(p) //首尾直接加入
+                    let last = path[path.length-1]//和上一个比
+                    let dis = last.distanceTo(p)
+                    if(dis <= minDis){
+                        console.log(`第${i}个点(${p.toArray()})因为和上一个数据(${last.toArray()})太接近(dis:${dis})所以删除`)
+                    }else if(i == options.extrudePath.length - 2){//因为最后一个必定加入,所以倒数第二个还也不能太靠近最后一个
+                        last = options.extrudePath[options.extrudePath.length-1] //和下一个(最后一个比)
+                        if(dis <= minDis){
+                            console.log(`第${i}个点(${p.toArray()})因为和下一个数据(${last.toArray()})太接近(dis:${dis})所以删除`)
+                        }else{
+                            path.push(p)
+                        }
+                    }else{
+                        path.push(p)
+                    }
+                    
+                }) 
+                options.extrudePath = path               
+            }
+            
+            
+            if(!options.dontSmooth){
+                //平滑连续的曲线(但经常会有扭曲的问题,tension:0能缓解, 另外shape和path都最好在原点附近,也就是点需减去bound.min )
+                options.extrudePath = new THREE.CatmullRomCurve3(options.extrudePath, options.closed ,  'catmullrom'  /* 'centripetal' */  , options.tension)//tension:张力, 越大弯曲越大。 随着长度增长,该值需要减小,否则会扭曲
+                if(options.lessPoints !== false ){//曲线但压缩直线部分点数量
+                    options.extrudePath.UtoTMapArr = [] //用于存储 getSpacedPoints得到的点对应points的百分比对应
+                    let count = Math.max(2, Math.round(length* (options.lessSpace || 200)  )) //为了防止有大拐弯才设置这么高
+                    let points = options.extrudePath.getSpacedPoints(count-1) //拆分为更密集的点 
+                    let result = this.lessCurvePoints(points, options.extrudePath.points.length,  options.minRad,   options.extrudePath.UtoTMapArr  )  //传UtoTMapArr的话点太多了卡住了
+                    //options.extrudePath = points
+                    options.extrudePath = result.newPoints 
+                    options.dontSmooth = true        
+                } 
+            }
+            
+              
+            
+            if(options.dontSmooth){
+                let curvePath = new THREE.CurvePath()//通用的曲线路径对象,它可以包含直线段和曲线段。在这里只做折线
+                curvePath.points = options.extrudePath//add
+                for (let i = 0; i < options.extrudePath.length - 1; i++){ 
+                    let curve3 = new THREE.LineCurve3(options.extrudePath[i], options.extrudePath[i + 1]);//添加线段
+                    curvePath.add(curve3);
+                } 
+                options.steps = options.extrudePath.length - 1  
+                options.extrudePath = curvePath 
+                options.tension = 0
+                //已修改过three,原本会平分细分,现在dontSmooth时会直接按照控制点来分段
+            } 
+        }
+        
+ 
+        var extrudeSettings = $.extend(options,{
+            steps: options.steps != void 0 ? options.steps : ( options.extrudePath ? Math.round(length/(options.spaceDis || 0.2)) : 1), //分成几段    spaceDis每段长度
+            bevelEnabled: false, //不加的话,height为0时会有圆弧高度
+            //openEnded默认false 
+        }) 
+        var geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings ); //修改了three.js文件,  buildLidFaces处,创建顶底面加了选项,可以选择开口。 
+        return geometry;
+        
+        
+        /*     tension = 0:曲线会变成一条直线,没有弯曲。
+        tension = 0.5:曲线会经过所有控制点,并保持自然的弯曲。  
+        tension > 0.5:曲线会更平滑,远离控制点之间的路径。
+        tension < 0.5:曲线会更贴近控制点之间的路径,弯曲更明显。 */
+    },
+    
+    
+	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} ;

Разница между файлами не показана из-за своего большого размера
+ 2033 - 0
src/modelViewer/utils/TransformControls.js


Разница между файлами не показана из-за своего большого размера
+ 1381 - 0
src/modelViewer/utils/TransformationTool.js


+ 58 - 0
src/modelViewer/utils/math.js

@@ -0,0 +1,58 @@
+ 
+import { View} from '../View.js'
+const view = new View
+ 
+var math = {
+    getBaseLog(x, y) {//返回以 x 为底 y 的对数(即 logx y) .  Math.log 返回一个数的自然对数
+        return Math.log(y) / Math.log(x);
+    },
+    
+    getQuaFromPosAim( position, target, forward = new THREE.Vector3(0, 0, -1)) { //类似相机roll=0
+        /* let matrix = (new THREE.Matrix4).lookAt(position, target, new THREE.Vector3(0,0,1)) //这里垂直的话会默认给一个右向所以不这么写
+        return (new THREE.Quaternion).setFromRotationMatrix(matrix) */
+        
+        view.yaw = 0 //reset     direction.z = 1  
+        view.direction = new THREE.Vector3().subVectors(target,position) 
+        return view.quaternion  //通常得到的可能要再multiply一个自身旋转baseQua
+         
+    },
+    linearClamp(value, xArr , yArr){ //xArr需要按顺序从小到大,yArr对应xArr中的值
+        
+        let len = xArr.length 
+        if(value <= xArr[0]) return yArr[0]
+        if(value >= xArr[len - 1]) return yArr[len - 1]
+        let i = 0 
+        
+        while(++i < len ){
+            if(value < xArr[i]){
+                let x1 = xArr[i-1], x2 = xArr[i], y1 = yArr[i-1], y2 = yArr[i] 
+                value = y1 + ( y2 - y1) * (value - x1)  / (x2 - x1)  
+                break
+            }
+        }
+        return value
+        
+         
+    },
+    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{ 
+                    let c = typeof b == 'number' ? b : b[name]
+                    return f(a[name], c)
+                }
+            }
+            return judge('x') && judge('y') && judge('z') && judge('w')  
+        } 
+        
+    }, 
+}
+
+export default math

+ 491 - 0
src/modelViewer/utils/transitions.js

@@ -0,0 +1,491 @@
+ 
+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)
+    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, delta) {
+            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, delta);
+        }
+    },
+    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, ignoreFirstFrame=true) {
+        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,   //取消时执行的函数
+            updateCount:0,
+            ignoreFirstFrame,
+        }),
+        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.updateCount++ == 0 && t.ignoreFirstFrame) return //add start可能发生在一帧中任意时刻,而每次update的是在一帧中的固定时刻,所以从start到第一次update的时间并不是所传入的delta,该delta 是上一帧的update到这一帧的update的耗时。 故去掉了第一次的update,相当于延迟一帧再update.   
+            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), n = 0; n < u.length; n++) {
+            var r = u[n];
+            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) 
+			}/* else if(is && e.cancelFun){
+                console.warn('cancelById', e.id,e.name)
+            } */
+            return !is
+        })
+        
+        cancels.forEach(e=>{e()}) //先从funcs中去除后再执行
+        
+        
+    },
+    cancel: function(e) {
+        //console.warn('cancel', e )
+        this.funcs = this.funcs.filter(function(t) {
+            return t.func !== e
+        })
+    },
+    getUniqueId: function() {
+        return this.uniqueID -= 1,
+        this.uniqueID
+    } 
+};
+
+export    {transitions, lerp, easing}

+ 247 - 0
src/modelViewer/viewerBase.js

@@ -0,0 +1,247 @@
+     
+import * as THREE from 'three';
+
+
+export class ViewerBase extends THREE.EventDispatcher{
+    constructor( args = {}){
+        super()
+        this.name = args.name
+        this.renderArea = args.renderArea 
+        this.oldResolution = new THREE.Vector2()
+        this.oldResolution2 = new THREE.Vector2()
+        this.viewports = [] 
+        this.screenSizeInfo = {
+            W:0, H:0, pixelRatio:1 , windowWidth:0, windowHeight:0
+        }
+        
+        this.initContext(args);
+        
+         
+        this.addEventListener('content_changed', ()=>{//画面改变,需要渲染
+            this.needRender = true
+            //console.log('needRender')
+        })
+        
+        this.scene = new THREE.Scene
+    }
+    
+    
+    
+    
+    
+    initContext(args){ 
+
+        let width = this.renderArea.clientWidth;
+        let height = this.renderArea.clientHeight; 
+        let contextAttributes = {
+            alpha: true,//支持透明
+            depth: true,
+            stencil: false,
+            antialias: !!args.antialias, 
+            preserveDrawingBuffer: args.preserveDrawingBuffer || false ,
+            powerPreference: "high-performance",
+        }; 
+
+        let canvas = document.createElement("canvas");
+                                            
+        let webglVer = (args.webgl1 ||/*  browser.urlHasValue('webgl1') || */ !Features.webgl2RealSupport() ) ? 'webgl' : 'webgl2' 
+        let context = canvas.getContext(webglVer, contextAttributes );
+ 
+         
+
+        this.renderer = new THREE.WebGLRenderer({ 
+            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 && 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();
+        });
+      
+         
+        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);
+        }
+                      
+       
+    }
+    
+    
+    
+    
+    updateScreenSize(o={}) { //有可能需要让viewport来判断,当窗口大小不变但viewport大小变时 
+        if(this.screenshoting && !o.forceUpdateSize)   return //截图时不允许因窗口改变大小而updateScreenSize
+           
+        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 =/*  Math.min(settings.maxPixelRatio,  */window.devicePixelRatio/* ) */
+                 
+            }     
+        }
+        if (render) { 
+            this.setSize(w, h, ratio, o.forTarget  ); 
+        } 
+    }    
+
+
+ 
+     
+    setSize(width, height, devicePixelRatio, onlyForTarget){ 
+        //console.log('setSize', width) 
+        if(!onlyForTarget){//onlyForTarget表示不更改当前renderer,只是为了rendertarget才要改变viewport
+            this.renderer.setPixelRatio(devicePixelRatio)    
+            this.renderer.setSize(width, height ); // resize之后会自动clear(似乎因为setScissor ),所以一定要立刻绘制,所以setSize要在cameraChanged、update之前
+                                                                   
+         }
+        
+        
+        if(this.viewports){
+            this.viewports.forEach((view,i)=>{
+                //if(!view.active)return
+                
+                var width_ = width * view.width
+                var height_ = height * view.height
+                 
+                view.setResolution(Math.ceil(width_), Math.ceil(height_), width, height, devicePixelRatio ) 
+                
+                
+                if(height_ == 0)return  //avoid NAN
+                let aspect = width_ / height_;  //camera的参数精确些,不用视口的归整的resolution像素值,否则hasChange无法为true, 导致canvasResize了但map没update从而闪烁
+                view.camera.aspect = aspect;
+                
+                if(view.camera.type == "OrthographicCamera"){ 
+                    view.camera.left = -width_/2 
+                    view.camera.right = width_/2
+                    view.camera.bottom = -height_/2;
+                    view.camera.top = height_/2 
+                }else{   
+                    if(view.camera.minFov){
+                        view.camera.setMinFov()  //update fov
+                    }  
+                }
+                
+                view.camera.updateProjectionMatrix();
+            })
+        }
+        
+        
+        if(!onlyForTarget){//因为onlyForTarget不传递devicePixelRatio所以不发送了 
+            this.dispatchEvent( 'viewerResize', {width, height, devicePixelRatio})
+            this.viewports.forEach(e=>{
+                this.ifEmitResize({viewport:e,  deviceRatio:devicePixelRatio})
+            })
+        }  
+         
+    }  
+    
+    ifEmitResize(e){//切换viewport渲染时, 若这些viewport大小不同就发送一次, 通知一些材质更新resolution。  
+        //console.log('ifEmitResize',e.viewport.name,e.viewport.resolution2 )
+        if(!e.viewport.resolution.equals(this.oldResolution)||!e.viewport.resolution2.equals(this.oldResolution2)){ 
+            this.dispatchEvent(Object.assign(e, {type:'resize'})) 
+            this.oldResolution.copy(e.viewport.resolution)
+            this.oldResolution2.copy(e.viewport.resolution2)  
+         } 
+    }
+    
+    
+    
+    cameraChanged() {//判断相机是否改变
+        var changed = false; 
+        
+        for(let i=0,j=this.viewports.length;i<j;i++){
+            let viewport = this.viewports[i]
+            let changeInfo = viewport.cameraChanged()
+            if(changeInfo.changed){
+                changed = true 
+                this.dispatchEvent({
+                    type: "camera_changed", 
+                    camera: viewport.camera,
+                    viewport ,
+                    changeInfo 
+                })  
+                viewport.needRender = true  //直接写这咯  
+                if(changeInfo.resolutionChanged){
+                    this.ifEmitResize({viewport}) //for map
+                }  
+                  
+            }                
+        }
+        return changed
+    }
+    
+    
+    clear(params={}){ 
+        let background = params.background || this.background;
+        let backgroundOpacity = params.backgroundOpacity == void 0 ? this.backgroundOpacity : params.backgroundOpacity//如果想完全透明,只需要backgroundOpacity为0
+		let renderer = this.renderer
+        //let gl = renderer.getContext()
+         
+        if(background instanceof THREE.Color){ //add
+            renderer.setClearColor(background, backgroundOpacity);
+        }else if(background === "skybox"){
+			renderer.setClearColor(0x000000, backgroundOpacity);
+		} else if (background === 'gradient') {
+			renderer.setClearColor(0x000000, backgroundOpacity);
+		} else if (background === 'black') {
+			renderer.setClearColor(0x000000, 1);
+		} else if (background === 'white') {
+			renderer.setClearColor(0xFFFFFF, 1);
+		} else {
+			renderer.setClearColor(background, backgroundOpacity);
+		}
+		
+		/* params.target ||  */renderer.clear();
+    
+	}
+    
+    
+    dispose(){ 
+        //scene.clear();
+        this.renderer.dispose()
+        this.renderer.forceContextLoss()
+        let gl = this.renderer.getContext();
+        gl.getExtension("WEBGL_lose_context") && gl.getExtension("WEBGL_lose_context").loseContext()
+        this.renderArea.removeChild(this.renderer.domElement)
+        this.dispatchEvent('dispose') 
+    }
+    
+}